Make LXC fail verification if cgroup is not mounted
[ganeti-local] / lib / hypervisor / hv_lxc.py
1 #
2 #
3
4 # Copyright (C) 2010, 2013 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """LXC hypervisor
23
24 """
25
26 import os
27 import os.path
28 import time
29 import logging
30
31 from ganeti import constants
32 from ganeti import errors # pylint: disable=W0611
33 from ganeti import utils
34 from ganeti import objects
35 from ganeti import pathutils
36 from ganeti.hypervisor import hv_base
37 from ganeti.errors import HypervisorError
38
39
40 class LXCHypervisor(hv_base.BaseHypervisor):
41   """LXC-based virtualization.
42
43   TODO:
44     - move hardcoded parameters into hypervisor parameters, once we
45       have the container-parameter support
46
47   Problems/issues:
48     - LXC is very temperamental; in daemon mode, it succeeds or fails
49       in launching the instance silently, without any error
50       indication, and when failing it can leave network interfaces
51       around, and future successful startups will list the instance
52       twice
53
54   """
55   _ROOT_DIR = pathutils.RUN_DIR + "/lxc"
56   _DEVS = [
57     "c 1:3",   # /dev/null
58     "c 1:5",   # /dev/zero
59     "c 1:7",   # /dev/full
60     "c 1:8",   # /dev/random
61     "c 1:9",   # /dev/urandom
62     "c 1:10",  # /dev/aio
63     "c 5:0",   # /dev/tty
64     "c 5:1",   # /dev/console
65     "c 5:2",   # /dev/ptmx
66     "c 136:*", # first block of Unix98 PTY slaves
67     ]
68   _DENIED_CAPABILITIES = [
69     "mac_override",    # Allow MAC configuration or state changes
70     # TODO: remove sys_admin too, for safety
71     #"sys_admin",       # Perform  a range of system administration operations
72     "sys_boot",        # Use reboot(2) and kexec_load(2)
73     "sys_module",      # Load  and  unload kernel modules
74     "sys_time",        # Set  system  clock, set real-time (hardware) clock
75     ]
76   _DIR_MODE = 0755
77
78   PARAMETERS = {
79     constants.HV_CPU_MASK: hv_base.OPT_CPU_MASK_CHECK,
80     }
81
82   def __init__(self):
83     hv_base.BaseHypervisor.__init__(self)
84     utils.EnsureDirs([(self._ROOT_DIR, self._DIR_MODE)])
85
86   @staticmethod
87   def _GetMountSubdirs(path):
88     """Return the list of mountpoints under a given path.
89
90     """
91     result = []
92     for _, mountpoint, _, _ in utils.GetMounts():
93       if (mountpoint.startswith(path) and
94           mountpoint != path):
95         result.append(mountpoint)
96
97     result.sort(key=lambda x: x.count("/"), reverse=True)
98     return result
99
100   @classmethod
101   def _InstanceDir(cls, instance_name):
102     """Return the root directory for an instance.
103
104     """
105     return utils.PathJoin(cls._ROOT_DIR, instance_name)
106
107   @classmethod
108   def _InstanceConfFile(cls, instance_name):
109     """Return the configuration file for an instance.
110
111     """
112     return utils.PathJoin(cls._ROOT_DIR, instance_name + ".conf")
113
114   @classmethod
115   def _InstanceLogFile(cls, instance_name):
116     """Return the log file for an instance.
117
118     """
119     return utils.PathJoin(cls._ROOT_DIR, instance_name + ".log")
120
121   @classmethod
122   def _GetCgroupMountPoint(cls):
123     for _, mountpoint, fstype, _ in utils.GetMounts():
124       if fstype == "cgroup":
125         return mountpoint
126     raise errors.HypervisorError("The cgroup filesystem is not mounted")
127
128   @classmethod
129   def _GetCgroupCpuList(cls, instance_name):
130     """Return the list of CPU ids for an instance.
131
132     """
133     cgroup = cls._GetCgroupMountPoint()
134     try:
135       cpus = utils.ReadFile(utils.PathJoin(cgroup, 'lxc',
136                                            instance_name,
137                                            "cpuset.cpus"))
138     except EnvironmentError, err:
139       raise errors.HypervisorError("Getting CPU list for instance"
140                                    " %s failed: %s" % (instance_name, err))
141
142     return utils.ParseCpuMask(cpus)
143
144   @classmethod
145   def _GetCgroupMemoryLimit(cls, instance_name):
146     """Return the memory limit for an instance
147
148     """
149     cgroup = cls._GetCgroupMountPoint()
150     try:
151       memory = int(utils.ReadFile(utils.PathJoin(cgroup, 'lxc',
152                                                  instance_name,
153                                                  "memory.limit_in_bytes")))
154     except EnvironmentError:
155       # memory resource controller may be disabled, ignore
156       memory = 0
157
158     return memory
159
160   def ListInstances(self):
161     """Get the list of running instances.
162
163     """
164     return [iinfo[0] for iinfo in self.GetAllInstancesInfo()]
165
166   def GetInstanceInfo(self, instance_name):
167     """Get instance properties.
168
169     @type instance_name: string
170     @param instance_name: the instance name
171     @rtype: tuple of strings
172     @return: (name, id, memory, vcpus, stat, times)
173
174     """
175     # TODO: read container info from the cgroup mountpoint
176
177     result = utils.RunCmd(["lxc-info", "-s", "-n", instance_name])
178     if result.failed:
179       raise errors.HypervisorError("Running lxc-info failed: %s" %
180                                    result.output)
181     # lxc-info output examples:
182     # 'state: STOPPED
183     # 'state: RUNNING
184     _, state = result.stdout.rsplit(None, 1)
185     if state != "RUNNING":
186       return None
187
188     cpu_list = self._GetCgroupCpuList(instance_name)
189     memory = self._GetCgroupMemoryLimit(instance_name) / (1024 ** 2)
190     return (instance_name, 0, memory, len(cpu_list), 0, 0)
191
192   def GetAllInstancesInfo(self):
193     """Get properties of all instances.
194
195     @return: [(name, id, memory, vcpus, stat, times),...]
196
197     """
198     data = []
199     for name in os.listdir(self._ROOT_DIR):
200       try:
201         info = self.GetInstanceInfo(name)
202       except errors.HypervisorError:
203         continue
204       if info:
205         data.append(info)
206     return data
207
208   def _CreateConfigFile(self, instance, root_dir):
209     """Create an lxc.conf file for an instance.
210
211     """
212     out = []
213     # hostname
214     out.append("lxc.utsname = %s" % instance.name)
215
216     # separate pseudo-TTY instances
217     out.append("lxc.pts = 255")
218     # standard TTYs
219     out.append("lxc.tty = 6")
220     # console log file
221     console_log = utils.PathJoin(self._ROOT_DIR, instance.name + ".console")
222     try:
223       utils.WriteFile(console_log, data="", mode=constants.SECURE_FILE_MODE)
224     except EnvironmentError, err:
225       raise errors.HypervisorError("Creating console log file %s for"
226                                    " instance %s failed: %s" %
227                                    (console_log, instance.name, err))
228     out.append("lxc.console = %s" % console_log)
229
230     # root FS
231     out.append("lxc.rootfs = %s" % root_dir)
232
233     # TODO: additional mounts, if we disable CAP_SYS_ADMIN
234
235     # CPUs
236     if instance.hvparams[constants.HV_CPU_MASK]:
237       cpu_list = utils.ParseCpuMask(instance.hvparams[constants.HV_CPU_MASK])
238       cpus_in_mask = len(cpu_list)
239       if cpus_in_mask != instance.beparams["vcpus"]:
240         raise errors.HypervisorError("Number of VCPUs (%d) doesn't match"
241                                      " the number of CPUs in the"
242                                      " cpu_mask (%d)" %
243                                      (instance.beparams["vcpus"],
244                                       cpus_in_mask))
245       out.append("lxc.cgroup.cpuset.cpus = %s" %
246                  instance.hvparams[constants.HV_CPU_MASK])
247
248     # Memory
249     # Conditionally enable, memory resource controller might be disabled
250     cgroup = self._GetCgroupMountPoint()
251     if os.path.exists(utils.PathJoin(cgroup, 'memory.limit_in_bytes')):
252       out.append("lxc.cgroup.memory.limit_in_bytes = %dM" %
253                  instance.beparams[constants.BE_MAXMEM])
254
255     if os.path.exists(utils.PathJoin(cgroup, 'memory.memsw.limit_in_bytes')):
256       out.append("lxc.cgroup.memory.memsw.limit_in_bytes = %dM" %
257                  instance.beparams[constants.BE_MAXMEM])
258
259     # Device control
260     # deny direct device access
261     out.append("lxc.cgroup.devices.deny = a")
262     for devinfo in self._DEVS:
263       out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
264
265     # Networking
266     for idx, nic in enumerate(instance.nics):
267       out.append("# NIC %d" % idx)
268       mode = nic.nicparams[constants.NIC_MODE]
269       link = nic.nicparams[constants.NIC_LINK]
270       if mode == constants.NIC_MODE_BRIDGED:
271         out.append("lxc.network.type = veth")
272         out.append("lxc.network.link = %s" % link)
273       else:
274         raise errors.HypervisorError("LXC hypervisor only supports"
275                                      " bridged mode (NIC %d has mode %s)" %
276                                      (idx, mode))
277       out.append("lxc.network.hwaddr = %s" % nic.mac)
278       out.append("lxc.network.flags = up")
279
280     # Capabilities
281     for cap in self._DENIED_CAPABILITIES:
282       out.append("lxc.cap.drop = %s" % cap)
283
284     return "\n".join(out) + "\n"
285
286   def StartInstance(self, instance, block_devices, startup_paused):
287     """Start an instance.
288
289     For LXC, we try to mount the block device and execute 'lxc-start'.
290     We use volatile containers.
291
292     """
293     root_dir = self._InstanceDir(instance.name)
294     try:
295       utils.EnsureDirs([(root_dir, self._DIR_MODE)])
296     except errors.GenericError, err:
297       raise HypervisorError("Creating instance directory failed: %s", str(err))
298
299     conf_file = self._InstanceConfFile(instance.name)
300     utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
301
302     log_file = self._InstanceLogFile(instance.name)
303     if not os.path.exists(log_file):
304       try:
305         utils.WriteFile(log_file, data="", mode=constants.SECURE_FILE_MODE)
306       except EnvironmentError, err:
307         raise errors.HypervisorError("Creating hypervisor log file %s for"
308                                      " instance %s failed: %s" %
309                                      (log_file, instance.name, err))
310
311     if not os.path.ismount(root_dir):
312       if not block_devices:
313         raise HypervisorError("LXC needs at least one disk")
314
315       sda_dev_path = block_devices[0][1]
316       result = utils.RunCmd(["mount", sda_dev_path, root_dir])
317       if result.failed:
318         raise HypervisorError("Mounting the root dir of LXC instance %s"
319                               " failed: %s" % (instance.name, result.output))
320     result = utils.RunCmd(["lxc-start", "-n", instance.name,
321                            "-o", log_file,
322                            "-l", "DEBUG",
323                            "-f", conf_file, "-d"])
324     if result.failed:
325       raise HypervisorError("Running the lxc-start script failed: %s" %
326                             result.output)
327
328   def StopInstance(self, instance, force=False, retry=False, name=None):
329     """Stop an instance.
330
331     This method has complicated cleanup tests, as we must:
332       - try to kill all leftover processes
333       - try to unmount any additional sub-mountpoints
334       - finally unmount the instance dir
335
336     """
337     if name is None:
338       name = instance.name
339
340     root_dir = self._InstanceDir(name)
341     if not os.path.exists(root_dir):
342       return
343
344     if name in self.ListInstances():
345       # Signal init to shutdown; this is a hack
346       if not retry and not force:
347         result = utils.RunCmd(["chroot", root_dir, "poweroff"])
348         if result.failed:
349           raise HypervisorError("Running 'poweroff' on the instance"
350                                 " failed: %s" % result.output)
351       time.sleep(2)
352       result = utils.RunCmd(["lxc-stop", "-n", name])
353       if result.failed:
354         logging.warning("Error while doing lxc-stop for %s: %s", name,
355                         result.output)
356
357     if not os.path.ismount(root_dir):
358       return
359
360     for mpath in self._GetMountSubdirs(root_dir):
361       result = utils.RunCmd(["umount", mpath])
362       if result.failed:
363         logging.warning("Error while umounting subpath %s for instance %s: %s",
364                         mpath, name, result.output)
365
366     result = utils.RunCmd(["umount", root_dir])
367     if result.failed and force:
368       msg = ("Processes still alive in the chroot: %s" %
369              utils.RunCmd("fuser -vm %s" % root_dir).output)
370       logging.error(msg)
371       raise HypervisorError("Unmounting the chroot dir failed: %s (%s)" %
372                             (result.output, msg))
373
374   def RebootInstance(self, instance):
375     """Reboot an instance.
376
377     This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
378
379     """
380     # TODO: implement reboot
381     raise HypervisorError("The LXC hypervisor doesn't implement the"
382                           " reboot functionality")
383
384   def BalloonInstanceMemory(self, instance, mem):
385     """Balloon an instance memory to a certain value.
386
387     @type instance: L{objects.Instance}
388     @param instance: instance to be accepted
389     @type mem: int
390     @param mem: actual memory size to use for instance runtime
391
392     """
393     # Currently lxc instances don't have memory limits
394     pass
395
396   def GetNodeInfo(self):
397     """Return information about the node.
398
399     This is just a wrapper over the base GetLinuxNodeInfo method.
400
401     @return: a dict with the following keys (values in MiB):
402           - memory_total: the total memory size on the node
403           - memory_free: the available memory on the node for instances
404           - memory_dom0: the memory used by the node itself, if available
405
406     """
407     return self.GetLinuxNodeInfo()
408
409   @classmethod
410   def GetInstanceConsole(cls, instance, hvparams, beparams):
411     """Return a command for connecting to the console of an instance.
412
413     """
414     return objects.InstanceConsole(instance=instance.name,
415                                    kind=constants.CONS_SSH,
416                                    host=instance.primary_node,
417                                    user=constants.SSH_CONSOLE_USER,
418                                    command=["lxc-console", "-n", instance.name])
419
420   def Verify(self):
421     """Verify the hypervisor.
422
423     For the LXC manager, it just checks the existence of the base dir.
424
425     @return: Problem description if something is wrong, C{None} otherwise
426
427     """
428     msgs = []
429
430     if not os.path.exists(self._ROOT_DIR):
431       msgs.append("The required directory '%s' does not exist" %
432                   self._ROOT_DIR)
433
434     try:
435       self._GetCgroupMountPoint()
436     except errors.HypervisorError, err:
437       msgs.append(str(err))
438
439     return self._FormatVerifyResults(msgs)
440
441   @classmethod
442   def PowercycleNode(cls):
443     """LXC powercycle, just a wrapper over Linux powercycle.
444
445     """
446     cls.LinuxPowercycle()
447
448   def MigrateInstance(self, instance, target, live):
449     """Migrate an instance.
450
451     @type instance: L{objects.Instance}
452     @param instance: the instance to be migrated
453     @type target: string
454     @param target: hostname (usually ip) of the target node
455     @type live: boolean
456     @param live: whether to do a live or non-live migration
457
458     """
459     raise HypervisorError("Migration is not supported by the LXC hypervisor")
460
461   def GetMigrationStatus(self, instance):
462     """Get the migration status
463
464     @type instance: L{objects.Instance}
465     @param instance: the instance that is being migrated
466     @rtype: L{objects.MigrationStatus}
467     @return: the status of the current migration (one of
468              L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
469              progress info that can be retrieved from the hypervisor
470
471     """
472     raise HypervisorError("Migration is not supported by the LXC hypervisor")