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