LXC: Fix wording of error messages
[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   _DEVS = [
69     "c 1:3",   # /dev/null
70     "c 1:5",   # /dev/zero
71     "c 1:7",   # /dev/full
72     "c 1:8",   # /dev/random
73     "c 1:9",   # /dev/urandom
74     "c 1:10",  # /dev/aio
75     "c 5:0",   # /dev/tty
76     "c 5:1",   # /dev/console
77     "c 5:2",   # /dev/ptmx
78     "c 136:*", # first block of Unix98 PTY slaves
79     ]
80   _DENIED_CAPABILITIES = [
81     "mac_override",    # Allow MAC configuration or state changes
82     # TODO: remove sys_admin too, for safety
83     #"sys_admin",       # Perform  a range of system administration operations
84     "sys_boot",        # Use reboot(2) and kexec_load(2)
85     "sys_module",      # Load  and  unload kernel modules
86     "sys_time",        # Set  system  clock, set real-time (hardware) clock
87     ]
88   _DIR_MODE = 0755
89
90   PARAMETERS = {
91     }
92
93   def __init__(self):
94     hv_base.BaseHypervisor.__init__(self)
95     utils.EnsureDirs([(self._ROOT_DIR, self._DIR_MODE)])
96
97   @staticmethod
98   def _GetMountSubdirs(path):
99     """Return the list of mountpoints under a given path.
100
101     """
102     result = []
103     for _, mountpoint, _, _ in utils.GetMounts():
104       if (mountpoint.startswith(path) and
105           mountpoint != path):
106         result.append(mountpoint)
107
108     result.sort(key=lambda x: x.count("/"), reverse=True)
109     return result
110
111   @classmethod
112   def _InstanceDir(cls, instance_name):
113     """Return the root directory for an instance.
114
115     """
116     return utils.PathJoin(cls._ROOT_DIR, instance_name)
117
118   @classmethod
119   def _InstanceConfFile(cls, instance_name):
120     """Return the configuration file for an instance.
121
122     """
123     return utils.PathJoin(cls._ROOT_DIR, instance_name + ".conf")
124
125   @classmethod
126   def _InstanceLogFile(cls, instance_name):
127     """Return the log file for an instance.
128
129     """
130     return utils.PathJoin(cls._ROOT_DIR, instance_name + ".log")
131
132   @classmethod
133   def _GetCgroupMountPoint(cls):
134     for _, mountpoint, fstype, _ in utils.GetMounts():
135       if fstype == "cgroup":
136         return mountpoint
137     raise errors.HypervisorError("The cgroup filesystem is not mounted")
138
139   @classmethod
140   def _GetCgroupCpuList(cls, instance_name):
141     """Return the list of CPU ids for an instance.
142
143     """
144     cgroup = cls._GetCgroupMountPoint()
145     try:
146       cpus = utils.ReadFile(utils.PathJoin(cgroup,
147                                            instance_name,
148                                            "cpuset.cpus"))
149     except EnvironmentError, err:
150       raise errors.HypervisorError("Getting CPU list for instance"
151                                    " %s failed: %s" % (instance_name, err))
152     # cpuset.cpus format: comma-separated list of CPU ids
153     # or dash-separated id ranges
154     # Example: "0-1,3"
155     cpu_list = []
156     for range_def in cpus.split(","):
157       boundaries = range_def.split("-")
158       n_elements = len(boundaries)
159       lower = int(boundaries[0])
160       higher = int(boundaries[n_elements - 1])
161       cpu_list.extend(range(lower, higher + 1))
162     return cpu_list
163
164   def ListInstances(self):
165     """Get the list of running instances.
166
167     """
168     result = utils.RunCmd(["lxc-ls"])
169     if result.failed:
170       raise errors.HypervisorError("Running lxc-ls failed: %s" % result.output)
171     return result.stdout.splitlines()
172
173   def GetInstanceInfo(self, instance_name):
174     """Get instance properties.
175
176     @type instance_name: string
177     @param instance_name: the instance name
178
179     @return: (name, id, memory, vcpus, stat, times)
180
181     """
182     # TODO: read container info from the cgroup mountpoint
183
184     result = utils.RunCmd(["lxc-info", "-n", instance_name])
185     if result.failed:
186       raise errors.HypervisorError("Running lxc-info failed: %s" %
187                                    result.output)
188     # lxc-info output examples:
189     # 'ganeti-lxc-test1' is STOPPED
190     # 'ganeti-lxc-test1' is RUNNING
191     _, state = result.stdout.rsplit(None, 1)
192
193     cpu_list = self._GetCgroupCpuList(instance_name)
194
195     if state == "RUNNING":
196       return (instance_name, 0, 0, len(cpu_list), 0, 0)
197     return None
198
199   def GetAllInstancesInfo(self):
200     """Get properties of all instances.
201
202     @return: [(name, id, memory, vcpus, stat, times),...]
203
204     """
205     data = []
206     for name in self.ListInstances():
207       data.append(self.GetInstanceInfo(name))
208     return data
209
210   def _CreateConfigFile(self, instance, root_dir):
211     """Create an lxc.conf file for an instance"""
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     # Device control
236     # deny direct device access
237     out.append("lxc.cgroup.devices.deny = a")
238     for devinfo in self._DEVS:
239       out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
240
241     # Networking
242     for idx, nic in enumerate(instance.nics):
243       out.append("# NIC %d" % idx)
244       mode = nic.nicparams[constants.NIC_MODE]
245       link = nic.nicparams[constants.NIC_LINK]
246       if mode == constants.NIC_MODE_BRIDGED:
247         out.append("lxc.network.type = veth")
248         out.append("lxc.network.link = %s" % link)
249       else:
250         raise errors.HypervisorError("LXC hypervisor only supports"
251                                      " bridged mode (NIC %d has mode %s)" %
252                                      (idx, mode))
253       out.append("lxc.network.hwaddr = %s" % nic.mac)
254       out.append("lxc.network.flags = up")
255
256     # Capabilities
257     for cap in self._DENIED_CAPABILITIES:
258       out.append("lxc.cap.drop = %s" % cap)
259
260     return "\n".join(out) + "\n"
261
262   def StartInstance(self, instance, block_devices):
263     """Start an instance.
264
265     For LCX, we try to mount the block device and execute 'lxc-start
266     start' (we use volatile containers).
267
268     """
269     root_dir = self._InstanceDir(instance.name)
270     try:
271       utils.EnsureDirs([(root_dir, self._DIR_MODE)])
272     except errors.GenericError, err:
273       raise HypervisorError("Creating instance directory failed: %s", str(err))
274
275     conf_file = self._InstanceConfFile(instance.name)
276     utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
277
278     log_file = self._InstanceLogFile(instance.name)
279     if not os.path.exists(log_file):
280       try:
281         utils.WriteFile(log_file, data="", mode=constants.SECURE_FILE_MODE)
282       except EnvironmentError, err:
283         raise errors.HypervisorError("Creating hypervisor log file %s for"
284                                      " instance %s failed: %s" %
285                                      (log_file, instance.name, err))
286
287     if not os.path.ismount(root_dir):
288       if not block_devices:
289         raise HypervisorError("LXC needs at least one disk")
290
291       sda_dev_path = block_devices[0][1]
292       result = utils.RunCmd(["mount", sda_dev_path, root_dir])
293       if result.failed:
294         raise HypervisorError("Mounting the root dir of LXC instance %s"
295                               " failed: %s" % (instance.name, result.output))
296     result = utils.RunCmd(["lxc-start", "-n", instance.name,
297                            "-o", log_file,
298                            "-l", "DEBUG",
299                            "-f", conf_file, "-d"])
300     if result.failed:
301       raise HypervisorError("Running the lxc-start script failed: %s" %
302                             result.output)
303
304   def StopInstance(self, instance, force=False, retry=False, name=None):
305     """Stop an instance.
306
307     This method has complicated cleanup tests, as we must:
308       - try to kill all leftover processes
309       - try to unmount any additional sub-mountpoints
310       - finally unmount the instance dir
311
312     """
313     if name is None:
314       name = instance.name
315
316     root_dir = self._InstanceDir(name)
317     if not os.path.exists(root_dir):
318       return
319
320     if name in self.ListInstances():
321       # Signal init to shutdown; this is a hack
322       if not retry and not force:
323         result = utils.RunCmd(["chroot", root_dir, "poweroff"])
324         if result.failed:
325           raise HypervisorError("Running 'poweroff' on the instance"
326                                 " failed: %s" % result.output)
327       time.sleep(2)
328       result = utils.RunCmd(["lxc-stop", "-n", name])
329       if result.failed:
330         logging.warning("Error while doing lxc-stop for %s: %s", name,
331                         result.output)
332
333     for mpath in self._GetMountSubdirs(root_dir):
334       result = utils.RunCmd(["umount", mpath])
335       if result.failed:
336         logging.warning("Error while umounting subpath %s for instance %s: %s",
337                         mpath, name, result.output)
338
339     result = utils.RunCmd(["umount", root_dir])
340     if result.failed and force:
341       msg = ("Processes still alive in the chroot: %s" %
342              utils.RunCmd("fuser -vm %s" % root_dir).output)
343       logging.error(msg)
344       raise HypervisorError("Unmounting the chroot dir failed: %s (%s)" %
345                             (result.output, msg))
346
347   def RebootInstance(self, instance):
348     """Reboot an instance.
349
350     This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
351
352     """
353     # TODO: implement reboot
354     raise HypervisorError("The LXC hypervisor doesn't implement the"
355                           " reboot functionality")
356
357   def GetNodeInfo(self):
358     """Return information about the node.
359
360     This is just a wrapper over the base GetLinuxNodeInfo method.
361
362     @return: a dict with the following keys (values in MiB):
363           - memory_total: the total memory size on the node
364           - memory_free: the available memory on the node for instances
365           - memory_dom0: the memory used by the node itself, if available
366
367     """
368     return self.GetLinuxNodeInfo()
369
370   @classmethod
371   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
372     """Return a command for connecting to the console of an instance.
373
374     """
375     return "lxc-console -n %s" % instance.name
376
377   def Verify(self):
378     """Verify the hypervisor.
379
380     For the chroot manager, it just checks the existence of the base dir.
381
382     """
383     if not os.path.exists(self._ROOT_DIR):
384       return "The required directory '%s' does not exist." % self._ROOT_DIR
385
386   @classmethod
387   def PowercycleNode(cls):
388     """LXC powercycle, just a wrapper over Linux powercycle.
389
390     """
391     cls.LinuxPowercycle()
392
393   def MigrateInstance(self, instance, target, live):
394     """Migrate an instance.
395
396     @type instance: L{objects.Instance}
397     @param instance: the instance to be migrated
398     @type target: string
399     @param target: hostname (usually ip) of the target node
400     @type live: boolean
401     @param live: whether to do a live or non-live migration
402
403     """
404     raise HypervisorError("Migration is not supported by the LXC hypervisor")