LXC: Create per-instance log files
[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("Can't run lxc-ls: %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("Can't run lxc-info: %s" % result.output)
187     # lxc-info output examples:
188     # 'ganeti-lxc-test1' is STOPPED
189     # 'ganeti-lxc-test1' is RUNNING
190     _, state = result.stdout.rsplit(None, 1)
191
192     cpu_list = self._GetCgroupCpuList(instance_name)
193
194     if state == "RUNNING":
195       return (instance_name, 0, 0, len(cpu_list), 0, 0)
196     return None
197
198   def GetAllInstancesInfo(self):
199     """Get properties of all instances.
200
201     @return: [(name, id, memory, vcpus, stat, times),...]
202
203     """
204     data = []
205     for name in self.ListInstances():
206       data.append(self.GetInstanceInfo(name))
207     return data
208
209   def _CreateConfigFile(self, instance, root_dir):
210     """Create an lxc.conf file for an instance"""
211     out = []
212     # hostname
213     out.append("lxc.utsname = %s" % instance.name)
214
215     # separate pseudo-TTY instances
216     out.append("lxc.pts = 255")
217     # standard TTYs
218     out.append("lxc.tty = 6")
219     # console log file
220     console_log = utils.PathJoin(self._ROOT_DIR, instance.name + ".console")
221     try:
222       utils.WriteFile(console_log, data="", mode=constants.SECURE_FILE_MODE)
223     except EnvironmentError, err:
224       raise errors.HypervisorError("Creating console log file %s for"
225                                    " instance %s failed: %s" %
226                                    (console_log, instance.name, err))
227     out.append("lxc.console = %s" % console_log)
228
229     # root FS
230     out.append("lxc.rootfs = %s" % root_dir)
231
232     # TODO: additional mounts, if we disable CAP_SYS_ADMIN
233
234     # Device control
235     # deny direct device access
236     out.append("lxc.cgroup.devices.deny = a")
237     for devinfo in self._DEVS:
238       out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
239
240     # Networking
241     for idx, nic in enumerate(instance.nics):
242       out.append("# NIC %d" % idx)
243       mode = nic.nicparams[constants.NIC_MODE]
244       link = nic.nicparams[constants.NIC_LINK]
245       if mode == constants.NIC_MODE_BRIDGED:
246         out.append("lxc.network.type = veth")
247         out.append("lxc.network.link = %s" % link)
248       else:
249         raise errors.HypervisorError("LXC hypervisor only supports"
250                                      " bridged mode (NIC %d has mode %s)" %
251                                      (idx, mode))
252       out.append("lxc.network.hwaddr = %s" % nic.mac)
253       out.append("lxc.network.flags = up")
254
255     # Capabilities
256     for cap in self._DENIED_CAPABILITIES:
257       out.append("lxc.cap.drop = %s" % cap)
258
259     return "\n".join(out) + "\n"
260
261   def StartInstance(self, instance, block_devices):
262     """Start an instance.
263
264     For LCX, we try to mount the block device and execute 'lxc-start
265     start' (we use volatile containers).
266
267     """
268     root_dir = self._InstanceDir(instance.name)
269     try:
270       utils.EnsureDirs([(root_dir, self._DIR_MODE)])
271     except errors.GenericError, err:
272       raise HypervisorError("Cannot create instance directory: %s", str(err))
273
274     conf_file = self._InstanceConfFile(instance.name)
275     utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
276
277     log_file = self._InstanceLogFile(instance.name)
278     if not os.path.exists(log_file):
279       try:
280         utils.WriteFile(log_file, data="", mode=constants.SECURE_FILE_MODE)
281       except EnvironmentError, err:
282         raise errors.HypervisorError("Creating hypervisor log file %s for"
283                                      " instance %s failed: %s" %
284                                      (log_file, instance.name, err))
285
286     if not os.path.ismount(root_dir):
287       if not block_devices:
288         raise HypervisorError("LXC needs at least one disk")
289
290       sda_dev_path = block_devices[0][1]
291       result = utils.RunCmd(["mount", sda_dev_path, root_dir])
292       if result.failed:
293         raise HypervisorError("Can't mount the chroot dir: %s" % 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("Can't run 'poweroff' for the instance: %s" %
324                                 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     for mpath in self._GetMountSubdirs(root_dir):
332       result = utils.RunCmd(["umount", mpath])
333       if result.failed:
334         logging.warning("Error while umounting subpath %s for instance %s: %s",
335                         mpath, name, result.output)
336
337     result = utils.RunCmd(["umount", root_dir])
338     if result.failed and force:
339       msg = ("Processes still alive in the chroot: %s" %
340              utils.RunCmd("fuser -vm %s" % root_dir).output)
341       logging.error(msg)
342       raise HypervisorError("Can't umount the chroot dir: %s (%s)" %
343                             (result.output, msg))
344
345   def RebootInstance(self, instance):
346     """Reboot an instance.
347
348     This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
349
350     """
351     # TODO: implement reboot
352     raise HypervisorError("The LXC hypervisor doesn't implement the"
353                           " reboot functionality")
354
355   def GetNodeInfo(self):
356     """Return information about the node.
357
358     This is just a wrapper over the base GetLinuxNodeInfo method.
359
360     @return: a dict with the following keys (values in MiB):
361           - memory_total: the total memory size on the node
362           - memory_free: the available memory on the node for instances
363           - memory_dom0: the memory used by the node itself, if available
364
365     """
366     return self.GetLinuxNodeInfo()
367
368   @classmethod
369   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
370     """Return a command for connecting to the console of an instance.
371
372     """
373     return "lxc-console -n %s" % instance.name
374
375   def Verify(self):
376     """Verify the hypervisor.
377
378     For the chroot manager, it just checks the existence of the base dir.
379
380     """
381     if not os.path.exists(self._ROOT_DIR):
382       return "The required directory '%s' does not exist." % self._ROOT_DIR
383
384   @classmethod
385   def PowercycleNode(cls):
386     """LXC powercycle, just a wrapper over Linux powercycle.
387
388     """
389     cls.LinuxPowercycle()
390
391   def MigrateInstance(self, instance, target, live):
392     """Migrate an instance.
393
394     @type instance: L{objects.Instance}
395     @param instance: the instance to be migrated
396     @type target: string
397     @param target: hostname (usually ip) of the target node
398     @type live: boolean
399     @param live: whether to do a live or non-live migration
400
401     """
402     raise HypervisorError("Migration not supported by the LXC hypervisor")