X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/6555373d341c2652d0ad119479d586a0026b049b..8351df2fae34414d07c2ee326c53d7510088addb:/lib/hypervisor/hv_xen.py?ds=sidebyside diff --git a/lib/hypervisor/hv_xen.py b/lib/hypervisor/hv_xen.py index d2638f9..63ab47c 100644 --- a/lib/hypervisor/hv_xen.py +++ b/lib/hypervisor/hv_xen.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc. +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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 @@ -24,6 +24,7 @@ """ import logging +import string # pylint: disable=W0402 from cStringIO import StringIO from ganeti import constants @@ -32,6 +33,272 @@ from ganeti import utils from ganeti.hypervisor import hv_base from ganeti import netutils from ganeti import objects +from ganeti import pathutils +from ganeti import ssconf + + +XEND_CONFIG_FILE = utils.PathJoin(pathutils.XEN_CONFIG_DIR, "xend-config.sxp") +XL_CONFIG_FILE = utils.PathJoin(pathutils.XEN_CONFIG_DIR, "xen/xl.conf") +VIF_BRIDGE_SCRIPT = utils.PathJoin(pathutils.XEN_CONFIG_DIR, + "scripts/vif-bridge") +_DOM0_NAME = "Domain-0" +_DISK_LETTERS = string.ascii_lowercase + +_FILE_DRIVER_MAP = { + constants.FD_LOOP: "file", + constants.FD_BLKTAP: "tap:aio", + } + + +def _CreateConfigCpus(cpu_mask): + """Create a CPU config string for Xen's config file. + + """ + # Convert the string CPU mask to a list of list of int's + cpu_list = utils.ParseMultiCpuMask(cpu_mask) + + if len(cpu_list) == 1: + all_cpu_mapping = cpu_list[0] + if all_cpu_mapping == constants.CPU_PINNING_OFF: + # If CPU pinning has 1 entry that's "all", then remove the + # parameter from the config file + return None + else: + # If CPU pinning has one non-all entry, mapping all vCPUS (the entire + # VM) to one physical CPU, using format 'cpu = "C"' + return "cpu = \"%s\"" % ",".join(map(str, all_cpu_mapping)) + else: + + def _GetCPUMap(vcpu): + if vcpu[0] == constants.CPU_PINNING_ALL_VAL: + cpu_map = constants.CPU_PINNING_ALL_XEN + else: + cpu_map = ",".join(map(str, vcpu)) + return "\"%s\"" % cpu_map + + # build the result string in format 'cpus = [ "c", "c", "c" ]', + # where each c is a physical CPU number, a range, a list, or any + # combination + return "cpus = [ %s ]" % ", ".join(map(_GetCPUMap, cpu_list)) + + +def _RunInstanceList(fn, instance_list_errors): + """Helper function for L{_GetInstanceList} to retrieve the list of instances + from xen. + + @type fn: callable + @param fn: Function to query xen for the list of instances + @type instance_list_errors: list + @param instance_list_errors: Error list + @rtype: list + + """ + result = fn() + if result.failed: + logging.error("Retrieving the instance list from xen failed (%s): %s", + result.fail_reason, result.output) + instance_list_errors.append(result) + raise utils.RetryAgain() + + # skip over the heading + return result.stdout.splitlines() + + +def _ParseXmList(lines, include_node): + """Parses the output of C{xm list}. + + @type lines: list + @param lines: Output lines of C{xm list} + @type include_node: boolean + @param include_node: If True, return information for Dom0 + @return: list of tuple containing (name, id, memory, vcpus, state, time + spent) + + """ + result = [] + + # Iterate through all lines while ignoring header + for line in lines[1:]: + # The format of lines is: + # Name ID Mem(MiB) VCPUs State Time(s) + # Domain-0 0 3418 4 r----- 266.2 + data = line.split() + if len(data) != 6: + raise errors.HypervisorError("Can't parse output of xm list," + " line: %s" % line) + try: + data[1] = int(data[1]) + data[2] = int(data[2]) + data[3] = int(data[3]) + data[5] = float(data[5]) + except (TypeError, ValueError), err: + raise errors.HypervisorError("Can't parse output of xm list," + " line: %s, error: %s" % (line, err)) + + # skip the Domain-0 (optional) + if include_node or data[0] != _DOM0_NAME: + result.append(data) + + return result + + +def _GetInstanceList(fn, include_node, _timeout=5): + """Return the list of running instances. + + See L{_RunInstanceList} and L{_ParseXmList} for parameter details. + + """ + instance_list_errors = [] + try: + lines = utils.Retry(_RunInstanceList, (0.3, 1.5, 1.0), _timeout, + args=(fn, instance_list_errors)) + except utils.RetryTimeout: + if instance_list_errors: + instance_list_result = instance_list_errors.pop() + + errmsg = ("listing instances failed, timeout exceeded (%s): %s" % + (instance_list_result.fail_reason, instance_list_result.output)) + else: + errmsg = "listing instances failed" + + raise errors.HypervisorError(errmsg) + + return _ParseXmList(lines, include_node) + + +def _ParseNodeInfo(info): + """Return information about the node. + + @return: a dict with the following keys (memory values in MiB): + - memory_total: the total memory size on the node + - memory_free: the available memory on the node for instances + - nr_cpus: total number of CPUs + - nr_nodes: in a NUMA system, the number of domains + - nr_sockets: the number of physical CPU sockets in the node + - hv_version: the hypervisor version in the form (major, minor) + + """ + result = {} + cores_per_socket = threads_per_core = nr_cpus = None + xen_major, xen_minor = None, None + memory_total = None + memory_free = None + + for line in info.splitlines(): + fields = line.split(":", 1) + + if len(fields) < 2: + continue + + (key, val) = map(lambda s: s.strip(), fields) + + # Note: in Xen 3, memory has changed to total_memory + if key in ("memory", "total_memory"): + memory_total = int(val) + elif key == "free_memory": + memory_free = int(val) + elif key == "nr_cpus": + nr_cpus = result["cpu_total"] = int(val) + elif key == "nr_nodes": + result["cpu_nodes"] = int(val) + elif key == "cores_per_socket": + cores_per_socket = int(val) + elif key == "threads_per_core": + threads_per_core = int(val) + elif key == "xen_major": + xen_major = int(val) + elif key == "xen_minor": + xen_minor = int(val) + + if None not in [cores_per_socket, threads_per_core, nr_cpus]: + result["cpu_sockets"] = nr_cpus / (cores_per_socket * threads_per_core) + + if memory_free is not None: + result["memory_free"] = memory_free + + if memory_total is not None: + result["memory_total"] = memory_total + + if not (xen_major is None or xen_minor is None): + result[constants.HV_NODEINFO_KEY_VERSION] = (xen_major, xen_minor) + + return result + + +def _MergeInstanceInfo(info, fn): + """Updates node information from L{_ParseNodeInfo} with instance info. + + @type info: dict + @param info: Result from L{_ParseNodeInfo} + @type fn: callable + @param fn: Function returning result of running C{xm list} + @rtype: dict + + """ + total_instmem = 0 + + for (name, _, mem, vcpus, _, _) in fn(True): + if name == _DOM0_NAME: + info["memory_dom0"] = mem + info["dom0_cpus"] = vcpus + + # Include Dom0 in total memory usage + total_instmem += mem + + memory_free = info.get("memory_free") + memory_total = info.get("memory_total") + + # Calculate memory used by hypervisor + if None not in [memory_total, memory_free, total_instmem]: + info["memory_hv"] = memory_total - memory_free - total_instmem + + return info + + +def _GetNodeInfo(info, fn): + """Combines L{_MergeInstanceInfo} and L{_ParseNodeInfo}. + + """ + return _MergeInstanceInfo(_ParseNodeInfo(info), fn) + + +def _GetConfigFileDiskData(block_devices, blockdev_prefix, + _letters=_DISK_LETTERS): + """Get disk directives for Xen config file. + + This method builds the xen config disk directive according to the + given disk_template and block_devices. + + @param block_devices: list of tuples (cfdev, rldev): + - cfdev: dict containing ganeti config disk part + - rldev: ganeti.block.bdev.BlockDev object + @param blockdev_prefix: a string containing blockdevice prefix, + e.g. "sd" for /dev/sda + + @return: string containing disk directive for xen instance config file + + """ + if len(block_devices) > len(_letters): + raise errors.HypervisorError("Too many disks") + + disk_data = [] + + for sd_suffix, (cfdev, dev_path) in zip(_letters, block_devices): + sd_name = blockdev_prefix + sd_suffix + + if cfdev.mode == constants.DISK_RDWR: + mode = "w" + else: + mode = "r" + + if cfdev.dev_type == constants.LD_FILE: + driver = _FILE_DRIVER_MAP[cfdev.physical_id[0]] + else: + driver = "phy" + + disk_data.append("'%s:%s,%s,%s'" % (driver, dev_path, sd_name, mode)) + + return disk_data class XenHypervisor(hv_base.BaseHypervisor): @@ -46,13 +313,65 @@ class XenHypervisor(hv_base.BaseHypervisor): REBOOT_RETRY_INTERVAL = 10 ANCILLARY_FILES = [ - "/etc/xen/xend-config.sxp", - "/etc/xen/xl.conf", - "/etc/xen/scripts/vif-bridge", + XEND_CONFIG_FILE, + XL_CONFIG_FILE, + VIF_BRIDGE_SCRIPT, ] + ANCILLARY_FILES_OPT = [ + XL_CONFIG_FILE, + ] + + def __init__(self, _cfgdir=None, _run_cmd_fn=None, _cmd=None): + hv_base.BaseHypervisor.__init__(self) + + if _cfgdir is None: + self._cfgdir = pathutils.XEN_CONFIG_DIR + else: + self._cfgdir = _cfgdir + + if _run_cmd_fn is None: + self._run_cmd_fn = utils.RunCmd + else: + self._run_cmd_fn = _run_cmd_fn + + self._cmd = _cmd + + def _GetCommand(self, hvparams=None): + """Returns Xen command to use. + + @type hvparams: dict of strings + @param hvparams: hypervisor parameters + + """ + if self._cmd is None: + if hvparams is not None: + cmd = hvparams[constants.HV_XEN_CMD] + else: + # TODO: Remove autoconf option once retrieving the command from + # the hvparams is fully implemented. + cmd = constants.XEN_CMD + else: + cmd = self._cmd - @staticmethod - def _ConfigFileName(instance_name): + if cmd not in constants.KNOWN_XEN_COMMANDS: + raise errors.ProgrammerError("Unknown Xen command '%s'" % cmd) + + return cmd + + def _RunXen(self, args, hvparams=None): + """Wrapper around L{utils.process.RunCmd} to run Xen command. + + @type hvparams: dict of strings + @param hvparams: dictionary of hypervisor params + @see: L{utils.process.RunCmd} + + """ + cmd = [self._GetCommand(hvparams=hvparams)] + cmd.extend(args) + + return self._run_cmd_fn(cmd) + + def _ConfigFileName(self, instance_name): """Get the config file name for an instance. @param instance_name: instance name @@ -61,112 +380,74 @@ class XenHypervisor(hv_base.BaseHypervisor): @rtype: str """ - return "/etc/xen/%s" % instance_name + return utils.PathJoin(self._cfgdir, instance_name) @classmethod - def _WriteConfigFile(cls, instance, block_devices): - """Write the Xen config file for the instance. + def _GetConfig(cls, instance, startup_memory, block_devices): + """Build Xen configuration for an instance. """ raise NotImplementedError - @staticmethod - def _WriteConfigFileStatic(instance_name, data): + def _WriteConfigFile(self, instance_name, data): """Write the Xen config file for the instance. This version of the function just writes the config file from static data. """ - utils.WriteFile(XenHypervisor._ConfigFileName(instance_name), data=data) + # just in case it exists + utils.RemoveFile(utils.PathJoin(self._cfgdir, "auto", instance_name)) + + cfg_file = self._ConfigFileName(instance_name) + try: + utils.WriteFile(cfg_file, data=data) + except EnvironmentError, err: + raise errors.HypervisorError("Cannot write Xen instance configuration" + " file %s: %s" % (cfg_file, err)) - @staticmethod - def _ReadConfigFile(instance_name): + def _ReadConfigFile(self, instance_name): """Returns the contents of the instance config file. """ + filename = self._ConfigFileName(instance_name) + try: - file_content = utils.ReadFile( - XenHypervisor._ConfigFileName(instance_name)) + file_content = utils.ReadFile(filename) except EnvironmentError, err: raise errors.HypervisorError("Failed to load Xen config file: %s" % err) + return file_content - @staticmethod - def _RemoveConfigFile(instance_name): + def _RemoveConfigFile(self, instance_name): """Remove the xen configuration file. """ - utils.RemoveFile(XenHypervisor._ConfigFileName(instance_name)) + utils.RemoveFile(self._ConfigFileName(instance_name)) - @staticmethod - def _RunXmList(xmlist_errors): - """Helper function for L{_GetXMList} to run "xm list". + def _StashConfigFile(self, instance_name): + """Move the Xen config file to the log directory and return its new path. """ - result = utils.RunCmd([constants.XEN_CMD, "list"]) - if result.failed: - logging.error("xm list failed (%s): %s", result.fail_reason, - result.output) - xmlist_errors.append(result) - raise utils.RetryAgain() - - # skip over the heading - return result.stdout.splitlines()[1:] - - @classmethod - def _GetXMList(cls, include_node): - """Return the list of running instances. - - If the include_node argument is True, then we return information - for dom0 also, otherwise we filter that from the return value. + old_filename = self._ConfigFileName(instance_name) + base = ("%s-%s" % + (instance_name, utils.TimestampForFilename())) + new_filename = utils.PathJoin(pathutils.LOG_XEN_DIR, base) + utils.RenameFile(old_filename, new_filename) + return new_filename - @return: list of (name, id, memory, vcpus, state, time spent) + def _GetInstanceList(self, include_node, hvparams=None): + """Wrapper around module level L{_GetInstanceList}. """ - xmlist_errors = [] - try: - lines = utils.Retry(cls._RunXmList, 1, 5, args=(xmlist_errors, )) - except utils.RetryTimeout: - if xmlist_errors: - xmlist_result = xmlist_errors.pop() + return _GetInstanceList(lambda: self._RunXen(["list"], hvparams=hvparams), + include_node) - errmsg = ("xm list failed, timeout exceeded (%s): %s" % - (xmlist_result.fail_reason, xmlist_result.output)) - else: - errmsg = "xm list failed" - - raise errors.HypervisorError(errmsg) - - result = [] - for line in lines: - # The format of lines is: - # Name ID Mem(MiB) VCPUs State Time(s) - # Domain-0 0 3418 4 r----- 266.2 - data = line.split() - if len(data) != 6: - raise errors.HypervisorError("Can't parse output of xm list," - " line: %s" % line) - try: - data[1] = int(data[1]) - data[2] = int(data[2]) - data[3] = int(data[3]) - data[5] = float(data[5]) - except (TypeError, ValueError), err: - raise errors.HypervisorError("Can't parse output of xm list," - " line: %s, error: %s" % (line, err)) - - # skip the Domain-0 (optional) - if include_node or data[0] != "Domain-0": - result.append(data) - - return result - - def ListInstances(self): + def ListInstances(self, hvparams=None): """Get the list of running instances. """ - xm_list = self._GetXMList(False) - names = [info[0] for info in xm_list] + instance_list = self._GetInstanceList(False, hvparams=hvparams) + names = [info[0] for info in instance_list] return names def GetInstanceInfo(self, instance_name): @@ -177,9 +458,9 @@ class XenHypervisor(hv_base.BaseHypervisor): @return: tuple (name, id, memory, vcpus, stat, times) """ - xm_list = self._GetXMList(instance_name == "Domain-0") + instance_list = self._GetInstanceList(instance_name == _DOM0_NAME) result = None - for data in xm_list: + for data in instance_list: if data[0] == instance_name: result = data break @@ -191,24 +472,44 @@ class XenHypervisor(hv_base.BaseHypervisor): @return: list of tuples (name, id, memory, vcpus, stat, times) """ - xm_list = self._GetXMList(False) - return xm_list + return self._GetInstanceList(False) + + def _MakeConfigFile(self, instance, startup_memory, block_devices): + """Gather configuration details and write to disk. + + See L{_GetConfig} for arguments. + + """ + buf = StringIO() + buf.write("# Automatically generated by Ganeti. Do not edit!\n") + buf.write("\n") + buf.write(self._GetConfig(instance, startup_memory, block_devices)) + buf.write("\n") + + self._WriteConfigFile(instance.name, buf.getvalue()) def StartInstance(self, instance, block_devices, startup_paused): """Start an instance. """ - self._WriteConfigFile(instance, block_devices) - cmd = [constants.XEN_CMD, "create"] + startup_memory = self._InstanceStartupMemory(instance) + + self._MakeConfigFile(instance, startup_memory, block_devices) + + cmd = ["create"] if startup_paused: - cmd.extend(["-p"]) - cmd.extend([self._ConfigFileName(instance.name)]) - result = utils.RunCmd(cmd) + cmd.append("-p") + cmd.append(self._ConfigFileName(instance.name)) + result = self._RunXen(cmd, hvparams=instance.hvparams) if result.failed: - raise errors.HypervisorError("Failed to start instance %s: %s (%s)" % + # Move the Xen configuration file to the log directory to avoid + # leaving a stale config file behind. + stashed_config = self._StashConfigFile(instance.name) + raise errors.HypervisorError("Failed to start instance %s: %s (%s). Moved" + " config file to %s" % (instance.name, result.fail_reason, - result.output)) + result.output, stashed_config)) def StopInstance(self, instance, force=False, retry=False, name=None): """Stop an instance. @@ -216,17 +517,33 @@ class XenHypervisor(hv_base.BaseHypervisor): """ if name is None: name = instance.name - self._RemoveConfigFile(name) + + return self._StopInstance(name, force, instance.hvparams) + + def _StopInstance(self, name, force, hvparams): + """Stop an instance. + + @type name: string + @param name: name of the instance to be shutdown + @type force: boolean + @param force: flag specifying whether shutdown should be forced + @type hvparams: dict of string + @param hvparams: hypervisor parameters of the instance + + """ if force: - command = [constants.XEN_CMD, "destroy", name] + action = "destroy" else: - command = [constants.XEN_CMD, "shutdown", name] - result = utils.RunCmd(command) + action = "shutdown" + result = self._RunXen([action, name], hvparams=hvparams) if result.failed: raise errors.HypervisorError("Failed to stop instance %s: %s, %s" % (name, result.fail_reason, result.output)) + # Remove configuration file if stopping/starting instance was successful + self._RemoveConfigFile(name) + def RebootInstance(self, instance): """Reboot an instance. @@ -237,7 +554,7 @@ class XenHypervisor(hv_base.BaseHypervisor): raise errors.HypervisorError("Failed to reboot instance %s," " not running" % instance.name) - result = utils.RunCmd([constants.XEN_CMD, "reboot", instance.name]) + result = self._RunXen(["reboot", instance.name], hvparams=instance.hvparams) if result.failed: raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" % (instance.name, result.fail_reason, @@ -261,65 +578,45 @@ class XenHypervisor(hv_base.BaseHypervisor): " did not reboot in the expected interval" % (instance.name, )) + def BalloonInstanceMemory(self, instance, mem): + """Balloon an instance memory to a certain value. + + @type instance: L{objects.Instance} + @param instance: instance to be accepted + @type mem: int + @param mem: actual memory size to use for instance runtime + + """ + result = self._RunXen(["mem-set", instance.name, mem], + hvparams=instance.hvparams) + if result.failed: + raise errors.HypervisorError("Failed to balloon instance %s: %s (%s)" % + (instance.name, result.fail_reason, + result.output)) + + # Update configuration file + cmd = ["sed", "-ie", "s/^memory.*$/memory = %s/" % mem] + cmd.append(self._ConfigFileName(instance.name)) + + result = utils.RunCmd(cmd) + if result.failed: + raise errors.HypervisorError("Failed to update memory for %s: %s (%s)" % + (instance.name, result.fail_reason, + result.output)) + def GetNodeInfo(self): """Return information about the node. - @return: a dict with the following keys (memory 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 - - nr_cpus: total number of CPUs - - nr_nodes: in a NUMA system, the number of domains - - nr_sockets: the number of physical CPU sockets in the node - - hv_version: the hypervisor version in the form (major, minor) + @see: L{_GetNodeInfo} and L{_ParseNodeInfo} """ - # note: in xen 3, memory has changed to total_memory - result = utils.RunCmd([constants.XEN_CMD, "info"]) + result = self._RunXen(["info"]) if result.failed: logging.error("Can't run 'xm info' (%s): %s", result.fail_reason, result.output) return None - xmoutput = result.stdout.splitlines() - result = {} - cores_per_socket = threads_per_core = nr_cpus = None - xen_major, xen_minor = None, None - for line in xmoutput: - splitfields = line.split(":", 1) - - if len(splitfields) > 1: - key = splitfields[0].strip() - val = splitfields[1].strip() - if key == "memory" or key == "total_memory": - result["memory_total"] = int(val) - elif key == "free_memory": - result["memory_free"] = int(val) - elif key == "nr_cpus": - nr_cpus = result["cpu_total"] = int(val) - elif key == "nr_nodes": - result["cpu_nodes"] = int(val) - elif key == "cores_per_socket": - cores_per_socket = int(val) - elif key == "threads_per_core": - threads_per_core = int(val) - elif key == "xen_major": - xen_major = int(val) - elif key == "xen_minor": - xen_minor = int(val) - - if (cores_per_socket is not None and - threads_per_core is not None and nr_cpus is not None): - result["cpu_sockets"] = nr_cpus / (cores_per_socket * threads_per_core) - - dom0_info = self.GetInstanceInfo("Domain-0") - if dom0_info is not None: - result["memory_dom0"] = dom0_info[2] - - if not (xen_major is None or xen_minor is None): - result[constants.HV_NODEINFO_KEY_VERSION] = (xen_major, xen_minor) - - return result + return _GetNodeInfo(result.stdout, self._GetInstanceList) @classmethod def GetInstanceConsole(cls, instance, hvparams, beparams): @@ -329,58 +626,39 @@ class XenHypervisor(hv_base.BaseHypervisor): return objects.InstanceConsole(instance=instance.name, kind=constants.CONS_SSH, host=instance.primary_node, - user=constants.GANETI_RUNAS, - command=[constants.XM_CONSOLE_WRAPPER, - instance.name]) + user=constants.SSH_CONSOLE_USER, + command=[pathutils.XEN_CONSOLE_WRAPPER, + constants.XEN_CMD, instance.name]) - def Verify(self): + def Verify(self, hvparams=None): """Verify the hypervisor. For Xen, this verifies that the xend process is running. + @type hvparams: dict of strings + @param hvparams: hypervisor parameters to be verified against + + @return: Problem description if something is wrong, C{None} otherwise + """ - result = utils.RunCmd([constants.XEN_CMD, "info"]) + if hvparams is None: + return "Could not verify the hypervisor, because no hvparams were" \ + " provided." + + if constants.HV_XEN_CMD in hvparams: + xen_cmd = hvparams[constants.HV_XEN_CMD] + try: + self._CheckToolstack(xen_cmd) + except errors.HypervisorError: + return "The configured xen toolstack '%s' is not available on this" \ + " node." % xen_cmd + + result = self._RunXen(["info"], hvparams=hvparams) if result.failed: - return "'xm info' failed: %s, %s" % (result.fail_reason, result.output) - - @staticmethod - def _GetConfigFileDiskData(block_devices, blockdev_prefix): - """Get disk directive for xen config file. - - This method builds the xen config disk directive according to the - given disk_template and block_devices. - - @param block_devices: list of tuples (cfdev, rldev): - - cfdev: dict containing ganeti config disk part - - rldev: ganeti.bdev.BlockDev object - @param blockdev_prefix: a string containing blockdevice prefix, - e.g. "sd" for /dev/sda - - @return: string containing disk directive for xen instance config file - - """ - FILE_DRIVER_MAP = { - constants.FD_LOOP: "file", - constants.FD_BLKTAP: "tap:aio", - } - disk_data = [] - if len(block_devices) > 24: - # 'z' - 'a' = 24 - raise errors.HypervisorError("Too many disks") - namespace = [blockdev_prefix + chr(i + ord("a")) for i in range(24)] - for sd_name, (cfdev, dev_path) in zip(namespace, block_devices): - if cfdev.mode == constants.DISK_RDWR: - mode = "w" - else: - mode = "r" - if cfdev.dev_type == constants.LD_FILE: - line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]], - dev_path, sd_name, mode) - else: - line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode) - disk_data.append(line) + return "Retrieving information from xen failed: %s, %s" % \ + (result.fail_reason, result.output) - return disk_data + return None def MigrationInfo(self, instance): """Get instance information to perform a migration. @@ -406,7 +684,7 @@ class XenHypervisor(hv_base.BaseHypervisor): """ pass - def FinalizeMigration(self, instance, info, success): + def FinalizeMigrationDst(self, instance, info, success): """Finalize an instance migration. After a successful migration we write the xen config file. @@ -421,7 +699,7 @@ class XenHypervisor(hv_base.BaseHypervisor): """ if success: - self._WriteConfigFileStatic(instance.name, info) + self._WriteConfigFile(instance.name, info) def MigrateInstance(self, instance, target, live): """Migrate an instance to a target node. @@ -437,33 +715,92 @@ class XenHypervisor(hv_base.BaseHypervisor): @param live: perform a live migration """ - if self.GetInstanceInfo(instance.name) is None: + port = instance.hvparams[constants.HV_MIGRATION_PORT] + + # TODO: Pass cluster name via RPC + cluster_name = ssconf.SimpleStore().GetClusterName() + + return self._MigrateInstance(cluster_name, instance.name, target, port, + live, instance.hvparams) + + def _MigrateInstance(self, cluster_name, instance_name, target, port, live, + hvparams, _ping_fn=netutils.TcpPing): + """Migrate an instance to a target node. + + @see: L{MigrateInstance} for details + + """ + if hvparams is None: + raise errors.HypervisorError("No hvparams provided.") + + if self.GetInstanceInfo(instance_name, hvparams=hvparams) is None: raise errors.HypervisorError("Instance not running, cannot migrate") - port = instance.hvparams[constants.HV_MIGRATION_PORT] + cmd = self._GetCommand(hvparams=hvparams) - if not netutils.TcpPing(target, port, live_port_needed=True): + if (cmd == constants.XEN_CMD_XM and + not _ping_fn(target, port, live_port_needed=True)): raise errors.HypervisorError("Remote host %s not listening on port" " %s, cannot migrate" % (target, port)) - # FIXME: migrate must be upgraded for transitioning to "xl" (xen 4.1). - # -l doesn't exist anymore - # -p doesn't exist anymore - # -C config_file must be passed - # ssh must recognize the key of the target host for the migration - args = [constants.XEN_CMD, "migrate", "-p", "%d" % port] - if live: - args.append("-l") - args.extend([instance.name, target]) - result = utils.RunCmd(args) + args = ["migrate"] + + if cmd == constants.XEN_CMD_XM: + args.extend(["-p", "%d" % port]) + if live: + args.append("-l") + + elif cmd == constants.XEN_CMD_XL: + args.extend([ + "-s", constants.XL_SSH_CMD % cluster_name, + "-C", self._ConfigFileName(instance_name), + ]) + + else: + raise errors.HypervisorError("Unsupported Xen command: %s" % self._cmd) + + args.extend([instance_name, target]) + + result = self._RunXen(args, hvparams=hvparams) if result.failed: raise errors.HypervisorError("Failed to migrate instance %s: %s" % - (instance.name, result.output)) - # remove old xen file after migration succeeded - try: - self._RemoveConfigFile(instance.name) - except EnvironmentError: - logging.exception("Failure while removing instance config file") + (instance_name, result.output)) + + def FinalizeMigrationSource(self, instance, success, live): + """Finalize the instance migration on the source node. + + @type instance: L{objects.Instance} + @param instance: the instance that was migrated + @type success: bool + @param success: whether the migration succeeded or not + @type live: bool + @param live: whether the user requested a live migration or not + + """ + # pylint: disable=W0613 + if success: + # remove old xen file after migration succeeded + try: + self._RemoveConfigFile(instance.name) + except EnvironmentError: + logging.exception("Failure while removing instance config file") + + def GetMigrationStatus(self, instance): + """Get the migration status + + As MigrateInstance for Xen is still blocking, if this method is called it + means that MigrateInstance has completed successfully. So we can safely + assume that the migration was successful and notify this fact to the client. + + @type instance: L{objects.Instance} + @param instance: the instance that is being migrated + @rtype: L{objects.MigrationStatus} + @return: the status of the current migration (one of + L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional + progress info that can be retrieved from the hypervisor + + """ + return objects.MigrationStatus(status=constants.HV_MIGRATION_COMPLETED) @classmethod def PowercycleNode(cls): @@ -482,6 +819,48 @@ class XenHypervisor(hv_base.BaseHypervisor): finally: utils.RunCmd([constants.XEN_CMD, "debug", "R"]) + def _CheckToolstack(self, xen_cmd): + """Check whether the given toolstack is available on the node. + + @type xen_cmd: string + @param xen_cmd: xen command (e.g. 'xm' or 'xl') + + """ + binary_found = self._CheckToolstackBinary(xen_cmd) + if not binary_found: + raise errors.HypervisorError("No '%s' binary found on node." % xen_cmd) + elif xen_cmd == constants.XEN_CMD_XL: + if not self._CheckToolstackXlConfigured(): + raise errors.HypervisorError("Toolstack '%s' is not enabled on this" + "node." % xen_cmd) + + def _CheckToolstackBinary(self, xen_cmd): + """Checks whether the xen command's binary is found on the machine. + + """ + if xen_cmd not in constants.KNOWN_XEN_COMMANDS: + raise errors.HypervisorError("Unknown xen command '%s'." % xen_cmd) + result = self._run_cmd_fn(["which", xen_cmd]) + return not result.failed + + def _CheckToolstackXlConfigured(self): + """Checks whether xl is enabled on an xl-capable node. + + @rtype: bool + @returns: C{True} if 'xl' is enabled, C{False} otherwise + + """ + result = self._run_cmd_fn([constants.XEN_CMD_XL, "help"]) + if not result.failed: + return True + elif result.failed: + if "toolstack" in result.stderr: + return False + # xl fails for some other reason than the toolstack + else: + raise errors.HypervisorError("Cannot run xen ('%s'). Error: %s." + % (constants.XEN_CMD_XL, result.stderr)) + class XenPvmHypervisor(XenHypervisor): """Xen PVM hypervisor interface""" @@ -499,11 +878,16 @@ class XenPvmHypervisor(XenHypervisor): # TODO: Add a check for the blockdev prefix (matching [a-z:] or similar). constants.HV_BLOCKDEV_PREFIX: hv_base.NO_CHECK, constants.HV_REBOOT_BEHAVIOR: - hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS) + hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS), + constants.HV_CPU_MASK: hv_base.OPT_MULTI_CPU_MASK_CHECK, + constants.HV_CPU_CAP: hv_base.OPT_NONNEGATIVE_INT_CHECK, + constants.HV_CPU_WEIGHT: + (False, lambda x: 0 < x < 65536, "invalid weight", None, None), + constants.HV_XEN_CMD: + hv_base.ParamInSet(True, constants.KNOWN_XEN_COMMANDS), } - @classmethod - def _WriteConfigFile(cls, instance, block_devices): + def _GetConfig(self, instance, startup_memory, block_devices): """Write the Xen config file for the instance. """ @@ -536,8 +920,19 @@ class XenPvmHypervisor(XenHypervisor): config.write("ramdisk = '%s'\n" % initrd_path) # rest of the settings - config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY]) + config.write("memory = %d\n" % startup_memory) + config.write("maxmem = %d\n" % instance.beparams[constants.BE_MAXMEM]) config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS]) + cpu_pinning = _CreateConfigCpus(hvp[constants.HV_CPU_MASK]) + if cpu_pinning: + config.write("%s\n" % cpu_pinning) + cpu_cap = hvp[constants.HV_CPU_CAP] + if cpu_cap: + config.write("cpu_cap=%d\n" % cpu_cap) + cpu_weight = hvp[constants.HV_CPU_WEIGHT] + if cpu_weight: + config.write("cpu_weight=%d\n" % cpu_weight) + config.write("name = '%s'\n" % instance.name) vif_data = [] @@ -550,8 +945,8 @@ class XenPvmHypervisor(XenHypervisor): nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK] vif_data.append("'%s'" % nic_str) - disk_data = cls._GetConfigFileDiskData(block_devices, - hvp[constants.HV_BLOCKDEV_PREFIX]) + disk_data = \ + _GetConfigFileDiskData(block_devices, hvp[constants.HV_BLOCKDEV_PREFIX]) config.write("vif = [%s]\n" % ",".join(vif_data)) config.write("disk = [%s]\n" % ",".join(disk_data)) @@ -565,24 +960,18 @@ class XenPvmHypervisor(XenHypervisor): config.write("on_reboot = 'destroy'\n") config.write("on_crash = 'restart'\n") config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS]) - # just in case it exists - utils.RemoveFile("/etc/xen/auto/%s" % instance.name) - try: - utils.WriteFile(cls._ConfigFileName(instance.name), - data=config.getvalue()) - except EnvironmentError, err: - raise errors.HypervisorError("Cannot write Xen instance confile" - " file %s: %s" % - (cls._ConfigFileName(instance.name), err)) - return True + return config.getvalue() class XenHvmHypervisor(XenHypervisor): """Xen HVM hypervisor interface""" ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + [ - constants.VNC_PASSWORD_FILE, + pathutils.VNC_PASSWORD_FILE, + ] + ANCILLARY_FILES_OPT = XenHypervisor.ANCILLARY_FILES_OPT + [ + pathutils.VNC_PASSWORD_FILE, ] PARAMETERS = { @@ -608,27 +997,46 @@ class XenHvmHypervisor(XenHypervisor): constants.HV_USE_LOCALTIME: hv_base.NO_CHECK, # TODO: Add a check for the blockdev prefix (matching [a-z:] or similar). constants.HV_BLOCKDEV_PREFIX: hv_base.NO_CHECK, + # Add PCI passthrough + constants.HV_PASSTHROUGH: hv_base.NO_CHECK, constants.HV_REBOOT_BEHAVIOR: - hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS) + hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS), + constants.HV_CPU_MASK: hv_base.OPT_MULTI_CPU_MASK_CHECK, + constants.HV_CPU_CAP: hv_base.NO_CHECK, + constants.HV_CPU_WEIGHT: + (False, lambda x: 0 < x < 65535, "invalid weight", None, None), + constants.HV_VIF_TYPE: + hv_base.ParamInSet(False, constants.HT_HVM_VALID_VIF_TYPES), + constants.HV_XEN_CMD: + hv_base.ParamInSet(True, constants.KNOWN_XEN_COMMANDS), } - @classmethod - def _WriteConfigFile(cls, instance, block_devices): + def _GetConfig(self, instance, startup_memory, block_devices): """Create a Xen 3.1 HVM config file. """ hvp = instance.hvparams config = StringIO() - config.write("# this is autogenerated by Ganeti, please do not edit\n#\n") # kernel handling kpath = hvp[constants.HV_KERNEL_PATH] config.write("kernel = '%s'\n" % kpath) config.write("builder = 'hvm'\n") - config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY]) + config.write("memory = %d\n" % startup_memory) + config.write("maxmem = %d\n" % instance.beparams[constants.BE_MAXMEM]) config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS]) + cpu_pinning = _CreateConfigCpus(hvp[constants.HV_CPU_MASK]) + if cpu_pinning: + config.write("%s\n" % cpu_pinning) + cpu_cap = hvp[constants.HV_CPU_CAP] + if cpu_cap: + config.write("cpu_cap=%d\n" % cpu_cap) + cpu_weight = hvp[constants.HV_CPU_WEIGHT] + if cpu_weight: + config.write("cpu_weight=%d\n" % cpu_weight) + config.write("name = '%s'\n" % instance.name) if hvp[constants.HV_PAE]: config.write("pae = 1\n") @@ -672,14 +1080,23 @@ class XenHvmHypervisor(XenHypervisor): config.write("localtime = 1\n") vif_data = [] + # Note: what is called 'nic_type' here, is used as value for the xen nic + # vif config parameter 'model'. For the xen nic vif parameter 'type', we use + # the 'vif_type' to avoid a clash of notation. nic_type = hvp[constants.HV_NIC_TYPE] + if nic_type is None: + vif_type_str = "" + if hvp[constants.HV_VIF_TYPE]: + vif_type_str = ", type=%s" % hvp[constants.HV_VIF_TYPE] # ensure old instances don't change - nic_type_str = ", type=ioemu" + nic_type_str = vif_type_str elif nic_type == constants.HT_NIC_PARAVIRTUAL: nic_type_str = ", type=paravirtualized" else: - nic_type_str = ", model=%s, type=ioemu" % nic_type + # parameter 'model' is only valid with type 'ioemu' + nic_type_str = ", model=%s, type=%s" % \ + (nic_type, constants.HT_HVM_VIF_IOEMU) for nic in instance.nics: nic_str = "mac=%s%s" % (nic.mac, nic_type_str) ip = getattr(nic, "ip", None) @@ -691,8 +1108,8 @@ class XenHvmHypervisor(XenHypervisor): config.write("vif = [%s]\n" % ",".join(vif_data)) - disk_data = cls._GetConfigFileDiskData(block_devices, - hvp[constants.HV_BLOCKDEV_PREFIX]) + disk_data = \ + _GetConfigFileDiskData(block_devices, hvp[constants.HV_BLOCKDEV_PREFIX]) iso_path = hvp[constants.HV_CDROM_IMAGE_PATH] if iso_path: @@ -700,21 +1117,17 @@ class XenHvmHypervisor(XenHypervisor): disk_data.append(iso) config.write("disk = [%s]\n" % (",".join(disk_data))) - + # Add PCI passthrough + pci_pass_arr = [] + pci_pass = hvp[constants.HV_PASSTHROUGH] + if pci_pass: + pci_pass_arr = pci_pass.split(";") + config.write("pci = %s\n" % pci_pass_arr) config.write("on_poweroff = 'destroy'\n") if hvp[constants.HV_REBOOT_BEHAVIOR] == constants.INSTANCE_REBOOT_ALLOWED: config.write("on_reboot = 'restart'\n") else: config.write("on_reboot = 'destroy'\n") config.write("on_crash = 'restart'\n") - # just in case it exists - utils.RemoveFile("/etc/xen/auto/%s" % instance.name) - try: - utils.WriteFile(cls._ConfigFileName(instance.name), - data=config.getvalue()) - except EnvironmentError, err: - raise errors.HypervisorError("Cannot write Xen instance confile" - " file %s: %s" % - (cls._ConfigFileName(instance.name), err)) - return True + return config.getvalue()