From 48297fa279792c8e456bb432bf2c8d108bcb7d1f Mon Sep 17 00:00:00 2001 From: Iustin Pop <iustin@google.com> Date: Sat, 9 May 2009 21:20:35 +0200 Subject: [PATCH] New hypervisor implementation: chroot manager This patch adds a new hypervisor implementation: a chroot manager. This hypervisor type can be used to manage (in combination with special OS definitions) the start and stop of chroot areas, and if used with drbd instances, it allows (via failover) the migration of chroots between nodes. This is a work in progress, and the way chroots should work is not very clear and does not fit very well in the OS definition framework. However, the hypervisor works and (if the sshd in the chroot is well configured) it allows login to the instance both via ssh and console as for a normal instance. TODOs: - implement instance IP add/remove to/from the bridge, if the instance has a defined IP - investigate improvements to the OS API so that the create script has more information available, e.g. about the hypervisor type - mount extra disks in the chroot or alternatively refuse to start with more than one disk Signed-off-by: Iustin Pop <iustin@google.com> Reviewed-by: Guido Trotter <ultrotter@google.com> --- Makefile.am | 1 + lib/constants.py | 8 +- lib/hypervisor/__init__.py | 12 +- lib/hypervisor/hv_chroot.py | 258 ++++++++++++++++++++++++++++++++++++ 4 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 lib/hypervisor/hv_chroot.py diff --git a/Makefile.am b/Makefile.am index 42b41b447..eefa1daf2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -91,6 +91,7 @@ pkgpython_PYTHON = \ hypervisor_PYTHON = \ lib/hypervisor/__init__.py \ lib/hypervisor/hv_base.py \ + lib/hypervisor/hv_chroot.py \ lib/hypervisor/hv_fake.py \ lib/hypervisor/hv_kvm.py \ lib/hypervisor/hv_xen.py diff --git a/lib/constants.py b/lib/constants.py index 058f2dd62..c6f67a14c 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -305,6 +305,7 @@ HV_INITRD_PATH = "initrd_path" HV_ROOT_PATH = "root_path" HV_SERIAL_CONSOLE = "serial_console" HV_USB_MOUSE = "usb_mouse" +HV_INIT_SCRIPT = "init_script" HVS_PARAMETER_TYPES = { HV_BOOT_ORDER: VTYPE_STRING, @@ -323,6 +324,7 @@ HVS_PARAMETER_TYPES = { HV_ROOT_PATH: VTYPE_STRING, HV_SERIAL_CONSOLE: VTYPE_BOOL, HV_USB_MOUSE: VTYPE_STRING, + HV_INIT_SCRIPT: VTYPE_STRING, } HVS_PARAMETERS = frozenset(HVS_PARAMETER_TYPES.keys()) @@ -348,7 +350,8 @@ HT_XEN_PVM = "xen-pvm" HT_FAKE = "fake" HT_XEN_HVM = "xen-hvm" HT_KVM = "kvm" -HYPER_TYPES = frozenset([HT_XEN_PVM, HT_FAKE, HT_XEN_HVM, HT_KVM]) +HT_CHROOT = "chroot" +HYPER_TYPES = frozenset([HT_XEN_PVM, HT_FAKE, HT_XEN_HVM, HT_KVM, HT_CHROOT]) HTS_REQ_PORT = frozenset([HT_XEN_HVM, HT_KVM]) HTS_COPY_VNC_PASSWORD = frozenset([HT_XEN_HVM]) @@ -508,6 +511,9 @@ HVC_DEFAULTS = { }, HT_FAKE: { }, + HT_CHROOT: { + HV_INIT_SCRIPT: "/ganeti-chroot", + }, } BEC_DEFAULTS = { diff --git a/lib/hypervisor/__init__.py b/lib/hypervisor/__init__.py index 83604022d..c2da1144d 100644 --- a/lib/hypervisor/__init__.py +++ b/lib/hypervisor/__init__.py @@ -29,14 +29,16 @@ from ganeti import errors from ganeti.hypervisor import hv_fake from ganeti.hypervisor import hv_xen from ganeti.hypervisor import hv_kvm +from ganeti.hypervisor import hv_chroot _HYPERVISOR_MAP = { - constants.HT_XEN_PVM: hv_xen.XenPvmHypervisor, - constants.HT_XEN_HVM: hv_xen.XenHvmHypervisor, - constants.HT_FAKE: hv_fake.FakeHypervisor, - constants.HT_KVM: hv_kvm.KVMHypervisor, - } + constants.HT_XEN_PVM: hv_xen.XenPvmHypervisor, + constants.HT_XEN_HVM: hv_xen.XenHvmHypervisor, + constants.HT_FAKE: hv_fake.FakeHypervisor, + constants.HT_KVM: hv_kvm.KVMHypervisor, + constants.HT_CHROOT: hv_chroot.ChrootManager, + } def GetHypervisor(ht_kind): diff --git a/lib/hypervisor/hv_chroot.py b/lib/hypervisor/hv_chroot.py new file mode 100644 index 000000000..c27c3f809 --- /dev/null +++ b/lib/hypervisor/hv_chroot.py @@ -0,0 +1,258 @@ +# +# + +# Copyright (C) 2006, 2007, 2008, 2009 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Chroot manager hypervisor + +""" + +import os +import os.path +import time +import logging +from cStringIO import StringIO + +from ganeti import constants +from ganeti import errors +from ganeti import utils +from ganeti.hypervisor import hv_base +from ganeti.errors import HypervisorError + + +class ChrootManager(hv_base.BaseHypervisor): + """Chroot manager. + + This not-really hypervisor allows ganeti to manage chroots. It has + special behaviour and requirements on the OS definition and the node + environemnt: + - the start and stop of the chroot environment are done via a + script called ganeti-chroot located in the root directory of the + first drive, which should be created by the OS definition + - this script must accept the start and stop argument and, on + shutdown, it should cleanly shutdown the daemons/processes + using the chroot + - the daemons run in chroot should only bind to the instance IP + (to which the OS create script has access via the instance name) + - since some daemons in the node could be listening on the wildcard + address, some ports might be unavailable + - the instance listing will show no memory usage + - on shutdown, the chroot manager will try to find all mountpoints + under the root dir of the instance and unmount them + - instance alive check is based on whether any process is using the chroot + + """ + _ROOT_DIR = constants.RUN_GANETI_DIR + "/chroot-hypervisor" + + PARAMETERS = [ + constants.HV_INIT_SCRIPT, + ] + + def __init__(self): + hv_base.BaseHypervisor.__init__(self) + if not os.path.exists(self._ROOT_DIR): + os.mkdir(self._ROOT_DIR) + if not os.path.isdir(self._ROOT_DIR): + raise HypervisorError("Needed path %s is not a directory" % + self._ROOT_DIR) + + @staticmethod + def _IsDirLive(path): + """Check if a directory looks like a live chroot. + + """ + if not os.path.ismount(path): + return False + result = utils.RunCmd(["fuser", "-m", path]) + return not result.failed + + @staticmethod + def _GetMountSubdirs(path): + """Return the list of mountpoints under a given path. + + This function is Linux-specific. + + """ + #TODO(iustin): investigate and document non-linux options + #(e.g. via mount output) + data = [] + fh = open("/proc/mounts", "r") + try: + for line in fh: + fstype, mountpoint, rest = line.split(" ", 2) + if (mountpoint.startswith(path) and + mountpoint != path): + data.append(mountpoint) + finally: + fh.close() + data.sort(key=lambda x: x.count("/"), reverse=True) + return data + + def ListInstances(self): + """Get the list of running instances. + + """ + return [name for name in os.listdir(self._ROOT_DIR) + if self._IsDirLive(os.path.join(self._ROOT_DIR, name))] + + def GetInstanceInfo(self, instance_name): + """Get instance properties. + + Args: + instance_name: the instance name + + Returns: + (name, id, memory, vcpus, stat, times) + """ + dir_name = "%s/%s" % (self._ROOT_DIR, instance_name) + if not self._IsDirLive(dir_name): + raise HypervisorError("Instance %s is not running" % instance_name) + return (instance_name, 0, 0, 0, 0, 0) + + def GetAllInstancesInfo(self): + """Get properties of all instances. + + Returns: + [(name, id, memory, vcpus, stat, times),...] + """ + data = [] + for file_name in os.listdir(self._ROOT_DIR): + path = os.path.join(self._ROOT_DIR, file_name) + if self._IsDirLive(path): + data.append((file_name, 0, 0, 0, 0, 0)) + return data + + def StartInstance(self, instance, block_devices): + """Start an instance. + + For the chroot manager, we try to mount the block device and + execute '/ganeti-chroot start'. + + """ + root_dir = "%s/%s" % (self._ROOT_DIR, instance.name) + if not os.path.exists(root_dir): + try: + os.mkdir(root_dir) + except IOError, err: + raise HypervisorError("Failed to start instance %s: %s" % + (instance.name, err)) + if not os.path.isdir(root_dir): + raise HypervisorError("Needed path %s is not a directory" % root_dir) + + if not os.path.ismount(root_dir): + if not block_devices: + raise HypervisorError("The chroot manager needs at least one disk") + + sda_dev_path = block_devices[0][1] + result = utils.RunCmd(["mount", sda_dev_path, root_dir]) + if result.failed: + raise HypervisorError("Can't mount the chroot dir: %s" % result.output) + init_script = instance.hvparams[constants.HV_INIT_SCRIPT] + result = utils.RunCmd(["chroot", root_dir, init_script, "start"]) + if result.failed: + raise HypervisorError("Can't run the chroot start script: %s" % + result.output) + + def StopInstance(self, instance, force=False): + """Stop an instance. + + This method has complicated cleanup tests, as we must: + - try to kill all leftover processes + - try to unmount any additional sub-mountpoints + - finally unmount the instance dir + + """ + root_dir = "%s/%s" % (self._ROOT_DIR, instance.name) + if not os.path.exists(root_dir): + return + + if self._IsDirLive(root_dir): + result = utils.RunCmd(["chroot", root_dir, "/ganeti-chroot", "stop"]) + if result.failed: + raise HypervisorError("Can't run the chroot stop script: %s" % + result.output) + retry = 20 + while not force and self._IsDirLive(root_dir) and retry > 0: + time.sleep(1) + retry -= 1 + if retry < 10: + result = utils.RunCmd(["fuser", "-k", "-TERM", "-m", root_dir]) + retry = 5 + while force and self._IsDirLive(root_dir) and retry > 0: + time.sleep(1) + retry -= 1 + utils.RunCmd(["fuser", "-k", "-KILL", "-m", root_dir]) + if self._IsDirLive(root_dir): + raise HypervisorError("Can't stop the processes using the chroot") + for mpath in self._GetMountSubdirs(root_dir): + utils.RunCmd(["umount", mpath]) + retry = 10 + while retry > 0: + result = utils.RunCmd(["umount", root_dir]) + if not result.failed: + break + retry -= 1 + time.sleep(1) + if result.failed: + logging.error("Processes still alive in the chroot: %s", + utils.RunCmd("fuser -vm %s" % root_dir).output) + raise HypervisorError("Can't umount the chroot dir: %s" % result.output) + + def RebootInstance(self, instance): + """Reboot an instance. + + This is not (yet) implemented for the chroot manager. + + """ + raise HypervisorError("The chroot manager doesn't implement the" + " reboot functionality") + + def GetNodeInfo(self): + """Return information about the node. + + This is just a wrapper over the base GetLinuxNodeInfo method. + + @return: a dict with the following keys (values in MiB): + - memory_total: the total memory size on the node + - memory_free: the available memory on the node for instances + - memory_dom0: the memory used by the node itself, if available + + """ + return self.GetLinuxNodeInfo() + + @classmethod + def GetShellCommandForConsole(cls, instance, hvparams, beparams): + """Return a command for connecting to the console of an instance. + + """ + root_dir = "%s/%s" % (cls._ROOT_DIR, instance.name) + if not os.path.ismount(root_dir): + raise HypervisorError("Instance %s is not running" % instance.name) + + return "chroot %s" % root_dir + + def Verify(self): + """Verify the hypervisor. + + For the chroot manager, it just checks the existence of the base + dir. + + """ + if not os.path.exists(self._ROOT_DIR): + return "The required directory '%s' does not exist." % self._ROOT_DIR -- GitLab