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