LXC: Report actual number of CPUs
[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     # TODO: read container info from the cgroup mountpoint
199     data = []
200     for name in self.ListInstances():
201       data.append((name, 0, 0, 0, 0, 0))
202     return data
203
204   def _CreateConfigFile(self, instance, root_dir):
205     """Create an lxc.conf file for an instance"""
206     out = []
207     # hostname
208     out.append("lxc.utsname = %s" % instance.name)
209
210     # separate pseudo-TTY instances
211     out.append("lxc.pts = 255")
212     # standard TTYs
213     out.append("lxc.tty = 6")
214     # console log file
215     console_log = utils.PathJoin(self._ROOT_DIR, instance.name + ".console")
216     try:
217       utils.WriteFile(console_log, data="", mode=constants.SECURE_FILE_MODE)
218     except EnvironmentError, err:
219       raise errors.HypervisorError("Creating console log file %s for"
220                                    " instance %s failed: %s" %
221                                    (console_log, instance.name, err))
222     out.append("lxc.console = %s" % console_log)
223
224     # root FS
225     out.append("lxc.rootfs = %s" % root_dir)
226
227     # TODO: additional mounts, if we disable CAP_SYS_ADMIN
228
229     # Device control
230     # deny direct device access
231     out.append("lxc.cgroup.devices.deny = a")
232     for devinfo in self._DEVS:
233       out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
234
235     # Networking
236     for idx, nic in enumerate(instance.nics):
237       out.append("# NIC %d" % idx)
238       mode = nic.nicparams[constants.NIC_MODE]
239       link = nic.nicparams[constants.NIC_LINK]
240       if mode == constants.NIC_MODE_BRIDGED:
241         out.append("lxc.network.type = veth")
242         out.append("lxc.network.link = %s" % link)
243       else:
244         raise errors.HypervisorError("LXC hypervisor only supports"
245                                      " bridged mode (NIC %d has mode %s)" %
246                                      (idx, mode))
247       out.append("lxc.network.hwaddr = %s" % nic.mac)
248       out.append("lxc.network.flags = up")
249
250     # Capabilities
251     for cap in self._DENIED_CAPABILITIES:
252       out.append("lxc.cap.drop = %s" % cap)
253
254     return "\n".join(out) + "\n"
255
256   def StartInstance(self, instance, block_devices):
257     """Start an instance.
258
259     For LCX, we try to mount the block device and execute 'lxc-start
260     start' (we use volatile containers).
261
262     """
263     root_dir = self._InstanceDir(instance.name)
264     try:
265       utils.EnsureDirs([(root_dir, self._DIR_MODE)])
266     except errors.GenericError, err:
267       raise HypervisorError("Cannot create instance directory: %s", str(err))
268
269     conf_file = self._InstanceConfFile(instance.name)
270     utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
271
272     if not os.path.ismount(root_dir):
273       if not block_devices:
274         raise HypervisorError("LXC needs at least one disk")
275
276       sda_dev_path = block_devices[0][1]
277       result = utils.RunCmd(["mount", sda_dev_path, root_dir])
278       if result.failed:
279         raise HypervisorError("Can't mount the chroot dir: %s" % result.output)
280     # TODO: replace the global log file with a per-instance log file
281     result = utils.RunCmd(["lxc-start", "-n", instance.name,
282                            "-o", self._LOG_FILE, "-l", "DEBUG",
283                            "-f", conf_file, "-d"])
284     if result.failed:
285       raise HypervisorError("Running the lxc-start script failed: %s" %
286                             result.output)
287
288   def StopInstance(self, instance, force=False, retry=False, name=None):
289     """Stop an instance.
290
291     This method has complicated cleanup tests, as we must:
292       - try to kill all leftover processes
293       - try to unmount any additional sub-mountpoints
294       - finally unmount the instance dir
295
296     """
297     if name is None:
298       name = instance.name
299
300     root_dir = self._InstanceDir(name)
301     if not os.path.exists(root_dir):
302       return
303
304     if name in self.ListInstances():
305       # Signal init to shutdown; this is a hack
306       if not retry and not force:
307         result = utils.RunCmd(["chroot", root_dir, "poweroff"])
308         if result.failed:
309           raise HypervisorError("Can't run 'poweroff' for the instance: %s" %
310                                 result.output)
311       time.sleep(2)
312       result = utils.RunCmd(["lxc-stop", "-n", name])
313       if result.failed:
314         logging.warning("Error while doing lxc-stop for %s: %s", name,
315                         result.output)
316
317     for mpath in self._GetMountSubdirs(root_dir):
318       result = utils.RunCmd(["umount", mpath])
319       if result.failed:
320         logging.warning("Error while umounting subpath %s for instance %s: %s",
321                         mpath, name, result.output)
322
323     result = utils.RunCmd(["umount", root_dir])
324     if result.failed and force:
325       msg = ("Processes still alive in the chroot: %s" %
326              utils.RunCmd("fuser -vm %s" % root_dir).output)
327       logging.error(msg)
328       raise HypervisorError("Can't umount the chroot dir: %s (%s)" %
329                             (result.output, msg))
330
331   def RebootInstance(self, instance):
332     """Reboot an instance.
333
334     This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
335
336     """
337     # TODO: implement reboot
338     raise HypervisorError("The LXC hypervisor doesn't implement the"
339                           " reboot functionality")
340
341   def GetNodeInfo(self):
342     """Return information about the node.
343
344     This is just a wrapper over the base GetLinuxNodeInfo method.
345
346     @return: a dict with the following keys (values in MiB):
347           - memory_total: the total memory size on the node
348           - memory_free: the available memory on the node for instances
349           - memory_dom0: the memory used by the node itself, if available
350
351     """
352     return self.GetLinuxNodeInfo()
353
354   @classmethod
355   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
356     """Return a command for connecting to the console of an instance.
357
358     """
359     return "lxc-console -n %s" % instance.name
360
361   def Verify(self):
362     """Verify the hypervisor.
363
364     For the chroot manager, it just checks the existence of the base dir.
365
366     """
367     if not os.path.exists(self._ROOT_DIR):
368       return "The required directory '%s' does not exist." % self._ROOT_DIR
369
370   @classmethod
371   def PowercycleNode(cls):
372     """LXC powercycle, just a wrapper over Linux powercycle.
373
374     """
375     cls.LinuxPowercycle()
376
377   def MigrateInstance(self, instance, target, live):
378     """Migrate an instance.
379
380     @type instance: L{objects.Instance}
381     @param instance: the instance to be migrated
382     @type target: string
383     @param target: hostname (usually ip) of the target node
384     @type live: boolean
385     @param live: whether to do a live or non-live migration
386
387     """
388     raise HypervisorError("Migration not supported by the LXC hypervisor")