Remove XEN_CMD from constants, adjust to PowercycleNode
[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, hvparams=None):
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, hvparams=None):
167     """Get instance properties.
168
169     @type instance_name: string
170     @param instance_name: the instance name
171     @type hvparams: dict of strings
172     @param hvparams: hvparams to be used with this instance
173     @rtype: tuple of strings
174     @return: (name, id, memory, vcpus, stat, times)
175
176     """
177     # TODO: read container info from the cgroup mountpoint
178
179     result = utils.RunCmd(["lxc-info", "-s", "-n", instance_name])
180     if result.failed:
181       raise errors.HypervisorError("Running lxc-info failed: %s" %
182                                    result.output)
183     # lxc-info output examples:
184     # 'state: STOPPED
185     # 'state: RUNNING
186     _, state = result.stdout.rsplit(None, 1)
187     if state != "RUNNING":
188       return None
189
190     cpu_list = self._GetCgroupCpuList(instance_name)
191     memory = self._GetCgroupMemoryLimit(instance_name) / (1024 ** 2)
192     return (instance_name, 0, memory, len(cpu_list), 0, 0)
193
194   def GetAllInstancesInfo(self, hvparams=None):
195     """Get properties of all instances.
196
197     @type hvparams: dict of strings
198     @param hvparams: hypervisor parameter
199     @return: [(name, id, memory, vcpus, stat, times),...]
200
201     """
202     data = []
203     for name in os.listdir(self._ROOT_DIR):
204       try:
205         info = self.GetInstanceInfo(name)
206       except errors.HypervisorError:
207         continue
208       if info:
209         data.append(info)
210     return data
211
212   def _CreateConfigFile(self, instance, root_dir):
213     """Create an lxc.conf file for an instance.
214
215     """
216     out = []
217     # hostname
218     out.append("lxc.utsname = %s" % instance.name)
219
220     # separate pseudo-TTY instances
221     out.append("lxc.pts = 255")
222     # standard TTYs
223     out.append("lxc.tty = 6")
224     # console log file
225     console_log = utils.PathJoin(self._ROOT_DIR, instance.name + ".console")
226     try:
227       utils.WriteFile(console_log, data="", mode=constants.SECURE_FILE_MODE)
228     except EnvironmentError, err:
229       raise errors.HypervisorError("Creating console log file %s for"
230                                    " instance %s failed: %s" %
231                                    (console_log, instance.name, err))
232     out.append("lxc.console = %s" % console_log)
233
234     # root FS
235     out.append("lxc.rootfs = %s" % root_dir)
236
237     # TODO: additional mounts, if we disable CAP_SYS_ADMIN
238
239     # CPUs
240     if instance.hvparams[constants.HV_CPU_MASK]:
241       cpu_list = utils.ParseCpuMask(instance.hvparams[constants.HV_CPU_MASK])
242       cpus_in_mask = len(cpu_list)
243       if cpus_in_mask != instance.beparams["vcpus"]:
244         raise errors.HypervisorError("Number of VCPUs (%d) doesn't match"
245                                      " the number of CPUs in the"
246                                      " cpu_mask (%d)" %
247                                      (instance.beparams["vcpus"],
248                                       cpus_in_mask))
249       out.append("lxc.cgroup.cpuset.cpus = %s" %
250                  instance.hvparams[constants.HV_CPU_MASK])
251
252     # Memory
253     # Conditionally enable, memory resource controller might be disabled
254     cgroup = self._GetCgroupMountPoint()
255     if os.path.exists(utils.PathJoin(cgroup, 'memory.limit_in_bytes')):
256       out.append("lxc.cgroup.memory.limit_in_bytes = %dM" %
257                  instance.beparams[constants.BE_MAXMEM])
258
259     if os.path.exists(utils.PathJoin(cgroup, 'memory.memsw.limit_in_bytes')):
260       out.append("lxc.cgroup.memory.memsw.limit_in_bytes = %dM" %
261                  instance.beparams[constants.BE_MAXMEM])
262
263     # Device control
264     # deny direct device access
265     out.append("lxc.cgroup.devices.deny = a")
266     for devinfo in self._DEVS:
267       out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
268
269     # Networking
270     for idx, nic in enumerate(instance.nics):
271       out.append("# NIC %d" % idx)
272       mode = nic.nicparams[constants.NIC_MODE]
273       link = nic.nicparams[constants.NIC_LINK]
274       if mode == constants.NIC_MODE_BRIDGED:
275         out.append("lxc.network.type = veth")
276         out.append("lxc.network.link = %s" % link)
277       else:
278         raise errors.HypervisorError("LXC hypervisor only supports"
279                                      " bridged mode (NIC %d has mode %s)" %
280                                      (idx, mode))
281       out.append("lxc.network.hwaddr = %s" % nic.mac)
282       out.append("lxc.network.flags = up")
283
284     # Capabilities
285     for cap in self._DENIED_CAPABILITIES:
286       out.append("lxc.cap.drop = %s" % cap)
287
288     return "\n".join(out) + "\n"
289
290   def StartInstance(self, instance, block_devices, startup_paused):
291     """Start an instance.
292
293     For LXC, we try to mount the block device and execute 'lxc-start'.
294     We use volatile containers.
295
296     """
297     root_dir = self._InstanceDir(instance.name)
298     try:
299       utils.EnsureDirs([(root_dir, self._DIR_MODE)])
300     except errors.GenericError, err:
301       raise HypervisorError("Creating instance directory failed: %s", str(err))
302
303     conf_file = self._InstanceConfFile(instance.name)
304     utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
305
306     log_file = self._InstanceLogFile(instance.name)
307     if not os.path.exists(log_file):
308       try:
309         utils.WriteFile(log_file, data="", mode=constants.SECURE_FILE_MODE)
310       except EnvironmentError, err:
311         raise errors.HypervisorError("Creating hypervisor log file %s for"
312                                      " instance %s failed: %s" %
313                                      (log_file, instance.name, err))
314
315     if not os.path.ismount(root_dir):
316       if not block_devices:
317         raise HypervisorError("LXC needs at least one disk")
318
319       sda_dev_path = block_devices[0][1]
320       result = utils.RunCmd(["mount", sda_dev_path, root_dir])
321       if result.failed:
322         raise HypervisorError("Mounting the root dir of LXC instance %s"
323                               " failed: %s" % (instance.name, result.output))
324     result = utils.RunCmd(["lxc-start", "-n", instance.name,
325                            "-o", log_file,
326                            "-l", "DEBUG",
327                            "-f", conf_file, "-d"])
328     if result.failed:
329       raise HypervisorError("Running the lxc-start script failed: %s" %
330                             result.output)
331
332   def StopInstance(self, instance, force=False, retry=False, name=None):
333     """Stop an instance.
334
335     This method has complicated cleanup tests, as we must:
336       - try to kill all leftover processes
337       - try to unmount any additional sub-mountpoints
338       - finally unmount the instance dir
339
340     """
341     if name is None:
342       name = instance.name
343
344     root_dir = self._InstanceDir(name)
345     if not os.path.exists(root_dir):
346       return
347
348     if name in self.ListInstances():
349       # Signal init to shutdown; this is a hack
350       if not retry and not force:
351         result = utils.RunCmd(["chroot", root_dir, "poweroff"])
352         if result.failed:
353           raise HypervisorError("Running 'poweroff' on the instance"
354                                 " failed: %s" % result.output)
355       time.sleep(2)
356       result = utils.RunCmd(["lxc-stop", "-n", name])
357       if result.failed:
358         logging.warning("Error while doing lxc-stop for %s: %s", name,
359                         result.output)
360
361     if not os.path.ismount(root_dir):
362       return
363
364     for mpath in self._GetMountSubdirs(root_dir):
365       result = utils.RunCmd(["umount", mpath])
366       if result.failed:
367         logging.warning("Error while umounting subpath %s for instance %s: %s",
368                         mpath, name, result.output)
369
370     result = utils.RunCmd(["umount", root_dir])
371     if result.failed and force:
372       msg = ("Processes still alive in the chroot: %s" %
373              utils.RunCmd("fuser -vm %s" % root_dir).output)
374       logging.error(msg)
375       raise HypervisorError("Unmounting the chroot dir failed: %s (%s)" %
376                             (result.output, msg))
377
378   def RebootInstance(self, instance):
379     """Reboot an instance.
380
381     This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
382
383     """
384     # TODO: implement reboot
385     raise HypervisorError("The LXC hypervisor doesn't implement the"
386                           " reboot functionality")
387
388   def BalloonInstanceMemory(self, instance, mem):
389     """Balloon an instance memory to a certain value.
390
391     @type instance: L{objects.Instance}
392     @param instance: instance to be accepted
393     @type mem: int
394     @param mem: actual memory size to use for instance runtime
395
396     """
397     # Currently lxc instances don't have memory limits
398     pass
399
400   def GetNodeInfo(self, hvparams=None):
401     """Return information about the node.
402
403     This is just a wrapper over the base GetLinuxNodeInfo method.
404
405     @type hvparams: dict of strings
406     @param hvparams: hypervisor parameters, not used in this class
407
408     @return: a dict with the following keys (values in MiB):
409           - memory_total: the total memory size on the node
410           - memory_free: the available memory on the node for instances
411           - memory_dom0: the memory used by the node itself, if available
412
413     """
414     return self.GetLinuxNodeInfo()
415
416   @classmethod
417   def GetInstanceConsole(cls, instance, hvparams, beparams):
418     """Return a command for connecting to the console of an instance.
419
420     """
421     return objects.InstanceConsole(instance=instance.name,
422                                    kind=constants.CONS_SSH,
423                                    host=instance.primary_node,
424                                    user=constants.SSH_CONSOLE_USER,
425                                    command=["lxc-console", "-n", instance.name])
426
427   def Verify(self, hvparams=None):
428     """Verify the hypervisor.
429
430     For the LXC manager, it just checks the existence of the base dir.
431
432     @type hvparams: dict of strings
433     @param hvparams: hypervisor parameters to be verified against; not used here
434
435     @return: Problem description if something is wrong, C{None} otherwise
436
437     """
438     msgs = []
439
440     if not os.path.exists(self._ROOT_DIR):
441       msgs.append("The required directory '%s' does not exist" %
442                   self._ROOT_DIR)
443
444     try:
445       self._GetCgroupMountPoint()
446     except errors.HypervisorError, err:
447       msgs.append(str(err))
448
449     return self._FormatVerifyResults(msgs)
450
451   @classmethod
452   def PowercycleNode(cls, hvparams=None):
453     """LXC powercycle, just a wrapper over Linux powercycle.
454
455     @type hvparams: dict of strings
456     @param hvparams: hypervisor params to be used on this node
457
458     """
459     cls.LinuxPowercycle()
460
461   def MigrateInstance(self, instance, target, live):
462     """Migrate an instance.
463
464     @type instance: L{objects.Instance}
465     @param instance: the instance to be migrated
466     @type target: string
467     @param target: hostname (usually ip) of the target node
468     @type live: boolean
469     @param live: whether to do a live or non-live migration
470
471     """
472     raise HypervisorError("Migration is not supported by the LXC hypervisor")
473
474   def GetMigrationStatus(self, instance):
475     """Get the migration status
476
477     @type instance: L{objects.Instance}
478     @param instance: the instance that is being migrated
479     @rtype: L{objects.MigrationStatus}
480     @return: the status of the current migration (one of
481              L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
482              progress info that can be retrieved from the hypervisor
483
484     """
485     raise HypervisorError("Migration is not supported by the LXC hypervisor")