X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/d0bb3f2463e1a1fe0cbe01e952856d6f01ac7dcb..8351df2fae34414d07c2ee326c53d7510088addb:/lib/hypervisor/hv_xen.py diff --git a/lib/hypervisor/hv_xen.py b/lib/hypervisor/hv_xen.py index 4641a7b..63ab47c 100644 --- a/lib/hypervisor/hv_xen.py +++ b/lib/hypervisor/hv_xen.py @@ -82,21 +82,22 @@ def _CreateConfigCpus(cpu_mask): return "cpus = [ %s ]" % ", ".join(map(_GetCPUMap, cpu_list)) -def _RunXmList(fn, xmllist_errors): - """Helper function for L{_GetXmList} to run "xm 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 returning result of running C{xm list} - @type xmllist_errors: list - @param xmllist_errors: Error list + @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("xm list failed (%s): %s", result.fail_reason, - result.output) - xmllist_errors.append(result) + 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 @@ -141,24 +142,24 @@ def _ParseXmList(lines, include_node): return result -def _GetXmList(fn, include_node, _timeout=5): +def _GetInstanceList(fn, include_node, _timeout=5): """Return the list of running instances. - See L{_RunXmList} and L{_ParseXmList} for parameter details. + See L{_RunInstanceList} and L{_ParseXmList} for parameter details. """ - xmllist_errors = [] + instance_list_errors = [] try: - lines = utils.Retry(_RunXmList, (0.3, 1.5, 1.0), _timeout, - args=(fn, xmllist_errors)) + lines = utils.Retry(_RunInstanceList, (0.3, 1.5, 1.0), _timeout, + args=(fn, instance_list_errors)) except utils.RetryTimeout: - if xmllist_errors: - xmlist_result = xmllist_errors.pop() + if instance_list_errors: + instance_list_result = instance_list_errors.pop() - errmsg = ("xm list failed, timeout exceeded (%s): %s" % - (xmlist_result.fail_reason, xmlist_result.output)) + errmsg = ("listing instances failed, timeout exceeded (%s): %s" % + (instance_list_result.fail_reason, instance_list_result.output)) else: - errmsg = "xm list failed" + errmsg = "listing instances failed" raise errors.HypervisorError(errmsg) @@ -270,7 +271,7 @@ def _GetConfigFileDiskData(block_devices, blockdev_prefix, @param block_devices: list of tuples (cfdev, rldev): - cfdev: dict containing ganeti config disk part - - rldev: ganeti.bdev.BlockDev object + - rldev: ganeti.block.bdev.BlockDev object @param blockdev_prefix: a string containing blockdevice prefix, e.g. "sd" for /dev/sda @@ -320,8 +321,57 @@ class XenHypervisor(hv_base.BaseHypervisor): XL_CONFIG_FILE, ] - @staticmethod - def _ConfigFileName(instance_name): + 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 + + 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 @@ -330,39 +380,36 @@ class XenHypervisor(hv_base.BaseHypervisor): @rtype: str """ - return utils.PathJoin(pathutils.XEN_CONFIG_DIR, instance_name) + return utils.PathJoin(self._cfgdir, instance_name) @classmethod - def _WriteConfigFile(cls, instance, startup_memory, 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. """ # just in case it exists - utils.RemoveFile(utils.PathJoin(pathutils.XEN_CONFIG_DIR, "auto", - instance_name)) + utils.RemoveFile(utils.PathJoin(self._cfgdir, "auto", instance_name)) - cfg_file = XenHypervisor._ConfigFileName(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 = XenHypervisor._ConfigFileName(instance_name) + filename = self._ConfigFileName(instance_name) try: file_content = utils.ReadFile(filename) @@ -371,28 +418,36 @@ class XenHypervisor(hv_base.BaseHypervisor): 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 _GetXmList(include_node): - """Wrapper around module level L{_GetXmList}. + def _StashConfigFile(self, instance_name): + """Move the Xen config file to the log directory and return its new path. """ - # TODO: Abstract running Xen command for testing - return _GetXmList(lambda: utils.RunCmd([constants.XEN_CMD, "list"]), - include_node) + 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 + + def _GetInstanceList(self, include_node, hvparams=None): + """Wrapper around module level L{_GetInstanceList}. - def ListInstances(self): + """ + return _GetInstanceList(lambda: self._RunXen(["list"], hvparams=hvparams), + include_node) + + 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): @@ -403,9 +458,9 @@ class XenHypervisor(hv_base.BaseHypervisor): @return: tuple (name, id, memory, vcpus, stat, times) """ - xm_list = self._GetXmList(instance_name == _DOM0_NAME) + 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 @@ -417,25 +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. """ startup_memory = self._InstanceStartupMemory(instance) - self._WriteConfigFile(instance, startup_memory, block_devices) - cmd = [constants.XEN_CMD, "create"] + + 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. @@ -443,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. @@ -464,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, @@ -497,14 +587,17 @@ class XenHypervisor(hv_base.BaseHypervisor): @param mem: actual memory size to use for instance runtime """ - cmd = [constants.XEN_CMD, "mem-set", instance.name, mem] - result = utils.RunCmd(cmd) + 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(XenHypervisor._ConfigFileName(instance.name)) + cmd.append(self._ConfigFileName(instance.name)) + result = utils.RunCmd(cmd) if result.failed: raise errors.HypervisorError("Failed to update memory for %s: %s (%s)" % @@ -517,14 +610,13 @@ class XenHypervisor(hv_base.BaseHypervisor): @see: L{_GetNodeInfo} and L{_ParseNodeInfo} """ - # TODO: Abstract running Xen command for testing - 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 - return _GetNodeInfo(result.stdout, self._GetXmList) + return _GetNodeInfo(result.stdout, self._GetInstanceList) @classmethod def GetInstanceConsole(cls, instance, hvparams, beparams): @@ -538,17 +630,33 @@ class XenHypervisor(hv_base.BaseHypervisor): 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) + return "Retrieving information from xen failed: %s, %s" % \ + (result.fail_reason, result.output) return None @@ -591,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. @@ -607,34 +715,56 @@ 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 (constants.XEN_CMD == constants.XEN_CMD_XM and - 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)) - args = [constants.XEN_CMD, "migrate"] - if constants.XEN_CMD == constants.XEN_CMD_XM: + args = ["migrate"] + + if cmd == constants.XEN_CMD_XM: args.extend(["-p", "%d" % port]) if live: args.append("-l") - elif constants.XEN_CMD == constants.XEN_CMD_XL: - cluster_name = ssconf.SimpleStore().GetClusterName() - args.extend(["-s", constants.XL_SSH_CMD % cluster_name]) - args.extend(["-C", self._ConfigFileName(instance.name)]) + + 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" % - constants.XEN_CMD) + raise errors.HypervisorError("Unsupported Xen command: %s" % self._cmd) + + args.extend([instance_name, target]) - args.extend([instance.name, target]) - result = utils.RunCmd(args) + result = self._RunXen(args, hvparams=hvparams) if result.failed: raise errors.HypervisorError("Failed to migrate instance %s: %s" % - (instance.name, result.output)) + (instance_name, result.output)) def FinalizeMigrationSource(self, instance, success, live): """Finalize the instance migration on the source node. @@ -689,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""" @@ -711,10 +883,11 @@ class XenPvmHypervisor(XenHypervisor): 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, startup_memory, block_devices): + def _GetConfig(self, instance, startup_memory, block_devices): """Write the Xen config file for the instance. """ @@ -787,9 +960,8 @@ 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]) - cls._WriteConfigFileStatic(instance.name, config.getvalue()) - return True + return config.getvalue() class XenHvmHypervisor(XenHypervisor): @@ -833,17 +1005,19 @@ class XenHvmHypervisor(XenHypervisor): 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, startup_memory, 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] @@ -906,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) @@ -946,6 +1129,5 @@ class XenHvmHypervisor(XenHypervisor): else: config.write("on_reboot = 'destroy'\n") config.write("on_crash = 'restart'\n") - cls._WriteConfigFileStatic(instance.name, config.getvalue()) - return True + return config.getvalue()