LXC: Fix GetAllInstancesInfo()
[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-msg=W0611
33 from ganeti import utils
34 from ganeti.hypervisor import hv_base
35 from ganeti.errors import HypervisorError
36
37
38 class LXCHypervisor(hv_base.BaseHypervisor):
39   """LXC-based virtualization.
40
41   Since current (Spring 2010) distributions are not yet ready for
42   running under a container, the following changes must be done
43   manually:
44     - remove udev
45     - disable the kernel log component of sysklogd/rsyslog/etc.,
46       otherwise they will fail to read the log, and at least rsyslog
47       will fill the filesystem with error messages
48
49   TODO:
50     - move hardcoded parameters into hypervisor parameters, once we
51       have the container-parameter support
52     - implement memory limits, but only optionally, depending on host
53       kernel support
54
55   Problems/issues:
56     - LXC is very temperamental; in daemon mode, it succeeds or fails
57       in launching the instance silently, without any error
58       indication, and when failing it can leave network interfaces
59       around, and future successful startups will list the instance
60       twice
61     - shutdown sequence of containers leaves the init 'dead', and the
62       container effectively stopped, but LXC still believes the
63       container to be running; need to investigate using the
64       notify_on_release and release_agent feature of cgroups
65
66   """
67   _ROOT_DIR = constants.RUN_GANETI_DIR + "/lxc"
68   _LOG_FILE = constants.LOG_DIR + "hv_lxc.log"
69   _DEVS = [
70     "c 1:3",   # /dev/null
71     "c 1:5",   # /dev/zero
72     "c 1:7",   # /dev/full
73     "c 1:8",   # /dev/random
74     "c 1:9",   # /dev/urandom
75     "c 1:10",  # /dev/aio
76     "c 5:0",   # /dev/tty
77     "c 5:1",   # /dev/console
78     "c 5:2",   # /dev/ptmx
79     "c 136:*", # first block of Unix98 PTY slaves
80     ]
81   _DENIED_CAPABILITIES = [
82     "mac_override",    # Allow MAC configuration or state changes
83     # TODO: remove sys_admin too, for safety
84     #"sys_admin",       # Perform  a range of system administration operations
85     "sys_boot",        # Use reboot(2) and kexec_load(2)
86     "sys_module",      # Load  and  unload kernel modules
87     "sys_time",        # Set  system  clock, set real-time (hardware) clock
88     ]
89   _DIR_MODE = 0755
90
91   PARAMETERS = {
92     }
93
94   def __init__(self):
95     hv_base.BaseHypervisor.__init__(self)
96     utils.EnsureDirs([(self._ROOT_DIR, self._DIR_MODE)])
97
98   @staticmethod
99   def _GetMountSubdirs(path):
100     """Return the list of mountpoints under a given path.
101
102     """
103     result = []
104     for _, mountpoint, _, _ in utils.GetMounts():
105       if (mountpoint.startswith(path) and
106           mountpoint != path):
107         result.append(mountpoint)
108
109     result.sort(key=lambda x: x.count("/"), reverse=True)
110     return result
111
112   @classmethod
113   def _InstanceDir(cls, instance_name):
114     """Return the root directory for an instance.
115
116     """
117     return utils.PathJoin(cls._ROOT_DIR, instance_name)
118
119   @classmethod
120   def _InstanceConfFile(cls, instance_name):
121     """Return the configuration file for an instance.
122
123     """
124     return utils.PathJoin(cls._ROOT_DIR, instance_name + ".conf")
125
126   @classmethod
127   def _GetCgroupMountPoint(cls):
128     for _, mountpoint, fstype, _ in utils.GetMounts():
129       if fstype == "cgroup":
130         return mountpoint
131     raise errors.HypervisorError("The cgroup filesystem is not mounted")
132
133   @classmethod
134   def _GetCgroupCpuList(cls, instance_name):
135     """Return the list of CPU ids for an instance.
136
137     """
138     cgroup = cls._GetCgroupMountPoint()
139     try:
140       cpus = utils.ReadFile(utils.PathJoin(cgroup,
141                                            instance_name,
142                                            "cpuset.cpus"))
143     except EnvironmentError, err:
144       raise errors.HypervisorError("Getting CPU list for instance"
145                                    " %s failed: %s" % (instance_name, err))
146     # cpuset.cpus format: comma-separated list of CPU ids
147     # or dash-separated id ranges
148     # Example: "0-1,3"
149     cpu_list = []
150     for range_def in cpus.split(","):
151       boundaries = range_def.split("-")
152       n_elements = len(boundaries)
153       lower = int(boundaries[0])
154       higher = int(boundaries[n_elements - 1])
155       cpu_list.extend(range(lower, higher + 1))
156     return cpu_list
157
158   def ListInstances(self):
159     """Get the list of running instances.
160
161     """
162     result = utils.RunCmd(["lxc-ls"])
163     if result.failed:
164       raise errors.HypervisorError("Can't run lxc-ls: %s" % result.output)
165     return result.stdout.splitlines()
166
167   def GetInstanceInfo(self, instance_name):
168     """Get instance properties.
169
170     @type instance_name: string
171     @param instance_name: the instance name
172
173     @return: (name, id, memory, vcpus, stat, times)
174
175     """
176     # TODO: read container info from the cgroup mountpoint
177
178     result = utils.RunCmd(["lxc-info", "-n", instance_name])
179     if result.failed:
180       raise errors.HypervisorError("Can't run lxc-info: %s" % result.output)
181     # lxc-info output examples:
182     # 'ganeti-lxc-test1' is STOPPED
183     # 'ganeti-lxc-test1' is RUNNING
184     _, state = result.stdout.rsplit(None, 1)
185
186     cpu_list = self._GetCgroupCpuList(instance_name)
187
188     if state == "RUNNING":
189       return (instance_name, 0, 0, len(cpu_list), 0, 0)
190     return None
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 self.ListInstances():
200       data.append(self.GetInstanceInfo(name))
201     return data
202
203   def _CreateConfigFile(self, instance, root_dir):
204     """Create an lxc.conf file for an instance"""
205     out = []
206     # hostname
207     out.append("lxc.utsname = %s" % instance.name)
208
209     # separate pseudo-TTY instances
210     out.append("lxc.pts = 255")
211     # standard TTYs
212     out.append("lxc.tty = 6")
213     # console log file
214     console_log = utils.PathJoin(self._ROOT_DIR, instance.name + ".console")
215     try:
216       utils.WriteFile(console_log, data="", mode=constants.SECURE_FILE_MODE)
217     except EnvironmentError, err:
218       raise errors.HypervisorError("Creating console log file %s for"
219                                    " instance %s failed: %s" %
220                                    (console_log, instance.name, err))
221     out.append("lxc.console = %s" % console_log)
222
223     # root FS
224     out.append("lxc.rootfs = %s" % root_dir)
225
226     # TODO: additional mounts, if we disable CAP_SYS_ADMIN
227
228     # Device control
229     # deny direct device access
230     out.append("lxc.cgroup.devices.deny = a")
231     for devinfo in self._DEVS:
232       out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
233
234     # Networking
235     for idx, nic in enumerate(instance.nics):
236       out.append("# NIC %d" % idx)
237       mode = nic.nicparams[constants.NIC_MODE]
238       link = nic.nicparams[constants.NIC_LINK]
239       if mode == constants.NIC_MODE_BRIDGED:
240         out.append("lxc.network.type = veth")
241         out.append("lxc.network.link = %s" % link)
242       else:
243         raise errors.HypervisorError("LXC hypervisor only supports"
244                                      " bridged mode (NIC %d has mode %s)" %
245                                      (idx, mode))
246       out.append("lxc.network.hwaddr = %s" % nic.mac)
247       out.append("lxc.network.flags = up")
248
249     # Capabilities
250     for cap in self._DENIED_CAPABILITIES:
251       out.append("lxc.cap.drop = %s" % cap)
252
253     return "\n".join(out) + "\n"
254
255   def StartInstance(self, instance, block_devices):
256     """Start an instance.
257
258     For LCX, we try to mount the block device and execute 'lxc-start
259     start' (we use volatile containers).
260
261     """
262     root_dir = self._InstanceDir(instance.name)
263     try:
264       utils.EnsureDirs([(root_dir, self._DIR_MODE)])
265     except errors.GenericError, err:
266       raise HypervisorError("Cannot create instance directory: %s", str(err))
267
268     conf_file = self._InstanceConfFile(instance.name)
269     utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
270
271     if not os.path.ismount(root_dir):
272       if not block_devices:
273         raise HypervisorError("LXC needs at least one disk")
274
275       sda_dev_path = block_devices[0][1]
276       result = utils.RunCmd(["mount", sda_dev_path, root_dir])
277       if result.failed:
278         raise HypervisorError("Can't mount the chroot dir: %s" % result.output)
279     # TODO: replace the global log file with a per-instance log file
280     result = utils.RunCmd(["lxc-start", "-n", instance.name,
281                            "-o", self._LOG_FILE, "-l", "DEBUG",
282                            "-f", conf_file, "-d"])
283     if result.failed:
284       raise HypervisorError("Running the lxc-start script failed: %s" %
285                             result.output)
286
287   def StopInstance(self, instance, force=False, retry=False, name=None):
288     """Stop an instance.
289
290     This method has complicated cleanup tests, as we must:
291       - try to kill all leftover processes
292       - try to unmount any additional sub-mountpoints
293       - finally unmount the instance dir
294
295     """
296     if name is None:
297       name = instance.name
298
299     root_dir = self._InstanceDir(name)
300     if not os.path.exists(root_dir):
301       return
302
303     if name in self.ListInstances():
304       # Signal init to shutdown; this is a hack
305       if not retry and not force:
306         result = utils.RunCmd(["chroot", root_dir, "poweroff"])
307         if result.failed:
308           raise HypervisorError("Can't run 'poweroff' for the instance: %s" %
309                                 result.output)
310       time.sleep(2)
311       result = utils.RunCmd(["lxc-stop", "-n", name])
312       if result.failed:
313         logging.warning("Error while doing lxc-stop for %s: %s", name,
314                         result.output)
315
316     for mpath in self._GetMountSubdirs(root_dir):
317       result = utils.RunCmd(["umount", mpath])
318       if result.failed:
319         logging.warning("Error while umounting subpath %s for instance %s: %s",
320                         mpath, name, result.output)
321
322     result = utils.RunCmd(["umount", root_dir])
323     if result.failed and force:
324       msg = ("Processes still alive in the chroot: %s" %
325              utils.RunCmd("fuser -vm %s" % root_dir).output)
326       logging.error(msg)
327       raise HypervisorError("Can't umount the chroot dir: %s (%s)" %
328                             (result.output, msg))
329
330   def RebootInstance(self, instance):
331     """Reboot an instance.
332
333     This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
334
335     """
336     # TODO: implement reboot
337     raise HypervisorError("The LXC hypervisor doesn't implement the"
338                           " reboot functionality")
339
340   def GetNodeInfo(self):
341     """Return information about the node.
342
343     This is just a wrapper over the base GetLinuxNodeInfo method.
344
345     @return: a dict with the following keys (values in MiB):
346           - memory_total: the total memory size on the node
347           - memory_free: the available memory on the node for instances
348           - memory_dom0: the memory used by the node itself, if available
349
350     """
351     return self.GetLinuxNodeInfo()
352
353   @classmethod
354   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
355     """Return a command for connecting to the console of an instance.
356
357     """
358     return "lxc-console -n %s" % instance.name
359
360   def Verify(self):
361     """Verify the hypervisor.
362
363     For the chroot manager, it just checks the existence of the base dir.
364
365     """
366     if not os.path.exists(self._ROOT_DIR):
367       return "The required directory '%s' does not exist." % self._ROOT_DIR
368
369   @classmethod
370   def PowercycleNode(cls):
371     """LXC powercycle, just a wrapper over Linux powercycle.
372
373     """
374     cls.LinuxPowercycle()
375
376   def MigrateInstance(self, instance, target, live):
377     """Migrate an instance.
378
379     @type instance: L{objects.Instance}
380     @param instance: the instance to be migrated
381     @type target: string
382     @param target: hostname (usually ip) of the target node
383     @type live: boolean
384     @param live: whether to do a live or non-live migration
385
386     """
387     raise HypervisorError("Migration not supported by the LXC hypervisor")