X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/fbe9022f32370e999aa52d827499dcae9f2e369e..a5229439a0908a071d13dc8c1e9099e171e896bd:/lib/cmdlib.py?ds=sidebyside diff --git a/lib/cmdlib.py b/lib/cmdlib.py index a9f091c..41e2408 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -25,25 +25,21 @@ import os import os.path -import sha import time -import tempfile import re import platform +import logging +import copy -from ganeti import rpc from ganeti import ssh -from ganeti import logger from ganeti import utils from ganeti import errors from ganeti import hypervisor from ganeti import locking -from ganeti import config from ganeti import constants from ganeti import objects -from ganeti import opcodes -from ganeti import ssconf from ganeti import serializer +from ganeti import ssconf class LogicalUnit(object): @@ -51,39 +47,54 @@ class LogicalUnit(object): Subclasses must follow these rules: - implement ExpandNames - - implement CheckPrereq - - implement Exec + - implement CheckPrereq (except when tasklets are used) + - implement Exec (except when tasklets are used) - implement BuildHooksEnv - redefine HPATH and HTYPE - optionally redefine their run requirements: - REQ_MASTER: the LU needs to run on the master node - REQ_WSSTORE: the LU needs a writable SimpleStore REQ_BGL: the LU needs to hold the Big Ganeti Lock exclusively Note that all commands require root permissions. + @ivar dry_run_result: the value (if any) that will be returned to the caller + in dry-run mode (signalled by opcode dry_run parameter) + """ HPATH = None HTYPE = None _OP_REQP = [] - REQ_MASTER = True - REQ_WSSTORE = False REQ_BGL = True - def __init__(self, processor, op, context, sstore): + def __init__(self, processor, op, context, rpc): """Constructor for LogicalUnit. - This needs to be overriden in derived classes in order to check op + This needs to be overridden in derived classes in order to check op validity. """ self.proc = processor self.op = op self.cfg = context.cfg - self.sstore = sstore self.context = context + self.rpc = rpc + # Dicts used to declare locking needs to mcpu self.needed_locks = None + self.acquired_locks = {} + self.share_locks = dict.fromkeys(locking.LEVELS, 0) + self.add_locks = {} + self.remove_locks = {} + # Used to force good behavior when calling helper functions + self.recalculate_locks = {} self.__ssh = None + # logging + self.LogWarning = processor.LogWarning + self.LogInfo = processor.LogInfo + self.LogStep = processor.LogStep + # support for dry-run + self.dry_run_result = None + + # Tasklets + self.tasklets = None for attr_name in self._OP_REQP: attr_val = getattr(op, attr_name, None) @@ -91,25 +102,36 @@ class LogicalUnit(object): raise errors.OpPrereqError("Required parameter '%s' missing" % attr_name) - if not self.cfg.IsCluster(): - raise errors.OpPrereqError("Cluster not initialized yet," - " use 'gnt-cluster init' first.") - if self.REQ_MASTER: - master = sstore.GetMasterNode() - if master != utils.HostInfo().name: - raise errors.OpPrereqError("Commands must be run on the master" - " node %s" % master) + self.CheckArguments() def __GetSSH(self): """Returns the SshRunner object """ if not self.__ssh: - self.__ssh = ssh.SshRunner(self.sstore) + self.__ssh = ssh.SshRunner(self.cfg.GetClusterName()) return self.__ssh ssh = property(fget=__GetSSH) + def CheckArguments(self): + """Check syntactic validity for the opcode arguments. + + This method is for doing a simple syntactic check and ensure + validity of opcode parameters, without any cluster-related + checks. While the same can be accomplished in ExpandNames and/or + CheckPrereq, doing these separate is better because: + + - ExpandNames is left as as purely a lock-related function + - CheckPrereq is run after we have acquired locks (and possible + waited for them) + + The function is allowed to change the self.op attribute so that + later methods can no longer worry about missing parameters. + + """ + pass + def ExpandNames(self): """Expand names for this LU. @@ -121,25 +143,33 @@ class LogicalUnit(object): LUs which implement this method must also populate the self.needed_locks member, as a dict with lock levels as keys, and a list of needed lock names as values. Rules: - - Use an empty dict if you don't need any lock - - If you don't need any lock at a particular level omit that level - - Don't put anything for the BGL level - - If you want all locks at a level use None as a value - (this reflects what LockSet does, and will be replaced before - CheckPrereq with the full list of nodes that have been locked) - - Examples: - # Acquire all nodes and one instance - self.needed_locks = { - locking.LEVEL_NODE: None, - locking.LEVEL_INSTANCES: ['instance1.example.tld'], - } - # Acquire just two nodes - self.needed_locks = { - locking.LEVEL_NODE: ['node1.example.tld', 'node2.example.tld'], - } - # Acquire no locks - self.needed_locks = {} # No, you can't leave it to the default value None + + - use an empty dict if you don't need any lock + - if you don't need any lock at a particular level omit that level + - don't put anything for the BGL level + - if you want all locks at a level use locking.ALL_SET as a value + + If you need to share locks (rather than acquire them exclusively) at one + level you can modify self.share_locks, setting a true value (usually 1) for + that level. By default locks are not shared. + + This function can also define a list of tasklets, which then will be + executed in order instead of the usual LU-level CheckPrereq and Exec + functions, if those are not defined by the LU. + + Examples:: + + # Acquire all nodes and one instance + self.needed_locks = { + locking.LEVEL_NODE: locking.ALL_SET, + locking.LEVEL_INSTANCE: ['instance1.example.tld'], + } + # Acquire just two nodes + self.needed_locks = { + locking.LEVEL_NODE: ['node1.example.tld', 'node2.example.tld'], + } + # Acquire no locks + self.needed_locks = {} # No, you can't leave it to the default value None """ # The implementation of this method is mandatory only if the new LU is @@ -150,6 +180,24 @@ class LogicalUnit(object): else: raise NotImplementedError + def DeclareLocks(self, level): + """Declare LU locking needs for a level + + While most LUs can just declare their locking needs at ExpandNames time, + sometimes there's the need to calculate some locks after having acquired + the ones before. This function is called just before acquiring locks at a + particular level, but after acquiring the ones at lower levels, and permits + such calculations. It can be used to modify self.needed_locks, and by + default it does nothing. + + This function is only called if you have something already set in + self.needed_locks for the level. + + @param level: Locking level which is going to be locked + @type level: member of ganeti.locking.LEVELS + + """ + def CheckPrereq(self): """Check prerequisites for this LU. @@ -165,7 +213,13 @@ class LogicalUnit(object): their canonical form if it hasn't been done by ExpandNames before. """ - raise NotImplementedError + if self.tasklets is not None: + for (idx, tl) in enumerate(self.tasklets): + logging.debug("Checking prerequisites for tasklet %s/%s", + idx + 1, len(self.tasklets)) + tl.CheckPrereq() + else: + raise NotImplementedError def Exec(self, feedback_fn): """Execute the LU. @@ -175,7 +229,12 @@ class LogicalUnit(object): code, or expected. """ - raise NotImplementedError + if self.tasklets is not None: + for (idx, tl) in enumerate(self.tasklets): + logging.debug("Executing tasklet %s/%s", idx + 1, len(self.tasklets)) + tl.Exec(feedback_fn) + else: + raise NotImplementedError def BuildHooksEnv(self): """Build hooks environment for this LU. @@ -208,15 +267,85 @@ class LogicalUnit(object): previous result is passed back unchanged but any LU can define it if it wants to use the local cluster hook-scripts somehow. - Args: - phase: the hooks phase that has just been run - hooks_results: the results of the multi-node hooks rpc call - feedback_fn: function to send feedback back to the caller - lu_result: the previous result this LU had, or None in the PRE phase. + @param phase: one of L{constants.HOOKS_PHASE_POST} or + L{constants.HOOKS_PHASE_PRE}; it denotes the hooks phase + @param hook_results: the results of the multi-node hooks rpc call + @param feedback_fn: function used send feedback back to the caller + @param lu_result: the previous Exec result this LU had, or None + in the PRE phase + @return: the new Exec result, based on the previous result + and hook results """ return lu_result + def _ExpandAndLockInstance(self): + """Helper function to expand and lock an instance. + + Many LUs that work on an instance take its name in self.op.instance_name + and need to expand it and then declare the expanded name for locking. This + function does it, and then updates self.op.instance_name to the expanded + name. It also initializes needed_locks as a dict, if this hasn't been done + before. + + """ + if self.needed_locks is None: + self.needed_locks = {} + else: + assert locking.LEVEL_INSTANCE not in self.needed_locks, \ + "_ExpandAndLockInstance called with instance-level locks set" + expanded_name = self.cfg.ExpandInstanceName(self.op.instance_name) + if expanded_name is None: + raise errors.OpPrereqError("Instance '%s' not known" % + self.op.instance_name) + self.needed_locks[locking.LEVEL_INSTANCE] = expanded_name + self.op.instance_name = expanded_name + + def _LockInstancesNodes(self, primary_only=False): + """Helper function to declare instances' nodes for locking. + + This function should be called after locking one or more instances to lock + their nodes. Its effect is populating self.needed_locks[locking.LEVEL_NODE] + with all primary or secondary nodes for instances already locked and + present in self.needed_locks[locking.LEVEL_INSTANCE]. + + It should be called from DeclareLocks, and for safety only works if + self.recalculate_locks[locking.LEVEL_NODE] is set. + + In the future it may grow parameters to just lock some instance's nodes, or + to just lock primaries or secondary nodes, if needed. + + If should be called in DeclareLocks in a way similar to:: + + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() + + @type primary_only: boolean + @param primary_only: only lock primary nodes of locked instances + + """ + assert locking.LEVEL_NODE in self.recalculate_locks, \ + "_LockInstancesNodes helper function called with no nodes to recalculate" + + # TODO: check if we're really been called with the instance locks held + + # For now we'll replace self.needed_locks[locking.LEVEL_NODE], but in the + # future we might want to have different behaviors depending on the value + # of self.recalculate_locks[locking.LEVEL_NODE] + wanted_nodes = [] + for instance_name in self.acquired_locks[locking.LEVEL_INSTANCE]: + instance = self.context.cfg.GetInstanceInfo(instance_name) + wanted_nodes.append(instance.primary_node) + if not primary_only: + wanted_nodes.extend(instance.secondary_nodes) + + if self.recalculate_locks[locking.LEVEL_NODE] == constants.LOCKS_REPLACE: + self.needed_locks[locking.LEVEL_NODE] = wanted_nodes + elif self.recalculate_locks[locking.LEVEL_NODE] == constants.LOCKS_APPEND: + self.needed_locks[locking.LEVEL_NODE].extend(wanted_nodes) + + del self.recalculate_locks[locking.LEVEL_NODE] + class NoHooksLU(LogicalUnit): """Simple LU which runs no hooks. @@ -229,35 +358,92 @@ class NoHooksLU(LogicalUnit): HTYPE = None +class Tasklet: + """Tasklet base class. + + Tasklets are subcomponents for LUs. LUs can consist entirely of tasklets or + they can mix legacy code with tasklets. Locking needs to be done in the LU, + tasklets know nothing about locks. + + Subclasses must follow these rules: + - Implement CheckPrereq + - Implement Exec + + """ + def __init__(self, lu): + self.lu = lu + + # Shortcuts + self.cfg = lu.cfg + self.rpc = lu.rpc + + def CheckPrereq(self): + """Check prerequisites for this tasklets. + + This method should check whether the prerequisites for the execution of + this tasklet are fulfilled. It can do internode communication, but it + should be idempotent - no cluster or system changes are allowed. + + The method should raise errors.OpPrereqError in case something is not + fulfilled. Its return value is ignored. + + This method should also update all parameters to their canonical form if it + hasn't been done before. + + """ + raise NotImplementedError + + def Exec(self, feedback_fn): + """Execute the tasklet. + + This method should implement the actual work. It should raise + errors.OpExecError for failures that are somewhat dealt with in code, or + expected. + + """ + raise NotImplementedError + + def _GetWantedNodes(lu, nodes): """Returns list of checked and expanded node names. - Args: - nodes: List of nodes (strings) or None for all + @type lu: L{LogicalUnit} + @param lu: the logical unit on whose behalf we execute + @type nodes: list + @param nodes: list of node names or None for all nodes + @rtype: list + @return: the list of nodes, sorted + @raise errors.OpProgrammerError: if the nodes parameter is wrong type """ if not isinstance(nodes, list): raise errors.OpPrereqError("Invalid argument type 'nodes'") - if nodes: - wanted = [] + if not nodes: + raise errors.ProgrammerError("_GetWantedNodes should only be called with a" + " non-empty list of nodes whose name is to be expanded.") - for name in nodes: - node = lu.cfg.ExpandNodeName(name) - if node is None: - raise errors.OpPrereqError("No such node name '%s'" % name) - wanted.append(node) + wanted = [] + for name in nodes: + node = lu.cfg.ExpandNodeName(name) + if node is None: + raise errors.OpPrereqError("No such node name '%s'" % name) + wanted.append(node) - else: - wanted = lu.cfg.GetNodeList() return utils.NiceSort(wanted) def _GetWantedInstances(lu, instances): """Returns list of checked and expanded instance names. - Args: - instances: List of instances (strings) or None for all + @type lu: L{LogicalUnit} + @param lu: the logical unit on whose behalf we execute + @type instances: list + @param instances: list of instance names or None for all instances + @rtype: list + @return: the list of instances, sorted + @raise errors.OpPrereqError: if the instances parameter is wrong type + @raise errors.OpPrereqError: if any of the passed instances is not found """ if not isinstance(instances, list): @@ -273,112 +459,368 @@ def _GetWantedInstances(lu, instances): wanted.append(instance) else: - wanted = lu.cfg.GetInstanceList() - return utils.NiceSort(wanted) + wanted = utils.NiceSort(lu.cfg.GetInstanceList()) + return wanted def _CheckOutputFields(static, dynamic, selected): """Checks whether all selected fields are valid. - Args: - static: Static fields - dynamic: Dynamic fields + @type static: L{utils.FieldSet} + @param static: static fields set + @type dynamic: L{utils.FieldSet} + @param dynamic: dynamic fields set """ - static_fields = frozenset(static) - dynamic_fields = frozenset(dynamic) + f = utils.FieldSet() + f.Extend(static) + f.Extend(dynamic) - all_fields = static_fields | dynamic_fields - - if not all_fields.issuperset(selected): + delta = f.NonMatching(selected) + if delta: raise errors.OpPrereqError("Unknown output fields selected: %s" - % ",".join(frozenset(selected). - difference(all_fields))) + % ",".join(delta)) + + +def _CheckBooleanOpField(op, name): + """Validates boolean opcode parameters. + + This will ensure that an opcode parameter is either a boolean value, + or None (but that it always exists). + + """ + val = getattr(op, name, None) + if not (val is None or isinstance(val, bool)): + raise errors.OpPrereqError("Invalid boolean parameter '%s' (%s)" % + (name, str(val))) + setattr(op, name, val) + + +def _CheckNodeOnline(lu, node): + """Ensure that a given node is online. + + @param lu: the LU on behalf of which we make the check + @param node: the node to check + @raise errors.OpPrereqError: if the node is offline + + """ + if lu.cfg.GetNodeInfo(node).offline: + raise errors.OpPrereqError("Can't use offline node %s" % node) + + +def _CheckNodeNotDrained(lu, node): + """Ensure that a given node is not drained. + + @param lu: the LU on behalf of which we make the check + @param node: the node to check + @raise errors.OpPrereqError: if the node is drained + + """ + if lu.cfg.GetNodeInfo(node).drained: + raise errors.OpPrereqError("Can't use drained node %s" % node) def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status, - memory, vcpus, nics): - """Builds instance related env variables for hooks from single variables. + memory, vcpus, nics, disk_template, disks, + bep, hvp, hypervisor_name): + """Builds instance related env variables for hooks + + This builds the hook environment from individual variables. + + @type name: string + @param name: the name of the instance + @type primary_node: string + @param primary_node: the name of the instance's primary node + @type secondary_nodes: list + @param secondary_nodes: list of secondary nodes as strings + @type os_type: string + @param os_type: the name of the instance's OS + @type status: boolean + @param status: the should_run status of the instance + @type memory: string + @param memory: the memory size of the instance + @type vcpus: string + @param vcpus: the count of VCPUs the instance has + @type nics: list + @param nics: list of tuples (ip, mac, mode, link) representing + the NICs the instance has + @type disk_template: string + @param disk_template: the disk template of the instance + @type disks: list + @param disks: the list of (size, mode) pairs + @type bep: dict + @param bep: the backend parameters for the instance + @type hvp: dict + @param hvp: the hypervisor parameters for the instance + @type hypervisor_name: string + @param hypervisor_name: the hypervisor for the instance + @rtype: dict + @return: the hook environment for this instance - Args: - secondary_nodes: List of secondary nodes as strings """ + if status: + str_status = "up" + else: + str_status = "down" env = { "OP_TARGET": name, "INSTANCE_NAME": name, "INSTANCE_PRIMARY": primary_node, "INSTANCE_SECONDARIES": " ".join(secondary_nodes), "INSTANCE_OS_TYPE": os_type, - "INSTANCE_STATUS": status, + "INSTANCE_STATUS": str_status, "INSTANCE_MEMORY": memory, "INSTANCE_VCPUS": vcpus, + "INSTANCE_DISK_TEMPLATE": disk_template, + "INSTANCE_HYPERVISOR": hypervisor_name, } if nics: nic_count = len(nics) - for idx, (ip, bridge, mac) in enumerate(nics): + for idx, (ip, mac, mode, link) in enumerate(nics): if ip is None: ip = "" env["INSTANCE_NIC%d_IP" % idx] = ip - env["INSTANCE_NIC%d_BRIDGE" % idx] = bridge - env["INSTANCE_NIC%d_HWADDR" % idx] = mac + env["INSTANCE_NIC%d_MAC" % idx] = mac + env["INSTANCE_NIC%d_MODE" % idx] = mode + env["INSTANCE_NIC%d_LINK" % idx] = link + if mode == constants.NIC_MODE_BRIDGED: + env["INSTANCE_NIC%d_BRIDGE" % idx] = link else: nic_count = 0 env["INSTANCE_NIC_COUNT"] = nic_count + if disks: + disk_count = len(disks) + for idx, (size, mode) in enumerate(disks): + env["INSTANCE_DISK%d_SIZE" % idx] = size + env["INSTANCE_DISK%d_MODE" % idx] = mode + else: + disk_count = 0 + + env["INSTANCE_DISK_COUNT"] = disk_count + + for source, kind in [(bep, "BE"), (hvp, "HV")]: + for key, value in source.items(): + env["INSTANCE_%s_%s" % (kind, key)] = value + return env -def _BuildInstanceHookEnvByObject(instance, override=None): +def _NICListToTuple(lu, nics): + """Build a list of nic information tuples. + + This list is suitable to be passed to _BuildInstanceHookEnv or as a return + value in LUQueryInstanceData. + + @type lu: L{LogicalUnit} + @param lu: the logical unit on whose behalf we execute + @type nics: list of L{objects.NIC} + @param nics: list of nics to convert to hooks tuples + + """ + hooks_nics = [] + c_nicparams = lu.cfg.GetClusterInfo().nicparams[constants.PP_DEFAULT] + for nic in nics: + ip = nic.ip + mac = nic.mac + filled_params = objects.FillDict(c_nicparams, nic.nicparams) + mode = filled_params[constants.NIC_MODE] + link = filled_params[constants.NIC_LINK] + hooks_nics.append((ip, mac, mode, link)) + return hooks_nics + + +def _BuildInstanceHookEnvByObject(lu, instance, override=None): """Builds instance related env variables for hooks from an object. - Args: - instance: objects.Instance object of instance - override: dict of values to override + @type lu: L{LogicalUnit} + @param lu: the logical unit on whose behalf we execute + @type instance: L{objects.Instance} + @param instance: the instance for which we should build the + environment + @type override: dict + @param override: dictionary with key/values that will override + our values + @rtype: dict + @return: the hook environment dictionary + """ + cluster = lu.cfg.GetClusterInfo() + bep = cluster.FillBE(instance) + hvp = cluster.FillHV(instance) args = { 'name': instance.name, 'primary_node': instance.primary_node, 'secondary_nodes': instance.secondary_nodes, 'os_type': instance.os, - 'status': instance.os, - 'memory': instance.memory, - 'vcpus': instance.vcpus, - 'nics': [(nic.ip, nic.bridge, nic.mac) for nic in instance.nics], + 'status': instance.admin_up, + 'memory': bep[constants.BE_MEMORY], + 'vcpus': bep[constants.BE_VCPUS], + 'nics': _NICListToTuple(lu, instance.nics), + 'disk_template': instance.disk_template, + 'disks': [(disk.size, disk.mode) for disk in instance.disks], + 'bep': bep, + 'hvp': hvp, + 'hypervisor_name': instance.hypervisor, } if override: args.update(override) return _BuildInstanceHookEnv(**args) -def _CheckInstanceBridgesExist(instance): +def _AdjustCandidatePool(lu): + """Adjust the candidate pool after node operations. + + """ + mod_list = lu.cfg.MaintainCandidatePool() + if mod_list: + lu.LogInfo("Promoted nodes to master candidate role: %s", + ", ".join(node.name for node in mod_list)) + for name in mod_list: + lu.context.ReaddNode(name) + mc_now, mc_max = lu.cfg.GetMasterCandidateStats() + if mc_now > mc_max: + lu.LogInfo("Note: more nodes are candidates (%d) than desired (%d)" % + (mc_now, mc_max)) + + +def _CheckNicsBridgesExist(lu, target_nics, target_node, + profile=constants.PP_DEFAULT): + """Check that the brigdes needed by a list of nics exist. + + """ + c_nicparams = lu.cfg.GetClusterInfo().nicparams[profile] + paramslist = [objects.FillDict(c_nicparams, nic.nicparams) + for nic in target_nics] + brlist = [params[constants.NIC_LINK] for params in paramslist + if params[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED] + if brlist: + result = lu.rpc.call_bridges_exist(target_node, brlist) + result.Raise("Error checking bridges on destination node '%s'" % + target_node, prereq=True) + + +def _CheckInstanceBridgesExist(lu, instance, node=None): """Check that the brigdes needed by an instance exist. """ - # check bridges existance - brlist = [nic.bridge for nic in instance.nics] - if not rpc.call_bridges_exist(instance.primary_node, brlist): - raise errors.OpPrereqError("one or more target bridges %s does not" - " exist on destination node '%s'" % - (brlist, instance.primary_node)) + if node is None: + node = instance.primary_node + _CheckNicsBridgesExist(lu, instance.nics, node) + + +def _GetNodeInstancesInner(cfg, fn): + return [i for i in cfg.GetAllInstancesInfo().values() if fn(i)] + + +def _GetNodeInstances(cfg, node_name): + """Returns a list of all primary and secondary instances on a node. + + """ + + return _GetNodeInstancesInner(cfg, lambda inst: node_name in inst.all_nodes) + + +def _GetNodePrimaryInstances(cfg, node_name): + """Returns primary instances on a node. + + """ + return _GetNodeInstancesInner(cfg, + lambda inst: node_name == inst.primary_node) + + +def _GetNodeSecondaryInstances(cfg, node_name): + """Returns secondary instances on a node. + + """ + return _GetNodeInstancesInner(cfg, + lambda inst: node_name in inst.secondary_nodes) + + +def _GetStorageTypeArgs(cfg, storage_type): + """Returns the arguments for a storage type. + + """ + # Special case for file storage + if storage_type == constants.ST_FILE: + # storage.FileStorage wants a list of storage directories + return [[cfg.GetFileStorageDir()]] + + return [] + + +def _FindFaultyInstanceDisks(cfg, rpc, instance, node_name, prereq): + faulty = [] + + for dev in instance.disks: + cfg.SetDiskID(dev, node_name) + + result = rpc.call_blockdev_getmirrorstatus(node_name, instance.disks) + result.Raise("Failed to get disk status from node %s" % node_name, + prereq=prereq) + + for idx, bdev_status in enumerate(result.payload): + if bdev_status and bdev_status.ldisk_status == constants.LDS_FAULTY: + faulty.append(idx) + + return faulty + + +class LUPostInitCluster(LogicalUnit): + """Logical unit for running hooks after cluster initialization. + + """ + HPATH = "cluster-init" + HTYPE = constants.HTYPE_CLUSTER + _OP_REQP = [] + def BuildHooksEnv(self): + """Build hooks env. + + """ + env = {"OP_TARGET": self.cfg.GetClusterName()} + mn = self.cfg.GetMasterNode() + return env, [], [mn] + + def CheckPrereq(self): + """No prerequisites to check. + + """ + return True + + def Exec(self, feedback_fn): + """Nothing to do. + + """ + return True -class LUDestroyCluster(NoHooksLU): + +class LUDestroyCluster(LogicalUnit): """Logical unit for destroying the cluster. """ + HPATH = "cluster-destroy" + HTYPE = constants.HTYPE_CLUSTER _OP_REQP = [] + def BuildHooksEnv(self): + """Build hooks env. + + """ + env = {"OP_TARGET": self.cfg.GetClusterName()} + return env, [], [] + def CheckPrereq(self): """Check prerequisites. This checks whether the cluster is empty. - Any errors are signalled by raising errors.OpPrereqError. + Any errors are signaled by raising errors.OpPrereqError. """ - master = self.sstore.GetMasterNode() + master = self.cfg.GetMasterNode() nodelist = self.cfg.GetNodeList() if len(nodelist) != 1 or nodelist[0] != master: @@ -393,13 +835,21 @@ class LUDestroyCluster(NoHooksLU): """Destroys the cluster. """ - master = self.sstore.GetMasterNode() - if not rpc.call_node_stop_master(master): - raise errors.OpExecError("Could not disable the master role") + master = self.cfg.GetMasterNode() + + # Run post hooks on master node before it's removed + hm = self.proc.hmclass(self.rpc.call_hooks_runner, self) + try: + hm.RunPhase(constants.HOOKS_PHASE_POST, [master]) + except: + self.LogWarning("Errors occurred running hooks on %s" % master) + + result = self.rpc.call_node_stop_master(master, False) + result.Raise("Could not disable the master role") priv_key, pub_key, _ = ssh.GetUserFiles(constants.GANETI_RUNAS) utils.CreateBackup(priv_key) utils.CreateBackup(pub_key) - rpc.call_node_leave_cluster(master) + return master class LUVerifyCluster(LogicalUnit): @@ -408,169 +858,296 @@ class LUVerifyCluster(LogicalUnit): """ HPATH = "cluster-verify" HTYPE = constants.HTYPE_CLUSTER - _OP_REQP = ["skip_checks"] + _OP_REQP = ["skip_checks", "verbose", "error_codes", "debug_simulate_errors"] + REQ_BGL = False + + TCLUSTER = "cluster" + TNODE = "node" + TINSTANCE = "instance" + + ECLUSTERCFG = (TCLUSTER, "ECLUSTERCFG") + EINSTANCEBADNODE = (TINSTANCE, "EINSTANCEBADNODE") + EINSTANCEDOWN = (TINSTANCE, "EINSTANCEDOWN") + EINSTANCELAYOUT = (TINSTANCE, "EINSTANCELAYOUT") + EINSTANCEMISSINGDISK = (TINSTANCE, "EINSTANCEMISSINGDISK") + EINSTANCEMISSINGDISK = (TINSTANCE, "EINSTANCEMISSINGDISK") + EINSTANCEWRONGNODE = (TINSTANCE, "EINSTANCEWRONGNODE") + ENODEDRBD = (TNODE, "ENODEDRBD") + ENODEFILECHECK = (TNODE, "ENODEFILECHECK") + ENODEHOOKS = (TNODE, "ENODEHOOKS") + ENODEHV = (TNODE, "ENODEHV") + ENODELVM = (TNODE, "ENODELVM") + ENODEN1 = (TNODE, "ENODEN1") + ENODENET = (TNODE, "ENODENET") + ENODEORPHANINSTANCE = (TNODE, "ENODEORPHANINSTANCE") + ENODEORPHANLV = (TNODE, "ENODEORPHANLV") + ENODERPC = (TNODE, "ENODERPC") + ENODESSH = (TNODE, "ENODESSH") + ENODEVERSION = (TNODE, "ENODEVERSION") + + ETYPE_FIELD = "code" + ETYPE_ERROR = "ERROR" + ETYPE_WARNING = "WARNING" + + def ExpandNames(self): + self.needed_locks = { + locking.LEVEL_NODE: locking.ALL_SET, + locking.LEVEL_INSTANCE: locking.ALL_SET, + } + self.share_locks = dict.fromkeys(locking.LEVELS, 1) + + def _Error(self, ecode, item, msg, *args, **kwargs): + """Format an error message. + + Based on the opcode's error_codes parameter, either format a + parseable error code, or a simpler error string. - def _VerifyNode(self, node, file_list, local_cksum, vglist, node_result, - remote_version, feedback_fn): + This must be called only from Exec and functions called from Exec. + + """ + ltype = kwargs.get(self.ETYPE_FIELD, self.ETYPE_ERROR) + itype, etxt = ecode + # first complete the msg + if args: + msg = msg % args + # then format the whole message + if self.op.error_codes: + msg = "%s:%s:%s:%s:%s" % (ltype, etxt, itype, item, msg) + else: + if item: + item = " " + item + else: + item = "" + msg = "%s: %s%s: %s" % (ltype, itype, item, msg) + # and finally report it via the feedback_fn + self._feedback_fn(" - %s" % msg) + + def _ErrorIf(self, cond, *args, **kwargs): + """Log an error message if the passed condition is True. + + """ + cond = bool(cond) or self.op.debug_simulate_errors + if cond: + self._Error(*args, **kwargs) + # do not mark the operation as failed for WARN cases only + if kwargs.get(self.ETYPE_FIELD, self.ETYPE_ERROR) == self.ETYPE_ERROR: + self.bad = self.bad or cond + + def _VerifyNode(self, nodeinfo, file_list, local_cksum, + node_result, master_files, drbd_map, vg_name): """Run multiple tests against a node. Test list: + - compares ganeti version - - checks vg existance and size > 20G + - checks vg existence and size > 20G - checks config file checksum - checks ssh to other nodes - Args: - node: name of the node to check - file_list: required list of files - local_cksum: dictionary of local files and their checksums + @type nodeinfo: L{objects.Node} + @param nodeinfo: the node to check + @param file_list: required list of files + @param local_cksum: dictionary of local files and their checksums + @param node_result: the results from the node + @param master_files: list of files that only masters should have + @param drbd_map: the useddrbd minors for this node, in + form of minor: (instance, must_exist) which correspond to instances + and their running status + @param vg_name: Ganeti Volume Group (result of self.cfg.GetVGName()) """ + node = nodeinfo.name + _ErrorIf = self._ErrorIf + + # main result, node_result should be a non-empty dict + test = not node_result or not isinstance(node_result, dict) + _ErrorIf(test, self.ENODERPC, node, + "unable to verify node: no data returned") + if test: + return + # compares ganeti version local_version = constants.PROTOCOL_VERSION - if not remote_version: - feedback_fn(" - ERROR: connection to %s failed" % (node)) - return True - - if local_version != remote_version: - feedback_fn(" - ERROR: sw version mismatch: master %s, node(%s) %s" % - (local_version, node, remote_version)) - return True - - # checks vg existance and size > 20G - - bad = False - if not vglist: - feedback_fn(" - ERROR: unable to check volume groups on node %s." % - (node,)) - bad = True - else: - vgstatus = utils.CheckVolumeGroupSize(vglist, self.cfg.GetVGName(), - constants.MIN_VG_SIZE) - if vgstatus: - feedback_fn(" - ERROR: %s on node %s" % (vgstatus, node)) - bad = True + remote_version = node_result.get('version', None) + test = not (remote_version and + isinstance(remote_version, (list, tuple)) and + len(remote_version) == 2) + _ErrorIf(test, self.ENODERPC, node, + "connection to node returned invalid data") + if test: + return + + test = local_version != remote_version[0] + _ErrorIf(test, self.ENODEVERSION, node, + "incompatible protocol versions: master %s," + " node %s", local_version, remote_version[0]) + if test: + return + + # node seems compatible, we can actually try to look into its results + + # full package version + self._ErrorIf(constants.RELEASE_VERSION != remote_version[1], + self.ENODEVERSION, node, + "software version mismatch: master %s, node %s", + constants.RELEASE_VERSION, remote_version[1], + code=self.ETYPE_WARNING) + + # checks vg existence and size > 20G + if vg_name is not None: + vglist = node_result.get(constants.NV_VGLIST, None) + test = not vglist + _ErrorIf(test, self.ENODELVM, node, "unable to check volume groups") + if not test: + vgstatus = utils.CheckVolumeGroupSize(vglist, vg_name, + constants.MIN_VG_SIZE) + _ErrorIf(vgstatus, self.ENODELVM, node, vgstatus) # checks config file checksum - # checks ssh to any - if 'filelist' not in node_result: - bad = True - feedback_fn(" - ERROR: node hasn't returned file checksum data") - else: - remote_cksum = node_result['filelist'] + remote_cksum = node_result.get(constants.NV_FILELIST, None) + test = not isinstance(remote_cksum, dict) + _ErrorIf(test, self.ENODEFILECHECK, node, + "node hasn't returned file checksum data") + if not test: for file_name in file_list: - if file_name not in remote_cksum: - bad = True - feedback_fn(" - ERROR: file '%s' missing" % file_name) - elif remote_cksum[file_name] != local_cksum[file_name]: - bad = True - feedback_fn(" - ERROR: file '%s' has wrong checksum" % file_name) - - if 'nodelist' not in node_result: - bad = True - feedback_fn(" - ERROR: node hasn't returned node ssh connectivity data") - else: - if node_result['nodelist']: - bad = True - for node in node_result['nodelist']: - feedback_fn(" - ERROR: ssh communication with node '%s': %s" % - (node, node_result['nodelist'][node])) - if 'node-net-test' not in node_result: - bad = True - feedback_fn(" - ERROR: node hasn't returned node tcp connectivity data") - else: - if node_result['node-net-test']: - bad = True - nlist = utils.NiceSort(node_result['node-net-test'].keys()) - for node in nlist: - feedback_fn(" - ERROR: tcp communication with node '%s': %s" % - (node, node_result['node-net-test'][node])) - - hyp_result = node_result.get('hypervisor', None) - if hyp_result is not None: - feedback_fn(" - ERROR: hypervisor verify failure: '%s'" % hyp_result) - return bad + node_is_mc = nodeinfo.master_candidate + must_have = (file_name not in master_files) or node_is_mc + # missing + test1 = file_name not in remote_cksum + # invalid checksum + test2 = not test1 and remote_cksum[file_name] != local_cksum[file_name] + # existing and good + test3 = not test1 and remote_cksum[file_name] == local_cksum[file_name] + _ErrorIf(test1 and must_have, self.ENODEFILECHECK, node, + "file '%s' missing", file_name) + _ErrorIf(test2 and must_have, self.ENODEFILECHECK, node, + "file '%s' has wrong checksum", file_name) + # not candidate and this is not a must-have file + _ErrorIf(test2 and not must_have, self.ENODEFILECHECK, node, + "file '%s' should not exist on non master" + " candidates (and the file is outdated)", file_name) + # all good, except non-master/non-must have combination + _ErrorIf(test3 and not must_have, self.ENODEFILECHECK, node, + "file '%s' should not exist" + " on non master candidates", file_name) + + # checks ssh to any + + test = constants.NV_NODELIST not in node_result + _ErrorIf(test, self.ENODESSH, node, + "node hasn't returned node ssh connectivity data") + if not test: + if node_result[constants.NV_NODELIST]: + for a_node, a_msg in node_result[constants.NV_NODELIST].items(): + _ErrorIf(True, self.ENODESSH, node, + "ssh communication with node '%s': %s", a_node, a_msg) + + test = constants.NV_NODENETTEST not in node_result + _ErrorIf(test, self.ENODENET, node, + "node hasn't returned node tcp connectivity data") + if not test: + if node_result[constants.NV_NODENETTEST]: + nlist = utils.NiceSort(node_result[constants.NV_NODENETTEST].keys()) + for anode in nlist: + _ErrorIf(True, self.ENODENET, node, + "tcp communication with node '%s': %s", + anode, node_result[constants.NV_NODENETTEST][anode]) + + hyp_result = node_result.get(constants.NV_HYPERVISOR, None) + if isinstance(hyp_result, dict): + for hv_name, hv_result in hyp_result.iteritems(): + test = hv_result is not None + _ErrorIf(test, self.ENODEHV, node, + "hypervisor %s verify failure: '%s'", hv_name, hv_result) + + # check used drbd list + if vg_name is not None: + used_minors = node_result.get(constants.NV_DRBDLIST, []) + test = not isinstance(used_minors, (tuple, list)) + _ErrorIf(test, self.ENODEDRBD, node, + "cannot parse drbd status file: %s", str(used_minors)) + if not test: + for minor, (iname, must_exist) in drbd_map.items(): + test = minor not in used_minors and must_exist + _ErrorIf(test, self.ENODEDRBD, node, + "drbd minor %d of instance %s is not active", + minor, iname) + for minor in used_minors: + test = minor not in drbd_map + _ErrorIf(test, self.ENODEDRBD, node, + "unallocated drbd minor %d is in use", minor) def _VerifyInstance(self, instance, instanceconfig, node_vol_is, - node_instance, feedback_fn): + node_instance, n_offline): """Verify an instance. This function checks to see if the required block devices are available on the instance's node. """ - bad = False - + _ErrorIf = self._ErrorIf node_current = instanceconfig.primary_node node_vol_should = {} instanceconfig.MapLVsByNode(node_vol_should) for node in node_vol_should: + if node in n_offline: + # ignore missing volumes on offline nodes + continue for volume in node_vol_should[node]: - if node not in node_vol_is or volume not in node_vol_is[node]: - feedback_fn(" - ERROR: volume %s missing on node %s" % - (volume, node)) - bad = True - - if not instanceconfig.status == 'down': - if (node_current not in node_instance or - not instance in node_instance[node_current]): - feedback_fn(" - ERROR: instance %s not running on node %s" % - (instance, node_current)) - bad = True + test = node not in node_vol_is or volume not in node_vol_is[node] + _ErrorIf(test, self.EINSTANCEMISSINGDISK, instance, + "volume %s missing on node %s", volume, node) + + if instanceconfig.admin_up: + test = ((node_current not in node_instance or + not instance in node_instance[node_current]) and + node_current not in n_offline) + _ErrorIf(test, self.EINSTANCEDOWN, instance, + "instance not running on its primary node %s", + node_current) for node in node_instance: if (not node == node_current): - if instance in node_instance[node]: - feedback_fn(" - ERROR: instance %s should not run on node %s" % - (instance, node)) - bad = True - - return bad + test = instance in node_instance[node] + _ErrorIf(test, self.EINSTANCEWRONGNODE, instance, + "instance should not run on node %s", node) - def _VerifyOrphanVolumes(self, node_vol_should, node_vol_is, feedback_fn): + def _VerifyOrphanVolumes(self, node_vol_should, node_vol_is): """Verify if there are any unknown volumes in the cluster. The .os, .swap and backup volumes are ignored. All other volumes are reported as unknown. """ - bad = False - for node in node_vol_is: for volume in node_vol_is[node]: - if node not in node_vol_should or volume not in node_vol_should[node]: - feedback_fn(" - ERROR: volume %s on node %s should not exist" % - (volume, node)) - bad = True - return bad + test = (node not in node_vol_should or + volume not in node_vol_should[node]) + self._ErrorIf(test, self.ENODEORPHANLV, node, + "volume %s is unknown", volume) - def _VerifyOrphanInstances(self, instancelist, node_instance, feedback_fn): + def _VerifyOrphanInstances(self, instancelist, node_instance): """Verify the list of running instances. This checks what instances are running but unknown to the cluster. """ - bad = False for node in node_instance: - for runninginstance in node_instance[node]: - if runninginstance not in instancelist: - feedback_fn(" - ERROR: instance %s on node %s should not exist" % - (runninginstance, node)) - bad = True - return bad - - def _VerifyNPlusOneMemory(self, node_info, instance_cfg, feedback_fn): + for o_inst in node_instance[node]: + test = o_inst not in instancelist + self._ErrorIf(test, self.ENODEORPHANINSTANCE, node, + "instance %s on node %s should not exist", o_inst, node) + + def _VerifyNPlusOneMemory(self, node_info, instance_cfg): """Verify N+1 Memory Resilience. Check that if one single node dies we can still start all the instances it was primary for. """ - bad = False - for node, nodeinfo in node_info.iteritems(): # This code checks that every node which is now listed as secondary has # enough memory to host all instances it is supposed to should a single @@ -583,12 +1160,13 @@ class LUVerifyCluster(LogicalUnit): for prinode, instances in nodeinfo['sinst-by-pnode'].iteritems(): needed_mem = 0 for instance in instances: - needed_mem += instance_cfg[instance].memory - if nodeinfo['mfree'] < needed_mem: - feedback_fn(" - ERROR: not enough memory on node %s to accomodate" - " failovers should node %s fail" % (node, prinode)) - bad = True - return bad + bep = self.cfg.GetClusterInfo().FillBE(instance_cfg[instance]) + if bep[constants.BE_AUTO_BALANCE]: + needed_mem += bep[constants.BE_MEMORY] + test = nodeinfo['mfree'] < needed_mem + self._ErrorIf(test, self.ENODEN1, node, + "not enough memory on to accommodate" + " failovers should peer node %s fail", prinode) def CheckPrereq(self): """Check prerequisites. @@ -604,29 +1182,42 @@ class LUVerifyCluster(LogicalUnit): def BuildHooksEnv(self): """Build hooks env. - Cluster-Verify hooks just rone in the post phase and their failure makes + Cluster-Verify hooks just ran in the post phase and their failure makes the output be logged in the verify output and the verification to fail. """ all_nodes = self.cfg.GetNodeList() - # TODO: populate the environment with useful information for verify hooks - env = {} + env = { + "CLUSTER_TAGS": " ".join(self.cfg.GetClusterInfo().GetTags()) + } + for node in self.cfg.GetAllNodesInfo().values(): + env["NODE_TAGS_%s" % node.name] = " ".join(node.GetTags()) + return env, [], all_nodes def Exec(self, feedback_fn): """Verify integrity of cluster, performing various test on nodes. """ - bad = False + self.bad = False + _ErrorIf = self._ErrorIf + verbose = self.op.verbose + self._feedback_fn = feedback_fn feedback_fn("* Verifying global settings") for msg in self.cfg.VerifyConfig(): - feedback_fn(" - ERROR: %s" % msg) + _ErrorIf(True, self.ECLUSTERCFG, None, msg) vg_name = self.cfg.GetVGName() + hypervisors = self.cfg.GetClusterInfo().enabled_hypervisors nodelist = utils.NiceSort(self.cfg.GetNodeList()) nodeinfo = [self.cfg.GetNodeInfo(nname) for nname in nodelist] instancelist = utils.NiceSort(self.cfg.GetInstanceList()) + instanceinfo = dict((iname, self.cfg.GetInstanceInfo(iname)) + for iname in instancelist) i_non_redundant = [] # Non redundant instances + i_non_a_balanced = [] # Non auto-balanced instances + n_offline = [] # List of offline nodes + n_drained = [] # List of nodes being drained node_volume = {} node_instance = {} node_info = {} @@ -634,68 +1225,116 @@ class LUVerifyCluster(LogicalUnit): # FIXME: verify OS list # do local checksums - file_names = list(self.sstore.GetFileList()) + master_files = [constants.CLUSTER_CONF_FILE] + + file_names = ssconf.SimpleStore().GetFileList() file_names.append(constants.SSL_CERT_FILE) - file_names.append(constants.CLUSTER_CONF_FILE) + file_names.append(constants.RAPI_CERT_FILE) + file_names.extend(master_files) + local_checksums = utils.FingerprintFiles(file_names) feedback_fn("* Gathering data (%d nodes)" % len(nodelist)) - all_volumeinfo = rpc.call_volume_list(nodelist, vg_name) - all_instanceinfo = rpc.call_instance_list(nodelist) - all_vglist = rpc.call_vg_list(nodelist) node_verify_param = { - 'filelist': file_names, - 'nodelist': nodelist, - 'hypervisor': None, - 'node-net-test': [(node.name, node.primary_ip, node.secondary_ip) - for node in nodeinfo] + constants.NV_FILELIST: file_names, + constants.NV_NODELIST: [node.name for node in nodeinfo + if not node.offline], + constants.NV_HYPERVISOR: hypervisors, + constants.NV_NODENETTEST: [(node.name, node.primary_ip, + node.secondary_ip) for node in nodeinfo + if not node.offline], + constants.NV_INSTANCELIST: hypervisors, + constants.NV_VERSION: None, + constants.NV_HVINFO: self.cfg.GetHypervisorType(), } - all_nvinfo = rpc.call_node_verify(nodelist, node_verify_param) - all_rversion = rpc.call_version(nodelist) - all_ninfo = rpc.call_node_info(nodelist, self.cfg.GetVGName()) + if vg_name is not None: + node_verify_param[constants.NV_VGLIST] = None + node_verify_param[constants.NV_LVLIST] = vg_name + node_verify_param[constants.NV_DRBDLIST] = None + all_nvinfo = self.rpc.call_node_verify(nodelist, node_verify_param, + self.cfg.GetClusterName()) + + cluster = self.cfg.GetClusterInfo() + master_node = self.cfg.GetMasterNode() + all_drbd_map = self.cfg.ComputeDRBDMap() + + feedback_fn("* Verifying node status") + for node_i in nodeinfo: + node = node_i.name + + if node_i.offline: + if verbose: + feedback_fn("* Skipping offline node %s" % (node,)) + n_offline.append(node) + continue - for node in nodelist: - feedback_fn("* Verifying node %s" % node) - result = self._VerifyNode(node, file_names, local_checksums, - all_vglist[node], all_nvinfo[node], - all_rversion[node], feedback_fn) - bad = bad or result + if node == master_node: + ntype = "master" + elif node_i.master_candidate: + ntype = "master candidate" + elif node_i.drained: + ntype = "drained" + n_drained.append(node) + else: + ntype = "regular" + if verbose: + feedback_fn("* Verifying node %s (%s)" % (node, ntype)) - # node_volume - volumeinfo = all_volumeinfo[node] + msg = all_nvinfo[node].fail_msg + _ErrorIf(msg, self.ENODERPC, node, "while contacting node: %s", msg) + if msg: + continue + + nresult = all_nvinfo[node].payload + node_drbd = {} + for minor, instance in all_drbd_map[node].items(): + test = instance not in instanceinfo + _ErrorIf(test, self.ECLUSTERCFG, None, + "ghost instance '%s' in temporary DRBD map", instance) + # ghost instance should not be running, but otherwise we + # don't give double warnings (both ghost instance and + # unallocated minor in use) + if test: + node_drbd[minor] = (instance, False) + else: + instance = instanceinfo[instance] + node_drbd[minor] = (instance.name, instance.admin_up) + self._VerifyNode(node_i, file_names, local_checksums, + nresult, master_files, node_drbd, vg_name) - if isinstance(volumeinfo, basestring): - feedback_fn(" - ERROR: LVM problem on node %s: %s" % - (node, volumeinfo[-400:].encode('string_escape'))) - bad = True + lvdata = nresult.get(constants.NV_LVLIST, "Missing LV data") + if vg_name is None: + node_volume[node] = {} + elif isinstance(lvdata, basestring): + _ErrorIf(True, self.ENODELVM, node, "LVM problem on node: %s", + utils.SafeEncode(lvdata)) node_volume[node] = {} - elif not isinstance(volumeinfo, dict): - feedback_fn(" - ERROR: connection to %s failed" % (node,)) - bad = True + elif not isinstance(lvdata, dict): + _ErrorIf(True, self.ENODELVM, node, "rpc call to node failed (lvlist)") continue else: - node_volume[node] = volumeinfo + node_volume[node] = lvdata # node_instance - nodeinstance = all_instanceinfo[node] - if type(nodeinstance) != list: - feedback_fn(" - ERROR: connection to %s failed" % (node,)) - bad = True + idata = nresult.get(constants.NV_INSTANCELIST, None) + test = not isinstance(idata, list) + _ErrorIf(test, self.ENODEHV, node, + "rpc call to node failed (instancelist)") + if test: continue - node_instance[node] = nodeinstance + node_instance[node] = idata # node_info - nodeinfo = all_ninfo[node] - if not isinstance(nodeinfo, dict): - feedback_fn(" - ERROR: connection to %s failed" % (node,)) - bad = True + nodeinfo = nresult.get(constants.NV_HVINFO, None) + test = not isinstance(nodeinfo, dict) + _ErrorIf(test, self.ENODEHV, node, "rpc call to node failed (hvinfo)") + if test: continue try: node_info[node] = { "mfree": int(nodeinfo['memory_free']), - "dfree": int(nodeinfo['vg_free']), "pinst": [], "sinst": [], # dictionary holding all instances this node is secondary for, @@ -706,31 +1345,45 @@ class LUVerifyCluster(LogicalUnit): # secondary. "sinst-by-pnode": {}, } - except ValueError: - feedback_fn(" - ERROR: invalid value returned from node %s" % (node,)) - bad = True + # FIXME: devise a free space model for file based instances as well + if vg_name is not None: + test = (constants.NV_VGLIST not in nresult or + vg_name not in nresult[constants.NV_VGLIST]) + _ErrorIf(test, self.ENODELVM, node, + "node didn't return data for the volume group '%s'" + " - it is either missing or broken", vg_name) + if test: + continue + node_info[node]["dfree"] = int(nresult[constants.NV_VGLIST][vg_name]) + except (ValueError, KeyError): + _ErrorIf(True, self.ENODERPC, node, + "node returned invalid nodeinfo, check lvm/hypervisor") continue node_vol_should = {} + feedback_fn("* Verifying instance status") for instance in instancelist: - feedback_fn("* Verifying instance %s" % instance) - inst_config = self.cfg.GetInstanceInfo(instance) - result = self._VerifyInstance(instance, inst_config, node_volume, - node_instance, feedback_fn) - bad = bad or result + if verbose: + feedback_fn("* Verifying instance %s" % instance) + inst_config = instanceinfo[instance] + self._VerifyInstance(instance, inst_config, node_volume, + node_instance, n_offline) + inst_nodes_offline = [] inst_config.MapLVsByNode(node_vol_should) instance_cfg[instance] = inst_config pnode = inst_config.primary_node + _ErrorIf(pnode not in node_info and pnode not in n_offline, + self.ENODERPC, pnode, "instance %s, connection to" + " primary node failed", instance) if pnode in node_info: node_info[pnode]['pinst'].append(instance) - else: - feedback_fn(" - ERROR: instance %s, connection to primary node" - " %s failed" % (instance, pnode)) - bad = True + + if pnode in n_offline: + inst_nodes_offline.append(pnode) # If the instance is non-redundant we cannot survive losing its primary # node, so we are not N+1 compliant. On the other hand we have no disk @@ -739,80 +1392,103 @@ class LUVerifyCluster(LogicalUnit): # FIXME: does not support file-backed instances if len(inst_config.secondary_nodes) == 0: i_non_redundant.append(instance) - elif len(inst_config.secondary_nodes) > 1: - feedback_fn(" - WARNING: multiple secondaries for instance %s" - % instance) + _ErrorIf(len(inst_config.secondary_nodes) > 1, + self.EINSTANCELAYOUT, instance, + "instance has multiple secondary nodes", code="WARNING") + + if not cluster.FillBE(inst_config)[constants.BE_AUTO_BALANCE]: + i_non_a_balanced.append(instance) for snode in inst_config.secondary_nodes: + _ErrorIf(snode not in node_info and snode not in n_offline, + self.ENODERPC, snode, + "instance %s, connection to secondary node" + "failed", instance) + if snode in node_info: node_info[snode]['sinst'].append(instance) if pnode not in node_info[snode]['sinst-by-pnode']: node_info[snode]['sinst-by-pnode'][pnode] = [] node_info[snode]['sinst-by-pnode'][pnode].append(instance) - else: - feedback_fn(" - ERROR: instance %s, connection to secondary node" - " %s failed" % (instance, snode)) + + if snode in n_offline: + inst_nodes_offline.append(snode) + + # warn that the instance lives on offline nodes + _ErrorIf(inst_nodes_offline, self.EINSTANCEBADNODE, instance, + "instance lives on offline node(s) %s", + ", ".join(inst_nodes_offline)) feedback_fn("* Verifying orphan volumes") - result = self._VerifyOrphanVolumes(node_vol_should, node_volume, - feedback_fn) - bad = bad or result + self._VerifyOrphanVolumes(node_vol_should, node_volume) feedback_fn("* Verifying remaining instances") - result = self._VerifyOrphanInstances(instancelist, node_instance, - feedback_fn) - bad = bad or result + self._VerifyOrphanInstances(instancelist, node_instance) if constants.VERIFY_NPLUSONE_MEM not in self.skip_set: feedback_fn("* Verifying N+1 Memory redundancy") - result = self._VerifyNPlusOneMemory(node_info, instance_cfg, feedback_fn) - bad = bad or result + self._VerifyNPlusOneMemory(node_info, instance_cfg) feedback_fn("* Other Notes") if i_non_redundant: feedback_fn(" - NOTICE: %d non-redundant instance(s) found." % len(i_non_redundant)) - return int(bad) + if i_non_a_balanced: + feedback_fn(" - NOTICE: %d non-auto-balanced instance(s) found." + % len(i_non_a_balanced)) + + if n_offline: + feedback_fn(" - NOTICE: %d offline node(s) found." % len(n_offline)) + + if n_drained: + feedback_fn(" - NOTICE: %d drained node(s) found." % len(n_drained)) + + return not self.bad def HooksCallBack(self, phase, hooks_results, feedback_fn, lu_result): - """Analize the post-hooks' result, handle it, and send some + """Analyze the post-hooks' result + + This method analyses the hook result, handles it, and sends some nicely-formatted feedback back to the user. - Args: - phase: the hooks phase that has just been run - hooks_results: the results of the multi-node hooks rpc call - feedback_fn: function to send feedback back to the caller - lu_result: previous Exec result + @param phase: one of L{constants.HOOKS_PHASE_POST} or + L{constants.HOOKS_PHASE_PRE}; it denotes the hooks phase + @param hooks_results: the results of the multi-node hooks rpc call + @param feedback_fn: function used send feedback back to the caller + @param lu_result: previous Exec result + @return: the new Exec result, based on the previous result + and hook results """ - # We only really run POST phase hooks, and are only interested in their results + # We only really run POST phase hooks, and are only interested in + # their results if phase == constants.HOOKS_PHASE_POST: # Used to change hooks' output to proper indentation indent_re = re.compile('^', re.M) feedback_fn("* Hooks Results") - if not hooks_results: - feedback_fn(" - ERROR: general communication failure") - lu_result = 1 - else: - for node_name in hooks_results: - show_node_header = True - res = hooks_results[node_name] - if res is False or not isinstance(res, list): - feedback_fn(" Communication failure") + assert hooks_results, "invalid result from hooks" + + for node_name in hooks_results: + show_node_header = True + res = hooks_results[node_name] + msg = res.fail_msg + test = msg and not res.offline + self._ErrorIf(test, self.ENODEHOOKS, node_name, + "Communication failure in hooks execution: %s", msg) + if test: + # override manually lu_result here as _ErrorIf only + # overrides self.bad + lu_result = 1 + continue + for script, hkr, output in res.payload: + test = hkr == constants.HKR_FAIL + self._ErrorIf(test, self.ENODEHOOKS, node_name, + "Script %s failed, output:", script) + if test: + output = indent_re.sub(' ', output) + feedback_fn("%s" % output) lu_result = 1 - continue - for script, hkr, output in res: - if hkr == constants.HKR_FAIL: - # The node header is only shown once, if there are - # failing hooks on that node - if show_node_header: - feedback_fn(" Node %s:" % node_name) - show_node_header = False - feedback_fn(" ERROR: Script %s failed, output:" % script) - output = indent_re.sub(' ', output) - feedback_fn("%s" % output) - lu_result = 1 return lu_result @@ -822,6 +1498,14 @@ class LUVerifyDisks(NoHooksLU): """ _OP_REQP = [] + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = { + locking.LEVEL_NODE: locking.ALL_SET, + locking.LEVEL_INSTANCE: locking.ALL_SET, + } + self.share_locks = dict.fromkeys(locking.LEVELS, 1) def CheckPrereq(self): """Check prerequisites. @@ -834,8 +1518,13 @@ class LUVerifyDisks(NoHooksLU): def Exec(self, feedback_fn): """Verify integrity of cluster disks. + @rtype: tuple of three items + @return: a tuple of (dict of node-to-node_error, list of instances + which need activate-disks, dict of instance: (node, volume) for + missing volumes + """ - result = res_nodes, res_nlvm, res_instances, res_missing = [], {}, [], {} + result = res_nodes, res_instances, res_missing = {}, [], {} vg_name = self.cfg.GetVGName() nodes = utils.NiceSort(self.cfg.GetNodeList()) @@ -845,7 +1534,7 @@ class LUVerifyDisks(NoHooksLU): nv_dict = {} for inst in instances: inst_lvs = {} - if (inst.status != "up" or + if (not inst.admin_up or inst.disk_template not in constants.DTS_NET_MIRROR): continue inst.MapLVsByNode(inst_lvs) @@ -857,23 +1546,21 @@ class LUVerifyDisks(NoHooksLU): if not nv_dict: return result - node_lvs = rpc.call_volume_list(nodes, vg_name) + node_lvs = self.rpc.call_lv_list(nodes, vg_name) - to_act = set() for node in nodes: # node_volume - lvs = node_lvs[node] - - if isinstance(lvs, basestring): - logger.Info("error enumerating LVs on node %s: %s" % (node, lvs)) - res_nlvm[node] = lvs - elif not isinstance(lvs, dict): - logger.Info("connection to node %s failed or invalid data returned" % - (node,)) - res_nodes.append(node) + node_res = node_lvs[node] + if node_res.offline: + continue + msg = node_res.fail_msg + if msg: + logging.warning("Error enumerating LVs on node %s: %s", node, msg) + res_nodes[node] = msg continue - for lv_name, (_, lv_inactive, lv_online) in lvs.iteritems(): + lvs = node_res.payload + for lv_name, (_, lv_inactive, lv_online) in lvs.items(): inst = nv_dict.pop((node, lv_name), None) if (not lv_online and inst is not None and inst.name not in res_instances): @@ -889,27 +1576,119 @@ class LUVerifyDisks(NoHooksLU): return result -class LURenameCluster(LogicalUnit): - """Rename the cluster. +class LURepairDiskSizes(NoHooksLU): + """Verifies the cluster disks sizes. """ - HPATH = "cluster-rename" - HTYPE = constants.HTYPE_CLUSTER - _OP_REQP = ["name"] - REQ_WSSTORE = True - - def BuildHooksEnv(self): - """Build hooks env. + _OP_REQP = ["instances"] + REQ_BGL = False - """ - env = { - "OP_TARGET": self.sstore.GetClusterName(), - "NEW_NAME": self.op.name, - } - mn = self.sstore.GetMasterNode() - return env, [mn], [mn] + def ExpandNames(self): + if not isinstance(self.op.instances, list): + raise errors.OpPrereqError("Invalid argument type 'instances'") - def CheckPrereq(self): + if self.op.instances: + self.wanted_names = [] + for name in self.op.instances: + full_name = self.cfg.ExpandInstanceName(name) + if full_name is None: + raise errors.OpPrereqError("Instance '%s' not known" % name) + self.wanted_names.append(full_name) + self.needed_locks[locking.LEVEL_INSTANCE] = self.wanted_names + self.needed_locks = { + locking.LEVEL_NODE: [], + locking.LEVEL_INSTANCE: self.wanted_names, + } + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + else: + self.wanted_names = None + self.needed_locks = { + locking.LEVEL_NODE: locking.ALL_SET, + locking.LEVEL_INSTANCE: locking.ALL_SET, + } + self.share_locks = dict(((i, 1) for i in locking.LEVELS)) + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE and self.wanted_names is not None: + self._LockInstancesNodes(primary_only=True) + + def CheckPrereq(self): + """Check prerequisites. + + This only checks the optional instance list against the existing names. + + """ + if self.wanted_names is None: + self.wanted_names = self.acquired_locks[locking.LEVEL_INSTANCE] + + self.wanted_instances = [self.cfg.GetInstanceInfo(name) for name + in self.wanted_names] + + def Exec(self, feedback_fn): + """Verify the size of cluster disks. + + """ + # TODO: check child disks too + # TODO: check differences in size between primary/secondary nodes + per_node_disks = {} + for instance in self.wanted_instances: + pnode = instance.primary_node + if pnode not in per_node_disks: + per_node_disks[pnode] = [] + for idx, disk in enumerate(instance.disks): + per_node_disks[pnode].append((instance, idx, disk)) + + changed = [] + for node, dskl in per_node_disks.items(): + result = self.rpc.call_blockdev_getsizes(node, [v[2] for v in dskl]) + if result.fail_msg: + self.LogWarning("Failure in blockdev_getsizes call to node" + " %s, ignoring", node) + continue + if len(result.data) != len(dskl): + self.LogWarning("Invalid result from node %s, ignoring node results", + node) + continue + for ((instance, idx, disk), size) in zip(dskl, result.data): + if size is None: + self.LogWarning("Disk %d of instance %s did not return size" + " information, ignoring", idx, instance.name) + continue + if not isinstance(size, (int, long)): + self.LogWarning("Disk %d of instance %s did not return valid" + " size information, ignoring", idx, instance.name) + continue + size = size >> 20 + if size != disk.size: + self.LogInfo("Disk %d of instance %s has mismatched size," + " correcting: recorded %d, actual %d", idx, + instance.name, disk.size, size) + disk.size = size + self.cfg.Update(instance) + changed.append((instance.name, idx, size)) + return changed + + +class LURenameCluster(LogicalUnit): + """Rename the cluster. + + """ + HPATH = "cluster-rename" + HTYPE = constants.HTYPE_CLUSTER + _OP_REQP = ["name"] + + def BuildHooksEnv(self): + """Build hooks env. + + """ + env = { + "OP_TARGET": self.cfg.GetClusterName(), + "NEW_NAME": self.op.name, + } + mn = self.cfg.GetMasterNode() + return env, [mn], [mn] + + def CheckPrereq(self): """Verify that the passed name is a valid one. """ @@ -917,8 +1696,8 @@ class LURenameCluster(LogicalUnit): new_name = hostname.name self.ip = new_ip = hostname.ip - old_name = self.sstore.GetClusterName() - old_ip = self.sstore.GetMasterIP() + old_name = self.cfg.GetClusterName() + old_ip = self.cfg.GetMasterIP() if new_name == old_name and new_ip == old_ip: raise errors.OpPrereqError("Neither the name nor the IP address of the" " cluster has changed") @@ -936,46 +1715,49 @@ class LURenameCluster(LogicalUnit): """ clustername = self.op.name ip = self.ip - ss = self.sstore # shutdown the master IP - master = ss.GetMasterNode() - if not rpc.call_node_stop_master(master): - raise errors.OpExecError("Could not disable the master role") + master = self.cfg.GetMasterNode() + result = self.rpc.call_node_stop_master(master, False) + result.Raise("Could not disable the master role") try: - # modify the sstore - ss.SetKey(ss.SS_MASTER_IP, ip) - ss.SetKey(ss.SS_CLUSTER_NAME, clustername) - - # Distribute updated ss config to all nodes - myself = self.cfg.GetNodeInfo(master) - dist_nodes = self.cfg.GetNodeList() - if myself.name in dist_nodes: - dist_nodes.remove(myself.name) - - logger.Debug("Copying updated ssconf data to all nodes") - for keyname in [ss.SS_CLUSTER_NAME, ss.SS_MASTER_IP]: - fname = ss.KeyToFilename(keyname) - result = rpc.call_upload_file(dist_nodes, fname) - for to_node in dist_nodes: - if not result[to_node]: - logger.Error("copy of file %s to node %s failed" % - (fname, to_node)) + cluster = self.cfg.GetClusterInfo() + cluster.cluster_name = clustername + cluster.master_ip = ip + self.cfg.Update(cluster) + + # update the known hosts file + ssh.WriteKnownHostsFile(self.cfg, constants.SSH_KNOWN_HOSTS_FILE) + node_list = self.cfg.GetNodeList() + try: + node_list.remove(master) + except ValueError: + pass + result = self.rpc.call_upload_file(node_list, + constants.SSH_KNOWN_HOSTS_FILE) + for to_node, to_result in result.iteritems(): + msg = to_result.fail_msg + if msg: + msg = ("Copy of file %s to node %s failed: %s" % + (constants.SSH_KNOWN_HOSTS_FILE, to_node, msg)) + self.proc.LogWarning(msg) + finally: - if not rpc.call_node_start_master(master): - logger.Error("Could not re-enable the master role on the master," - " please restart manually.") + result = self.rpc.call_node_start_master(master, False, False) + msg = result.fail_msg + if msg: + self.LogWarning("Could not re-enable the master role on" + " the master, please restart manually: %s", msg) def _RecursiveCheckIfLVMBased(disk): """Check if the given disk or its children are lvm-based. - Args: - disk: ganeti.objects.Disk object - - Returns: - boolean indicating whether a LD_LV dev_type was found or not + @type disk: L{objects.Disk} + @param disk: the disk to check + @rtype: boolean + @return: boolean indicating whether a LD_LV dev_type was found or not """ if disk.children: @@ -992,16 +1774,40 @@ class LUSetClusterParams(LogicalUnit): HPATH = "cluster-modify" HTYPE = constants.HTYPE_CLUSTER _OP_REQP = [] + REQ_BGL = False + + def CheckArguments(self): + """Check parameters + + """ + if not hasattr(self.op, "candidate_pool_size"): + self.op.candidate_pool_size = None + if self.op.candidate_pool_size is not None: + try: + self.op.candidate_pool_size = int(self.op.candidate_pool_size) + except (ValueError, TypeError), err: + raise errors.OpPrereqError("Invalid candidate_pool_size value: %s" % + str(err)) + if self.op.candidate_pool_size < 1: + raise errors.OpPrereqError("At least one master candidate needed") + + def ExpandNames(self): + # FIXME: in the future maybe other cluster params won't require checking on + # all nodes to be modified. + self.needed_locks = { + locking.LEVEL_NODE: locking.ALL_SET, + } + self.share_locks[locking.LEVEL_NODE] = 1 def BuildHooksEnv(self): """Build hooks env. """ env = { - "OP_TARGET": self.sstore.GetClusterName(), + "OP_TARGET": self.cfg.GetClusterName(), "NEW_VG_NAME": self.op.vg_name, } - mn = self.sstore.GetMasterNode() + mn = self.cfg.GetMasterNode() return env, [mn], [mn] def CheckPrereq(self): @@ -1011,38 +1817,184 @@ class LUSetClusterParams(LogicalUnit): if the given volume group is valid. """ - if not self.op.vg_name: - instances = [self.cfg.GetInstanceInfo(name) - for name in self.cfg.GetInstanceList()] + if self.op.vg_name is not None and not self.op.vg_name: + instances = self.cfg.GetAllInstancesInfo().values() for inst in instances: for disk in inst.disks: if _RecursiveCheckIfLVMBased(disk): raise errors.OpPrereqError("Cannot disable lvm storage while" " lvm-based instances exist") + node_list = self.acquired_locks[locking.LEVEL_NODE] + # if vg_name not None, checks given volume group on all nodes if self.op.vg_name: - node_list = self.cfg.GetNodeList() - vglist = rpc.call_vg_list(node_list) + vglist = self.rpc.call_vg_list(node_list) for node in node_list: - vgstatus = utils.CheckVolumeGroupSize(vglist[node], self.op.vg_name, + msg = vglist[node].fail_msg + if msg: + # ignoring down node + self.LogWarning("Error while gathering data on node %s" + " (ignoring node): %s", node, msg) + continue + vgstatus = utils.CheckVolumeGroupSize(vglist[node].payload, + self.op.vg_name, constants.MIN_VG_SIZE) if vgstatus: raise errors.OpPrereqError("Error on node '%s': %s" % (node, vgstatus)) + self.cluster = cluster = self.cfg.GetClusterInfo() + # validate params changes + if self.op.beparams: + utils.ForceDictType(self.op.beparams, constants.BES_PARAMETER_TYPES) + self.new_beparams = objects.FillDict( + cluster.beparams[constants.PP_DEFAULT], self.op.beparams) + + if self.op.nicparams: + utils.ForceDictType(self.op.nicparams, constants.NICS_PARAMETER_TYPES) + self.new_nicparams = objects.FillDict( + cluster.nicparams[constants.PP_DEFAULT], self.op.nicparams) + objects.NIC.CheckParameterSyntax(self.new_nicparams) + + # hypervisor list/parameters + self.new_hvparams = objects.FillDict(cluster.hvparams, {}) + if self.op.hvparams: + if not isinstance(self.op.hvparams, dict): + raise errors.OpPrereqError("Invalid 'hvparams' parameter on input") + for hv_name, hv_dict in self.op.hvparams.items(): + if hv_name not in self.new_hvparams: + self.new_hvparams[hv_name] = hv_dict + else: + self.new_hvparams[hv_name].update(hv_dict) + + if self.op.enabled_hypervisors is not None: + self.hv_list = self.op.enabled_hypervisors + if not self.hv_list: + raise errors.OpPrereqError("Enabled hypervisors list must contain at" + " least one member") + invalid_hvs = set(self.hv_list) - constants.HYPER_TYPES + if invalid_hvs: + raise errors.OpPrereqError("Enabled hypervisors contains invalid" + " entries: %s" % + utils.CommaJoin(invalid_hvs)) + else: + self.hv_list = cluster.enabled_hypervisors + + if self.op.hvparams or self.op.enabled_hypervisors is not None: + # either the enabled list has changed, or the parameters have, validate + for hv_name, hv_params in self.new_hvparams.items(): + if ((self.op.hvparams and hv_name in self.op.hvparams) or + (self.op.enabled_hypervisors and + hv_name in self.op.enabled_hypervisors)): + # either this is a new hypervisor, or its parameters have changed + hv_class = hypervisor.GetHypervisor(hv_name) + utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES) + hv_class.CheckParameterSyntax(hv_params) + _CheckHVParams(self, node_list, hv_name, hv_params) + def Exec(self, feedback_fn): """Change the parameters of the cluster. """ - if self.op.vg_name != self.cfg.GetVGName(): - self.cfg.SetVGName(self.op.vg_name) - else: - feedback_fn("Cluster LVM configuration already in desired" - " state, not changing") + if self.op.vg_name is not None: + new_volume = self.op.vg_name + if not new_volume: + new_volume = None + if new_volume != self.cfg.GetVGName(): + self.cfg.SetVGName(new_volume) + else: + feedback_fn("Cluster LVM configuration already in desired" + " state, not changing") + if self.op.hvparams: + self.cluster.hvparams = self.new_hvparams + if self.op.enabled_hypervisors is not None: + self.cluster.enabled_hypervisors = self.op.enabled_hypervisors + if self.op.beparams: + self.cluster.beparams[constants.PP_DEFAULT] = self.new_beparams + if self.op.nicparams: + self.cluster.nicparams[constants.PP_DEFAULT] = self.new_nicparams + + if self.op.candidate_pool_size is not None: + self.cluster.candidate_pool_size = self.op.candidate_pool_size + # we need to update the pool size here, otherwise the save will fail + _AdjustCandidatePool(self) + + self.cfg.Update(self.cluster) + + +def _RedistributeAncillaryFiles(lu, additional_nodes=None): + """Distribute additional files which are part of the cluster configuration. + + ConfigWriter takes care of distributing the config and ssconf files, but + there are more files which should be distributed to all nodes. This function + makes sure those are copied. + + @param lu: calling logical unit + @param additional_nodes: list of nodes not in the config to distribute to + + """ + # 1. Gather target nodes + myself = lu.cfg.GetNodeInfo(lu.cfg.GetMasterNode()) + dist_nodes = lu.cfg.GetNodeList() + if additional_nodes is not None: + dist_nodes.extend(additional_nodes) + if myself.name in dist_nodes: + dist_nodes.remove(myself.name) + # 2. Gather files to distribute + dist_files = set([constants.ETC_HOSTS, + constants.SSH_KNOWN_HOSTS_FILE, + constants.RAPI_CERT_FILE, + constants.RAPI_USERS_FILE, + constants.HMAC_CLUSTER_KEY, + ]) + + enabled_hypervisors = lu.cfg.GetClusterInfo().enabled_hypervisors + for hv_name in enabled_hypervisors: + hv_class = hypervisor.GetHypervisor(hv_name) + dist_files.update(hv_class.GetAncillaryFiles()) + + # 3. Perform the files upload + for fname in dist_files: + if os.path.exists(fname): + result = lu.rpc.call_upload_file(dist_nodes, fname) + for to_node, to_result in result.items(): + msg = to_result.fail_msg + if msg: + msg = ("Copy of file %s to node %s failed: %s" % + (fname, to_node, msg)) + lu.proc.LogWarning(msg) + + +class LURedistributeConfig(NoHooksLU): + """Force the redistribution of cluster configuration. + + This is a very simple LU. + + """ + _OP_REQP = [] + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = { + locking.LEVEL_NODE: locking.ALL_SET, + } + self.share_locks[locking.LEVEL_NODE] = 1 + + def CheckPrereq(self): + """Check prerequisites. + + """ + + def Exec(self, feedback_fn): + """Redistribute the configuration. + + """ + self.cfg.Update(self.cfg.GetClusterInfo()) + _RedistributeAncillaryFiles(self) -def _WaitForSync(cfgw, instance, proc, oneshot=False, unlock=False): +def _WaitForSync(lu, instance, oneshot=False, unlock=False): """Sleep and poll for an instance's disk to sync. """ @@ -1050,65 +2002,70 @@ def _WaitForSync(cfgw, instance, proc, oneshot=False, unlock=False): return True if not oneshot: - proc.LogInfo("Waiting for instance %s to sync disks." % instance.name) + lu.proc.LogInfo("Waiting for instance %s to sync disks." % instance.name) node = instance.primary_node for dev in instance.disks: - cfgw.SetDiskID(dev, node) + lu.cfg.SetDiskID(dev, node) retries = 0 + degr_retries = 10 # in seconds, as we sleep 1 second each time while True: max_time = 0 done = True cumul_degraded = False - rstats = rpc.call_blockdev_getmirrorstatus(node, instance.disks) - if not rstats: - proc.LogWarning("Can't get any data from node %s" % node) + rstats = lu.rpc.call_blockdev_getmirrorstatus(node, instance.disks) + msg = rstats.fail_msg + if msg: + lu.LogWarning("Can't get any data from node %s: %s", node, msg) retries += 1 if retries >= 10: raise errors.RemoteError("Can't contact node %s for mirror data," " aborting." % node) time.sleep(6) continue + rstats = rstats.payload retries = 0 - for i in range(len(rstats)): - mstat = rstats[i] + for i, mstat in enumerate(rstats): if mstat is None: - proc.LogWarning("Can't compute data for node %s/%s" % - (node, instance.disks[i].iv_name)) + lu.LogWarning("Can't compute data for node %s/%s", + node, instance.disks[i].iv_name) continue - # we ignore the ldisk parameter - perc_done, est_time, is_degraded, _ = mstat - cumul_degraded = cumul_degraded or (is_degraded and perc_done is None) - if perc_done is not None: + + cumul_degraded = (cumul_degraded or + (mstat.is_degraded and mstat.sync_percent is None)) + if mstat.sync_percent is not None: done = False - if est_time is not None: - rem_time = "%d estimated seconds remaining" % est_time - max_time = est_time + if mstat.estimated_time is not None: + rem_time = "%d estimated seconds remaining" % mstat.estimated_time + max_time = mstat.estimated_time else: rem_time = "no time estimate" - proc.LogInfo("- device %s: %5.2f%% done, %s" % - (instance.disks[i].iv_name, perc_done, rem_time)) + lu.proc.LogInfo("- device %s: %5.2f%% done, %s" % + (instance.disks[i].iv_name, mstat.sync_percent, + rem_time)) + + # if we're done but degraded, let's do a few small retries, to + # make sure we see a stable and not transient situation; therefore + # we force restart of the loop + if (done or oneshot) and cumul_degraded and degr_retries > 0: + logging.info("Degraded disks found, %d retries left", degr_retries) + degr_retries -= 1 + time.sleep(1) + continue + if done or oneshot: break - if unlock: - #utils.Unlock('cmd') - pass - try: - time.sleep(min(60, max_time)) - finally: - if unlock: - #utils.Lock('cmd') - pass + time.sleep(min(60, max_time)) if done: - proc.LogInfo("Instance %s's disks are in sync." % instance.name) + lu.proc.LogInfo("Instance %s's disks are in sync." % instance.name) return not cumul_degraded -def _CheckDiskConsistency(cfgw, dev, node, on_primary, ldisk=False): +def _CheckDiskConsistency(lu, dev, node, on_primary, ldisk=False): """Check that mirrors are not degraded. The ldisk parameter, if True, will change the test from the @@ -1116,23 +2073,28 @@ def _CheckDiskConsistency(cfgw, dev, node, on_primary, ldisk=False): the device(s)) to the ldisk (representing the local storage status). """ - cfgw.SetDiskID(dev, node) - if ldisk: - idx = 6 - else: - idx = 5 + lu.cfg.SetDiskID(dev, node) result = True + if on_primary or dev.AssembleOnSecondary(): - rstats = rpc.call_blockdev_find(node, dev) - if not rstats: - logger.ToStderr("Node %s: Disk degraded, not found or node down" % node) + rstats = lu.rpc.call_blockdev_find(node, dev) + msg = rstats.fail_msg + if msg: + lu.LogWarning("Can't find disk on node %s: %s", node, msg) + result = False + elif not rstats.payload: + lu.LogWarning("Can't find disk on node %s", node) result = False else: - result = result and (not rstats[idx]) + if ldisk: + result = result and rstats.payload.ldisk_status == constants.LDS_OKAY + else: + result = result and not rstats.payload.is_degraded + if dev.children: for child in dev.children: - result = result and _CheckDiskConsistency(cfgw, child, node, on_primary) + result = result and _CheckDiskConsistency(lu, child, node, on_primary) return result @@ -1142,73 +2104,86 @@ class LUDiagnoseOS(NoHooksLU): """ _OP_REQP = ["output_fields", "names"] + REQ_BGL = False + _FIELDS_STATIC = utils.FieldSet() + _FIELDS_DYNAMIC = utils.FieldSet("name", "valid", "node_status") - def CheckPrereq(self): - """Check prerequisites. - - This always succeeds, since this is a pure query LU. - - """ + def ExpandNames(self): if self.op.names: raise errors.OpPrereqError("Selective OS query not supported") - self.dynamic_fields = frozenset(["name", "valid", "node_status"]) - _CheckOutputFields(static=[], - dynamic=self.dynamic_fields, + _CheckOutputFields(static=self._FIELDS_STATIC, + dynamic=self._FIELDS_DYNAMIC, selected=self.op.output_fields) + # Lock all nodes, in shared mode + # Temporary removal of locks, should be reverted later + # TODO: reintroduce locks when they are lighter-weight + self.needed_locks = {} + #self.share_locks[locking.LEVEL_NODE] = 1 + #self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + + def CheckPrereq(self): + """Check prerequisites. + + """ + @staticmethod def _DiagnoseByOS(node_list, rlist): """Remaps a per-node return list into an a per-os per-node dictionary - Args: - node_list: a list with the names of all nodes - rlist: a map with node names as keys and OS objects as values + @param node_list: a list with the names of all nodes + @param rlist: a map with node names as keys and OS objects as values + + @rtype: dict + @return: a dictionary with osnames as keys and as value another map, with + nodes as keys and tuples of (path, status, diagnose) as values, eg:: - Returns: - map: a map with osnames as keys and as value another map, with - nodes as - keys and list of OS objects as values - e.g. {"debian-etch": {"node1": [,...], - "node2": [,]} - } + {"debian-etch": {"node1": [(/usr/lib/..., True, ""), + (/srv/..., False, "invalid api")], + "node2": [(/srv/..., True, "")]} + } """ all_os = {} - for node_name, nr in rlist.iteritems(): - if not nr: + # we build here the list of nodes that didn't fail the RPC (at RPC + # level), so that nodes with a non-responding node daemon don't + # make all OSes invalid + good_nodes = [node_name for node_name in rlist + if not rlist[node_name].fail_msg] + for node_name, nr in rlist.items(): + if nr.fail_msg or not nr.payload: continue - for os_obj in nr: - if os_obj.name not in all_os: + for name, path, status, diagnose in nr.payload: + if name not in all_os: # build a list of nodes for this os containing empty lists # for each node in node_list - all_os[os_obj.name] = {} - for nname in node_list: - all_os[os_obj.name][nname] = [] - all_os[os_obj.name][node_name].append(os_obj) + all_os[name] = {} + for nname in good_nodes: + all_os[name][nname] = [] + all_os[name][node_name].append((path, status, diagnose)) return all_os def Exec(self, feedback_fn): """Compute the list of OSes. """ - node_list = self.cfg.GetNodeList() - node_data = rpc.call_os_diagnose(node_list) - if node_data == False: - raise errors.OpExecError("Can't gather the list of OSes") - pol = self._DiagnoseByOS(node_list, node_data) + valid_nodes = [node for node in self.cfg.GetOnlineNodeList()] + node_data = self.rpc.call_os_diagnose(valid_nodes) + pol = self._DiagnoseByOS(valid_nodes, node_data) output = [] - for os_name, os_data in pol.iteritems(): + for os_name, os_data in pol.items(): row = [] for field in self.op.output_fields: if field == "name": val = os_name elif field == "valid": - val = utils.all([osl and osl[0] for osl in os_data.values()]) + val = utils.all([osl and osl[0][1] for osl in os_data.values()]) elif field == "node_status": + # this is just a copy of the dict val = {} - for node_name, nos_list in os_data.iteritems(): - val[node_name] = [(v.status, v.path) for v in nos_list] + for node_name, nos_list in os_data.items(): + val[node_name] = nos_list else: raise errors.ParameterError(field) row.append(val) @@ -1237,7 +2212,8 @@ class LURemoveNode(LogicalUnit): "NODE_NAME": self.op.node_name, } all_nodes = self.cfg.GetNodeList() - all_nodes.remove(self.op.node_name) + if self.op.node_name in all_nodes: + all_nodes.remove(self.op.node_name) return env, all_nodes, all_nodes def CheckPrereq(self): @@ -1248,7 +2224,7 @@ class LURemoveNode(LogicalUnit): - it does not have primary or secondary instances - it's not the master - Any errors are signalled by raising errors.OpPrereqError. + Any errors are signaled by raising errors.OpPrereqError. """ node = self.cfg.GetNodeInfo(self.cfg.ExpandNodeName(self.op.node_name)) @@ -1257,18 +2233,15 @@ class LURemoveNode(LogicalUnit): instance_list = self.cfg.GetInstanceList() - masternode = self.sstore.GetMasterNode() + masternode = self.cfg.GetMasterNode() if node.name == masternode: raise errors.OpPrereqError("Node is the master node," " you need to failover first.") for instance_name in instance_list: instance = self.cfg.GetInstanceInfo(instance_name) - if node.name == instance.primary_node: - raise errors.OpPrereqError("Instance %s still running on the node," - " please remove first." % instance_name) - if node.name in instance.secondary_nodes: - raise errors.OpPrereqError("Instance %s has node as a secondary," + if node.name in instance.all_nodes: + raise errors.OpPrereqError("Instance %s is still running on the node," " please remove first." % instance_name) self.op.node_name = node.name self.node = node @@ -1278,70 +2251,121 @@ class LURemoveNode(LogicalUnit): """ node = self.node - logger.Info("stopping the node daemon and removing configs from node %s" % - node.name) + logging.info("Stopping the node daemon and removing configs from node %s", + node.name) - rpc.call_node_leave_cluster(node.name) + self.context.RemoveNode(node.name) - logger.Info("Removing node %s from config" % node.name) + # Run post hooks on the node before it's removed + hm = self.proc.hmclass(self.rpc.call_hooks_runner, self) + try: + h_results = hm.RunPhase(constants.HOOKS_PHASE_POST, [node.name]) + except: + self.LogWarning("Errors occurred running hooks on %s" % node.name) - self.cfg.RemoveNode(node.name) - # Remove the node from the Ganeti Lock Manager - self.context.glm.remove(locking.LEVEL_NODE, node.name) + result = self.rpc.call_node_leave_cluster(node.name) + msg = result.fail_msg + if msg: + self.LogWarning("Errors encountered on the remote node while leaving" + " the cluster: %s", msg) - utils.RemoveHostFromEtcHosts(node.name) + # Promote nodes to master candidate as needed + _AdjustCandidatePool(self) class LUQueryNodes(NoHooksLU): """Logical unit for querying nodes. """ - _OP_REQP = ["output_fields", "names"] + _OP_REQP = ["output_fields", "names", "use_locking"] + REQ_BGL = False - def CheckPrereq(self): - """Check prerequisites. + _SIMPLE_FIELDS = ["name", "serial_no", "ctime", "mtime", "uuid", + "master_candidate", "offline", "drained"] - This checks that the fields required are valid output fields. + _FIELDS_DYNAMIC = utils.FieldSet( + "dtotal", "dfree", + "mtotal", "mnode", "mfree", + "bootid", + "ctotal", "cnodes", "csockets", + ) - """ - self.dynamic_fields = frozenset([ - "dtotal", "dfree", - "mtotal", "mnode", "mfree", - "bootid", - "ctotal", - ]) + _FIELDS_STATIC = utils.FieldSet(*[ + "pinst_cnt", "sinst_cnt", + "pinst_list", "sinst_list", + "pip", "sip", "tags", + "master", + "role"] + _SIMPLE_FIELDS + ) - _CheckOutputFields(static=["name", "pinst_cnt", "sinst_cnt", - "pinst_list", "sinst_list", - "pip", "sip", "tags"], - dynamic=self.dynamic_fields, + def ExpandNames(self): + _CheckOutputFields(static=self._FIELDS_STATIC, + dynamic=self._FIELDS_DYNAMIC, selected=self.op.output_fields) - self.wanted = _GetWantedNodes(self, self.op.names) + self.needed_locks = {} + self.share_locks[locking.LEVEL_NODE] = 1 + + if self.op.names: + self.wanted = _GetWantedNodes(self, self.op.names) + else: + self.wanted = locking.ALL_SET + + self.do_node_query = self._FIELDS_STATIC.NonMatching(self.op.output_fields) + self.do_locking = self.do_node_query and self.op.use_locking + if self.do_locking: + # if we don't request only static fields, we need to lock the nodes + self.needed_locks[locking.LEVEL_NODE] = self.wanted + + + def CheckPrereq(self): + """Check prerequisites. + + """ + # The validation of the node list is done in the _GetWantedNodes, + # if non empty, and if empty, there's no validation to do + pass def Exec(self, feedback_fn): """Computes the list of nodes and their attributes. """ - nodenames = self.wanted - nodelist = [self.cfg.GetNodeInfo(name) for name in nodenames] + all_info = self.cfg.GetAllNodesInfo() + if self.do_locking: + nodenames = self.acquired_locks[locking.LEVEL_NODE] + elif self.wanted != locking.ALL_SET: + nodenames = self.wanted + missing = set(nodenames).difference(all_info.keys()) + if missing: + raise errors.OpExecError( + "Some nodes were removed before retrieving their data: %s" % missing) + else: + nodenames = all_info.keys() + + nodenames = utils.NiceSort(nodenames) + nodelist = [all_info[name] for name in nodenames] # begin data gathering - if self.dynamic_fields.intersection(self.op.output_fields): + if self.do_node_query: live_data = {} - node_data = rpc.call_node_info(nodenames, self.cfg.GetVGName()) + node_data = self.rpc.call_node_info(nodenames, self.cfg.GetVGName(), + self.cfg.GetHypervisorType()) for name in nodenames: - nodeinfo = node_data.get(name, None) - if nodeinfo: + nodeinfo = node_data[name] + if not nodeinfo.fail_msg and nodeinfo.payload: + nodeinfo = nodeinfo.payload + fn = utils.TryConvert live_data[name] = { - "mtotal": utils.TryConvert(int, nodeinfo['memory_total']), - "mnode": utils.TryConvert(int, nodeinfo['memory_dom0']), - "mfree": utils.TryConvert(int, nodeinfo['memory_free']), - "dtotal": utils.TryConvert(int, nodeinfo['vg_size']), - "dfree": utils.TryConvert(int, nodeinfo['vg_free']), - "ctotal": utils.TryConvert(int, nodeinfo['cpu_total']), - "bootid": nodeinfo['bootid'], + "mtotal": fn(int, nodeinfo.get('memory_total', None)), + "mnode": fn(int, nodeinfo.get('memory_dom0', None)), + "mfree": fn(int, nodeinfo.get('memory_free', None)), + "dtotal": fn(int, nodeinfo.get('vg_size', None)), + "dfree": fn(int, nodeinfo.get('vg_free', None)), + "ctotal": fn(int, nodeinfo.get('cpu_total', None)), + "bootid": nodeinfo.get('bootid', None), + "cnodes": fn(int, nodeinfo.get('cpu_nodes', None)), + "csockets": fn(int, nodeinfo.get('cpu_sockets', None)), } else: live_data[name] = {} @@ -1364,14 +2388,16 @@ class LUQueryNodes(NoHooksLU): if secnode in node_to_secondary: node_to_secondary[secnode].add(inst.name) + master_node = self.cfg.GetMasterNode() + # end data gathering output = [] for node in nodelist: node_output = [] for field in self.op.output_fields: - if field == "name": - val = node.name + if field in self._SIMPLE_FIELDS: + val = getattr(node, field) elif field == "pinst_list": val = list(node_to_primary[node.name]) elif field == "sinst_list": @@ -1386,8 +2412,21 @@ class LUQueryNodes(NoHooksLU): val = node.secondary_ip elif field == "tags": val = list(node.GetTags()) - elif field in self.dynamic_fields: + elif field == "master": + val = node.name == master_node + elif self._FIELDS_DYNAMIC.Matches(field): val = live_data[node.name].get(field, None) + elif field == "role": + if node.name == master_node: + val = "M" + elif node.master_candidate: + val = "C" + elif node.drained: + val = "D" + elif node.offline: + val = "O" + else: + val = "R" else: raise errors.ParameterError(field) node_output.append(val) @@ -1401,6 +2440,22 @@ class LUQueryNodeVolumes(NoHooksLU): """ _OP_REQP = ["nodes", "output_fields"] + REQ_BGL = False + _FIELDS_DYNAMIC = utils.FieldSet("phys", "vg", "name", "size", "instance") + _FIELDS_STATIC = utils.FieldSet("node") + + def ExpandNames(self): + _CheckOutputFields(static=self._FIELDS_STATIC, + dynamic=self._FIELDS_DYNAMIC, + selected=self.op.output_fields) + + self.needed_locks = {} + self.share_locks[locking.LEVEL_NODE] = 1 + if not self.op.nodes: + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + else: + self.needed_locks[locking.LEVEL_NODE] = \ + _GetWantedNodes(self, self.op.nodes) def CheckPrereq(self): """Check prerequisites. @@ -1408,19 +2463,14 @@ class LUQueryNodeVolumes(NoHooksLU): This checks that the fields required are valid output fields. """ - self.nodes = _GetWantedNodes(self, self.op.nodes) - - _CheckOutputFields(static=["node"], - dynamic=["phys", "vg", "name", "size", "instance"], - selected=self.op.output_fields) - + self.nodes = self.acquired_locks[locking.LEVEL_NODE] def Exec(self, feedback_fn): """Computes the list of nodes and their attributes. """ nodenames = self.nodes - volumes = rpc.call_node_volumes(nodenames) + volumes = self.rpc.call_node_volumes(nodenames) ilist = [self.cfg.GetInstanceInfo(iname) for iname in self.cfg.GetInstanceList()] @@ -1429,10 +2479,15 @@ class LUQueryNodeVolumes(NoHooksLU): output = [] for node in nodenames: - if node not in volumes or not volumes[node]: + nresult = volumes[node] + if nresult.offline: + continue + msg = nresult.fail_msg + if msg: + self.LogWarning("Can't compute volume data on node %s: %s", node, msg) continue - node_vols = volumes[node][:] + node_vols = nresult.payload[:] node_vols.sort(key=lambda vol: vol['dev']) for vol in node_vols: @@ -1466,6 +2521,154 @@ class LUQueryNodeVolumes(NoHooksLU): return output +class LUQueryNodeStorage(NoHooksLU): + """Logical unit for getting information on storage units on node(s). + + """ + _OP_REQP = ["nodes", "storage_type", "output_fields"] + REQ_BGL = False + _FIELDS_STATIC = utils.FieldSet("node") + + def ExpandNames(self): + storage_type = self.op.storage_type + + if storage_type not in constants.VALID_STORAGE_FIELDS: + raise errors.OpPrereqError("Unknown storage type: %s" % storage_type) + + dynamic_fields = constants.VALID_STORAGE_FIELDS[storage_type] + + _CheckOutputFields(static=self._FIELDS_STATIC, + dynamic=utils.FieldSet(*dynamic_fields), + selected=self.op.output_fields) + + self.needed_locks = {} + self.share_locks[locking.LEVEL_NODE] = 1 + + if self.op.nodes: + self.needed_locks[locking.LEVEL_NODE] = \ + _GetWantedNodes(self, self.op.nodes) + else: + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + + def CheckPrereq(self): + """Check prerequisites. + + This checks that the fields required are valid output fields. + + """ + self.op.name = getattr(self.op, "name", None) + + self.nodes = self.acquired_locks[locking.LEVEL_NODE] + + def Exec(self, feedback_fn): + """Computes the list of nodes and their attributes. + + """ + # Always get name to sort by + if constants.SF_NAME in self.op.output_fields: + fields = self.op.output_fields[:] + else: + fields = [constants.SF_NAME] + self.op.output_fields + + # Never ask for node as it's only known to the LU + while "node" in fields: + fields.remove("node") + + field_idx = dict([(name, idx) for (idx, name) in enumerate(fields)]) + name_idx = field_idx[constants.SF_NAME] + + st_args = _GetStorageTypeArgs(self.cfg, self.op.storage_type) + data = self.rpc.call_storage_list(self.nodes, + self.op.storage_type, st_args, + self.op.name, fields) + + result = [] + + for node in utils.NiceSort(self.nodes): + nresult = data[node] + if nresult.offline: + continue + + msg = nresult.fail_msg + if msg: + self.LogWarning("Can't get storage data from node %s: %s", node, msg) + continue + + rows = dict([(row[name_idx], row) for row in nresult.payload]) + + for name in utils.NiceSort(rows.keys()): + row = rows[name] + + out = [] + + for field in self.op.output_fields: + if field == "node": + val = node + elif field in field_idx: + val = row[field_idx[field]] + else: + raise errors.ParameterError(field) + + out.append(val) + + result.append(out) + + return result + + +class LUModifyNodeStorage(NoHooksLU): + """Logical unit for modifying a storage volume on a node. + + """ + _OP_REQP = ["node_name", "storage_type", "name", "changes"] + REQ_BGL = False + + def CheckArguments(self): + node_name = self.cfg.ExpandNodeName(self.op.node_name) + if node_name is None: + raise errors.OpPrereqError("Invalid node name '%s'" % self.op.node_name) + + self.op.node_name = node_name + + storage_type = self.op.storage_type + if storage_type not in constants.VALID_STORAGE_FIELDS: + raise errors.OpPrereqError("Unknown storage type: %s" % storage_type) + + def ExpandNames(self): + self.needed_locks = { + locking.LEVEL_NODE: self.op.node_name, + } + + def CheckPrereq(self): + """Check prerequisites. + + """ + storage_type = self.op.storage_type + + try: + modifiable = constants.MODIFIABLE_STORAGE_FIELDS[storage_type] + except KeyError: + raise errors.OpPrereqError("Storage units of type '%s' can not be" + " modified" % storage_type) + + diff = set(self.op.changes.keys()) - modifiable + if diff: + raise errors.OpPrereqError("The following fields can not be modified for" + " storage units of type '%s': %r" % + (storage_type, list(diff))) + + def Exec(self, feedback_fn): + """Computes the list of nodes and their attributes. + + """ + st_args = _GetStorageTypeArgs(self.cfg, self.op.storage_type) + result = self.rpc.call_storage_modify(self.op.node_name, + self.op.storage_type, st_args, + self.op.name, self.op.changes) + result.Raise("Failed to modify storage unit '%s' on %s" % + (self.op.name, self.op.node_name)) + + class LUAddNode(LogicalUnit): """Logical unit for adding node to the cluster. @@ -1498,7 +2701,7 @@ class LUAddNode(LogicalUnit): - it is resolvable - its parameters (single/dual homed) matches the cluster - Any errors are signalled by raising errors.OpPrereqError. + Any errors are signaled by raising errors.OpPrereqError. """ node_name = self.op.node_name @@ -1541,7 +2744,7 @@ class LUAddNode(LogicalUnit): # check that the type of the node (single versus dual homed) is the # same as for the master - myself = cfg.GetNodeInfo(self.sstore.GetMasterNode()) + myself = cfg.GetNodeInfo(self.cfg.GetMasterNode()) master_singlehomed = myself.secondary_ip == myself.primary_ip newbie_singlehomed = secondary_ip == primary_ip if master_singlehomed != newbie_singlehomed: @@ -1552,7 +2755,7 @@ class LUAddNode(LogicalUnit): raise errors.OpPrereqError("The master has a private ip but the" " new node doesn't have one") - # checks reachablity + # checks reachability if not utils.TcpPing(primary_ip, constants.DEFAULT_NODED_PORT): raise errors.OpPrereqError("Node not reachable by ping") @@ -1563,9 +2766,25 @@ class LUAddNode(LogicalUnit): raise errors.OpPrereqError("Node secondary ip not reachable by TCP" " based ping to noded port") - self.new_node = objects.Node(name=node, - primary_ip=primary_ip, - secondary_ip=secondary_ip) + cp_size = self.cfg.GetClusterInfo().candidate_pool_size + if self.op.readd: + exceptions = [node] + else: + exceptions = [] + mc_now, mc_max = self.cfg.GetMasterCandidateStats(exceptions) + # the new node will increase mc_max with one, so: + mc_max = min(mc_max + 1, cp_size) + self.master_candidate = mc_now < mc_max + + if self.op.readd: + self.new_node = self.cfg.GetNodeInfo(node) + assert self.new_node is not None, "Can't retrieve locked node %s" % node + else: + self.new_node = objects.Node(name=node, + primary_ip=primary_ip, + secondary_ip=secondary_ip, + master_candidate=self.master_candidate, + offline=False, drained=False) def Exec(self, feedback_fn): """Adds the new node to the cluster. @@ -1574,21 +2793,33 @@ class LUAddNode(LogicalUnit): new_node = self.new_node node = new_node.name + # for re-adds, reset the offline/drained/master-candidate flags; + # we need to reset here, otherwise offline would prevent RPC calls + # later in the procedure; this also means that if the re-add + # fails, we are left with a non-offlined, broken node + if self.op.readd: + new_node.drained = new_node.offline = False + self.LogInfo("Readding a node, the offline/drained flags were reset") + # if we demote the node, we do cleanup later in the procedure + new_node.master_candidate = self.master_candidate + + # notify the user about any possible mc promotion + if new_node.master_candidate: + self.LogInfo("Node will be a master candidate") + # check connectivity - result = rpc.call_version([node])[node] - if result: - if constants.PROTOCOL_VERSION == result: - logger.Info("communication to node %s fine, sw version %s match" % - (node, result)) - else: - raise errors.OpExecError("Version mismatch master version %s," - " node version %s" % - (constants.PROTOCOL_VERSION, result)) + result = self.rpc.call_version([node])[node] + result.Raise("Can't get version information from node %s" % node) + if constants.PROTOCOL_VERSION == result.payload: + logging.info("Communication to node %s fine, sw version %s match", + node, result.payload) else: - raise errors.OpExecError("Cannot get version from the new node") + raise errors.OpExecError("Version mismatch master version %s," + " node version %s" % + (constants.PROTOCOL_VERSION, result.payload)) # setup ssh on node - logger.Info("copy ssh key to node %s" % node) + logging.info("Copy ssh key to node %s", node) priv_key, pub_key, _ = ssh.GetUserFiles(constants.GANETI_RUNAS) keyarray = [] keyfiles = [constants.SSH_HOST_DSA_PRIV, constants.SSH_HOST_DSA_PUB, @@ -1596,150 +2827,237 @@ class LUAddNode(LogicalUnit): priv_key, pub_key] for i in keyfiles: - f = open(i, 'r') - try: - keyarray.append(f.read()) - finally: - f.close() + keyarray.append(utils.ReadFile(i)) - result = rpc.call_node_add(node, keyarray[0], keyarray[1], keyarray[2], - keyarray[3], keyarray[4], keyarray[5]) - - if not result: - raise errors.OpExecError("Cannot transfer ssh keys to the new node") + result = self.rpc.call_node_add(node, keyarray[0], keyarray[1], + keyarray[2], + keyarray[3], keyarray[4], keyarray[5]) + result.Raise("Cannot transfer ssh keys to the new node") # Add node to our /etc/hosts, and add key to known_hosts - utils.AddHostToEtcHosts(new_node.name) + if self.cfg.GetClusterInfo().modify_etc_hosts: + utils.AddHostToEtcHosts(new_node.name) if new_node.secondary_ip != new_node.primary_ip: - if not rpc.call_node_tcp_ping(new_node.name, - constants.LOCALHOST_IP_ADDRESS, - new_node.secondary_ip, - constants.DEFAULT_NODED_PORT, - 10, False): + result = self.rpc.call_node_has_ip_address(new_node.name, + new_node.secondary_ip) + result.Raise("Failure checking secondary ip on node %s" % new_node.name, + prereq=True) + if not result.payload: raise errors.OpExecError("Node claims it doesn't have the secondary ip" " you gave (%s). Please fix and re-run this" " command." % new_node.secondary_ip) - node_verify_list = [self.sstore.GetMasterNode()] + node_verify_list = [self.cfg.GetMasterNode()] node_verify_param = { - 'nodelist': [node], + constants.NV_NODELIST: [node], # TODO: do a node-net-test as well? } - result = rpc.call_node_verify(node_verify_list, node_verify_param) + result = self.rpc.call_node_verify(node_verify_list, node_verify_param, + self.cfg.GetClusterName()) for verifier in node_verify_list: - if not result[verifier]: - raise errors.OpExecError("Cannot communicate with %s's node daemon" - " for remote verification" % verifier) - if result[verifier]['nodelist']: - for failed in result[verifier]['nodelist']: + result[verifier].Raise("Cannot communicate with node %s" % verifier) + nl_payload = result[verifier].payload[constants.NV_NODELIST] + if nl_payload: + for failed in nl_payload: feedback_fn("ssh/hostname verification failed %s -> %s" % - (verifier, result[verifier]['nodelist'][failed])) + (verifier, nl_payload[failed])) raise errors.OpExecError("ssh/hostname verification failed.") - # Distribute updated /etc/hosts and known_hosts to all nodes, - # including the node just added - myself = self.cfg.GetNodeInfo(self.sstore.GetMasterNode()) - dist_nodes = self.cfg.GetNodeList() - if not self.op.readd: - dist_nodes.append(node) - if myself.name in dist_nodes: - dist_nodes.remove(myself.name) - - logger.Debug("Copying hosts and known_hosts to all nodes") - for fname in (constants.ETC_HOSTS, constants.SSH_KNOWN_HOSTS_FILE): - result = rpc.call_upload_file(dist_nodes, fname) - for to_node in dist_nodes: - if not result[to_node]: - logger.Error("copy of file %s to node %s failed" % - (fname, to_node)) - - to_copy = self.sstore.GetFileList() - if self.sstore.GetHypervisorType() == constants.HT_XEN_HVM31: - to_copy.append(constants.VNC_PASSWORD_FILE) - for fname in to_copy: - result = rpc.call_upload_file([node], fname) - if not result[node]: - logger.Error("could not copy file %s to node %s" % (fname, node)) - - if not self.op.readd: - logger.Info("adding node %s to cluster.conf" % node) - self.cfg.AddNode(new_node) - # Add the new node to the Ganeti Lock Manager - self.context.glm.add(locking.LEVEL_NODE, node) - - -class LUMasterFailover(LogicalUnit): - """Failover the master node to the current node. - - This is a special LU in that it must run on a non-master node. + if self.op.readd: + _RedistributeAncillaryFiles(self) + self.context.ReaddNode(new_node) + # make sure we redistribute the config + self.cfg.Update(new_node) + # and make sure the new node will not have old files around + if not new_node.master_candidate: + result = self.rpc.call_node_demote_from_mc(new_node.name) + msg = result.fail_msg + if msg: + self.LogWarning("Node failed to demote itself from master" + " candidate status: %s" % msg) + else: + _RedistributeAncillaryFiles(self, additional_nodes=[node]) + self.context.AddNode(new_node) + + +class LUSetNodeParams(LogicalUnit): + """Modifies the parameters of a node. """ - HPATH = "master-failover" - HTYPE = constants.HTYPE_CLUSTER - REQ_MASTER = False - REQ_WSSTORE = True - _OP_REQP = [] + HPATH = "node-modify" + HTYPE = constants.HTYPE_NODE + _OP_REQP = ["node_name"] + REQ_BGL = False + + def CheckArguments(self): + node_name = self.cfg.ExpandNodeName(self.op.node_name) + if node_name is None: + raise errors.OpPrereqError("Invalid node name '%s'" % self.op.node_name) + self.op.node_name = node_name + _CheckBooleanOpField(self.op, 'master_candidate') + _CheckBooleanOpField(self.op, 'offline') + _CheckBooleanOpField(self.op, 'drained') + all_mods = [self.op.offline, self.op.master_candidate, self.op.drained] + if all_mods.count(None) == 3: + raise errors.OpPrereqError("Please pass at least one modification") + if all_mods.count(True) > 1: + raise errors.OpPrereqError("Can't set the node into more than one" + " state at the same time") + + def ExpandNames(self): + self.needed_locks = {locking.LEVEL_NODE: self.op.node_name} def BuildHooksEnv(self): """Build hooks env. - This will run on the new master only in the pre phase, and on all - the nodes in the post phase. + This runs on the master node. """ env = { - "OP_TARGET": self.new_master, - "NEW_MASTER": self.new_master, - "OLD_MASTER": self.old_master, + "OP_TARGET": self.op.node_name, + "MASTER_CANDIDATE": str(self.op.master_candidate), + "OFFLINE": str(self.op.offline), + "DRAINED": str(self.op.drained), } - return env, [self.new_master], self.cfg.GetNodeList() + nl = [self.cfg.GetMasterNode(), + self.op.node_name] + return env, nl, nl def CheckPrereq(self): """Check prerequisites. - This checks that we are not already the master. + This only checks the instance list against the existing names. """ - self.new_master = utils.HostInfo().name - self.old_master = self.sstore.GetMasterNode() + node = self.node = self.cfg.GetNodeInfo(self.op.node_name) + + if (self.op.master_candidate is not None or + self.op.drained is not None or + self.op.offline is not None): + # we can't change the master's node flags + if self.op.node_name == self.cfg.GetMasterNode(): + raise errors.OpPrereqError("The master role can be changed" + " only via masterfailover") + + if ((self.op.master_candidate == False or self.op.offline == True or + self.op.drained == True) and node.master_candidate): + cp_size = self.cfg.GetClusterInfo().candidate_pool_size + num_candidates, _ = self.cfg.GetMasterCandidateStats() + if num_candidates <= cp_size: + msg = ("Not enough master candidates (desired" + " %d, new value will be %d)" % (cp_size, num_candidates-1)) + if self.op.force: + self.LogWarning(msg) + else: + raise errors.OpPrereqError(msg) - if self.old_master == self.new_master: - raise errors.OpPrereqError("This commands must be run on the node" - " where you want the new master to be." - " %s is already the master" % - self.old_master) + if (self.op.master_candidate == True and + ((node.offline and not self.op.offline == False) or + (node.drained and not self.op.drained == False))): + raise errors.OpPrereqError("Node '%s' is offline or drained, can't set" + " to master_candidate" % node.name) + + return def Exec(self, feedback_fn): - """Failover the master node. + """Modifies a node. - This command, when run on a non-master node, will cause the current - master to cease being master, and the non-master to become new - master. + """ + node = self.node + + result = [] + changed_mc = False + + if self.op.offline is not None: + node.offline = self.op.offline + result.append(("offline", str(self.op.offline))) + if self.op.offline == True: + if node.master_candidate: + node.master_candidate = False + changed_mc = True + result.append(("master_candidate", "auto-demotion due to offline")) + if node.drained: + node.drained = False + result.append(("drained", "clear drained status due to offline")) + + if self.op.master_candidate is not None: + node.master_candidate = self.op.master_candidate + changed_mc = True + result.append(("master_candidate", str(self.op.master_candidate))) + if self.op.master_candidate == False: + rrc = self.rpc.call_node_demote_from_mc(node.name) + msg = rrc.fail_msg + if msg: + self.LogWarning("Node failed to demote itself: %s" % msg) + + if self.op.drained is not None: + node.drained = self.op.drained + result.append(("drained", str(self.op.drained))) + if self.op.drained == True: + if node.master_candidate: + node.master_candidate = False + changed_mc = True + result.append(("master_candidate", "auto-demotion due to drain")) + rrc = self.rpc.call_node_demote_from_mc(node.name) + msg = rrc.fail_msg + if msg: + self.LogWarning("Node failed to demote itself: %s" % msg) + if node.offline: + node.offline = False + result.append(("offline", "clear offline status due to drain")) + + # this will trigger configuration file update, if needed + self.cfg.Update(node) + # this will trigger job queue propagation or cleanup + if changed_mc: + self.context.ReaddNode(node) + + return result + + +class LUPowercycleNode(NoHooksLU): + """Powercycles a node. + + """ + _OP_REQP = ["node_name", "force"] + REQ_BGL = False + + def CheckArguments(self): + node_name = self.cfg.ExpandNodeName(self.op.node_name) + if node_name is None: + raise errors.OpPrereqError("Invalid node name '%s'" % self.op.node_name) + self.op.node_name = node_name + if node_name == self.cfg.GetMasterNode() and not self.op.force: + raise errors.OpPrereqError("The node is the master and the force" + " parameter was not set") + + def ExpandNames(self): + """Locking for PowercycleNode. + + This is a last-resort option and shouldn't block on other + jobs. Therefore, we grab no locks. """ - #TODO: do not rely on gethostname returning the FQDN - logger.Info("setting master to %s, old master: %s" % - (self.new_master, self.old_master)) + self.needed_locks = {} - if not rpc.call_node_stop_master(self.old_master): - logger.Error("could disable the master role on the old master" - " %s, please disable manually" % self.old_master) + def CheckPrereq(self): + """Check prerequisites. - ss = self.sstore - ss.SetKey(ss.SS_MASTER_NODE, self.new_master) - if not rpc.call_upload_file(self.cfg.GetNodeList(), - ss.KeyToFilename(ss.SS_MASTER_NODE)): - logger.Error("could not distribute the new simple store master file" - " to the other nodes, please check.") + This LU has no prereqs. + + """ + pass - if not rpc.call_node_start_master(self.new_master): - logger.Error("could not start the master role on the new master" - " %s, please check" % self.new_master) - feedback_fn("Error in activating the master IP on the new master," - " please fix manually.") + def Exec(self, feedback_fn): + """Reboots a node. + """ + result = self.rpc.call_node_powercycle(self.op.node_name, + self.cfg.GetHypervisorType()) + result.Raise("Failed to schedule the reboot") + return result.payload class LUQueryClusterInfo(NoHooksLU): @@ -1747,7 +3065,10 @@ class LUQueryClusterInfo(NoHooksLU): """ _OP_REQP = [] - REQ_MASTER = False + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = {} def CheckPrereq(self): """No prerequsites needed for this LU. @@ -1759,26 +3080,51 @@ class LUQueryClusterInfo(NoHooksLU): """Return cluster config. """ + cluster = self.cfg.GetClusterInfo() result = { - "name": self.sstore.GetClusterName(), "software_version": constants.RELEASE_VERSION, "protocol_version": constants.PROTOCOL_VERSION, "config_version": constants.CONFIG_VERSION, - "os_api_version": constants.OS_API_VERSION, + "os_api_version": max(constants.OS_API_VERSIONS), "export_version": constants.EXPORT_VERSION, - "master": self.sstore.GetMasterNode(), "architecture": (platform.architecture()[0], platform.machine()), - "hypervisor_type": self.sstore.GetHypervisorType(), + "name": cluster.cluster_name, + "master": cluster.master_node, + "default_hypervisor": cluster.enabled_hypervisors[0], + "enabled_hypervisors": cluster.enabled_hypervisors, + "hvparams": dict([(hypervisor_name, cluster.hvparams[hypervisor_name]) + for hypervisor_name in cluster.enabled_hypervisors]), + "beparams": cluster.beparams, + "nicparams": cluster.nicparams, + "candidate_pool_size": cluster.candidate_pool_size, + "master_netdev": cluster.master_netdev, + "volume_group_name": cluster.volume_group_name, + "file_storage_dir": cluster.file_storage_dir, + "ctime": cluster.ctime, + "mtime": cluster.mtime, + "uuid": cluster.uuid, + "tags": list(cluster.GetTags()), } return result -class LUDumpClusterConfig(NoHooksLU): - """Return a text-representation of the cluster-config. +class LUQueryConfigValues(NoHooksLU): + """Return configuration values. """ _OP_REQP = [] + REQ_BGL = False + _FIELDS_DYNAMIC = utils.FieldSet() + _FIELDS_STATIC = utils.FieldSet("cluster_name", "master_node", "drain_flag", + "watcher_pause") + + def ExpandNames(self): + self.needed_locks = {} + + _CheckOutputFields(static=self._FIELDS_STATIC, + dynamic=self._FIELDS_DYNAMIC, + selected=self.op.output_fields) def CheckPrereq(self): """No prerequisites. @@ -1790,7 +3136,20 @@ class LUDumpClusterConfig(NoHooksLU): """Dump a representation of the cluster config to the standard output. """ - return self.cfg.DumpConfig() + values = [] + for field in self.op.output_fields: + if field == "cluster_name": + entry = self.cfg.GetClusterName() + elif field == "master_node": + entry = self.cfg.GetMasterNode() + elif field == "drain_flag": + entry = os.path.exists(constants.JOB_QUEUE_DRAIN_FILE) + elif field == "watcher_pause": + return utils.ReadWatcherPauseFile(constants.WATCHER_PAUSEFILE) + else: + raise errors.ParameterError(field) + values.append(entry) + return values class LUActivateInstanceDisks(NoHooksLU): @@ -1798,6 +3157,16 @@ class LUActivateInstanceDisks(NoHooksLU): """ _OP_REQP = ["instance_name"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() def CheckPrereq(self): """Check prerequisites. @@ -1805,39 +3174,47 @@ class LUActivateInstanceDisks(NoHooksLU): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) - self.instance = instance - + self.instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + _CheckNodeOnline(self, self.instance.primary_node) + if not hasattr(self.op, "ignore_size"): + self.op.ignore_size = False def Exec(self, feedback_fn): """Activate the disks. """ - disks_ok, disks_info = _AssembleInstanceDisks(self.instance, self.cfg) + disks_ok, disks_info = \ + _AssembleInstanceDisks(self, self.instance, + ignore_size=self.op.ignore_size) if not disks_ok: raise errors.OpExecError("Cannot activate block devices") return disks_info -def _AssembleInstanceDisks(instance, cfg, ignore_secondaries=False): +def _AssembleInstanceDisks(lu, instance, ignore_secondaries=False, + ignore_size=False): """Prepare the block devices for an instance. This sets up the block devices on all nodes. - Args: - instance: a ganeti.objects.Instance object - ignore_secondaries: if true, errors on secondary nodes won't result - in an error return from the function + @type lu: L{LogicalUnit} + @param lu: the logical unit on whose behalf we execute + @type instance: L{objects.Instance} + @param instance: the instance for whose disks we assemble + @type ignore_secondaries: boolean + @param ignore_secondaries: if true, errors on secondary nodes + won't result in an error return from the function + @type ignore_size: boolean + @param ignore_size: if true, the current known size of the disk + will not be used during the disk activation, useful for cases + when the size is wrong + @return: False if the operation failed, otherwise a list of + (host, instance_visible_name, node_visible_name) + with the mapping from node devices to instance devices - Returns: - false if the operation failed - list of (host, instance_visible_name, node_visible_name) if the operation - suceeded with the mapping from node devices to instance devices """ device_info = [] disks_ok = True @@ -1854,11 +3231,16 @@ def _AssembleInstanceDisks(instance, cfg, ignore_secondaries=False): # 1st pass, assemble on all nodes in secondary mode for inst_disk in instance.disks: for node, node_disk in inst_disk.ComputeNodeTree(instance.primary_node): - cfg.SetDiskID(node_disk, node) - result = rpc.call_blockdev_assemble(node, node_disk, iname, False) - if not result: - logger.Error("could not prepare block device %s on node %s" - " (is_primary=False, pass=1)" % (inst_disk.iv_name, node)) + if ignore_size: + node_disk = node_disk.Copy() + node_disk.UnsetSize() + lu.cfg.SetDiskID(node_disk, node) + result = lu.rpc.call_blockdev_assemble(node, node_disk, iname, False) + msg = result.fail_msg + if msg: + lu.proc.LogWarning("Could not prepare block device %s on node %s" + " (is_primary=False, pass=1): %s", + inst_disk.iv_name, node, msg) if not ignore_secondaries: disks_ok = False @@ -1869,34 +3251,41 @@ def _AssembleInstanceDisks(instance, cfg, ignore_secondaries=False): for node, node_disk in inst_disk.ComputeNodeTree(instance.primary_node): if node != instance.primary_node: continue - cfg.SetDiskID(node_disk, node) - result = rpc.call_blockdev_assemble(node, node_disk, iname, True) - if not result: - logger.Error("could not prepare block device %s on node %s" - " (is_primary=True, pass=2)" % (inst_disk.iv_name, node)) + if ignore_size: + node_disk = node_disk.Copy() + node_disk.UnsetSize() + lu.cfg.SetDiskID(node_disk, node) + result = lu.rpc.call_blockdev_assemble(node, node_disk, iname, True) + msg = result.fail_msg + if msg: + lu.proc.LogWarning("Could not prepare block device %s on node %s" + " (is_primary=True, pass=2): %s", + inst_disk.iv_name, node, msg) disks_ok = False - device_info.append((instance.primary_node, inst_disk.iv_name, result)) + device_info.append((instance.primary_node, inst_disk.iv_name, + result.payload)) # leave the disks configured for the primary node # this is a workaround that would be fixed better by # improving the logical/physical id handling for disk in instance.disks: - cfg.SetDiskID(disk, instance.primary_node) + lu.cfg.SetDiskID(disk, instance.primary_node) return disks_ok, device_info -def _StartInstanceDisks(cfg, instance, force): +def _StartInstanceDisks(lu, instance, force): """Start the disks of an instance. """ - disks_ok, dummy = _AssembleInstanceDisks(instance, cfg, + disks_ok, _ = _AssembleInstanceDisks(lu, instance, ignore_secondaries=force) if not disks_ok: - _ShutdownInstanceDisks(instance, cfg) + _ShutdownInstanceDisks(lu, instance) if force is not None and not force: - logger.Error("If the message above refers to a secondary node," - " you can retry the operation using '--force'.") + lu.proc.LogWarning("", hint="If the message above refers to a" + " secondary node," + " you can retry the operation using '--force'.") raise errors.OpExecError("Disk consistency error") @@ -1905,6 +3294,16 @@ class LUDeactivateInstanceDisks(NoHooksLU): """ _OP_REQP = ["instance_name"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() def CheckPrereq(self): """Check prerequisites. @@ -1912,32 +3311,37 @@ class LUDeactivateInstanceDisks(NoHooksLU): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) - self.instance = instance + self.instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name def Exec(self, feedback_fn): """Deactivate the disks """ instance = self.instance - ins_l = rpc.call_instance_list([instance.primary_node]) - ins_l = ins_l[instance.primary_node] - if not type(ins_l) is list: - raise errors.OpExecError("Can't contact node '%s'" % - instance.primary_node) + _SafeShutdownInstanceDisks(self, instance) + - if self.instance.name in ins_l: - raise errors.OpExecError("Instance is running, can't shutdown" - " block devices.") +def _SafeShutdownInstanceDisks(lu, instance): + """Shutdown block devices of an instance. + + This function checks if an instance is running, before calling + _ShutdownInstanceDisks. + + """ + pnode = instance.primary_node + ins_l = lu.rpc.call_instance_list([pnode], [instance.hypervisor])[pnode] + ins_l.Raise("Can't contact node %s" % pnode) - _ShutdownInstanceDisks(instance, self.cfg) + if instance.name in ins_l.payload: + raise errors.OpExecError("Instance is running, can't shutdown" + " block devices.") + _ShutdownInstanceDisks(lu, instance) -def _ShutdownInstanceDisks(instance, cfg, ignore_primary=False): + +def _ShutdownInstanceDisks(lu, instance, ignore_primary=False): """Shutdown block devices of an instance. This does the shutdown on all nodes of the instance. @@ -1946,19 +3350,21 @@ def _ShutdownInstanceDisks(instance, cfg, ignore_primary=False): ignored. """ - result = True + all_result = True for disk in instance.disks: for node, top_disk in disk.ComputeNodeTree(instance.primary_node): - cfg.SetDiskID(top_disk, node) - if not rpc.call_blockdev_shutdown(node, top_disk): - logger.Error("could not shutdown block device %s on node %s" % - (disk.iv_name, node)) + lu.cfg.SetDiskID(top_disk, node) + result = lu.rpc.call_blockdev_shutdown(node, top_disk) + msg = result.fail_msg + if msg: + lu.LogWarning("Could not shutdown block device %s on node %s: %s", + disk.iv_name, node, msg) if not ignore_primary or node != instance.primary_node: - result = False - return result + all_result = False + return all_result -def _CheckNodeFreeMemory(cfg, node, reason, requested): +def _CheckNodeFreeMemory(lu, node, reason, requested, hypervisor_name): """Checks if a node has enough free memory. This function check if a given node has the needed amount of free @@ -1966,26 +3372,30 @@ def _CheckNodeFreeMemory(cfg, node, reason, requested): information from the node, this function raise an OpPrereqError exception. - Args: - - cfg: a ConfigWriter instance - - node: the node name - - reason: string to use in the error message - - requested: the amount of memory in MiB + @type lu: C{LogicalUnit} + @param lu: a logical unit from which we get configuration data + @type node: C{str} + @param node: the node to check + @type reason: C{str} + @param reason: string to use in the error message + @type requested: C{int} + @param requested: the amount of memory in MiB to check for + @type hypervisor_name: C{str} + @param hypervisor_name: the hypervisor to ask for memory stats + @raise errors.OpPrereqError: if the node doesn't have enough memory, or + we cannot check the node """ - nodeinfo = rpc.call_node_info([node], cfg.GetVGName()) - if not nodeinfo or not isinstance(nodeinfo, dict): - raise errors.OpPrereqError("Could not contact node %s for resource" - " information" % (node,)) - - free_mem = nodeinfo[node].get('memory_free') + nodeinfo = lu.rpc.call_node_info([node], lu.cfg.GetVGName(), hypervisor_name) + nodeinfo[node].Raise("Can't get data from node %s" % node, prereq=True) + free_mem = nodeinfo[node].payload.get('memory_free', None) if not isinstance(free_mem, int): raise errors.OpPrereqError("Can't compute free memory on node %s, result" - " was '%s'" % (node, free_mem)) + " was '%s'" % (node, free_mem)) if requested > free_mem: raise errors.OpPrereqError("Not enough memory on node %s for %s:" - " needed %s MiB, available %s MiB" % - (node, reason, requested, free_mem)) + " needed %s MiB, available %s MiB" % + (node, reason, requested, free_mem)) class LUStartupInstance(LogicalUnit): @@ -1995,6 +3405,10 @@ class LUStartupInstance(LogicalUnit): HPATH = "instance-start" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name", "force"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() def BuildHooksEnv(self): """Build hooks env. @@ -2005,9 +3419,8 @@ class LUStartupInstance(LogicalUnit): env = { "FORCE": self.op.force, } - env.update(_BuildInstanceHookEnvByObject(self.instance)) - nl = ([self.sstore.GetMasterNode(), self.instance.primary_node] + - list(self.instance.secondary_nodes)) + env.update(_BuildInstanceHookEnvByObject(self, self.instance)) + nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) return env, nl, nl def CheckPrereq(self): @@ -2016,21 +3429,53 @@ class LUStartupInstance(LogicalUnit): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) - - # check bridges existance - _CheckInstanceBridgesExist(instance) - - _CheckNodeFreeMemory(self.cfg, instance.primary_node, - "starting instance %s" % instance.name, - instance.memory) - - self.instance = instance - self.op.instance_name = instance.name + self.instance = instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + + # extra beparams + self.beparams = getattr(self.op, "beparams", {}) + if self.beparams: + if not isinstance(self.beparams, dict): + raise errors.OpPrereqError("Invalid beparams passed: %s, expected" + " dict" % (type(self.beparams), )) + # fill the beparams dict + utils.ForceDictType(self.beparams, constants.BES_PARAMETER_TYPES) + self.op.beparams = self.beparams + + # extra hvparams + self.hvparams = getattr(self.op, "hvparams", {}) + if self.hvparams: + if not isinstance(self.hvparams, dict): + raise errors.OpPrereqError("Invalid hvparams passed: %s, expected" + " dict" % (type(self.hvparams), )) + + # check hypervisor parameter syntax (locally) + cluster = self.cfg.GetClusterInfo() + utils.ForceDictType(self.hvparams, constants.HVS_PARAMETER_TYPES) + filled_hvp = objects.FillDict(cluster.hvparams[instance.hypervisor], + instance.hvparams) + filled_hvp.update(self.hvparams) + hv_type = hypervisor.GetHypervisor(instance.hypervisor) + hv_type.CheckParameterSyntax(filled_hvp) + _CheckHVParams(self, instance.all_nodes, instance.hypervisor, filled_hvp) + self.op.hvparams = self.hvparams + + _CheckNodeOnline(self, instance.primary_node) + + bep = self.cfg.GetClusterInfo().FillBE(instance) + # check bridges existence + _CheckInstanceBridgesExist(self, instance) + + remote_info = self.rpc.call_instance_info(instance.primary_node, + instance.name, + instance.hypervisor) + remote_info.Raise("Error checking node %s" % instance.primary_node, + prereq=True) + if not remote_info.payload: # not running already + _CheckNodeFreeMemory(self, instance.primary_node, + "starting instance %s" % instance.name, + bep[constants.BE_MEMORY], instance.hypervisor) def Exec(self, feedback_fn): """Start the instance. @@ -2038,17 +3483,19 @@ class LUStartupInstance(LogicalUnit): """ instance = self.instance force = self.op.force - extra_args = getattr(self.op, "extra_args", "") self.cfg.MarkInstanceUp(instance.name) node_current = instance.primary_node - _StartInstanceDisks(self.cfg, instance, force) + _StartInstanceDisks(self, instance, force) - if not rpc.call_instance_start(node_current, instance, extra_args): - _ShutdownInstanceDisks(instance, self.cfg) - raise errors.OpExecError("Could not start instance") + result = self.rpc.call_instance_start(node_current, instance, + self.hvparams, self.beparams) + msg = result.fail_msg + if msg: + _ShutdownInstanceDisks(self, instance) + raise errors.OpExecError("Could not start instance: %s" % msg) class LURebootInstance(LogicalUnit): @@ -2058,6 +3505,17 @@ class LURebootInstance(LogicalUnit): HPATH = "instance-reboot" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name", "ignore_secondaries", "reboot_type"] + REQ_BGL = False + + def ExpandNames(self): + if self.op.reboot_type not in [constants.INSTANCE_REBOOT_SOFT, + constants.INSTANCE_REBOOT_HARD, + constants.INSTANCE_REBOOT_FULL]: + raise errors.ParameterError("reboot type not in [%s, %s, %s]" % + (constants.INSTANCE_REBOOT_SOFT, + constants.INSTANCE_REBOOT_HARD, + constants.INSTANCE_REBOOT_FULL)) + self._ExpandAndLockInstance() def BuildHooksEnv(self): """Build hooks env. @@ -2067,10 +3525,10 @@ class LURebootInstance(LogicalUnit): """ env = { "IGNORE_SECONDARIES": self.op.ignore_secondaries, + "REBOOT_TYPE": self.op.reboot_type, } - env.update(_BuildInstanceHookEnvByObject(self.instance)) - nl = ([self.sstore.GetMasterNode(), self.instance.primary_node] + - list(self.instance.secondary_nodes)) + env.update(_BuildInstanceHookEnvByObject(self, self.instance)) + nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) return env, nl, nl def CheckPrereq(self): @@ -2079,17 +3537,14 @@ class LURebootInstance(LogicalUnit): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) + self.instance = instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name - # check bridges existance - _CheckInstanceBridgesExist(instance) + _CheckNodeOnline(self, instance.primary_node) - self.instance = instance - self.op.instance_name = instance.name + # check bridges existence + _CheckInstanceBridgesExist(self, instance) def Exec(self, feedback_fn): """Reboot the instance. @@ -2098,31 +3553,27 @@ class LURebootInstance(LogicalUnit): instance = self.instance ignore_secondaries = self.op.ignore_secondaries reboot_type = self.op.reboot_type - extra_args = getattr(self.op, "extra_args", "") node_current = instance.primary_node - if reboot_type not in [constants.INSTANCE_REBOOT_SOFT, - constants.INSTANCE_REBOOT_HARD, - constants.INSTANCE_REBOOT_FULL]: - raise errors.ParameterError("reboot type not in [%s, %s, %s]" % - (constants.INSTANCE_REBOOT_SOFT, - constants.INSTANCE_REBOOT_HARD, - constants.INSTANCE_REBOOT_FULL)) - if reboot_type in [constants.INSTANCE_REBOOT_SOFT, constants.INSTANCE_REBOOT_HARD]: - if not rpc.call_instance_reboot(node_current, instance, - reboot_type, extra_args): - raise errors.OpExecError("Could not reboot instance") + for disk in instance.disks: + self.cfg.SetDiskID(disk, node_current) + result = self.rpc.call_instance_reboot(node_current, instance, + reboot_type) + result.Raise("Could not reboot instance") else: - if not rpc.call_instance_shutdown(node_current, instance): - raise errors.OpExecError("could not shutdown instance for full reboot") - _ShutdownInstanceDisks(instance, self.cfg) - _StartInstanceDisks(self.cfg, instance, ignore_secondaries) - if not rpc.call_instance_start(node_current, instance, extra_args): - _ShutdownInstanceDisks(instance, self.cfg) - raise errors.OpExecError("Could not start instance for full reboot") + result = self.rpc.call_instance_shutdown(node_current, instance) + result.Raise("Could not shutdown instance for full reboot") + _ShutdownInstanceDisks(self, instance) + _StartInstanceDisks(self, instance, ignore_secondaries) + result = self.rpc.call_instance_start(node_current, instance, None, None) + msg = result.fail_msg + if msg: + _ShutdownInstanceDisks(self, instance) + raise errors.OpExecError("Could not start instance for" + " full reboot: %s" % msg) self.cfg.MarkInstanceUp(instance.name) @@ -2134,6 +3585,10 @@ class LUShutdownInstance(LogicalUnit): HPATH = "instance-stop" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() def BuildHooksEnv(self): """Build hooks env. @@ -2141,9 +3596,8 @@ class LUShutdownInstance(LogicalUnit): This runs on master, primary and secondary nodes of the instance. """ - env = _BuildInstanceHookEnvByObject(self.instance) - nl = ([self.sstore.GetMasterNode(), self.instance.primary_node] + - list(self.instance.secondary_nodes)) + env = _BuildInstanceHookEnvByObject(self, self.instance) + nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) return env, nl, nl def CheckPrereq(self): @@ -2152,12 +3606,10 @@ class LUShutdownInstance(LogicalUnit): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) - self.instance = instance + self.instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + _CheckNodeOnline(self, self.instance.primary_node) def Exec(self, feedback_fn): """Shutdown the instance. @@ -2166,10 +3618,12 @@ class LUShutdownInstance(LogicalUnit): instance = self.instance node_current = instance.primary_node self.cfg.MarkInstanceDown(instance.name) - if not rpc.call_instance_shutdown(node_current, instance): - logger.Error("could not shutdown instance") + result = self.rpc.call_instance_shutdown(node_current, instance) + msg = result.fail_msg + if msg: + self.proc.LogWarning("Could not shutdown instance: %s" % msg) - _ShutdownInstanceDisks(instance, self.cfg) + _ShutdownInstanceDisks(self, instance) class LUReinstallInstance(LogicalUnit): @@ -2179,6 +3633,10 @@ class LUReinstallInstance(LogicalUnit): HPATH = "instance-reinstall" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() def BuildHooksEnv(self): """Build hooks env. @@ -2186,9 +3644,8 @@ class LUReinstallInstance(LogicalUnit): This runs on master, primary and secondary nodes of the instance. """ - env = _BuildInstanceHookEnvByObject(self.instance) - nl = ([self.sstore.GetMasterNode(), self.instance.primary_node] + - list(self.instance.secondary_nodes)) + env = _BuildInstanceHookEnvByObject(self, self.instance) + nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) return env, nl, nl def CheckPrereq(self): @@ -2197,19 +3654,23 @@ class LUReinstallInstance(LogicalUnit): This checks that the instance is in the cluster and is not running. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) + instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + _CheckNodeOnline(self, instance.primary_node) + if instance.disk_template == constants.DT_DISKLESS: raise errors.OpPrereqError("Instance '%s' has no disks" % self.op.instance_name) - if instance.status != "down": + if instance.admin_up: raise errors.OpPrereqError("Instance '%s' is marked to be up" % self.op.instance_name) - remote_info = rpc.call_instance_info(instance.primary_node, instance.name) - if remote_info: + remote_info = self.rpc.call_instance_info(instance.primary_node, + instance.name, + instance.hypervisor) + remote_info.Raise("Error checking node %s" % instance.primary_node, + prereq=True) + if remote_info.payload: raise errors.OpPrereqError("Instance '%s' is running on the node %s" % (self.op.instance_name, instance.primary_node)) @@ -2222,10 +3683,9 @@ class LUReinstallInstance(LogicalUnit): if pnode is None: raise errors.OpPrereqError("Primary node '%s' is unknown" % self.op.pnode) - os_obj = rpc.call_os_get(pnode.name, self.op.os_type) - if not os_obj: - raise errors.OpPrereqError("OS '%s' not in supported OS list for" - " primary node" % self.op.os_type) + result = self.rpc.call_os_get(pnode.name, self.op.os_type) + result.Raise("OS '%s' not in supported OS list for primary node %s" % + (self.op.os_type, pnode.name), prereq=True) self.instance = instance @@ -2238,17 +3698,99 @@ class LUReinstallInstance(LogicalUnit): if self.op.os_type is not None: feedback_fn("Changing OS to '%s'..." % self.op.os_type) inst.os = self.op.os_type - self.cfg.AddInstance(inst) + self.cfg.Update(inst) - _StartInstanceDisks(self.cfg, inst, None) + _StartInstanceDisks(self, inst, None) try: feedback_fn("Running the instance OS create scripts...") - if not rpc.call_instance_os_add(inst.primary_node, inst, "sda", "sdb"): - raise errors.OpExecError("Could not install OS for instance %s" - " on node %s" % - (inst.name, inst.primary_node)) + result = self.rpc.call_instance_os_add(inst.primary_node, inst, True) + result.Raise("Could not install OS for instance %s on node %s" % + (inst.name, inst.primary_node)) finally: - _ShutdownInstanceDisks(inst, self.cfg) + _ShutdownInstanceDisks(self, inst) + + +class LURecreateInstanceDisks(LogicalUnit): + """Recreate an instance's missing disks. + + """ + HPATH = "instance-recreate-disks" + HTYPE = constants.HTYPE_INSTANCE + _OP_REQP = ["instance_name", "disks"] + REQ_BGL = False + + def CheckArguments(self): + """Check the arguments. + + """ + if not isinstance(self.op.disks, list): + raise errors.OpPrereqError("Invalid disks parameter") + for item in self.op.disks: + if (not isinstance(item, int) or + item < 0): + raise errors.OpPrereqError("Invalid disk specification '%s'" % + str(item)) + + def ExpandNames(self): + self._ExpandAndLockInstance() + + def BuildHooksEnv(self): + """Build hooks env. + + This runs on master, primary and secondary nodes of the instance. + + """ + env = _BuildInstanceHookEnvByObject(self, self.instance) + nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) + return env, nl, nl + + def CheckPrereq(self): + """Check prerequisites. + + This checks that the instance is in the cluster and is not running. + + """ + instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + _CheckNodeOnline(self, instance.primary_node) + + if instance.disk_template == constants.DT_DISKLESS: + raise errors.OpPrereqError("Instance '%s' has no disks" % + self.op.instance_name) + if instance.admin_up: + raise errors.OpPrereqError("Instance '%s' is marked to be up" % + self.op.instance_name) + remote_info = self.rpc.call_instance_info(instance.primary_node, + instance.name, + instance.hypervisor) + remote_info.Raise("Error checking node %s" % instance.primary_node, + prereq=True) + if remote_info.payload: + raise errors.OpPrereqError("Instance '%s' is running on the node %s" % + (self.op.instance_name, + instance.primary_node)) + + if not self.op.disks: + self.op.disks = range(len(instance.disks)) + else: + for idx in self.op.disks: + if idx >= len(instance.disks): + raise errors.OpPrereqError("Invalid disk index passed '%s'" % idx) + + self.instance = instance + + def Exec(self, feedback_fn): + """Recreate the disks. + + """ + to_skip = [] + for idx, disk in enumerate(self.instance.disks): + if idx not in self.op.disks: # disk idx has not been passed in + to_skip.append(idx) + continue + + _CreateDisks(self, self.instance, to_skip=to_skip) class LURenameInstance(LogicalUnit): @@ -2265,10 +3807,9 @@ class LURenameInstance(LogicalUnit): This runs on master, primary and secondary nodes of the instance. """ - env = _BuildInstanceHookEnvByObject(self.instance) + env = _BuildInstanceHookEnvByObject(self, self.instance) env["INSTANCE_NEW_NAME"] = self.op.new_name - nl = ([self.sstore.GetMasterNode(), self.instance.primary_node] + - list(self.instance.secondary_nodes)) + nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) return env, nl, nl def CheckPrereq(self): @@ -2282,11 +3823,17 @@ class LURenameInstance(LogicalUnit): if instance is None: raise errors.OpPrereqError("Instance '%s' not known" % self.op.instance_name) - if instance.status != "down": + _CheckNodeOnline(self, instance.primary_node) + + if instance.admin_up: raise errors.OpPrereqError("Instance '%s' is marked to be up" % self.op.instance_name) - remote_info = rpc.call_instance_info(instance.primary_node, instance.name) - if remote_info: + remote_info = self.rpc.call_instance_info(instance.primary_node, + instance.name, + instance.hypervisor) + remote_info.Raise("Error checking node %s" % instance.primary_node, + prereq=True) + if remote_info.payload: raise errors.OpPrereqError("Instance '%s' is running on the node %s" % (self.op.instance_name, instance.primary_node)) @@ -2318,39 +3865,35 @@ class LURenameInstance(LogicalUnit): old_file_storage_dir = os.path.dirname(inst.disks[0].logical_id[1]) self.cfg.RenameInstance(inst.name, self.op.new_name) + # Change the instance lock. This is definitely safe while we hold the BGL + self.context.glm.remove(locking.LEVEL_INSTANCE, old_name) + self.context.glm.add(locking.LEVEL_INSTANCE, self.op.new_name) # re-read the instance from the configuration after rename inst = self.cfg.GetInstanceInfo(self.op.new_name) if inst.disk_template == constants.DT_FILE: new_file_storage_dir = os.path.dirname(inst.disks[0].logical_id[1]) - result = rpc.call_file_storage_dir_rename(inst.primary_node, - old_file_storage_dir, - new_file_storage_dir) - - if not result: - raise errors.OpExecError("Could not connect to node '%s' to rename" - " directory '%s' to '%s' (but the instance" - " has been renamed in Ganeti)" % ( - inst.primary_node, old_file_storage_dir, - new_file_storage_dir)) - - if not result[0]: - raise errors.OpExecError("Could not rename directory '%s' to '%s'" - " (but the instance has been renamed in" - " Ganeti)" % (old_file_storage_dir, - new_file_storage_dir)) - - _StartInstanceDisks(self.cfg, inst, None) + result = self.rpc.call_file_storage_dir_rename(inst.primary_node, + old_file_storage_dir, + new_file_storage_dir) + result.Raise("Could not rename on node %s directory '%s' to '%s'" + " (but the instance has been renamed in Ganeti)" % + (inst.primary_node, old_file_storage_dir, + new_file_storage_dir)) + + _StartInstanceDisks(self, inst, None) try: - if not rpc.call_instance_run_rename(inst.primary_node, inst, old_name, - "sda", "sdb"): - msg = ("Could run OS rename script for instance %s on node %s (but the" - " instance has been renamed in Ganeti)" % - (inst.name, inst.primary_node)) - logger.Error(msg) + result = self.rpc.call_instance_run_rename(inst.primary_node, inst, + old_name) + msg = result.fail_msg + if msg: + msg = ("Could not run OS rename script for instance %s on node %s" + " (but the instance has been renamed in Ganeti): %s" % + (inst.name, inst.primary_node, msg)) + self.proc.LogWarning(msg) finally: - _ShutdownInstanceDisks(inst, self.cfg) + _ShutdownInstanceDisks(self, inst) class LURemoveInstance(LogicalUnit): @@ -2360,6 +3903,16 @@ class LURemoveInstance(LogicalUnit): HPATH = "instance-remove" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name", "ignore_failures"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() def BuildHooksEnv(self): """Build hooks env. @@ -2367,8 +3920,8 @@ class LURemoveInstance(LogicalUnit): This runs on master, primary and secondary nodes of the instance. """ - env = _BuildInstanceHookEnvByObject(self.instance) - nl = [self.sstore.GetMasterNode()] + env = _BuildInstanceHookEnvByObject(self, self.instance) + nl = [self.cfg.GetMasterNode()] return env, nl, nl def CheckPrereq(self): @@ -2377,129 +3930,196 @@ class LURemoveInstance(LogicalUnit): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) - self.instance = instance + self.instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name def Exec(self, feedback_fn): """Remove the instance. """ instance = self.instance - logger.Info("shutting down instance %s on node %s" % - (instance.name, instance.primary_node)) + logging.info("Shutting down instance %s on node %s", + instance.name, instance.primary_node) - if not rpc.call_instance_shutdown(instance.primary_node, instance): + result = self.rpc.call_instance_shutdown(instance.primary_node, instance) + msg = result.fail_msg + if msg: if self.op.ignore_failures: - feedback_fn("Warning: can't shutdown instance") + feedback_fn("Warning: can't shutdown instance: %s" % msg) else: - raise errors.OpExecError("Could not shutdown instance %s on node %s" % - (instance.name, instance.primary_node)) + raise errors.OpExecError("Could not shutdown instance %s on" + " node %s: %s" % + (instance.name, instance.primary_node, msg)) - logger.Info("removing block devices for instance %s" % instance.name) + logging.info("Removing block devices for instance %s", instance.name) - if not _RemoveDisks(instance, self.cfg): + if not _RemoveDisks(self, instance): if self.op.ignore_failures: feedback_fn("Warning: can't remove instance's disks") else: raise errors.OpExecError("Can't remove instance's disks") - logger.Info("removing instance %s out of cluster config" % instance.name) + logging.info("Removing instance %s out of cluster config", instance.name) self.cfg.RemoveInstance(instance.name) - # Remove the new instance from the Ganeti Lock Manager - self.context.glm.remove(locking.LEVEL_INSTANCE, instance.name) + self.remove_locks[locking.LEVEL_INSTANCE] = instance.name class LUQueryInstances(NoHooksLU): """Logical unit for querying instances. """ - _OP_REQP = ["output_fields", "names"] + _OP_REQP = ["output_fields", "names", "use_locking"] + REQ_BGL = False + _SIMPLE_FIELDS = ["name", "os", "network_port", "hypervisor", + "serial_no", "ctime", "mtime", "uuid"] + _FIELDS_STATIC = utils.FieldSet(*["name", "os", "pnode", "snodes", + "admin_state", + "disk_template", "ip", "mac", "bridge", + "nic_mode", "nic_link", + "sda_size", "sdb_size", "vcpus", "tags", + "network_port", "beparams", + r"(disk)\.(size)/([0-9]+)", + r"(disk)\.(sizes)", "disk_usage", + r"(nic)\.(mac|ip|mode|link)/([0-9]+)", + r"(nic)\.(bridge)/([0-9]+)", + r"(nic)\.(macs|ips|modes|links|bridges)", + r"(disk|nic)\.(count)", + "hvparams", + ] + _SIMPLE_FIELDS + + ["hv/%s" % name + for name in constants.HVS_PARAMETERS] + + ["be/%s" % name + for name in constants.BES_PARAMETERS]) + _FIELDS_DYNAMIC = utils.FieldSet("oper_state", "oper_ram", "status") + + + def ExpandNames(self): + _CheckOutputFields(static=self._FIELDS_STATIC, + dynamic=self._FIELDS_DYNAMIC, + selected=self.op.output_fields) + + self.needed_locks = {} + self.share_locks[locking.LEVEL_INSTANCE] = 1 + self.share_locks[locking.LEVEL_NODE] = 1 + + if self.op.names: + self.wanted = _GetWantedInstances(self, self.op.names) + else: + self.wanted = locking.ALL_SET + + self.do_node_query = self._FIELDS_STATIC.NonMatching(self.op.output_fields) + self.do_locking = self.do_node_query and self.op.use_locking + if self.do_locking: + self.needed_locks[locking.LEVEL_INSTANCE] = self.wanted + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE and self.do_locking: + self._LockInstancesNodes() def CheckPrereq(self): """Check prerequisites. - This checks that the fields required are valid output fields. - """ - self.dynamic_fields = frozenset(["oper_state", "oper_ram", "status"]) - _CheckOutputFields(static=["name", "os", "pnode", "snodes", - "admin_state", "admin_ram", - "disk_template", "ip", "mac", "bridge", - "sda_size", "sdb_size", "vcpus", "tags"], - dynamic=self.dynamic_fields, - selected=self.op.output_fields) - - self.wanted = _GetWantedInstances(self, self.op.names) + pass def Exec(self, feedback_fn): """Computes the list of nodes and their attributes. """ - instance_names = self.wanted - instance_list = [self.cfg.GetInstanceInfo(iname) for iname - in instance_names] + all_info = self.cfg.GetAllInstancesInfo() + if self.wanted == locking.ALL_SET: + # caller didn't specify instance names, so ordering is not important + if self.do_locking: + instance_names = self.acquired_locks[locking.LEVEL_INSTANCE] + else: + instance_names = all_info.keys() + instance_names = utils.NiceSort(instance_names) + else: + # caller did specify names, so we must keep the ordering + if self.do_locking: + tgt_set = self.acquired_locks[locking.LEVEL_INSTANCE] + else: + tgt_set = all_info.keys() + missing = set(self.wanted).difference(tgt_set) + if missing: + raise errors.OpExecError("Some instances were removed before" + " retrieving their data: %s" % missing) + instance_names = self.wanted + + instance_list = [all_info[iname] for iname in instance_names] # begin data gathering nodes = frozenset([inst.primary_node for inst in instance_list]) + hv_list = list(set([inst.hypervisor for inst in instance_list])) bad_nodes = [] - if self.dynamic_fields.intersection(self.op.output_fields): + off_nodes = [] + if self.do_node_query: live_data = {} - node_data = rpc.call_all_instances_info(nodes) + node_data = self.rpc.call_all_instances_info(nodes, hv_list) for name in nodes: result = node_data[name] - if result: - live_data.update(result) - elif result == False: + if result.offline: + # offline nodes will be in both lists + off_nodes.append(name) + if result.fail_msg: bad_nodes.append(name) - # else no instance is alive + else: + if result.payload: + live_data.update(result.payload) + # else no instance is alive else: live_data = dict([(name, {}) for name in instance_names]) # end data gathering + HVPREFIX = "hv/" + BEPREFIX = "be/" output = [] + cluster = self.cfg.GetClusterInfo() for instance in instance_list: iout = [] + i_hv = cluster.FillHV(instance) + i_be = cluster.FillBE(instance) + i_nicp = [objects.FillDict(cluster.nicparams[constants.PP_DEFAULT], + nic.nicparams) for nic in instance.nics] for field in self.op.output_fields: - if field == "name": - val = instance.name - elif field == "os": - val = instance.os + st_match = self._FIELDS_STATIC.Matches(field) + if field in self._SIMPLE_FIELDS: + val = getattr(instance, field) elif field == "pnode": val = instance.primary_node elif field == "snodes": val = list(instance.secondary_nodes) elif field == "admin_state": - val = (instance.status != "down") + val = instance.admin_up elif field == "oper_state": if instance.primary_node in bad_nodes: val = None else: val = bool(live_data.get(instance.name)) elif field == "status": - if instance.primary_node in bad_nodes: + if instance.primary_node in off_nodes: + val = "ERROR_nodeoffline" + elif instance.primary_node in bad_nodes: val = "ERROR_nodedown" else: running = bool(live_data.get(instance.name)) if running: - if instance.status != "down": + if instance.admin_up: val = "running" else: val = "ERROR_up" else: - if instance.status != "down": + if instance.admin_up: val = "ERROR_down" else: val = "ADMIN_down" - elif field == "admin_ram": - val = instance.memory elif field == "oper_ram": if instance.primary_node in bad_nodes: val = None @@ -2507,26 +4127,117 @@ class LUQueryInstances(NoHooksLU): val = live_data[instance.name].get("memory", "?") else: val = "-" + elif field == "vcpus": + val = i_be[constants.BE_VCPUS] elif field == "disk_template": val = instance.disk_template elif field == "ip": - val = instance.nics[0].ip + if instance.nics: + val = instance.nics[0].ip + else: + val = None + elif field == "nic_mode": + if instance.nics: + val = i_nicp[0][constants.NIC_MODE] + else: + val = None + elif field == "nic_link": + if instance.nics: + val = i_nicp[0][constants.NIC_LINK] + else: + val = None elif field == "bridge": - val = instance.nics[0].bridge + if (instance.nics and + i_nicp[0][constants.NIC_MODE] == constants.NIC_MODE_BRIDGED): + val = i_nicp[0][constants.NIC_LINK] + else: + val = None elif field == "mac": - val = instance.nics[0].mac + if instance.nics: + val = instance.nics[0].mac + else: + val = None elif field == "sda_size" or field == "sdb_size": - disk = instance.FindDisk(field[:3]) - if disk is None: + idx = ord(field[2]) - ord('a') + try: + val = instance.FindDisk(idx).size + except errors.OpPrereqError: val = None - else: - val = disk.size - elif field == "vcpus": - val = instance.vcpus + elif field == "disk_usage": # total disk usage per node + disk_sizes = [{'size': disk.size} for disk in instance.disks] + val = _ComputeDiskSize(instance.disk_template, disk_sizes) elif field == "tags": val = list(instance.GetTags()) + elif field == "hvparams": + val = i_hv + elif (field.startswith(HVPREFIX) and + field[len(HVPREFIX):] in constants.HVS_PARAMETERS): + val = i_hv.get(field[len(HVPREFIX):], None) + elif field == "beparams": + val = i_be + elif (field.startswith(BEPREFIX) and + field[len(BEPREFIX):] in constants.BES_PARAMETERS): + val = i_be.get(field[len(BEPREFIX):], None) + elif st_match and st_match.groups(): + # matches a variable list + st_groups = st_match.groups() + if st_groups and st_groups[0] == "disk": + if st_groups[1] == "count": + val = len(instance.disks) + elif st_groups[1] == "sizes": + val = [disk.size for disk in instance.disks] + elif st_groups[1] == "size": + try: + val = instance.FindDisk(st_groups[2]).size + except errors.OpPrereqError: + val = None + else: + assert False, "Unhandled disk parameter" + elif st_groups[0] == "nic": + if st_groups[1] == "count": + val = len(instance.nics) + elif st_groups[1] == "macs": + val = [nic.mac for nic in instance.nics] + elif st_groups[1] == "ips": + val = [nic.ip for nic in instance.nics] + elif st_groups[1] == "modes": + val = [nicp[constants.NIC_MODE] for nicp in i_nicp] + elif st_groups[1] == "links": + val = [nicp[constants.NIC_LINK] for nicp in i_nicp] + elif st_groups[1] == "bridges": + val = [] + for nicp in i_nicp: + if nicp[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED: + val.append(nicp[constants.NIC_LINK]) + else: + val.append(None) + else: + # index-based item + nic_idx = int(st_groups[2]) + if nic_idx >= len(instance.nics): + val = None + else: + if st_groups[1] == "mac": + val = instance.nics[nic_idx].mac + elif st_groups[1] == "ip": + val = instance.nics[nic_idx].ip + elif st_groups[1] == "mode": + val = i_nicp[nic_idx][constants.NIC_MODE] + elif st_groups[1] == "link": + val = i_nicp[nic_idx][constants.NIC_LINK] + elif st_groups[1] == "bridge": + nic_mode = i_nicp[nic_idx][constants.NIC_MODE] + if nic_mode == constants.NIC_MODE_BRIDGED: + val = i_nicp[nic_idx][constants.NIC_LINK] + else: + val = None + else: + assert False, "Unhandled NIC parameter" + else: + assert False, ("Declared but unhandled variable parameter '%s'" % + field) else: - raise errors.ParameterError(field) + assert False, "Declared but unhandled parameter '%s'" % field iout.append(val) output.append(iout) @@ -2540,6 +4251,16 @@ class LUFailoverInstance(LogicalUnit): HPATH = "instance-failover" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name", "ignore_consistency"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() def BuildHooksEnv(self): """Build hooks env. @@ -2550,8 +4271,8 @@ class LUFailoverInstance(LogicalUnit): env = { "IGNORE_CONSISTENCY": self.op.ignore_consistency, } - env.update(_BuildInstanceHookEnvByObject(self.instance)) - nl = [self.sstore.GetMasterNode()] + list(self.instance.secondary_nodes) + env.update(_BuildInstanceHookEnvByObject(self, self.instance)) + nl = [self.cfg.GetMasterNode()] + list(self.instance.secondary_nodes) return env, nl, nl def CheckPrereq(self): @@ -2560,12 +4281,11 @@ class LUFailoverInstance(LogicalUnit): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) + self.instance = instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + bep = self.cfg.GetClusterInfo().FillBE(instance) if instance.disk_template not in constants.DTS_NET_MIRROR: raise errors.OpPrereqError("Instance's disk layout is not" " network mirrored, cannot failover.") @@ -2576,18 +4296,19 @@ class LUFailoverInstance(LogicalUnit): "a mirrored disk template") target_node = secondary_nodes[0] - # check memory requirements on the secondary node - _CheckNodeFreeMemory(self.cfg, target_node, "failing over instance %s" % - instance.name, instance.memory) + _CheckNodeOnline(self, target_node) + _CheckNodeNotDrained(self, target_node) + if instance.admin_up: + # check memory requirements on the secondary node + _CheckNodeFreeMemory(self, target_node, "failing over instance %s" % + instance.name, bep[constants.BE_MEMORY], + instance.hypervisor) + else: + self.LogInfo("Not checking memory on the secondary node as" + " instance will not be started") # check bridge existance - brlist = [nic.bridge for nic in instance.nics] - if not rpc.call_bridges_exist(target_node, brlist): - raise errors.OpPrereqError("One or more target bridges %s does not" - " exist on destination node '%s'" % - (brlist, target_node)) - - self.instance = instance + _CheckInstanceBridgesExist(self, instance, node=target_node) def Exec(self, feedback_fn): """Failover an instance. @@ -2604,26 +4325,30 @@ class LUFailoverInstance(LogicalUnit): feedback_fn("* checking disk consistency between source and target") for dev in instance.disks: # for drbd, these are drbd over lvm - if not _CheckDiskConsistency(self.cfg, dev, target_node, False): - if instance.status == "up" and not self.op.ignore_consistency: + if not _CheckDiskConsistency(self, dev, target_node, False): + if instance.admin_up and not self.op.ignore_consistency: raise errors.OpExecError("Disk %s is degraded on target node," " aborting failover." % dev.iv_name) feedback_fn("* shutting down instance on source node") - logger.Info("Shutting down instance %s on node %s" % - (instance.name, source_node)) + logging.info("Shutting down instance %s on node %s", + instance.name, source_node) - if not rpc.call_instance_shutdown(source_node, instance): + result = self.rpc.call_instance_shutdown(source_node, instance) + msg = result.fail_msg + if msg: if self.op.ignore_consistency: - logger.Error("Could not shutdown instance %s on node %s. Proceeding" - " anyway. Please make sure node %s is down" % - (instance.name, source_node, source_node)) + self.proc.LogWarning("Could not shutdown instance %s on node %s." + " Proceeding anyway. Please make sure node" + " %s is down. Error details: %s", + instance.name, source_node, source_node, msg) else: - raise errors.OpExecError("Could not shutdown instance %s on node %s" % - (instance.name, source_node)) + raise errors.OpExecError("Could not shutdown instance %s on" + " node %s: %s" % + (instance.name, source_node, msg)) feedback_fn("* deactivating the instance's disks on source node") - if not _ShutdownInstanceDisks(instance, self.cfg, ignore_primary=True): + if not _ShutdownInstanceDisks(self, instance, ignore_primary=True): raise errors.OpExecError("Can't shut down the instance's disks.") instance.primary_node = target_node @@ -2631,150 +4356,793 @@ class LUFailoverInstance(LogicalUnit): self.cfg.Update(instance) # Only start the instance if it's marked as up - if instance.status == "up": + if instance.admin_up: feedback_fn("* activating the instance's disks on target node") - logger.Info("Starting instance %s on node %s" % - (instance.name, target_node)) + logging.info("Starting instance %s on node %s", + instance.name, target_node) - disks_ok, dummy = _AssembleInstanceDisks(instance, self.cfg, + disks_ok, _ = _AssembleInstanceDisks(self, instance, ignore_secondaries=True) if not disks_ok: - _ShutdownInstanceDisks(instance, self.cfg) + _ShutdownInstanceDisks(self, instance) raise errors.OpExecError("Can't activate the instance's disks") feedback_fn("* starting the instance on the target node") - if not rpc.call_instance_start(target_node, instance, None): - _ShutdownInstanceDisks(instance, self.cfg) - raise errors.OpExecError("Could not start instance %s on node %s." % - (instance.name, target_node)) + result = self.rpc.call_instance_start(target_node, instance, None, None) + msg = result.fail_msg + if msg: + _ShutdownInstanceDisks(self, instance) + raise errors.OpExecError("Could not start instance %s on node %s: %s" % + (instance.name, target_node, msg)) -def _CreateBlockDevOnPrimary(cfg, node, instance, device, info): - """Create a tree of block devices on the primary node. +class LUMigrateInstance(LogicalUnit): + """Migrate an instance. - This always creates all devices. + This is migration without shutting down, compared to the failover, + which is done with shutdown. """ - if device.children: - for child in device.children: - if not _CreateBlockDevOnPrimary(cfg, node, instance, child, info): - return False - - cfg.SetDiskID(device, node) - new_id = rpc.call_blockdev_create(node, device, device.size, - instance.name, True, info) - if not new_id: - return False - if device.physical_id is None: - device.physical_id = new_id - return True + HPATH = "instance-migrate" + HTYPE = constants.HTYPE_INSTANCE + _OP_REQP = ["instance_name", "live", "cleanup"] + REQ_BGL = False -def _CreateBlockDevOnSecondary(cfg, node, instance, device, force, info): - """Create a tree of block devices on a secondary node. + def ExpandNames(self): + self._ExpandAndLockInstance() - If this device type has to be created on secondaries, create it and - all its children. + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE - If not, just recurse to children keeping the same 'force' value. + self._migrater = TLMigrateInstance(self, self.op.instance_name, + self.op.live, self.op.cleanup) + self.tasklets = [self._migrater] - """ - if device.CreateOnSecondary(): - force = True - if device.children: - for child in device.children: - if not _CreateBlockDevOnSecondary(cfg, node, instance, - child, force, info): - return False + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() - if not force: - return True - cfg.SetDiskID(device, node) - new_id = rpc.call_blockdev_create(node, device, device.size, - instance.name, False, info) - if not new_id: - return False - if device.physical_id is None: - device.physical_id = new_id - return True + def BuildHooksEnv(self): + """Build hooks env. + This runs on master, primary and secondary nodes of the instance. -def _GenerateUniqueNames(cfg, exts): - """Generate a suitable LV name. + """ + instance = self._migrater.instance + env = _BuildInstanceHookEnvByObject(self, instance) + env["MIGRATE_LIVE"] = self.op.live + env["MIGRATE_CLEANUP"] = self.op.cleanup + nl = [self.cfg.GetMasterNode()] + list(instance.secondary_nodes) + return env, nl, nl - This will generate a logical volume name for the given instance. + +class LUMoveInstance(LogicalUnit): + """Move an instance by data-copying. """ - results = [] - for val in exts: - new_id = cfg.GenerateUniqueID() - results.append("%s%s" % (new_id, val)) - return results + HPATH = "instance-move" + HTYPE = constants.HTYPE_INSTANCE + _OP_REQP = ["instance_name", "target_node"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() + target_node = self.cfg.ExpandNodeName(self.op.target_node) + if target_node is None: + raise errors.OpPrereqError("Node '%s' not known" % + self.op.target_node) + self.op.target_node = target_node + self.needed_locks[locking.LEVEL_NODE] = [target_node] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_APPEND + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes(primary_only=True) + + def BuildHooksEnv(self): + """Build hooks env. + + This runs on master, primary and secondary nodes of the instance. + + """ + env = { + "TARGET_NODE": self.op.target_node, + } + env.update(_BuildInstanceHookEnvByObject(self, self.instance)) + nl = [self.cfg.GetMasterNode()] + [self.instance.primary_node, + self.op.target_node] + return env, nl, nl + + def CheckPrereq(self): + """Check prerequisites. + + This checks that the instance is in the cluster. + + """ + self.instance = instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + + node = self.cfg.GetNodeInfo(self.op.target_node) + assert node is not None, \ + "Cannot retrieve locked node %s" % self.op.target_node + + self.target_node = target_node = node.name + + if target_node == instance.primary_node: + raise errors.OpPrereqError("Instance %s is already on the node %s" % + (instance.name, target_node)) + + bep = self.cfg.GetClusterInfo().FillBE(instance) + + for idx, dsk in enumerate(instance.disks): + if dsk.dev_type not in (constants.LD_LV, constants.LD_FILE): + raise errors.OpPrereqError("Instance disk %d has a complex layout," + " cannot copy") + + _CheckNodeOnline(self, target_node) + _CheckNodeNotDrained(self, target_node) + + if instance.admin_up: + # check memory requirements on the secondary node + _CheckNodeFreeMemory(self, target_node, "failing over instance %s" % + instance.name, bep[constants.BE_MEMORY], + instance.hypervisor) + else: + self.LogInfo("Not checking memory on the secondary node as" + " instance will not be started") + + # check bridge existance + _CheckInstanceBridgesExist(self, instance, node=target_node) + + def Exec(self, feedback_fn): + """Move an instance. + + The move is done by shutting it down on its present node, copying + the data over (slow) and starting it on the new node. + + """ + instance = self.instance + + source_node = instance.primary_node + target_node = self.target_node + + self.LogInfo("Shutting down instance %s on source node %s", + instance.name, source_node) + + result = self.rpc.call_instance_shutdown(source_node, instance) + msg = result.fail_msg + if msg: + if self.op.ignore_consistency: + self.proc.LogWarning("Could not shutdown instance %s on node %s." + " Proceeding anyway. Please make sure node" + " %s is down. Error details: %s", + instance.name, source_node, source_node, msg) + else: + raise errors.OpExecError("Could not shutdown instance %s on" + " node %s: %s" % + (instance.name, source_node, msg)) + + # create the target disks + try: + _CreateDisks(self, instance, target_node=target_node) + except errors.OpExecError: + self.LogWarning("Device creation failed, reverting...") + try: + _RemoveDisks(self, instance, target_node=target_node) + finally: + self.cfg.ReleaseDRBDMinors(instance.name) + raise + + cluster_name = self.cfg.GetClusterInfo().cluster_name + + errs = [] + # activate, get path, copy the data over + for idx, disk in enumerate(instance.disks): + self.LogInfo("Copying data for disk %d", idx) + result = self.rpc.call_blockdev_assemble(target_node, disk, + instance.name, True) + if result.fail_msg: + self.LogWarning("Can't assemble newly created disk %d: %s", + idx, result.fail_msg) + errs.append(result.fail_msg) + break + dev_path = result.payload + result = self.rpc.call_blockdev_export(source_node, disk, + target_node, dev_path, + cluster_name) + if result.fail_msg: + self.LogWarning("Can't copy data over for disk %d: %s", + idx, result.fail_msg) + errs.append(result.fail_msg) + break + + if errs: + self.LogWarning("Some disks failed to copy, aborting") + try: + _RemoveDisks(self, instance, target_node=target_node) + finally: + self.cfg.ReleaseDRBDMinors(instance.name) + raise errors.OpExecError("Errors during disk copy: %s" % + (",".join(errs),)) + + instance.primary_node = target_node + self.cfg.Update(instance) + + self.LogInfo("Removing the disks on the original node") + _RemoveDisks(self, instance, target_node=source_node) + + # Only start the instance if it's marked as up + if instance.admin_up: + self.LogInfo("Starting instance %s on node %s", + instance.name, target_node) + + disks_ok, _ = _AssembleInstanceDisks(self, instance, + ignore_secondaries=True) + if not disks_ok: + _ShutdownInstanceDisks(self, instance) + raise errors.OpExecError("Can't activate the instance's disks") + + result = self.rpc.call_instance_start(target_node, instance, None, None) + msg = result.fail_msg + if msg: + _ShutdownInstanceDisks(self, instance) + raise errors.OpExecError("Could not start instance %s on node %s: %s" % + (instance.name, target_node, msg)) + + +class LUMigrateNode(LogicalUnit): + """Migrate all instances from a node. + + """ + HPATH = "node-migrate" + HTYPE = constants.HTYPE_NODE + _OP_REQP = ["node_name", "live"] + REQ_BGL = False + + def ExpandNames(self): + self.op.node_name = self.cfg.ExpandNodeName(self.op.node_name) + if self.op.node_name is None: + raise errors.OpPrereqError("Node '%s' not known" % self.op.node_name) + + self.needed_locks = { + locking.LEVEL_NODE: [self.op.node_name], + } + + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_APPEND + + # Create tasklets for migrating instances for all instances on this node + names = [] + tasklets = [] + + for inst in _GetNodePrimaryInstances(self.cfg, self.op.node_name): + logging.debug("Migrating instance %s", inst.name) + names.append(inst.name) + + tasklets.append(TLMigrateInstance(self, inst.name, self.op.live, False)) + + self.tasklets = tasklets + + # Declare instance locks + self.needed_locks[locking.LEVEL_INSTANCE] = names + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() + + def BuildHooksEnv(self): + """Build hooks env. + + This runs on the master, the primary and all the secondaries. + + """ + env = { + "NODE_NAME": self.op.node_name, + } + + nl = [self.cfg.GetMasterNode()] + + return (env, nl, nl) + + +class TLMigrateInstance(Tasklet): + def __init__(self, lu, instance_name, live, cleanup): + """Initializes this class. + + """ + Tasklet.__init__(self, lu) + + # Parameters + self.instance_name = instance_name + self.live = live + self.cleanup = cleanup + + def CheckPrereq(self): + """Check prerequisites. + + This checks that the instance is in the cluster. + + """ + instance = self.cfg.GetInstanceInfo( + self.cfg.ExpandInstanceName(self.instance_name)) + if instance is None: + raise errors.OpPrereqError("Instance '%s' not known" % + self.instance_name) + + if instance.disk_template != constants.DT_DRBD8: + raise errors.OpPrereqError("Instance's disk layout is not" + " drbd8, cannot migrate.") + + secondary_nodes = instance.secondary_nodes + if not secondary_nodes: + raise errors.ConfigurationError("No secondary node but using" + " drbd8 disk template") + + i_be = self.cfg.GetClusterInfo().FillBE(instance) + + target_node = secondary_nodes[0] + # check memory requirements on the secondary node + _CheckNodeFreeMemory(self, target_node, "migrating instance %s" % + instance.name, i_be[constants.BE_MEMORY], + instance.hypervisor) + + # check bridge existance + _CheckInstanceBridgesExist(self, instance, node=target_node) + + if not self.cleanup: + _CheckNodeNotDrained(self, target_node) + result = self.rpc.call_instance_migratable(instance.primary_node, + instance) + result.Raise("Can't migrate, please use failover", prereq=True) + + self.instance = instance + + def _WaitUntilSync(self): + """Poll with custom rpc for disk sync. + + This uses our own step-based rpc call. + + """ + self.feedback_fn("* wait until resync is done") + all_done = False + while not all_done: + all_done = True + result = self.rpc.call_drbd_wait_sync(self.all_nodes, + self.nodes_ip, + self.instance.disks) + min_percent = 100 + for node, nres in result.items(): + nres.Raise("Cannot resync disks on node %s" % node) + node_done, node_percent = nres.payload + all_done = all_done and node_done + if node_percent is not None: + min_percent = min(min_percent, node_percent) + if not all_done: + if min_percent < 100: + self.feedback_fn(" - progress: %.1f%%" % min_percent) + time.sleep(2) + + def _EnsureSecondary(self, node): + """Demote a node to secondary. + + """ + self.feedback_fn("* switching node %s to secondary mode" % node) + + for dev in self.instance.disks: + self.cfg.SetDiskID(dev, node) + + result = self.rpc.call_blockdev_close(node, self.instance.name, + self.instance.disks) + result.Raise("Cannot change disk to secondary on node %s" % node) + + def _GoStandalone(self): + """Disconnect from the network. + + """ + self.feedback_fn("* changing into standalone mode") + result = self.rpc.call_drbd_disconnect_net(self.all_nodes, self.nodes_ip, + self.instance.disks) + for node, nres in result.items(): + nres.Raise("Cannot disconnect disks node %s" % node) + + def _GoReconnect(self, multimaster): + """Reconnect to the network. + + """ + if multimaster: + msg = "dual-master" + else: + msg = "single-master" + self.feedback_fn("* changing disks into %s mode" % msg) + result = self.rpc.call_drbd_attach_net(self.all_nodes, self.nodes_ip, + self.instance.disks, + self.instance.name, multimaster) + for node, nres in result.items(): + nres.Raise("Cannot change disks config on node %s" % node) + + def _ExecCleanup(self): + """Try to cleanup after a failed migration. + + The cleanup is done by: + - check that the instance is running only on one node + (and update the config if needed) + - change disks on its secondary node to secondary + - wait until disks are fully synchronized + - disconnect from the network + - change disks into single-master mode + - wait again until disks are fully synchronized + + """ + instance = self.instance + target_node = self.target_node + source_node = self.source_node + + # check running on only one node + self.feedback_fn("* checking where the instance actually runs" + " (if this hangs, the hypervisor might be in" + " a bad state)") + ins_l = self.rpc.call_instance_list(self.all_nodes, [instance.hypervisor]) + for node, result in ins_l.items(): + result.Raise("Can't contact node %s" % node) + + runningon_source = instance.name in ins_l[source_node].payload + runningon_target = instance.name in ins_l[target_node].payload + + if runningon_source and runningon_target: + raise errors.OpExecError("Instance seems to be running on two nodes," + " or the hypervisor is confused. You will have" + " to ensure manually that it runs only on one" + " and restart this operation.") + + if not (runningon_source or runningon_target): + raise errors.OpExecError("Instance does not seem to be running at all." + " In this case, it's safer to repair by" + " running 'gnt-instance stop' to ensure disk" + " shutdown, and then restarting it.") + + if runningon_target: + # the migration has actually succeeded, we need to update the config + self.feedback_fn("* instance running on secondary node (%s)," + " updating config" % target_node) + instance.primary_node = target_node + self.cfg.Update(instance) + demoted_node = source_node + else: + self.feedback_fn("* instance confirmed to be running on its" + " primary node (%s)" % source_node) + demoted_node = target_node + + self._EnsureSecondary(demoted_node) + try: + self._WaitUntilSync() + except errors.OpExecError: + # we ignore here errors, since if the device is standalone, it + # won't be able to sync + pass + self._GoStandalone() + self._GoReconnect(False) + self._WaitUntilSync() + + self.feedback_fn("* done") + + def _RevertDiskStatus(self): + """Try to revert the disk status after a failed migration. + + """ + target_node = self.target_node + try: + self._EnsureSecondary(target_node) + self._GoStandalone() + self._GoReconnect(False) + self._WaitUntilSync() + except errors.OpExecError, err: + self.lu.LogWarning("Migration failed and I can't reconnect the" + " drives: error '%s'\n" + "Please look and recover the instance status" % + str(err)) + + def _AbortMigration(self): + """Call the hypervisor code to abort a started migration. + + """ + instance = self.instance + target_node = self.target_node + migration_info = self.migration_info + + abort_result = self.rpc.call_finalize_migration(target_node, + instance, + migration_info, + False) + abort_msg = abort_result.fail_msg + if abort_msg: + logging.error("Aborting migration failed on target node %s: %s" % + (target_node, abort_msg)) + # Don't raise an exception here, as we stil have to try to revert the + # disk status, even if this step failed. + + def _ExecMigration(self): + """Migrate an instance. + + The migrate is done by: + - change the disks into dual-master mode + - wait until disks are fully synchronized again + - migrate the instance + - change disks on the new secondary node (the old primary) to secondary + - wait until disks are fully synchronized + - change disks into single-master mode + + """ + instance = self.instance + target_node = self.target_node + source_node = self.source_node + + self.feedback_fn("* checking disk consistency between source and target") + for dev in instance.disks: + if not _CheckDiskConsistency(self, dev, target_node, False): + raise errors.OpExecError("Disk %s is degraded or not fully" + " synchronized on target node," + " aborting migrate." % dev.iv_name) + + # First get the migration information from the remote node + result = self.rpc.call_migration_info(source_node, instance) + msg = result.fail_msg + if msg: + log_err = ("Failed fetching source migration information from %s: %s" % + (source_node, msg)) + logging.error(log_err) + raise errors.OpExecError(log_err) + + self.migration_info = migration_info = result.payload + + # Then switch the disks to master/master mode + self._EnsureSecondary(target_node) + self._GoStandalone() + self._GoReconnect(True) + self._WaitUntilSync() + + self.feedback_fn("* preparing %s to accept the instance" % target_node) + result = self.rpc.call_accept_instance(target_node, + instance, + migration_info, + self.nodes_ip[target_node]) + + msg = result.fail_msg + if msg: + logging.error("Instance pre-migration failed, trying to revert" + " disk status: %s", msg) + self._AbortMigration() + self._RevertDiskStatus() + raise errors.OpExecError("Could not pre-migrate instance %s: %s" % + (instance.name, msg)) + + self.feedback_fn("* migrating instance to %s" % target_node) + time.sleep(10) + result = self.rpc.call_instance_migrate(source_node, instance, + self.nodes_ip[target_node], + self.live) + msg = result.fail_msg + if msg: + logging.error("Instance migration failed, trying to revert" + " disk status: %s", msg) + self._AbortMigration() + self._RevertDiskStatus() + raise errors.OpExecError("Could not migrate instance %s: %s" % + (instance.name, msg)) + time.sleep(10) + + instance.primary_node = target_node + # distribute new instance config to the other nodes + self.cfg.Update(instance) + + result = self.rpc.call_finalize_migration(target_node, + instance, + migration_info, + True) + msg = result.fail_msg + if msg: + logging.error("Instance migration succeeded, but finalization failed:" + " %s" % msg) + raise errors.OpExecError("Could not finalize instance migration: %s" % + msg) + + self._EnsureSecondary(source_node) + self._WaitUntilSync() + self._GoStandalone() + self._GoReconnect(False) + self._WaitUntilSync() + + self.feedback_fn("* done") + + def Exec(self, feedback_fn): + """Perform the migration. + + """ + feedback_fn("Migrating instance %s" % self.instance.name) + + self.feedback_fn = feedback_fn + + self.source_node = self.instance.primary_node + self.target_node = self.instance.secondary_nodes[0] + self.all_nodes = [self.source_node, self.target_node] + self.nodes_ip = { + self.source_node: self.cfg.GetNodeInfo(self.source_node).secondary_ip, + self.target_node: self.cfg.GetNodeInfo(self.target_node).secondary_ip, + } + + if self.cleanup: + return self._ExecCleanup() + else: + return self._ExecMigration() + + +def _CreateBlockDev(lu, node, instance, device, force_create, + info, force_open): + """Create a tree of block devices on a given node. + + If this device type has to be created on secondaries, create it and + all its children. + + If not, just recurse to children keeping the same 'force' value. + + @param lu: the lu on whose behalf we execute + @param node: the node on which to create the device + @type instance: L{objects.Instance} + @param instance: the instance which owns the device + @type device: L{objects.Disk} + @param device: the device to create + @type force_create: boolean + @param force_create: whether to force creation of this device; this + will be change to True whenever we find a device which has + CreateOnSecondary() attribute + @param info: the extra 'metadata' we should attach to the device + (this will be represented as a LVM tag) + @type force_open: boolean + @param force_open: this parameter will be passes to the + L{backend.BlockdevCreate} function where it specifies + whether we run on primary or not, and it affects both + the child assembly and the device own Open() execution + + """ + if device.CreateOnSecondary(): + force_create = True + + if device.children: + for child in device.children: + _CreateBlockDev(lu, node, instance, child, force_create, + info, force_open) + + if not force_create: + return + + _CreateSingleBlockDev(lu, node, instance, device, info, force_open) + + +def _CreateSingleBlockDev(lu, node, instance, device, info, force_open): + """Create a single block device on a given node. + + This will not recurse over children of the device, so they must be + created in advance. + + @param lu: the lu on whose behalf we execute + @param node: the node on which to create the device + @type instance: L{objects.Instance} + @param instance: the instance which owns the device + @type device: L{objects.Disk} + @param device: the device to create + @param info: the extra 'metadata' we should attach to the device + (this will be represented as a LVM tag) + @type force_open: boolean + @param force_open: this parameter will be passes to the + L{backend.BlockdevCreate} function where it specifies + whether we run on primary or not, and it affects both + the child assembly and the device own Open() execution + + """ + lu.cfg.SetDiskID(device, node) + result = lu.rpc.call_blockdev_create(node, device, device.size, + instance.name, force_open, info) + result.Raise("Can't create block device %s on" + " node %s for instance %s" % (device, node, instance.name)) + if device.physical_id is None: + device.physical_id = result.payload + + +def _GenerateUniqueNames(lu, exts): + """Generate a suitable LV name. + + This will generate a logical volume name for the given instance. + + """ + results = [] + for val in exts: + new_id = lu.cfg.GenerateUniqueID() + results.append("%s%s" % (new_id, val)) + return results -def _GenerateDRBD8Branch(cfg, primary, secondary, size, names, iv_name): +def _GenerateDRBD8Branch(lu, primary, secondary, size, names, iv_name, + p_minor, s_minor): """Generate a drbd8 device complete with its children. """ - port = cfg.AllocatePort() - vgname = cfg.GetVGName() + port = lu.cfg.AllocatePort() + vgname = lu.cfg.GetVGName() + shared_secret = lu.cfg.GenerateDRBDSecret() dev_data = objects.Disk(dev_type=constants.LD_LV, size=size, logical_id=(vgname, names[0])) dev_meta = objects.Disk(dev_type=constants.LD_LV, size=128, logical_id=(vgname, names[1])) drbd_dev = objects.Disk(dev_type=constants.LD_DRBD8, size=size, - logical_id = (primary, secondary, port), - children = [dev_data, dev_meta], + logical_id=(primary, secondary, port, + p_minor, s_minor, + shared_secret), + children=[dev_data, dev_meta], iv_name=iv_name) return drbd_dev -def _GenerateDiskTemplate(cfg, template_name, +def _GenerateDiskTemplate(lu, template_name, instance_name, primary_node, - secondary_nodes, disk_sz, swap_sz, - file_storage_dir, file_driver): + secondary_nodes, disk_info, + file_storage_dir, file_driver, + base_index): """Generate the entire disk layout for a given template type. """ #TODO: compute space requirements - vgname = cfg.GetVGName() + vgname = lu.cfg.GetVGName() + disk_count = len(disk_info) + disks = [] if template_name == constants.DT_DISKLESS: - disks = [] + pass elif template_name == constants.DT_PLAIN: if len(secondary_nodes) != 0: raise errors.ProgrammerError("Wrong template configuration") - names = _GenerateUniqueNames(cfg, [".sda", ".sdb"]) - sda_dev = objects.Disk(dev_type=constants.LD_LV, size=disk_sz, - logical_id=(vgname, names[0]), - iv_name = "sda") - sdb_dev = objects.Disk(dev_type=constants.LD_LV, size=swap_sz, - logical_id=(vgname, names[1]), - iv_name = "sdb") - disks = [sda_dev, sdb_dev] + names = _GenerateUniqueNames(lu, [".disk%d" % (base_index + i) + for i in range(disk_count)]) + for idx, disk in enumerate(disk_info): + disk_index = idx + base_index + disk_dev = objects.Disk(dev_type=constants.LD_LV, size=disk["size"], + logical_id=(vgname, names[idx]), + iv_name="disk/%d" % disk_index, + mode=disk["mode"]) + disks.append(disk_dev) elif template_name == constants.DT_DRBD8: if len(secondary_nodes) != 1: raise errors.ProgrammerError("Wrong template configuration") remote_node = secondary_nodes[0] - names = _GenerateUniqueNames(cfg, [".sda_data", ".sda_meta", - ".sdb_data", ".sdb_meta"]) - drbd_sda_dev = _GenerateDRBD8Branch(cfg, primary_node, remote_node, - disk_sz, names[0:2], "sda") - drbd_sdb_dev = _GenerateDRBD8Branch(cfg, primary_node, remote_node, - swap_sz, names[2:4], "sdb") - disks = [drbd_sda_dev, drbd_sdb_dev] + minors = lu.cfg.AllocateDRBDMinor( + [primary_node, remote_node] * len(disk_info), instance_name) + + names = [] + for lv_prefix in _GenerateUniqueNames(lu, [".disk%d" % (base_index + i) + for i in range(disk_count)]): + names.append(lv_prefix + "_data") + names.append(lv_prefix + "_meta") + for idx, disk in enumerate(disk_info): + disk_index = idx + base_index + disk_dev = _GenerateDRBD8Branch(lu, primary_node, remote_node, + disk["size"], names[idx*2:idx*2+2], + "disk/%d" % disk_index, + minors[idx*2], minors[idx*2+1]) + disk_dev.mode = disk["mode"] + disks.append(disk_dev) elif template_name == constants.DT_FILE: if len(secondary_nodes) != 0: raise errors.ProgrammerError("Wrong template configuration") - file_sda_dev = objects.Disk(dev_type=constants.LD_FILE, size=disk_sz, - iv_name="sda", logical_id=(file_driver, - "%s/sda" % file_storage_dir)) - file_sdb_dev = objects.Disk(dev_type=constants.LD_FILE, size=swap_sz, - iv_name="sdb", logical_id=(file_driver, - "%s/sdb" % file_storage_dir)) - disks = [file_sda_dev, file_sdb_dev] + for idx, disk in enumerate(disk_info): + disk_index = idx + base_index + disk_dev = objects.Disk(dev_type=constants.LD_FILE, size=disk["size"], + iv_name="disk/%d" % disk_index, + logical_id=(file_driver, + "%s/disk%d" % (file_storage_dir, + disk_index)), + mode=disk["mode"]) + disks.append(disk_dev) else: raise errors.ProgrammerError("Invalid disk template '%s'" % template_name) return disks @@ -2787,54 +5155,52 @@ def _GetInstanceInfoText(instance): return "originstname+%s" % instance.name -def _CreateDisks(cfg, instance): +def _CreateDisks(lu, instance, to_skip=None, target_node=None): """Create all disks for an instance. This abstracts away some work from AddInstance. - Args: - instance: the instance object - - Returns: - True or False showing the success of the creation process + @type lu: L{LogicalUnit} + @param lu: the logical unit on whose behalf we execute + @type instance: L{objects.Instance} + @param instance: the instance whose disks we should create + @type to_skip: list + @param to_skip: list of indices to skip + @type target_node: string + @param target_node: if passed, overrides the target node for creation + @rtype: boolean + @return: the success of the creation """ info = _GetInstanceInfoText(instance) + if target_node is None: + pnode = instance.primary_node + all_nodes = instance.all_nodes + else: + pnode = target_node + all_nodes = [pnode] if instance.disk_template == constants.DT_FILE: file_storage_dir = os.path.dirname(instance.disks[0].logical_id[1]) - result = rpc.call_file_storage_dir_create(instance.primary_node, - file_storage_dir) + result = lu.rpc.call_file_storage_dir_create(pnode, file_storage_dir) - if not result: - logger.Error("Could not connect to node '%s'" % instance.primary_node) - return False + result.Raise("Failed to create directory '%s' on" + " node %s" % (file_storage_dir, pnode)) - if not result[0]: - logger.Error("failed to create directory '%s'" % file_storage_dir) - return False - - for device in instance.disks: - logger.Info("creating volume %s for instance %s" % - (device.iv_name, instance.name)) - #HARDCODE - for secondary_node in instance.secondary_nodes: - if not _CreateBlockDevOnSecondary(cfg, secondary_node, instance, - device, False, info): - logger.Error("failed to create volume %s (%s) on secondary node %s!" % - (device.iv_name, device, secondary_node)) - return False + # Note: this needs to be kept in sync with adding of disks in + # LUSetInstanceParams + for idx, device in enumerate(instance.disks): + if to_skip and idx in to_skip: + continue + logging.info("Creating volume %s for instance %s", + device.iv_name, instance.name) #HARDCODE - if not _CreateBlockDevOnPrimary(cfg, instance.primary_node, - instance, device, info): - logger.Error("failed to create volume %s on primary!" % - device.iv_name) - return False - - return True + for node in all_nodes: + f_create = node == pnode + _CreateBlockDev(lu, node, instance, device, f_create, info, f_create) -def _RemoveDisks(instance, cfg): +def _RemoveDisks(lu, instance, target_node=None): """Remove all disks for an instance. This abstracts away some work from `AddInstance()` and @@ -2842,47 +5208,57 @@ def _RemoveDisks(instance, cfg): be removed, the removal will continue with the other ones (compare with `_CreateDisks()`). - Args: - instance: the instance object - - Returns: - True or False showing the success of the removal proces + @type lu: L{LogicalUnit} + @param lu: the logical unit on whose behalf we execute + @type instance: L{objects.Instance} + @param instance: the instance whose disks we should remove + @type target_node: string + @param target_node: used to override the node on which to remove the disks + @rtype: boolean + @return: the success of the removal """ - logger.Info("removing block devices for instance %s" % instance.name) + logging.info("Removing block devices for instance %s", instance.name) - result = True + all_result = True for device in instance.disks: - for node, disk in device.ComputeNodeTree(instance.primary_node): - cfg.SetDiskID(disk, node) - if not rpc.call_blockdev_remove(node, disk): - logger.Error("could not remove block device %s on node %s," - " continuing anyway" % - (device.iv_name, node)) - result = False + if target_node: + edata = [(target_node, device)] + else: + edata = device.ComputeNodeTree(instance.primary_node) + for node, disk in edata: + lu.cfg.SetDiskID(disk, node) + msg = lu.rpc.call_blockdev_remove(node, disk).fail_msg + if msg: + lu.LogWarning("Could not remove block device %s on node %s," + " continuing anyway: %s", device.iv_name, node, msg) + all_result = False if instance.disk_template == constants.DT_FILE: file_storage_dir = os.path.dirname(instance.disks[0].logical_id[1]) - if not rpc.call_file_storage_dir_remove(instance.primary_node, - file_storage_dir): - logger.Error("could not remove directory '%s'" % file_storage_dir) - result = False + if target_node: + tgt = target_node + else: + tgt = instance.primary_node + result = lu.rpc.call_file_storage_dir_remove(tgt, file_storage_dir) + if result.fail_msg: + lu.LogWarning("Could not remove directory '%s' on node %s: %s", + file_storage_dir, instance.primary_node, result.fail_msg) + all_result = False - return result + return all_result -def _ComputeDiskSize(disk_template, disk_size, swap_size): +def _ComputeDiskSize(disk_template, disks): """Compute disk size requirements in the volume group - This is currently hard-coded for the two-drive layout. - """ # Required free disk space as a function of disk and swap space req_size_dict = { constants.DT_DISKLESS: None, - constants.DT_PLAIN: disk_size + swap_size, - # 256 MB are added for drbd metadata, 128MB for each drbd device - constants.DT_DRBD8: disk_size + swap_size + 256, + constants.DT_PLAIN: sum(d["size"] for d in disks), + # 128 MB are added for drbd metadata for each disk + constants.DT_DRBD8: sum(d["size"] + 128 for d in disks), constants.DT_FILE: None, } @@ -2893,34 +5269,265 @@ def _ComputeDiskSize(disk_template, disk_size, swap_size): return req_size_dict[disk_template] +def _CheckHVParams(lu, nodenames, hvname, hvparams): + """Hypervisor parameter validation. + + This function abstract the hypervisor parameter validation to be + used in both instance create and instance modify. + + @type lu: L{LogicalUnit} + @param lu: the logical unit for which we check + @type nodenames: list + @param nodenames: the list of nodes on which we should check + @type hvname: string + @param hvname: the name of the hypervisor we should use + @type hvparams: dict + @param hvparams: the parameters which we need to check + @raise errors.OpPrereqError: if the parameters are not valid + + """ + hvinfo = lu.rpc.call_hypervisor_validate_params(nodenames, + hvname, + hvparams) + for node in nodenames: + info = hvinfo[node] + if info.offline: + continue + info.Raise("Hypervisor parameter validation failed on node %s" % node) + + class LUCreateInstance(LogicalUnit): """Create an instance. """ HPATH = "instance-add" HTYPE = constants.HTYPE_INSTANCE - _OP_REQP = ["instance_name", "mem_size", "disk_size", - "disk_template", "swap_size", "mode", "start", "vcpus", - "wait_for_sync", "ip_check", "mac"] + _OP_REQP = ["instance_name", "disks", "disk_template", + "mode", "start", + "wait_for_sync", "ip_check", "nics", + "hvparams", "beparams"] + REQ_BGL = False + + def _ExpandNode(self, node): + """Expands and checks one node name. + + """ + node_full = self.cfg.ExpandNodeName(node) + if node_full is None: + raise errors.OpPrereqError("Unknown node %s" % node) + return node_full + + def ExpandNames(self): + """ExpandNames for CreateInstance. + + Figure out the right locks for instance creation. + + """ + self.needed_locks = {} + + # set optional parameters to none if they don't exist + for attr in ["pnode", "snode", "iallocator", "hypervisor"]: + if not hasattr(self.op, attr): + setattr(self.op, attr, None) + + # cheap checks, mostly valid constants given + + # verify creation mode + if self.op.mode not in (constants.INSTANCE_CREATE, + constants.INSTANCE_IMPORT): + raise errors.OpPrereqError("Invalid instance creation mode '%s'" % + self.op.mode) + + # disk template and mirror node verification + if self.op.disk_template not in constants.DISK_TEMPLATES: + raise errors.OpPrereqError("Invalid disk template name") + + if self.op.hypervisor is None: + self.op.hypervisor = self.cfg.GetHypervisorType() + + cluster = self.cfg.GetClusterInfo() + enabled_hvs = cluster.enabled_hypervisors + if self.op.hypervisor not in enabled_hvs: + raise errors.OpPrereqError("Selected hypervisor (%s) not enabled in the" + " cluster (%s)" % (self.op.hypervisor, + ",".join(enabled_hvs))) + + # check hypervisor parameter syntax (locally) + utils.ForceDictType(self.op.hvparams, constants.HVS_PARAMETER_TYPES) + filled_hvp = objects.FillDict(cluster.hvparams[self.op.hypervisor], + self.op.hvparams) + hv_type = hypervisor.GetHypervisor(self.op.hypervisor) + hv_type.CheckParameterSyntax(filled_hvp) + self.hv_full = filled_hvp + + # fill and remember the beparams dict + utils.ForceDictType(self.op.beparams, constants.BES_PARAMETER_TYPES) + self.be_full = objects.FillDict(cluster.beparams[constants.PP_DEFAULT], + self.op.beparams) + + #### instance parameters check + + # instance name verification + hostname1 = utils.HostInfo(self.op.instance_name) + self.op.instance_name = instance_name = hostname1.name + + # this is just a preventive check, but someone might still add this + # instance in the meantime, and creation will fail at lock-add time + if instance_name in self.cfg.GetInstanceList(): + raise errors.OpPrereqError("Instance '%s' is already in the cluster" % + instance_name) + + self.add_locks[locking.LEVEL_INSTANCE] = instance_name + + # NIC buildup + self.nics = [] + for idx, nic in enumerate(self.op.nics): + nic_mode_req = nic.get("mode", None) + nic_mode = nic_mode_req + if nic_mode is None: + nic_mode = cluster.nicparams[constants.PP_DEFAULT][constants.NIC_MODE] + + # in routed mode, for the first nic, the default ip is 'auto' + if nic_mode == constants.NIC_MODE_ROUTED and idx == 0: + default_ip_mode = constants.VALUE_AUTO + else: + default_ip_mode = constants.VALUE_NONE + + # ip validity checks + ip = nic.get("ip", default_ip_mode) + if ip is None or ip.lower() == constants.VALUE_NONE: + nic_ip = None + elif ip.lower() == constants.VALUE_AUTO: + nic_ip = hostname1.ip + else: + if not utils.IsValidIP(ip): + raise errors.OpPrereqError("Given IP address '%s' doesn't look" + " like a valid IP" % ip) + nic_ip = ip + + # TODO: check the ip for uniqueness !! + if nic_mode == constants.NIC_MODE_ROUTED and not nic_ip: + raise errors.OpPrereqError("Routed nic mode requires an ip address") + + # MAC address verification + mac = nic.get("mac", constants.VALUE_AUTO) + if mac not in (constants.VALUE_AUTO, constants.VALUE_GENERATE): + if not utils.IsValidMac(mac.lower()): + raise errors.OpPrereqError("Invalid MAC address specified: %s" % + mac) + else: + # or validate/reserve the current one + if self.cfg.IsMacInUse(mac): + raise errors.OpPrereqError("MAC address %s already in use" + " in cluster" % mac) + + # bridge verification + bridge = nic.get("bridge", None) + link = nic.get("link", None) + if bridge and link: + raise errors.OpPrereqError("Cannot pass 'bridge' and 'link'" + " at the same time") + elif bridge and nic_mode == constants.NIC_MODE_ROUTED: + raise errors.OpPrereqError("Cannot pass 'bridge' on a routed nic") + elif bridge: + link = bridge + + nicparams = {} + if nic_mode_req: + nicparams[constants.NIC_MODE] = nic_mode_req + if link: + nicparams[constants.NIC_LINK] = link + + check_params = objects.FillDict(cluster.nicparams[constants.PP_DEFAULT], + nicparams) + objects.NIC.CheckParameterSyntax(check_params) + self.nics.append(objects.NIC(mac=mac, ip=nic_ip, nicparams=nicparams)) + + # disk checks/pre-build + self.disks = [] + for disk in self.op.disks: + mode = disk.get("mode", constants.DISK_RDWR) + if mode not in constants.DISK_ACCESS_SET: + raise errors.OpPrereqError("Invalid disk access mode '%s'" % + mode) + size = disk.get("size", None) + if size is None: + raise errors.OpPrereqError("Missing disk size") + try: + size = int(size) + except ValueError: + raise errors.OpPrereqError("Invalid disk size '%s'" % size) + self.disks.append({"size": size, "mode": mode}) + + # used in CheckPrereq for ip ping check + self.check_ip = hostname1.ip + + # file storage checks + if (self.op.file_driver and + not self.op.file_driver in constants.FILE_DRIVER): + raise errors.OpPrereqError("Invalid file driver name '%s'" % + self.op.file_driver) + + if self.op.file_storage_dir and os.path.isabs(self.op.file_storage_dir): + raise errors.OpPrereqError("File storage directory path not absolute") + + ### Node/iallocator related checks + if [self.op.iallocator, self.op.pnode].count(None) != 1: + raise errors.OpPrereqError("One and only one of iallocator and primary" + " node must be given") + + if self.op.iallocator: + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + else: + self.op.pnode = self._ExpandNode(self.op.pnode) + nodelist = [self.op.pnode] + if self.op.snode is not None: + self.op.snode = self._ExpandNode(self.op.snode) + nodelist.append(self.op.snode) + self.needed_locks[locking.LEVEL_NODE] = nodelist + + # in case of import lock the source node too + if self.op.mode == constants.INSTANCE_IMPORT: + src_node = getattr(self.op, "src_node", None) + src_path = getattr(self.op, "src_path", None) + + if src_path is None: + self.op.src_path = src_path = self.op.instance_name + + if src_node is None: + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + self.op.src_node = None + if os.path.isabs(src_path): + raise errors.OpPrereqError("Importing an instance from an absolute" + " path requires a source node option.") + else: + self.op.src_node = src_node = self._ExpandNode(src_node) + if self.needed_locks[locking.LEVEL_NODE] is not locking.ALL_SET: + self.needed_locks[locking.LEVEL_NODE].append(src_node) + if not os.path.isabs(src_path): + self.op.src_path = src_path = \ + os.path.join(constants.EXPORT_DIR, src_path) + + else: # INSTANCE_CREATE + if getattr(self.op, "os_type", None) is None: + raise errors.OpPrereqError("No guest OS specified") def _RunAllocator(self): """Run the allocator based on input opcode. """ - disks = [{"size": self.op.disk_size, "mode": "w"}, - {"size": self.op.swap_size, "mode": "w"}] - nics = [{"mac": self.op.mac, "ip": getattr(self.op, "ip", None), - "bridge": self.op.bridge}] - ial = IAllocator(self.cfg, self.sstore, + nics = [n.ToDict() for n in self.nics] + ial = IAllocator(self.cfg, self.rpc, mode=constants.IALLOCATOR_MODE_ALLOC, name=self.op.instance_name, disk_template=self.op.disk_template, tags=[], os=self.op.os_type, - vcpus=self.op.vcpus, - mem_size=self.op.mem_size, - disks=disks, + vcpus=self.be_full[constants.BE_VCPUS], + mem_size=self.be_full[constants.BE_MEMORY], + disks=self.disks, nics=nics, + hypervisor=self.op.hypervisor, ) ial.Run(self.op.iallocator) @@ -2932,12 +5539,12 @@ class LUCreateInstance(LogicalUnit): if len(ial.nodes) != ial.required_nodes: raise errors.OpPrereqError("iallocator '%s' returned invalid number" " of nodes (%s), required %s" % - (len(ial.nodes), ial.required_nodes)) + (self.op.iallocator, len(ial.nodes), + ial.required_nodes)) self.op.pnode = ial.nodes[0] - logger.ToStdout("Selected nodes for the instance: %s" % - (", ".join(ial.nodes),)) - logger.Info("Selected nodes for instance %s via iallocator %s: %s" % - (self.op.instance_name, self.op.iallocator, ial.nodes)) + self.LogInfo("Selected nodes for instance %s via iallocator %s: %s", + self.op.instance_name, self.op.iallocator, + ", ".join(ial.nodes)) if ial.required_nodes == 2: self.op.snode = ial.nodes[1] @@ -2948,27 +5555,30 @@ class LUCreateInstance(LogicalUnit): """ env = { - "INSTANCE_DISK_TEMPLATE": self.op.disk_template, - "INSTANCE_DISK_SIZE": self.op.disk_size, - "INSTANCE_SWAP_SIZE": self.op.swap_size, - "INSTANCE_ADD_MODE": self.op.mode, + "ADD_MODE": self.op.mode, } if self.op.mode == constants.INSTANCE_IMPORT: - env["INSTANCE_SRC_NODE"] = self.op.src_node - env["INSTANCE_SRC_PATH"] = self.op.src_path - env["INSTANCE_SRC_IMAGE"] = self.src_image + env["SRC_NODE"] = self.op.src_node + env["SRC_PATH"] = self.op.src_path + env["SRC_IMAGES"] = self.src_images - env.update(_BuildInstanceHookEnv(name=self.op.instance_name, + env.update(_BuildInstanceHookEnv( + name=self.op.instance_name, primary_node=self.op.pnode, secondary_nodes=self.secondaries, - status=self.instance_status, + status=self.op.start, os_type=self.op.os_type, - memory=self.op.mem_size, - vcpus=self.op.vcpus, - nics=[(self.inst_ip, self.op.bridge, self.op.mac)], + memory=self.be_full[constants.BE_MEMORY], + vcpus=self.be_full[constants.BE_VCPUS], + nics=_NICListToTuple(self, self.nics), + disk_template=self.op.disk_template, + disks=[(d["size"], d["mode"]) for d in self.disks], + bep=self.be_full, + hvp=self.hv_full, + hypervisor_name=self.op.hypervisor, )) - nl = ([self.sstore.GetMasterNode(), self.op.pnode] + + nl = ([self.cfg.GetMasterNode(), self.op.pnode] + self.secondaries) return env, nl, nl @@ -2977,228 +5587,170 @@ class LUCreateInstance(LogicalUnit): """Check prerequisites. """ - # set optional parameters to none if they don't exist - for attr in ["kernel_path", "initrd_path", "hvm_boot_order", "pnode", - "iallocator", "hvm_acpi", "hvm_pae", "hvm_cdrom_image_path", - "vnc_bind_address"]: - if not hasattr(self.op, attr): - setattr(self.op, attr, None) - - if self.op.mode not in (constants.INSTANCE_CREATE, - constants.INSTANCE_IMPORT): - raise errors.OpPrereqError("Invalid instance creation mode '%s'" % - self.op.mode) - if (not self.cfg.GetVGName() and self.op.disk_template not in constants.DTS_NOT_LVM): raise errors.OpPrereqError("Cluster does not support lvm-based" " instances") if self.op.mode == constants.INSTANCE_IMPORT: - src_node = getattr(self.op, "src_node", None) - src_path = getattr(self.op, "src_path", None) - if src_node is None or src_path is None: - raise errors.OpPrereqError("Importing an instance requires source" - " node and path options") - src_node_full = self.cfg.ExpandNodeName(src_node) - if src_node_full is None: - raise errors.OpPrereqError("Unknown source node '%s'" % src_node) - self.op.src_node = src_node = src_node_full - - if not os.path.isabs(src_path): - raise errors.OpPrereqError("The source path must be absolute") - - export_info = rpc.call_export_info(src_node, src_path) - - if not export_info: - raise errors.OpPrereqError("No export found in dir %s" % src_path) - + src_node = self.op.src_node + src_path = self.op.src_path + + if src_node is None: + locked_nodes = self.acquired_locks[locking.LEVEL_NODE] + exp_list = self.rpc.call_export_list(locked_nodes) + found = False + for node in exp_list: + if exp_list[node].fail_msg: + continue + if src_path in exp_list[node].payload: + found = True + self.op.src_node = src_node = node + self.op.src_path = src_path = os.path.join(constants.EXPORT_DIR, + src_path) + break + if not found: + raise errors.OpPrereqError("No export found for relative path %s" % + src_path) + + _CheckNodeOnline(self, src_node) + result = self.rpc.call_export_info(src_node, src_path) + result.Raise("No export or invalid export found in dir %s" % src_path) + + export_info = objects.SerializableConfigParser.Loads(str(result.payload)) if not export_info.has_section(constants.INISECT_EXP): raise errors.ProgrammerError("Corrupted export config") - ei_version = export_info.get(constants.INISECT_EXP, 'version') - if (int(ei_version) != constants.EXPORT_VERSION): - raise errors.OpPrereqError("Wrong export version %s (wanted %d)" % - (ei_version, constants.EXPORT_VERSION)) - - if int(export_info.get(constants.INISECT_INS, 'disk_count')) > 1: - raise errors.OpPrereqError("Can't import instance with more than" - " one data disk") - - # FIXME: are the old os-es, disk sizes, etc. useful? - self.op.os_type = export_info.get(constants.INISECT_EXP, 'os') - diskimage = os.path.join(src_path, export_info.get(constants.INISECT_INS, - 'disk0_dump')) - self.src_image = diskimage - else: # INSTANCE_CREATE - if getattr(self.op, "os_type", None) is None: - raise errors.OpPrereqError("No guest OS specified") - - #### instance parameters check + ei_version = export_info.get(constants.INISECT_EXP, 'version') + if (int(ei_version) != constants.EXPORT_VERSION): + raise errors.OpPrereqError("Wrong export version %s (wanted %d)" % + (ei_version, constants.EXPORT_VERSION)) - # disk template and mirror node verification - if self.op.disk_template not in constants.DISK_TEMPLATES: - raise errors.OpPrereqError("Invalid disk template name") + # Check that the new instance doesn't have less disks than the export + instance_disks = len(self.disks) + export_disks = export_info.getint(constants.INISECT_INS, 'disk_count') + if instance_disks < export_disks: + raise errors.OpPrereqError("Not enough disks to import." + " (instance: %d, export: %d)" % + (instance_disks, export_disks)) - # instance name verification - hostname1 = utils.HostInfo(self.op.instance_name) + self.op.os_type = export_info.get(constants.INISECT_EXP, 'os') + disk_images = [] + for idx in range(export_disks): + option = 'disk%d_dump' % idx + if export_info.has_option(constants.INISECT_INS, option): + # FIXME: are the old os-es, disk sizes, etc. useful? + export_name = export_info.get(constants.INISECT_INS, option) + image = os.path.join(src_path, export_name) + disk_images.append(image) + else: + disk_images.append(False) - self.op.instance_name = instance_name = hostname1.name - instance_list = self.cfg.GetInstanceList() - if instance_name in instance_list: - raise errors.OpPrereqError("Instance '%s' is already in the cluster" % - instance_name) + self.src_images = disk_images - # ip validity checks - ip = getattr(self.op, "ip", None) - if ip is None or ip.lower() == "none": - inst_ip = None - elif ip.lower() == "auto": - inst_ip = hostname1.ip - else: - if not utils.IsValidIP(ip): - raise errors.OpPrereqError("given IP address '%s' doesn't look" - " like a valid IP" % ip) - inst_ip = ip - self.inst_ip = self.op.ip = inst_ip + old_name = export_info.get(constants.INISECT_INS, 'name') + # FIXME: int() here could throw a ValueError on broken exports + exp_nic_count = int(export_info.get(constants.INISECT_INS, 'nic_count')) + if self.op.instance_name == old_name: + for idx, nic in enumerate(self.nics): + if nic.mac == constants.VALUE_AUTO and exp_nic_count >= idx: + nic_mac_ini = 'nic%d_mac' % idx + nic.mac = export_info.get(constants.INISECT_INS, nic_mac_ini) + # ENDIF: self.op.mode == constants.INSTANCE_IMPORT + # ip ping checks (we use the same ip that was resolved in ExpandNames) if self.op.start and not self.op.ip_check: raise errors.OpPrereqError("Cannot ignore IP address conflicts when" " adding an instance in start mode") if self.op.ip_check: - if utils.TcpPing(hostname1.ip, constants.DEFAULT_NODED_PORT): + if utils.TcpPing(self.check_ip, constants.DEFAULT_NODED_PORT): raise errors.OpPrereqError("IP %s of instance %s already in use" % - (hostname1.ip, instance_name)) - - # MAC address verification - if self.op.mac != "auto": - if not utils.IsValidMac(self.op.mac.lower()): - raise errors.OpPrereqError("invalid MAC address specified: %s" % - self.op.mac) - - # bridge verification - bridge = getattr(self.op, "bridge", None) - if bridge is None: - self.op.bridge = self.cfg.GetDefBridge() - else: - self.op.bridge = bridge - - # boot order verification - if self.op.hvm_boot_order is not None: - if len(self.op.hvm_boot_order.strip("acdn")) != 0: - raise errors.OpPrereqError("invalid boot order specified," - " must be one or more of [acdn]") - # file storage checks - if (self.op.file_driver and - not self.op.file_driver in constants.FILE_DRIVER): - raise errors.OpPrereqError("Invalid file driver name '%s'" % - self.op.file_driver) + (self.check_ip, self.op.instance_name)) + + #### mac address generation + # By generating here the mac address both the allocator and the hooks get + # the real final mac address rather than the 'auto' or 'generate' value. + # There is a race condition between the generation and the instance object + # creation, which means that we know the mac is valid now, but we're not + # sure it will be when we actually add the instance. If things go bad + # adding the instance will abort because of a duplicate mac, and the + # creation job will fail. + for nic in self.nics: + if nic.mac in (constants.VALUE_AUTO, constants.VALUE_GENERATE): + nic.mac = self.cfg.GenerateMAC() - if self.op.file_storage_dir and os.path.isabs(self.op.file_storage_dir): - raise errors.OpPrereqError("File storage directory not a relative" - " path") #### allocator run - if [self.op.iallocator, self.op.pnode].count(None) != 1: - raise errors.OpPrereqError("One and only one of iallocator and primary" - " node must be given") - if self.op.iallocator is not None: self._RunAllocator() #### node related checks # check primary node - pnode = self.cfg.GetNodeInfo(self.cfg.ExpandNodeName(self.op.pnode)) - if pnode is None: - raise errors.OpPrereqError("Primary node '%s' is unknown" % - self.op.pnode) - self.op.pnode = pnode.name - self.pnode = pnode + self.pnode = pnode = self.cfg.GetNodeInfo(self.op.pnode) + assert self.pnode is not None, \ + "Cannot retrieve locked node %s" % self.op.pnode + if pnode.offline: + raise errors.OpPrereqError("Cannot use offline primary node '%s'" % + pnode.name) + if pnode.drained: + raise errors.OpPrereqError("Cannot use drained primary node '%s'" % + pnode.name) + self.secondaries = [] # mirror node verification if self.op.disk_template in constants.DTS_NET_MIRROR: - if getattr(self.op, "snode", None) is None: + if self.op.snode is None: raise errors.OpPrereqError("The networked disk templates need" " a mirror node") - - snode_name = self.cfg.ExpandNodeName(self.op.snode) - if snode_name is None: - raise errors.OpPrereqError("Unknown secondary node '%s'" % - self.op.snode) - elif snode_name == pnode.name: + if self.op.snode == pnode.name: raise errors.OpPrereqError("The secondary node cannot be" " the primary node.") - self.secondaries.append(snode_name) + _CheckNodeOnline(self, self.op.snode) + _CheckNodeNotDrained(self, self.op.snode) + self.secondaries.append(self.op.snode) + + nodenames = [pnode.name] + self.secondaries req_size = _ComputeDiskSize(self.op.disk_template, - self.op.disk_size, self.op.swap_size) + self.disks) # Check lv size requirements if req_size is not None: - nodenames = [pnode.name] + self.secondaries - nodeinfo = rpc.call_node_info(nodenames, self.cfg.GetVGName()) + nodeinfo = self.rpc.call_node_info(nodenames, self.cfg.GetVGName(), + self.op.hypervisor) for node in nodenames: - info = nodeinfo.get(node, None) - if not info: - raise errors.OpPrereqError("Cannot get current information" - " from node '%s'" % node) + info = nodeinfo[node] + info.Raise("Cannot get current information from node %s" % node) + info = info.payload vg_free = info.get('vg_free', None) if not isinstance(vg_free, int): raise errors.OpPrereqError("Can't compute free disk space on" " node %s" % node) - if req_size > info['vg_free']: + if req_size > vg_free: raise errors.OpPrereqError("Not enough disk space on target node %s." " %d MB available, %d MB required" % - (node, info['vg_free'], req_size)) + (node, vg_free, req_size)) - # os verification - os_obj = rpc.call_os_get(pnode.name, self.op.os_type) - if not os_obj: - raise errors.OpPrereqError("OS '%s' not in supported os list for" - " primary node" % self.op.os_type) - - if self.op.kernel_path == constants.VALUE_NONE: - raise errors.OpPrereqError("Can't set instance kernel to none") + _CheckHVParams(self, nodenames, self.op.hypervisor, self.op.hvparams) + # os verification + result = self.rpc.call_os_get(pnode.name, self.op.os_type) + result.Raise("OS '%s' not in supported os list for primary node %s" % + (self.op.os_type, pnode.name), prereq=True) - # bridge check on primary node - if not rpc.call_bridges_exist(self.pnode.name, [self.op.bridge]): - raise errors.OpPrereqError("target bridge '%s' does not exist on" - " destination node '%s'" % - (self.op.bridge, pnode.name)) + _CheckNicsBridgesExist(self, self.nics, self.pnode.name) # memory check on primary node if self.op.start: - _CheckNodeFreeMemory(self.cfg, self.pnode.name, + _CheckNodeFreeMemory(self, self.pnode.name, "creating instance %s" % self.op.instance_name, - self.op.mem_size) - - # hvm_cdrom_image_path verification - if self.op.hvm_cdrom_image_path is not None: - if not os.path.isabs(self.op.hvm_cdrom_image_path): - raise errors.OpPrereqError("The path to the HVM CDROM image must" - " be an absolute path or None, not %s" % - self.op.hvm_cdrom_image_path) - if not os.path.isfile(self.op.hvm_cdrom_image_path): - raise errors.OpPrereqError("The HVM CDROM image must either be a" - " regular file or a symlink pointing to" - " an existing regular file, not %s" % - self.op.hvm_cdrom_image_path) - - # vnc_bind_address verification - if self.op.vnc_bind_address is not None: - if not utils.IsValidIP(self.op.vnc_bind_address): - raise errors.OpPrereqError("given VNC bind address '%s' doesn't look" - " like a valid IP address" % - self.op.vnc_bind_address) + self.be_full[constants.BE_MEMORY], + self.op.hypervisor) - if self.op.start: - self.instance_status = 'up' - else: - self.instance_status = 'down' + self.dry_run_result = list(nodenames) def Exec(self, feedback_fn): """Create and add the instance to the cluster. @@ -3207,23 +5759,14 @@ class LUCreateInstance(LogicalUnit): instance = self.op.instance_name pnode_name = self.pnode.name - if self.op.mac == "auto": - mac_address = self.cfg.GenerateMAC() - else: - mac_address = self.op.mac - - nic = objects.NIC(bridge=self.op.bridge, mac=mac_address) - if self.inst_ip is not None: - nic.ip = self.inst_ip - - ht_kind = self.sstore.GetHypervisorType() + ht_kind = self.op.hypervisor if ht_kind in constants.HTS_REQ_PORT: network_port = self.cfg.AllocatePort() else: network_port = None - if self.op.vnc_bind_address is None: - self.op.vnc_bind_address = constants.VNC_DEFAULT_BIND_ADDRESS + ##if self.op.vnc_bind_address is None: + ## self.op.vnc_bind_address = constants.VNC_DEFAULT_BIND_ADDRESS # this is needed because os.path.join does not accept None arguments if self.op.file_storage_dir is None: @@ -3233,61 +5776,73 @@ class LUCreateInstance(LogicalUnit): # build the full file storage dir path file_storage_dir = os.path.normpath(os.path.join( - self.sstore.GetFileStorageDir(), + self.cfg.GetFileStorageDir(), string_file_storage_dir, instance)) - disks = _GenerateDiskTemplate(self.cfg, + disks = _GenerateDiskTemplate(self, self.op.disk_template, instance, pnode_name, - self.secondaries, self.op.disk_size, - self.op.swap_size, + self.secondaries, + self.disks, file_storage_dir, - self.op.file_driver) + self.op.file_driver, + 0) iobj = objects.Instance(name=instance, os=self.op.os_type, primary_node=pnode_name, - memory=self.op.mem_size, - vcpus=self.op.vcpus, - nics=[nic], disks=disks, + nics=self.nics, disks=disks, disk_template=self.op.disk_template, - status=self.instance_status, + admin_up=False, network_port=network_port, - kernel_path=self.op.kernel_path, - initrd_path=self.op.initrd_path, - hvm_boot_order=self.op.hvm_boot_order, - hvm_acpi=self.op.hvm_acpi, - hvm_pae=self.op.hvm_pae, - hvm_cdrom_image_path=self.op.hvm_cdrom_image_path, - vnc_bind_address=self.op.vnc_bind_address, + beparams=self.op.beparams, + hvparams=self.op.hvparams, + hypervisor=self.op.hypervisor, ) feedback_fn("* creating instance disks...") - if not _CreateDisks(self.cfg, iobj): - _RemoveDisks(iobj, self.cfg) - raise errors.OpExecError("Device creation failed, reverting...") + try: + _CreateDisks(self, iobj) + except errors.OpExecError: + self.LogWarning("Device creation failed, reverting...") + try: + _RemoveDisks(self, iobj) + finally: + self.cfg.ReleaseDRBDMinors(instance) + raise feedback_fn("adding instance %s to cluster config" % instance) self.cfg.AddInstance(iobj) - # Add the new instance to the Ganeti Lock Manager - self.context.glm.add(locking.LEVEL_INSTANCE, instance) + # Declare that we don't want to remove the instance lock anymore, as we've + # added the instance to the config + del self.remove_locks[locking.LEVEL_INSTANCE] + # Unlock all the nodes + if self.op.mode == constants.INSTANCE_IMPORT: + nodes_keep = [self.op.src_node] + nodes_release = [node for node in self.acquired_locks[locking.LEVEL_NODE] + if node != self.op.src_node] + self.context.glm.release(locking.LEVEL_NODE, nodes_release) + self.acquired_locks[locking.LEVEL_NODE] = nodes_keep + else: + self.context.glm.release(locking.LEVEL_NODE) + del self.acquired_locks[locking.LEVEL_NODE] if self.op.wait_for_sync: - disk_abort = not _WaitForSync(self.cfg, iobj, self.proc) + disk_abort = not _WaitForSync(self, iobj) elif iobj.disk_template in constants.DTS_NET_MIRROR: # make sure the disks are not degraded (still sync-ing is ok) time.sleep(15) feedback_fn("* checking mirrors status") - disk_abort = not _WaitForSync(self.cfg, iobj, self.proc, oneshot=True) + disk_abort = not _WaitForSync(self, iobj, oneshot=True) else: disk_abort = False if disk_abort: - _RemoveDisks(iobj, self.cfg) + _RemoveDisks(self, iobj) self.cfg.RemoveInstance(iobj.name) - # Remove the new instance from the Ganeti Lock Manager - self.context.glm.remove(locking.LEVEL_INSTANCE, iobj.name) + # Make sure the instance lock gets removed + self.remove_locks[locking.LEVEL_INSTANCE] = iobj.name raise errors.OpExecError("There are some degraded disks for" " this instance") @@ -3297,30 +5852,36 @@ class LUCreateInstance(LogicalUnit): if iobj.disk_template != constants.DT_DISKLESS: if self.op.mode == constants.INSTANCE_CREATE: feedback_fn("* running the instance OS create scripts...") - if not rpc.call_instance_os_add(pnode_name, iobj, "sda", "sdb"): - raise errors.OpExecError("could not add os for instance %s" - " on node %s" % - (instance, pnode_name)) + result = self.rpc.call_instance_os_add(pnode_name, iobj, False) + result.Raise("Could not add os for instance %s" + " on node %s" % (instance, pnode_name)) elif self.op.mode == constants.INSTANCE_IMPORT: feedback_fn("* running the instance OS import scripts...") src_node = self.op.src_node - src_image = self.src_image - if not rpc.call_instance_os_import(pnode_name, iobj, "sda", "sdb", - src_node, src_image): - raise errors.OpExecError("Could not import os for instance" - " %s on node %s" % - (instance, pnode_name)) + src_images = self.src_images + cluster_name = self.cfg.GetClusterName() + import_result = self.rpc.call_instance_os_import(pnode_name, iobj, + src_node, src_images, + cluster_name) + msg = import_result.fail_msg + if msg: + self.LogWarning("Error while importing the disk images for instance" + " %s on node %s: %s" % (instance, pnode_name, msg)) else: # also checked in the prereq part raise errors.ProgrammerError("Unknown OS initialization mode '%s'" % self.op.mode) if self.op.start: - logger.Info("starting instance %s on node %s" % (instance, pnode_name)) + iobj.admin_up = True + self.cfg.Update(iobj) + logging.info("Starting instance %s on node %s", instance, pnode_name) feedback_fn("* starting instance...") - if not rpc.call_instance_start(pnode_name, iobj, None): - raise errors.OpExecError("Could not start instance") + result = self.rpc.call_instance_start(pnode_name, iobj, None, None) + result.Raise("Could not start instance") + + return list(iobj.all_nodes) class LUConnectConsole(NoHooksLU): @@ -3332,6 +5893,10 @@ class LUConnectConsole(NoHooksLU): """ _OP_REQP = ["instance_name"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() def CheckPrereq(self): """Check prerequisites. @@ -3339,12 +5904,10 @@ class LUConnectConsole(NoHooksLU): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) - self.instance = instance + self.instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + _CheckNodeOnline(self, self.instance.primary_node) def Exec(self, feedback_fn): """Connect to the console of an instance @@ -3353,17 +5916,22 @@ class LUConnectConsole(NoHooksLU): instance = self.instance node = instance.primary_node - node_insts = rpc.call_instance_list([node])[node] - if node_insts is False: - raise errors.OpExecError("Can't connect to node %s." % node) + node_insts = self.rpc.call_instance_list([node], + [instance.hypervisor])[node] + node_insts.Raise("Can't get node information from %s" % node) - if instance.name not in node_insts: + if instance.name not in node_insts.payload: raise errors.OpExecError("Instance %s is not running." % instance.name) - logger.Debug("connecting to console of %s on %s" % (instance.name, node)) + logging.debug("Connecting to console of %s on %s", instance.name, node) - hyper = hypervisor.GetHypervisor() - console_cmd = hyper.GetShellCommandForConsole(instance) + hyper = hypervisor.GetHypervisor(instance.hypervisor) + cluster = self.cfg.GetClusterInfo() + # beparams and hvparams are passed separately, to avoid editing the + # instance and then saving the defaults in the instance itself. + hvparams = cluster.FillHV(instance) + beparams = cluster.FillBE(instance) + console_cmd = hyper.GetShellCommandForConsole(instance, hvparams, beparams) # build ssh cmdline return self.ssh.BuildCmd(node, "root", console_cmd, batch=True, tty=True) @@ -3376,29 +5944,54 @@ class LUReplaceDisks(LogicalUnit): HPATH = "mirrors-replace" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name", "mode", "disks"] + REQ_BGL = False - def _RunAllocator(self): - """Compute a new secondary node using an IAllocator. + def CheckArguments(self): + if not hasattr(self.op, "remote_node"): + self.op.remote_node = None + if not hasattr(self.op, "iallocator"): + self.op.iallocator = None - """ - ial = IAllocator(self.cfg, self.sstore, - mode=constants.IALLOCATOR_MODE_RELOC, - name=self.op.instance_name, - relocate_from=[self.sec_node]) + TLReplaceDisks.CheckArguments(self.op.mode, self.op.remote_node, + self.op.iallocator) - ial.Run(self.op.iallocator) + def ExpandNames(self): + self._ExpandAndLockInstance() - if not ial.success: - raise errors.OpPrereqError("Can't compute nodes using" - " iallocator '%s': %s" % (self.op.iallocator, - ial.info)) - if len(ial.nodes) != ial.required_nodes: - raise errors.OpPrereqError("iallocator '%s' returned invalid number" - " of nodes (%s), required %s" % - (len(ial.nodes), ial.required_nodes)) - self.op.remote_node = ial.nodes[0] - logger.ToStdout("Selected new secondary for the instance: %s" % - self.op.remote_node) + if self.op.iallocator is not None: + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + + elif self.op.remote_node is not None: + remote_node = self.cfg.ExpandNodeName(self.op.remote_node) + if remote_node is None: + raise errors.OpPrereqError("Node '%s' not known" % + self.op.remote_node) + + self.op.remote_node = remote_node + + # Warning: do not remove the locking of the new secondary here + # unless DRBD8.AddChildren is changed to work in parallel; + # currently it doesn't since parallel invocations of + # FindUnusedMinor will conflict + self.needed_locks[locking.LEVEL_NODE] = [remote_node] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_APPEND + + else: + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + self.replacer = TLReplaceDisks(self, self.op.instance_name, self.op.mode, + self.op.iallocator, self.op.remote_node, + self.op.disks) + + self.tasklets = [self.replacer] + + def DeclareLocks(self, level): + # If we're not already locking all nodes in the set we have to declare the + # instance's primary/secondary nodes. + if (level == locking.LEVEL_NODE and + self.needed_locks[locking.LEVEL_NODE] is not locking.ALL_SET): + self._LockInstancesNodes() def BuildHooksEnv(self): """Build hooks env. @@ -3406,200 +5999,498 @@ class LUReplaceDisks(LogicalUnit): This runs on the master, the primary and all the secondaries. """ + instance = self.replacer.instance env = { "MODE": self.op.mode, "NEW_SECONDARY": self.op.remote_node, - "OLD_SECONDARY": self.instance.secondary_nodes[0], + "OLD_SECONDARY": instance.secondary_nodes[0], } - env.update(_BuildInstanceHookEnvByObject(self.instance)) + env.update(_BuildInstanceHookEnvByObject(self, instance)) nl = [ - self.sstore.GetMasterNode(), - self.instance.primary_node, + self.cfg.GetMasterNode(), + instance.primary_node, ] if self.op.remote_node is not None: nl.append(self.op.remote_node) return env, nl, nl + +class LUEvacuateNode(LogicalUnit): + """Relocate the secondary instances from a node. + + """ + HPATH = "node-evacuate" + HTYPE = constants.HTYPE_NODE + _OP_REQP = ["node_name"] + REQ_BGL = False + + def CheckArguments(self): + if not hasattr(self.op, "remote_node"): + self.op.remote_node = None + if not hasattr(self.op, "iallocator"): + self.op.iallocator = None + + TLReplaceDisks.CheckArguments(constants.REPLACE_DISK_CHG, + self.op.remote_node, + self.op.iallocator) + + def ExpandNames(self): + self.op.node_name = self.cfg.ExpandNodeName(self.op.node_name) + if self.op.node_name is None: + raise errors.OpPrereqError("Node '%s' not known" % self.op.node_name) + + self.needed_locks = {} + + # Declare node locks + if self.op.iallocator is not None: + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + + elif self.op.remote_node is not None: + remote_node = self.cfg.ExpandNodeName(self.op.remote_node) + if remote_node is None: + raise errors.OpPrereqError("Node '%s' not known" % + self.op.remote_node) + + self.op.remote_node = remote_node + + # Warning: do not remove the locking of the new secondary here + # unless DRBD8.AddChildren is changed to work in parallel; + # currently it doesn't since parallel invocations of + # FindUnusedMinor will conflict + self.needed_locks[locking.LEVEL_NODE] = [remote_node] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_APPEND + + else: + raise errors.OpPrereqError("Invalid parameters") + + # Create tasklets for replacing disks for all secondary instances on this + # node + names = [] + tasklets = [] + + for inst in _GetNodeSecondaryInstances(self.cfg, self.op.node_name): + logging.debug("Replacing disks for instance %s", inst.name) + names.append(inst.name) + + replacer = TLReplaceDisks(self, inst.name, constants.REPLACE_DISK_CHG, + self.op.iallocator, self.op.remote_node, []) + tasklets.append(replacer) + + self.tasklets = tasklets + self.instance_names = names + + # Declare instance locks + self.needed_locks[locking.LEVEL_INSTANCE] = self.instance_names + + def DeclareLocks(self, level): + # If we're not already locking all nodes in the set we have to declare the + # instance's primary/secondary nodes. + if (level == locking.LEVEL_NODE and + self.needed_locks[locking.LEVEL_NODE] is not locking.ALL_SET): + self._LockInstancesNodes() + + def BuildHooksEnv(self): + """Build hooks env. + + This runs on the master, the primary and all the secondaries. + + """ + env = { + "NODE_NAME": self.op.node_name, + } + + nl = [self.cfg.GetMasterNode()] + + if self.op.remote_node is not None: + env["NEW_SECONDARY"] = self.op.remote_node + nl.append(self.op.remote_node) + + return (env, nl, nl) + + +class TLReplaceDisks(Tasklet): + """Replaces disks for an instance. + + Note: Locking is not within the scope of this class. + + """ + def __init__(self, lu, instance_name, mode, iallocator_name, remote_node, + disks): + """Initializes this class. + + """ + Tasklet.__init__(self, lu) + + # Parameters + self.instance_name = instance_name + self.mode = mode + self.iallocator_name = iallocator_name + self.remote_node = remote_node + self.disks = disks + + # Runtime data + self.instance = None + self.new_node = None + self.target_node = None + self.other_node = None + self.remote_node_info = None + self.node_secondary_ip = None + + @staticmethod + def CheckArguments(mode, remote_node, iallocator): + """Helper function for users of this class. + + """ + # check for valid parameter combination + if mode == constants.REPLACE_DISK_CHG: + if remote_node is None and iallocator is None: + raise errors.OpPrereqError("When changing the secondary either an" + " iallocator script must be used or the" + " new node given") + + if remote_node is not None and iallocator is not None: + raise errors.OpPrereqError("Give either the iallocator or the new" + " secondary, not both") + + elif remote_node is not None or iallocator is not None: + # Not replacing the secondary + raise errors.OpPrereqError("The iallocator and new node options can" + " only be used when changing the" + " secondary node") + + @staticmethod + def _RunAllocator(lu, iallocator_name, instance_name, relocate_from): + """Compute a new secondary node using an IAllocator. + + """ + ial = IAllocator(lu.cfg, lu.rpc, + mode=constants.IALLOCATOR_MODE_RELOC, + name=instance_name, + relocate_from=relocate_from) + + ial.Run(iallocator_name) + + if not ial.success: + raise errors.OpPrereqError("Can't compute nodes using iallocator '%s':" + " %s" % (iallocator_name, ial.info)) + + if len(ial.nodes) != ial.required_nodes: + raise errors.OpPrereqError("iallocator '%s' returned invalid number" + " of nodes (%s), required %s" % + (len(ial.nodes), ial.required_nodes)) + + remote_node_name = ial.nodes[0] + + lu.LogInfo("Selected new secondary for instance '%s': %s", + instance_name, remote_node_name) + + return remote_node_name + + def _FindFaultyDisks(self, node_name): + return _FindFaultyInstanceDisks(self.cfg, self.rpc, self.instance, + node_name, True) + def CheckPrereq(self): """Check prerequisites. This checks that the instance is in the cluster. """ - if not hasattr(self.op, "remote_node"): - self.op.remote_node = None + self.instance = self.cfg.GetInstanceInfo(self.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.instance_name - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) - self.instance = instance - self.op.instance_name = instance.name - - if instance.disk_template not in constants.DTS_NET_MIRROR: - raise errors.OpPrereqError("Instance's disk layout is not" - " network mirrored.") + if self.instance.disk_template != constants.DT_DRBD8: + raise errors.OpPrereqError("Can only run replace disks for DRBD8-based" + " instances") - if len(instance.secondary_nodes) != 1: + if len(self.instance.secondary_nodes) != 1: raise errors.OpPrereqError("The instance has a strange layout," " expected one secondary but found %d" % - len(instance.secondary_nodes)) + len(self.instance.secondary_nodes)) - self.sec_node = instance.secondary_nodes[0] + secondary_node = self.instance.secondary_nodes[0] - ia_name = getattr(self.op, "iallocator", None) - if ia_name is not None: - if self.op.remote_node is not None: - raise errors.OpPrereqError("Give either the iallocator or the new" - " secondary, not both") - self.op.remote_node = self._RunAllocator() + if self.iallocator_name is None: + remote_node = self.remote_node + else: + remote_node = self._RunAllocator(self.lu, self.iallocator_name, + self.instance.name, secondary_node) - remote_node = self.op.remote_node if remote_node is not None: - remote_node = self.cfg.ExpandNodeName(remote_node) - if remote_node is None: - raise errors.OpPrereqError("Node '%s' not known" % - self.op.remote_node) self.remote_node_info = self.cfg.GetNodeInfo(remote_node) + assert self.remote_node_info is not None, \ + "Cannot retrieve locked node %s" % remote_node else: self.remote_node_info = None - if remote_node == instance.primary_node: + + if remote_node == self.instance.primary_node: raise errors.OpPrereqError("The specified node is the primary node of" " the instance.") - elif remote_node == self.sec_node: - if self.op.mode == constants.REPLACE_DISK_SEC: - # this is for DRBD8, where we can't execute the same mode of - # replacement as for drbd7 (no different port allocated) - raise errors.OpPrereqError("Same secondary given, cannot execute" - " replacement") - if instance.disk_template == constants.DT_DRBD8: - if (self.op.mode == constants.REPLACE_DISK_ALL and - remote_node is not None): - # switch to replace secondary mode - self.op.mode = constants.REPLACE_DISK_SEC - - if self.op.mode == constants.REPLACE_DISK_ALL: - raise errors.OpPrereqError("Template 'drbd' only allows primary or" - " secondary disk replacement, not" - " both at once") - elif self.op.mode == constants.REPLACE_DISK_PRI: - if remote_node is not None: - raise errors.OpPrereqError("Template 'drbd' does not allow changing" - " the secondary while doing a primary" - " node disk replacement") - self.tgt_node = instance.primary_node - self.oth_node = instance.secondary_nodes[0] - elif self.op.mode == constants.REPLACE_DISK_SEC: - self.new_node = remote_node # this can be None, in which case - # we don't change the secondary - self.tgt_node = instance.secondary_nodes[0] - self.oth_node = instance.primary_node + + if remote_node == secondary_node: + raise errors.OpPrereqError("The specified node is already the" + " secondary node of the instance.") + + if self.disks and self.mode in (constants.REPLACE_DISK_AUTO, + constants.REPLACE_DISK_CHG): + raise errors.OpPrereqError("Cannot specify disks to be replaced") + + if self.mode == constants.REPLACE_DISK_AUTO: + faulty_primary = self._FindFaultyDisks(self.instance.primary_node) + faulty_secondary = self._FindFaultyDisks(secondary_node) + + if faulty_primary and faulty_secondary: + raise errors.OpPrereqError("Instance %s has faulty disks on more than" + " one node and can not be repaired" + " automatically" % self.instance_name) + + if faulty_primary: + self.disks = faulty_primary + self.target_node = self.instance.primary_node + self.other_node = secondary_node + check_nodes = [self.target_node, self.other_node] + elif faulty_secondary: + self.disks = faulty_secondary + self.target_node = secondary_node + self.other_node = self.instance.primary_node + check_nodes = [self.target_node, self.other_node] + else: + self.disks = [] + check_nodes = [] + + else: + # Non-automatic modes + if self.mode == constants.REPLACE_DISK_PRI: + self.target_node = self.instance.primary_node + self.other_node = secondary_node + check_nodes = [self.target_node, self.other_node] + + elif self.mode == constants.REPLACE_DISK_SEC: + self.target_node = secondary_node + self.other_node = self.instance.primary_node + check_nodes = [self.target_node, self.other_node] + + elif self.mode == constants.REPLACE_DISK_CHG: + self.new_node = remote_node + self.other_node = self.instance.primary_node + self.target_node = secondary_node + check_nodes = [self.new_node, self.other_node] + + _CheckNodeNotDrained(self.lu, remote_node) + + else: + raise errors.ProgrammerError("Unhandled disk replace mode (%s)" % + self.mode) + + # If not specified all disks should be replaced + if not self.disks: + self.disks = range(len(self.instance.disks)) + + for node in check_nodes: + _CheckNodeOnline(self.lu, node) + + # Check whether disks are valid + for disk_idx in self.disks: + self.instance.FindDisk(disk_idx) + + # Get secondary node IP addresses + node_2nd_ip = {} + + for node_name in [self.target_node, self.other_node, self.new_node]: + if node_name is not None: + node_2nd_ip[node_name] = self.cfg.GetNodeInfo(node_name).secondary_ip + + self.node_secondary_ip = node_2nd_ip + + def Exec(self, feedback_fn): + """Execute disk replacement. + + This dispatches the disk replacement to the appropriate handler. + + """ + if not self.disks: + feedback_fn("No disks need replacement") + return + + feedback_fn("Replacing disk(s) %s for %s" % + (", ".join([str(i) for i in self.disks]), self.instance.name)) + + activate_disks = (not self.instance.admin_up) + + # Activate the instance disks if we're replacing them on a down instance + if activate_disks: + _StartInstanceDisks(self.lu, self.instance, True) + + try: + # Should we replace the secondary node? + if self.new_node is not None: + return self._ExecDrbd8Secondary() else: - raise errors.ProgrammerError("Unhandled disk replace mode") + return self._ExecDrbd8DiskOnly() + + finally: + # Deactivate the instance disks if we're replacing them on a down instance + if activate_disks: + _SafeShutdownInstanceDisks(self.lu, self.instance) + + def _CheckVolumeGroup(self, nodes): + self.lu.LogInfo("Checking volume groups") + + vgname = self.cfg.GetVGName() + + # Make sure volume group exists on all involved nodes + results = self.rpc.call_vg_list(nodes) + if not results: + raise errors.OpExecError("Can't list volume groups on the nodes") + + for node in nodes: + res = results[node] + res.Raise("Error checking node %s" % node) + if vgname not in res.payload: + raise errors.OpExecError("Volume group '%s' not found on node %s" % + (vgname, node)) + + def _CheckDisksExistence(self, nodes): + # Check disk existence + for idx, dev in enumerate(self.instance.disks): + if idx not in self.disks: + continue + + for node in nodes: + self.lu.LogInfo("Checking disk/%d on %s" % (idx, node)) + self.cfg.SetDiskID(dev, node) + + result = self.rpc.call_blockdev_find(node, dev) + + msg = result.fail_msg + if msg or not result.payload: + if not msg: + msg = "disk not found" + raise errors.OpExecError("Can't find disk/%d on node %s: %s" % + (idx, node, msg)) + + def _CheckDisksConsistency(self, node_name, on_primary, ldisk): + for idx, dev in enumerate(self.instance.disks): + if idx not in self.disks: + continue + + self.lu.LogInfo("Checking disk/%d consistency on node %s" % + (idx, node_name)) + + if not _CheckDiskConsistency(self.lu, dev, node_name, on_primary, + ldisk=ldisk): + raise errors.OpExecError("Node %s has degraded storage, unsafe to" + " replace disks for instance %s" % + (node_name, self.instance.name)) + + def _CreateNewStorage(self, node_name): + vgname = self.cfg.GetVGName() + iv_names = {} + + for idx, dev in enumerate(self.instance.disks): + if idx not in self.disks: + continue + + self.lu.LogInfo("Adding storage on %s for disk/%d" % (node_name, idx)) + + self.cfg.SetDiskID(dev, node_name) + + lv_names = [".disk%d_%s" % (idx, suffix) for suffix in ["data", "meta"]] + names = _GenerateUniqueNames(self.lu, lv_names) + + lv_data = objects.Disk(dev_type=constants.LD_LV, size=dev.size, + logical_id=(vgname, names[0])) + lv_meta = objects.Disk(dev_type=constants.LD_LV, size=128, + logical_id=(vgname, names[1])) + + new_lvs = [lv_data, lv_meta] + old_lvs = dev.children + iv_names[dev.iv_name] = (dev, old_lvs, new_lvs) + + # we pass force_create=True to force the LVM creation + for new_lv in new_lvs: + _CreateBlockDev(self.lu, node_name, self.instance, new_lv, True, + _GetInstanceInfoText(self.instance), False) + + return iv_names + + def _CheckDevices(self, node_name, iv_names): + for name, (dev, old_lvs, new_lvs) in iv_names.iteritems(): + self.cfg.SetDiskID(dev, node_name) + + result = self.rpc.call_blockdev_find(node_name, dev) + + msg = result.fail_msg + if msg or not result.payload: + if not msg: + msg = "disk not found" + raise errors.OpExecError("Can't find DRBD device %s: %s" % + (name, msg)) + + if result.payload.is_degraded: + raise errors.OpExecError("DRBD device %s is degraded!" % name) + + def _RemoveOldStorage(self, node_name, iv_names): + for name, (dev, old_lvs, _) in iv_names.iteritems(): + self.lu.LogInfo("Remove logical volumes for %s" % name) + + for lv in old_lvs: + self.cfg.SetDiskID(lv, node_name) - for name in self.op.disks: - if instance.FindDisk(name) is None: - raise errors.OpPrereqError("Disk '%s' not found for instance '%s'" % - (name, instance.name)) - self.op.remote_node = remote_node + msg = self.rpc.call_blockdev_remove(node_name, lv).fail_msg + if msg: + self.lu.LogWarning("Can't remove old LV: %s" % msg, + hint="remove unused LVs manually") - def _ExecD8DiskOnly(self, feedback_fn): - """Replace a disk on the primary or secondary for dbrd8. + def _ExecDrbd8DiskOnly(self): + """Replace a disk on the primary or secondary for DRBD 8. The algorithm for replace is quite complicated: - - for each disk to be replaced: - - create new LVs on the target node with unique names - - detach old LVs from the drbd device - - rename old LVs to name_replaced. - - rename new LVs to old LVs - - attach the new LVs (with the old names now) to the drbd device - - wait for sync across all devices - - for each modified disk: - - remove old LVs (which have the name name_replaces.) + + 1. for each disk to be replaced: + + 1. create new LVs on the target node with unique names + 1. detach old LVs from the drbd device + 1. rename old LVs to name_replaced. + 1. rename new LVs to old LVs + 1. attach the new LVs (with the old names now) to the drbd device + + 1. wait for sync across all devices + + 1. for each modified disk: + + 1. remove old LVs (which have the name name_replaces.) Failures are not very well handled. """ steps_total = 6 - warning, info = (self.proc.LogWarning, self.proc.LogInfo) - instance = self.instance - iv_names = {} - vgname = self.cfg.GetVGName() - # start of work - cfg = self.cfg - tgt_node = self.tgt_node - oth_node = self.oth_node # Step: check device activation - self.proc.LogStep(1, steps_total, "check device existence") - info("checking volume groups") - my_vg = cfg.GetVGName() - results = rpc.call_vg_list([oth_node, tgt_node]) - if not results: - raise errors.OpExecError("Can't list volume groups on the nodes") - for node in oth_node, tgt_node: - res = results.get(node, False) - if not res or my_vg not in res: - raise errors.OpExecError("Volume group '%s' not found on %s" % - (my_vg, node)) - for dev in instance.disks: - if not dev.iv_name in self.op.disks: - continue - for node in tgt_node, oth_node: - info("checking %s on %s" % (dev.iv_name, node)) - cfg.SetDiskID(dev, node) - if not rpc.call_blockdev_find(node, dev): - raise errors.OpExecError("Can't find device %s on node %s" % - (dev.iv_name, node)) + self.lu.LogStep(1, steps_total, "Check device existence") + self._CheckDisksExistence([self.other_node, self.target_node]) + self._CheckVolumeGroup([self.target_node, self.other_node]) # Step: check other node consistency - self.proc.LogStep(2, steps_total, "check peer consistency") - for dev in instance.disks: - if not dev.iv_name in self.op.disks: - continue - info("checking %s consistency on %s" % (dev.iv_name, oth_node)) - if not _CheckDiskConsistency(self.cfg, dev, oth_node, - oth_node==instance.primary_node): - raise errors.OpExecError("Peer node (%s) has degraded storage, unsafe" - " to replace disks on this node (%s)" % - (oth_node, tgt_node)) + self.lu.LogStep(2, steps_total, "Check peer consistency") + self._CheckDisksConsistency(self.other_node, + self.other_node == self.instance.primary_node, + False) # Step: create new storage - self.proc.LogStep(3, steps_total, "allocate new storage") - for dev in instance.disks: - if not dev.iv_name in self.op.disks: - continue - size = dev.size - cfg.SetDiskID(dev, tgt_node) - lv_names = [".%s_%s" % (dev.iv_name, suf) for suf in ["data", "meta"]] - names = _GenerateUniqueNames(cfg, lv_names) - lv_data = objects.Disk(dev_type=constants.LD_LV, size=size, - logical_id=(vgname, names[0])) - lv_meta = objects.Disk(dev_type=constants.LD_LV, size=128, - logical_id=(vgname, names[1])) - new_lvs = [lv_data, lv_meta] - old_lvs = dev.children - iv_names[dev.iv_name] = (dev, old_lvs, new_lvs) - info("creating new local storage on %s for %s" % - (tgt_node, dev.iv_name)) - # since we *always* want to create this LV, we use the - # _Create...OnPrimary (which forces the creation), even if we - # are talking about the secondary node - for new_lv in new_lvs: - if not _CreateBlockDevOnPrimary(cfg, tgt_node, instance, new_lv, - _GetInstanceInfoText(instance)): - raise errors.OpExecError("Failed to create new LV named '%s' on" - " node '%s'" % - (new_lv.logical_id[1], tgt_node)) + self.lu.LogStep(3, steps_total, "Allocate new storage") + iv_names = self._CreateNewStorage(self.target_node) # Step: for each lv, detach+rename*2+attach - self.proc.LogStep(4, steps_total, "change drbd configuration") + self.lu.LogStep(4, steps_total, "Changing drbd configuration") for dev, old_lvs, new_lvs in iv_names.itervalues(): - info("detaching %s drbd from local storage" % dev.iv_name) - if not rpc.call_blockdev_removechildren(tgt_node, dev, old_lvs): - raise errors.OpExecError("Can't detach drbd from local storage on node" - " %s for device %s" % (tgt_node, dev.iv_name)) + self.lu.LogInfo("Detaching %s drbd from local storage" % dev.iv_name) + + result = self.rpc.call_blockdev_removechildren(self.target_node, dev, + old_lvs) + result.Raise("Can't detach drbd from local storage on node" + " %s for device %s" % (self.target_node, dev.iv_name)) #dev.children = [] #cfg.Update(instance) @@ -3613,69 +6504,70 @@ class LUReplaceDisks(LogicalUnit): temp_suffix = int(time.time()) ren_fn = lambda d, suff: (d.physical_id[0], d.physical_id[1] + "_replaced-%s" % suff) - # build the rename list based on what LVs exist on the node - rlist = [] + + # Build the rename list based on what LVs exist on the node + rename_old_to_new = [] for to_ren in old_lvs: - find_res = rpc.call_blockdev_find(tgt_node, to_ren) - if find_res is not None: # device exists - rlist.append((to_ren, ren_fn(to_ren, temp_suffix))) - - info("renaming the old LVs on the target node") - if not rpc.call_blockdev_rename(tgt_node, rlist): - raise errors.OpExecError("Can't rename old LVs on node %s" % tgt_node) - # now we rename the new LVs to the old LVs - info("renaming the new LVs on the target node") - rlist = [(new, old.physical_id) for old, new in zip(old_lvs, new_lvs)] - if not rpc.call_blockdev_rename(tgt_node, rlist): - raise errors.OpExecError("Can't rename new LVs on node %s" % tgt_node) + result = self.rpc.call_blockdev_find(self.target_node, to_ren) + if not result.fail_msg and result.payload: + # device exists + rename_old_to_new.append((to_ren, ren_fn(to_ren, temp_suffix))) + + self.lu.LogInfo("Renaming the old LVs on the target node") + result = self.rpc.call_blockdev_rename(self.target_node, + rename_old_to_new) + result.Raise("Can't rename old LVs on node %s" % self.target_node) + + # Now we rename the new LVs to the old LVs + self.lu.LogInfo("Renaming the new LVs on the target node") + rename_new_to_old = [(new, old.physical_id) + for old, new in zip(old_lvs, new_lvs)] + result = self.rpc.call_blockdev_rename(self.target_node, + rename_new_to_old) + result.Raise("Can't rename new LVs on node %s" % self.target_node) for old, new in zip(old_lvs, new_lvs): new.logical_id = old.logical_id - cfg.SetDiskID(new, tgt_node) + self.cfg.SetDiskID(new, self.target_node) for disk in old_lvs: disk.logical_id = ren_fn(disk, temp_suffix) - cfg.SetDiskID(disk, tgt_node) - - # now that the new lvs have the old name, we can add them to the device - info("adding new mirror component on %s" % tgt_node) - if not rpc.call_blockdev_addchildren(tgt_node, dev, new_lvs): + self.cfg.SetDiskID(disk, self.target_node) + + # Now that the new lvs have the old name, we can add them to the device + self.lu.LogInfo("Adding new mirror component on %s" % self.target_node) + result = self.rpc.call_blockdev_addchildren(self.target_node, dev, + new_lvs) + msg = result.fail_msg + if msg: for new_lv in new_lvs: - if not rpc.call_blockdev_remove(tgt_node, new_lv): - warning("Can't rollback device %s", hint="manually cleanup unused" - " logical volumes") - raise errors.OpExecError("Can't add local storage to drbd") + msg2 = self.rpc.call_blockdev_remove(self.target_node, + new_lv).fail_msg + if msg2: + self.lu.LogWarning("Can't rollback device %s: %s", dev, msg2, + hint=("cleanup manually the unused logical" + "volumes")) + raise errors.OpExecError("Can't add local storage to drbd: %s" % msg) dev.children = new_lvs - cfg.Update(instance) - # Step: wait for sync + self.cfg.Update(self.instance) - # this can fail as the old devices are degraded and _WaitForSync - # does a combined result over all disks, so we don't check its - # return value - self.proc.LogStep(5, steps_total, "sync devices") - _WaitForSync(cfg, instance, self.proc, unlock=True) + # Wait for sync + # This can fail as the old devices are degraded and _WaitForSync + # does a combined result over all disks, so we don't check its return value + self.lu.LogStep(5, steps_total, "Sync devices") + _WaitForSync(self.lu, self.instance, unlock=True) - # so check manually all the devices - for name, (dev, old_lvs, new_lvs) in iv_names.iteritems(): - cfg.SetDiskID(dev, instance.primary_node) - is_degr = rpc.call_blockdev_find(instance.primary_node, dev)[5] - if is_degr: - raise errors.OpExecError("DRBD device %s is degraded!" % name) + # Check all devices manually + self._CheckDevices(self.instance.primary_node, iv_names) # Step: remove old storage - self.proc.LogStep(6, steps_total, "removing old storage") - for name, (dev, old_lvs, new_lvs) in iv_names.iteritems(): - info("remove logical volumes for %s" % name) - for lv in old_lvs: - cfg.SetDiskID(lv, tgt_node) - if not rpc.call_blockdev_remove(tgt_node, lv): - warning("Can't remove old LV", hint="manually remove unused LVs") - continue + self.lu.LogStep(6, steps_total, "Removing old storage") + self._RemoveOldStorage(self.target_node, iv_names) - def _ExecD8Secondary(self, feedback_fn): - """Replace the secondary node for drbd8. + def _ExecDrbd8Secondary(self): + """Replace the secondary node for DRBD 8. The algorithm for replace is quite complicated: - for all disks of the instance: @@ -3694,179 +6586,186 @@ class LUReplaceDisks(LogicalUnit): """ steps_total = 6 - warning, info = (self.proc.LogWarning, self.proc.LogInfo) - instance = self.instance - iv_names = {} - vgname = self.cfg.GetVGName() - # start of work - cfg = self.cfg - old_node = self.tgt_node - new_node = self.new_node - pri_node = instance.primary_node # Step: check device activation - self.proc.LogStep(1, steps_total, "check device existence") - info("checking volume groups") - my_vg = cfg.GetVGName() - results = rpc.call_vg_list([pri_node, new_node]) - if not results: - raise errors.OpExecError("Can't list volume groups on the nodes") - for node in pri_node, new_node: - res = results.get(node, False) - if not res or my_vg not in res: - raise errors.OpExecError("Volume group '%s' not found on %s" % - (my_vg, node)) - for dev in instance.disks: - if not dev.iv_name in self.op.disks: - continue - info("checking %s on %s" % (dev.iv_name, pri_node)) - cfg.SetDiskID(dev, pri_node) - if not rpc.call_blockdev_find(pri_node, dev): - raise errors.OpExecError("Can't find device %s on node %s" % - (dev.iv_name, pri_node)) + self.lu.LogStep(1, steps_total, "Check device existence") + self._CheckDisksExistence([self.instance.primary_node]) + self._CheckVolumeGroup([self.instance.primary_node]) # Step: check other node consistency - self.proc.LogStep(2, steps_total, "check peer consistency") - for dev in instance.disks: - if not dev.iv_name in self.op.disks: - continue - info("checking %s consistency on %s" % (dev.iv_name, pri_node)) - if not _CheckDiskConsistency(self.cfg, dev, pri_node, True, ldisk=True): - raise errors.OpExecError("Primary node (%s) has degraded storage," - " unsafe to replace the secondary" % - pri_node) + self.lu.LogStep(2, steps_total, "Check peer consistency") + self._CheckDisksConsistency(self.instance.primary_node, True, True) # Step: create new storage - self.proc.LogStep(3, steps_total, "allocate new storage") - for dev in instance.disks: - size = dev.size - info("adding new local storage on %s for %s" % (new_node, dev.iv_name)) - # since we *always* want to create this LV, we use the - # _Create...OnPrimary (which forces the creation), even if we - # are talking about the secondary node + self.lu.LogStep(3, steps_total, "Allocate new storage") + for idx, dev in enumerate(self.instance.disks): + self.lu.LogInfo("Adding new local storage on %s for disk/%d" % + (self.new_node, idx)) + # we pass force_create=True to force LVM creation for new_lv in dev.children: - if not _CreateBlockDevOnPrimary(cfg, new_node, instance, new_lv, - _GetInstanceInfoText(instance)): - raise errors.OpExecError("Failed to create new LV named '%s' on" - " node '%s'" % - (new_lv.logical_id[1], new_node)) - - iv_names[dev.iv_name] = (dev, dev.children) - - self.proc.LogStep(4, steps_total, "changing drbd configuration") - for dev in instance.disks: - size = dev.size - info("activating a new drbd on %s for %s" % (new_node, dev.iv_name)) - # create new devices on new_node - new_drbd = objects.Disk(dev_type=constants.LD_DRBD8, - logical_id=(pri_node, new_node, - dev.logical_id[2]), - children=dev.children) - if not _CreateBlockDevOnSecondary(cfg, new_node, instance, - new_drbd, False, - _GetInstanceInfoText(instance)): - raise errors.OpExecError("Failed to create new DRBD on" - " node '%s'" % new_node) + _CreateBlockDev(self.lu, self.new_node, self.instance, new_lv, True, + _GetInstanceInfoText(self.instance), False) + + # Step 4: dbrd minors and drbd setups changes + # after this, we must manually remove the drbd minors on both the + # error and the success paths + self.lu.LogStep(4, steps_total, "Changing drbd configuration") + minors = self.cfg.AllocateDRBDMinor([self.new_node + for dev in self.instance.disks], + self.instance.name) + logging.debug("Allocated minors %r" % (minors,)) - for dev in instance.disks: - # we have new devices, shutdown the drbd on the old secondary - info("shutting down drbd for %s on old node" % dev.iv_name) - cfg.SetDiskID(dev, old_node) - if not rpc.call_blockdev_shutdown(old_node, dev): - warning("Failed to shutdown drbd for %s on old node" % dev.iv_name, - hint="Please cleanup this device manually as soon as possible") - - info("detaching primary drbds from the network (=> standalone)") - done = 0 - for dev in instance.disks: - cfg.SetDiskID(dev, pri_node) - # set the physical (unique in bdev terms) id to None, meaning - # detach from network - dev.physical_id = (None,) * len(dev.physical_id) - # and 'find' the device, which will 'fix' it to match the - # standalone state - if rpc.call_blockdev_find(pri_node, dev): - done += 1 + iv_names = {} + for idx, (dev, new_minor) in enumerate(zip(self.instance.disks, minors)): + self.lu.LogInfo("activating a new drbd on %s for disk/%d" % + (self.new_node, idx)) + # create new devices on new_node; note that we create two IDs: + # one without port, so the drbd will be activated without + # networking information on the new node at this stage, and one + # with network, for the latter activation in step 4 + (o_node1, o_node2, o_port, o_minor1, o_minor2, o_secret) = dev.logical_id + if self.instance.primary_node == o_node1: + p_minor = o_minor1 else: - warning("Failed to detach drbd %s from network, unusual case" % - dev.iv_name) + p_minor = o_minor2 - if not done: - # no detaches succeeded (very unlikely) - raise errors.OpExecError("Can't detach at least one DRBD from old node") + new_alone_id = (self.instance.primary_node, self.new_node, None, + p_minor, new_minor, o_secret) + new_net_id = (self.instance.primary_node, self.new_node, o_port, + p_minor, new_minor, o_secret) + + iv_names[idx] = (dev, dev.children, new_net_id) + logging.debug("Allocated new_minor: %s, new_logical_id: %s", new_minor, + new_net_id) + new_drbd = objects.Disk(dev_type=constants.LD_DRBD8, + logical_id=new_alone_id, + children=dev.children, + size=dev.size) + try: + _CreateSingleBlockDev(self.lu, self.new_node, self.instance, new_drbd, + _GetInstanceInfoText(self.instance), False) + except errors.GenericError: + self.cfg.ReleaseDRBDMinors(self.instance.name) + raise + + # We have new devices, shutdown the drbd on the old secondary + for idx, dev in enumerate(self.instance.disks): + self.lu.LogInfo("Shutting down drbd for disk/%d on old node" % idx) + self.cfg.SetDiskID(dev, self.target_node) + msg = self.rpc.call_blockdev_shutdown(self.target_node, dev).fail_msg + if msg: + self.lu.LogWarning("Failed to shutdown drbd for disk/%d on old" + "node: %s" % (idx, msg), + hint=("Please cleanup this device manually as" + " soon as possible")) + + self.lu.LogInfo("Detaching primary drbds from the network (=> standalone)") + result = self.rpc.call_drbd_disconnect_net([self.instance.primary_node], + self.node_secondary_ip, + self.instance.disks)\ + [self.instance.primary_node] + + msg = result.fail_msg + if msg: + # detaches didn't succeed (unlikely) + self.cfg.ReleaseDRBDMinors(self.instance.name) + raise errors.OpExecError("Can't detach the disks from the network on" + " old node: %s" % (msg,)) # if we managed to detach at least one, we update all the disks of # the instance to point to the new secondary - info("updating instance configuration") - for dev in instance.disks: - dev.logical_id = (pri_node, new_node) + dev.logical_id[2:] - cfg.SetDiskID(dev, pri_node) - cfg.Update(instance) + self.lu.LogInfo("Updating instance configuration") + for dev, _, new_logical_id in iv_names.itervalues(): + dev.logical_id = new_logical_id + self.cfg.SetDiskID(dev, self.instance.primary_node) + + self.cfg.Update(self.instance) # and now perform the drbd attach - info("attaching primary drbds to new secondary (standalone => connected)") - failures = [] - for dev in instance.disks: - info("attaching primary drbd for %s to new secondary node" % dev.iv_name) - # since the attach is smart, it's enough to 'find' the device, - # it will automatically activate the network, if the physical_id - # is correct - cfg.SetDiskID(dev, pri_node) - if not rpc.call_blockdev_find(pri_node, dev): - warning("can't attach drbd %s to new secondary!" % dev.iv_name, - "please do a gnt-instance info to see the status of disks") - - # this can fail as the old devices are degraded and _WaitForSync - # does a combined result over all disks, so we don't check its - # return value - self.proc.LogStep(5, steps_total, "sync devices") - _WaitForSync(cfg, instance, self.proc, unlock=True) - - # so check manually all the devices - for name, (dev, old_lvs) in iv_names.iteritems(): - cfg.SetDiskID(dev, pri_node) - is_degr = rpc.call_blockdev_find(pri_node, dev)[5] - if is_degr: - raise errors.OpExecError("DRBD device %s is degraded!" % name) + self.lu.LogInfo("Attaching primary drbds to new secondary" + " (standalone => connected)") + result = self.rpc.call_drbd_attach_net([self.instance.primary_node, + self.new_node], + self.node_secondary_ip, + self.instance.disks, + self.instance.name, + False) + for to_node, to_result in result.items(): + msg = to_result.fail_msg + if msg: + self.lu.LogWarning("Can't attach drbd disks on node %s: %s", + to_node, msg, + hint=("please do a gnt-instance info to see the" + " status of disks")) + + # Wait for sync + # This can fail as the old devices are degraded and _WaitForSync + # does a combined result over all disks, so we don't check its return value + self.lu.LogStep(5, steps_total, "Sync devices") + _WaitForSync(self.lu, self.instance, unlock=True) + + # Check all devices manually + self._CheckDevices(self.instance.primary_node, iv_names) - self.proc.LogStep(6, steps_total, "removing old storage") - for name, (dev, old_lvs) in iv_names.iteritems(): - info("remove logical volumes for %s" % name) - for lv in old_lvs: - cfg.SetDiskID(lv, old_node) - if not rpc.call_blockdev_remove(old_node, lv): - warning("Can't remove LV on old secondary", - hint="Cleanup stale volumes by hand") + # Step: remove old storage + self.lu.LogStep(6, steps_total, "Removing old storage") + self._RemoveOldStorage(self.target_node, iv_names) - def Exec(self, feedback_fn): - """Execute disk replacement. - This dispatches the disk replacement to the appropriate handler. +class LURepairNodeStorage(NoHooksLU): + """Repairs the volume group on a node. - """ - instance = self.instance + """ + _OP_REQP = ["node_name"] + REQ_BGL = False - # Activate the instance disks if we're replacing them on a down instance - if instance.status == "down": - op = opcodes.OpActivateInstanceDisks(instance_name=instance.name) - self.proc.ChainOpCode(op) + def CheckArguments(self): + node_name = self.cfg.ExpandNodeName(self.op.node_name) + if node_name is None: + raise errors.OpPrereqError("Invalid node name '%s'" % self.op.node_name) - if instance.disk_template == constants.DT_DRBD8: - if self.op.remote_node is None: - fn = self._ExecD8DiskOnly - else: - fn = self._ExecD8Secondary - else: - raise errors.ProgrammerError("Unhandled disk replacement case") + self.op.node_name = node_name + + def ExpandNames(self): + self.needed_locks = { + locking.LEVEL_NODE: [self.op.node_name], + } - ret = fn(feedback_fn) + def _CheckFaultyDisks(self, instance, node_name): + if _FindFaultyInstanceDisks(self.cfg, self.rpc, instance, + node_name, True): + raise errors.OpPrereqError("Instance '%s' has faulty disks on" + " node '%s'" % (instance.name, node_name)) - # Deactivate the instance disks if we're replacing them on a down instance - if instance.status == "down": - op = opcodes.OpDeactivateInstanceDisks(instance_name=instance.name) - self.proc.ChainOpCode(op) + def CheckPrereq(self): + """Check prerequisites. + + """ + storage_type = self.op.storage_type + + if (constants.SO_FIX_CONSISTENCY not in + constants.VALID_STORAGE_OPERATIONS.get(storage_type, [])): + raise errors.OpPrereqError("Storage units of type '%s' can not be" + " repaired" % storage_type) + + # Check whether any instance on this node has faulty disks + for inst in _GetNodeInstances(self.cfg, self.op.node_name): + check_nodes = set(inst.all_nodes) + check_nodes.discard(self.op.node_name) + for inst_node_name in check_nodes: + self._CheckFaultyDisks(inst, inst_node_name) + + def Exec(self, feedback_fn): + feedback_fn("Repairing storage unit '%s' on %s ..." % + (self.op.name, self.op.node_name)) - return ret + st_args = _GetStorageTypeArgs(self.cfg, self.op.storage_type) + result = self.rpc.call_storage_execute(self.op.node_name, + self.op.storage_type, st_args, + self.op.name, + constants.SO_FIX_CONSISTENCY) + result.Raise("Failed to repair storage unit '%s' on %s" % + (self.op.name, self.op.node_name)) class LUGrowDisk(LogicalUnit): @@ -3875,7 +6774,17 @@ class LUGrowDisk(LogicalUnit): """ HPATH = "disk-grow" HTYPE = constants.HTYPE_INSTANCE - _OP_REQP = ["instance_name", "disk", "amount"] + _OP_REQP = ["instance_name", "disk", "amount", "wait_for_sync"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() def BuildHooksEnv(self): """Build hooks env. @@ -3887,9 +6796,9 @@ class LUGrowDisk(LogicalUnit): "DISK": self.op.disk, "AMOUNT": self.op.amount, } - env.update(_BuildInstanceHookEnvByObject(self.instance)) + env.update(_BuildInstanceHookEnvByObject(self, self.instance)) nl = [ - self.sstore.GetMasterNode(), + self.cfg.GetMasterNode(), self.instance.primary_node, ] return env, nl, nl @@ -3900,62 +6809,87 @@ class LUGrowDisk(LogicalUnit): This checks that the instance is in the cluster. """ - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("Instance '%s' not known" % - self.op.instance_name) + instance = self.cfg.GetInstanceInfo(self.op.instance_name) + assert instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + nodenames = list(instance.all_nodes) + for node in nodenames: + _CheckNodeOnline(self, node) + + self.instance = instance - self.op.instance_name = instance.name if instance.disk_template not in (constants.DT_PLAIN, constants.DT_DRBD8): raise errors.OpPrereqError("Instance's disk layout does not support" " growing.") - if instance.FindDisk(self.op.disk) is None: - raise errors.OpPrereqError("Disk '%s' not found for instance '%s'" % - (self.op.disk, instance.name)) + self.disk = instance.FindDisk(self.op.disk) - nodenames = [instance.primary_node] + list(instance.secondary_nodes) - nodeinfo = rpc.call_node_info(nodenames, self.cfg.GetVGName()) + nodeinfo = self.rpc.call_node_info(nodenames, self.cfg.GetVGName(), + instance.hypervisor) for node in nodenames: - info = nodeinfo.get(node, None) - if not info: - raise errors.OpPrereqError("Cannot get current information" - " from node '%s'" % node) - vg_free = info.get('vg_free', None) + info = nodeinfo[node] + info.Raise("Cannot get current information from node %s" % node) + vg_free = info.payload.get('vg_free', None) if not isinstance(vg_free, int): raise errors.OpPrereqError("Can't compute free disk space on" " node %s" % node) - if self.op.amount > info['vg_free']: + if self.op.amount > vg_free: raise errors.OpPrereqError("Not enough disk space on target node %s:" " %d MiB available, %d MiB required" % - (node, info['vg_free'], self.op.amount)) + (node, vg_free, self.op.amount)) def Exec(self, feedback_fn): """Execute disk grow. """ instance = self.instance - disk = instance.FindDisk(self.op.disk) - for node in (instance.secondary_nodes + (instance.primary_node,)): + disk = self.disk + for node in instance.all_nodes: self.cfg.SetDiskID(disk, node) - result = rpc.call_blockdev_grow(node, disk, self.op.amount) - if not result or not isinstance(result, tuple) or len(result) != 2: - raise errors.OpExecError("grow request failed to node %s" % node) - elif not result[0]: - raise errors.OpExecError("grow request failed to node %s: %s" % - (node, result[1])) + result = self.rpc.call_blockdev_grow(node, disk, self.op.amount) + result.Raise("Grow request failed to node %s" % node) disk.RecordGrow(self.op.amount) self.cfg.Update(instance) - return + if self.op.wait_for_sync: + disk_abort = not _WaitForSync(self, instance) + if disk_abort: + self.proc.LogWarning("Warning: disk sync-ing has not returned a good" + " status.\nPlease check the instance.") class LUQueryInstanceData(NoHooksLU): """Query runtime instance data. """ - _OP_REQP = ["instances"] + _OP_REQP = ["instances", "static"] + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = {} + self.share_locks = dict.fromkeys(locking.LEVELS, 1) + + if not isinstance(self.op.instances, list): + raise errors.OpPrereqError("Invalid argument type 'instances'") + + if self.op.instances: + self.wanted_names = [] + for name in self.op.instances: + full_name = self.cfg.ExpandInstanceName(name) + if full_name is None: + raise errors.OpPrereqError("Instance '%s' not known" % name) + self.wanted_names.append(full_name) + self.needed_locks[locking.LEVEL_INSTANCE] = self.wanted_names + else: + self.wanted_names = None + self.needed_locks[locking.LEVEL_INSTANCE] = locking.ALL_SET + + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() def CheckPrereq(self): """Check prerequisites. @@ -3963,28 +6897,40 @@ class LUQueryInstanceData(NoHooksLU): This only checks the optional instance list against the existing names. """ - if not isinstance(self.op.instances, list): - raise errors.OpPrereqError("Invalid argument type 'instances'") - if self.op.instances: - self.wanted_instances = [] - names = self.op.instances - for name in names: - instance = self.cfg.GetInstanceInfo(self.cfg.ExpandInstanceName(name)) - if instance is None: - raise errors.OpPrereqError("No such instance name '%s'" % name) - self.wanted_instances.append(instance) - else: - self.wanted_instances = [self.cfg.GetInstanceInfo(name) for name - in self.cfg.GetInstanceList()] + if self.wanted_names is None: + self.wanted_names = self.acquired_locks[locking.LEVEL_INSTANCE] + + self.wanted_instances = [self.cfg.GetInstanceInfo(name) for name + in self.wanted_names] return + def _ComputeBlockdevStatus(self, node, instance_name, dev): + """Returns the status of a block device + + """ + if self.op.static or not node: + return None + + self.cfg.SetDiskID(dev, node) + + result = self.rpc.call_blockdev_find(node, dev) + if result.offline: + return None + + result.Raise("Can't compute disk status for %s" % instance_name) + + status = result.payload + if status is None: + return None + + return (status.dev_path, status.major, status.minor, + status.sync_percent, status.estimated_time, + status.is_degraded, status.ldisk_status) def _ComputeDiskStatus(self, instance, snode, dev): """Compute block device status. """ - self.cfg.SetDiskID(dev, instance.primary_node) - dev_pstatus = rpc.call_blockdev_find(instance.primary_node, dev) if dev.dev_type in constants.LDS_DRBD: # we change the snode then (otherwise we use the one passed in) if dev.logical_id[0] == instance.primary_node: @@ -3992,11 +6938,9 @@ class LUQueryInstanceData(NoHooksLU): else: snode = dev.logical_id[0] - if snode: - self.cfg.SetDiskID(dev, snode) - dev_sstatus = rpc.call_blockdev_find(snode, dev) - else: - dev_sstatus = None + dev_pstatus = self._ComputeBlockdevStatus(instance.primary_node, + instance.name, dev) + dev_sstatus = self._ComputeBlockdevStatus(snode, instance.name, dev) if dev.children: dev_children = [self._ComputeDiskStatus(instance, snode, child) @@ -4012,6 +6956,8 @@ class LUQueryInstanceData(NoHooksLU): "pstatus": dev_pstatus, "sstatus": dev_sstatus, "children": dev_children, + "mode": dev.mode, + "size": dev.size, } return data @@ -4019,17 +6965,26 @@ class LUQueryInstanceData(NoHooksLU): def Exec(self, feedback_fn): """Gather and return data""" result = {} + + cluster = self.cfg.GetClusterInfo() + for instance in self.wanted_instances: - remote_info = rpc.call_instance_info(instance.primary_node, - instance.name) - if remote_info and "state" in remote_info: - remote_state = "up" - else: - remote_state = "down" - if instance.status == "down": - config_state = "down" + if not self.op.static: + remote_info = self.rpc.call_instance_info(instance.primary_node, + instance.name, + instance.hypervisor) + remote_info.Raise("Error checking node %s" % instance.primary_node) + remote_info = remote_info.payload + if remote_info and "state" in remote_info: + remote_state = "up" + else: + remote_state = "down" else: + remote_state = None + if instance.admin_up: config_state = "up" + else: + config_state = "down" disks = [self._ComputeDiskStatus(instance, None, device) for device in instance.disks] @@ -4041,27 +6996,21 @@ class LUQueryInstanceData(NoHooksLU): "pnode": instance.primary_node, "snodes": instance.secondary_nodes, "os": instance.os, - "memory": instance.memory, - "nics": [(nic.mac, nic.ip, nic.bridge) for nic in instance.nics], + # this happens to be the same format used for hooks + "nics": _NICListToTuple(self, instance.nics), "disks": disks, - "vcpus": instance.vcpus, + "hypervisor": instance.hypervisor, + "network_port": instance.network_port, + "hv_instance": instance.hvparams, + "hv_actual": cluster.FillHV(instance), + "be_instance": instance.beparams, + "be_actual": cluster.FillBE(instance), + "serial_no": instance.serial_no, + "mtime": instance.mtime, + "ctime": instance.ctime, + "uuid": instance.uuid, } - htkind = self.sstore.GetHypervisorType() - if htkind == constants.HT_XEN_PVM30: - idict["kernel_path"] = instance.kernel_path - idict["initrd_path"] = instance.initrd_path - - if htkind == constants.HT_XEN_HVM31: - idict["hvm_boot_order"] = instance.hvm_boot_order - idict["hvm_acpi"] = instance.hvm_acpi - idict["hvm_pae"] = instance.hvm_pae - idict["hvm_cdrom_image_path"] = instance.hvm_cdrom_image_path - - if htkind in constants.HTS_REQ_PORT: - idict["vnc_bind_address"] = instance.vnc_bind_address - idict["network_port"] = instance.network_port - result[instance.name] = idict return result @@ -4074,6 +7023,120 @@ class LUSetInstanceParams(LogicalUnit): HPATH = "instance-modify" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name"] + REQ_BGL = False + + def CheckArguments(self): + if not hasattr(self.op, 'nics'): + self.op.nics = [] + if not hasattr(self.op, 'disks'): + self.op.disks = [] + if not hasattr(self.op, 'beparams'): + self.op.beparams = {} + if not hasattr(self.op, 'hvparams'): + self.op.hvparams = {} + self.op.force = getattr(self.op, "force", False) + if not (self.op.nics or self.op.disks or + self.op.hvparams or self.op.beparams): + raise errors.OpPrereqError("No changes submitted") + + # Disk validation + disk_addremove = 0 + for disk_op, disk_dict in self.op.disks: + if disk_op == constants.DDM_REMOVE: + disk_addremove += 1 + continue + elif disk_op == constants.DDM_ADD: + disk_addremove += 1 + else: + if not isinstance(disk_op, int): + raise errors.OpPrereqError("Invalid disk index") + if not isinstance(disk_dict, dict): + msg = "Invalid disk value: expected dict, got '%s'" % disk_dict + raise errors.OpPrereqError(msg) + + if disk_op == constants.DDM_ADD: + mode = disk_dict.setdefault('mode', constants.DISK_RDWR) + if mode not in constants.DISK_ACCESS_SET: + raise errors.OpPrereqError("Invalid disk access mode '%s'" % mode) + size = disk_dict.get('size', None) + if size is None: + raise errors.OpPrereqError("Required disk parameter size missing") + try: + size = int(size) + except ValueError, err: + raise errors.OpPrereqError("Invalid disk size parameter: %s" % + str(err)) + disk_dict['size'] = size + else: + # modification of disk + if 'size' in disk_dict: + raise errors.OpPrereqError("Disk size change not possible, use" + " grow-disk") + + if disk_addremove > 1: + raise errors.OpPrereqError("Only one disk add or remove operation" + " supported at a time") + + # NIC validation + nic_addremove = 0 + for nic_op, nic_dict in self.op.nics: + if nic_op == constants.DDM_REMOVE: + nic_addremove += 1 + continue + elif nic_op == constants.DDM_ADD: + nic_addremove += 1 + else: + if not isinstance(nic_op, int): + raise errors.OpPrereqError("Invalid nic index") + if not isinstance(nic_dict, dict): + msg = "Invalid nic value: expected dict, got '%s'" % nic_dict + raise errors.OpPrereqError(msg) + + # nic_dict should be a dict + nic_ip = nic_dict.get('ip', None) + if nic_ip is not None: + if nic_ip.lower() == constants.VALUE_NONE: + nic_dict['ip'] = None + else: + if not utils.IsValidIP(nic_ip): + raise errors.OpPrereqError("Invalid IP address '%s'" % nic_ip) + + nic_bridge = nic_dict.get('bridge', None) + nic_link = nic_dict.get('link', None) + if nic_bridge and nic_link: + raise errors.OpPrereqError("Cannot pass 'bridge' and 'link'" + " at the same time") + elif nic_bridge and nic_bridge.lower() == constants.VALUE_NONE: + nic_dict['bridge'] = None + elif nic_link and nic_link.lower() == constants.VALUE_NONE: + nic_dict['link'] = None + + if nic_op == constants.DDM_ADD: + nic_mac = nic_dict.get('mac', None) + if nic_mac is None: + nic_dict['mac'] = constants.VALUE_AUTO + + if 'mac' in nic_dict: + nic_mac = nic_dict['mac'] + if nic_mac not in (constants.VALUE_AUTO, constants.VALUE_GENERATE): + if not utils.IsValidMac(nic_mac): + raise errors.OpPrereqError("Invalid MAC address %s" % nic_mac) + if nic_op != constants.DDM_ADD and nic_mac == constants.VALUE_AUTO: + raise errors.OpPrereqError("'auto' is not a valid MAC address when" + " modifying an existing nic") + + if nic_addremove > 1: + raise errors.OpPrereqError("Only one NIC add or remove operation" + " supported at a time") + + def ExpandNames(self): + self._ExpandAndLockInstance() + self.needed_locks[locking.LEVEL_NODE] = [] + self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE + + def DeclareLocks(self, level): + if level == locking.LEVEL_NODE: + self._LockInstancesNodes() def BuildHooksEnv(self): """Build hooks env. @@ -4082,186 +7145,382 @@ class LUSetInstanceParams(LogicalUnit): """ args = dict() - if self.mem: - args['memory'] = self.mem - if self.vcpus: - args['vcpus'] = self.vcpus - if self.do_ip or self.do_bridge or self.mac: - if self.do_ip: - ip = self.ip - else: - ip = self.instance.nics[0].ip - if self.bridge: - bridge = self.bridge - else: - bridge = self.instance.nics[0].bridge - if self.mac: - mac = self.mac - else: - mac = self.instance.nics[0].mac - args['nics'] = [(ip, bridge, mac)] - env = _BuildInstanceHookEnvByObject(self.instance, override=args) - nl = [self.sstore.GetMasterNode(), - self.instance.primary_node] + list(self.instance.secondary_nodes) + if constants.BE_MEMORY in self.be_new: + args['memory'] = self.be_new[constants.BE_MEMORY] + if constants.BE_VCPUS in self.be_new: + args['vcpus'] = self.be_new[constants.BE_VCPUS] + # TODO: export disk changes. Note: _BuildInstanceHookEnv* don't export disk + # information at all. + if self.op.nics: + args['nics'] = [] + nic_override = dict(self.op.nics) + c_nicparams = self.cluster.nicparams[constants.PP_DEFAULT] + for idx, nic in enumerate(self.instance.nics): + if idx in nic_override: + this_nic_override = nic_override[idx] + else: + this_nic_override = {} + if 'ip' in this_nic_override: + ip = this_nic_override['ip'] + else: + ip = nic.ip + if 'mac' in this_nic_override: + mac = this_nic_override['mac'] + else: + mac = nic.mac + if idx in self.nic_pnew: + nicparams = self.nic_pnew[idx] + else: + nicparams = objects.FillDict(c_nicparams, nic.nicparams) + mode = nicparams[constants.NIC_MODE] + link = nicparams[constants.NIC_LINK] + args['nics'].append((ip, mac, mode, link)) + if constants.DDM_ADD in nic_override: + ip = nic_override[constants.DDM_ADD].get('ip', None) + mac = nic_override[constants.DDM_ADD]['mac'] + nicparams = self.nic_pnew[constants.DDM_ADD] + mode = nicparams[constants.NIC_MODE] + link = nicparams[constants.NIC_LINK] + args['nics'].append((ip, mac, mode, link)) + elif constants.DDM_REMOVE in nic_override: + del args['nics'][-1] + + env = _BuildInstanceHookEnvByObject(self, self.instance, override=args) + nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) return env, nl, nl + def _GetUpdatedParams(self, old_params, update_dict, + default_values, parameter_types): + """Return the new params dict for the given params. + + @type old_params: dict + @param old_params: old parameters + @type update_dict: dict + @param update_dict: dict containing new parameter values, + or constants.VALUE_DEFAULT to reset the + parameter to its default value + @type default_values: dict + @param default_values: default values for the filled parameters + @type parameter_types: dict + @param parameter_types: dict mapping target dict keys to types + in constants.ENFORCEABLE_TYPES + @rtype: (dict, dict) + @return: (new_parameters, filled_parameters) + + """ + params_copy = copy.deepcopy(old_params) + for key, val in update_dict.iteritems(): + if val == constants.VALUE_DEFAULT: + try: + del params_copy[key] + except KeyError: + pass + else: + params_copy[key] = val + utils.ForceDictType(params_copy, parameter_types) + params_filled = objects.FillDict(default_values, params_copy) + return (params_copy, params_filled) + def CheckPrereq(self): """Check prerequisites. This only checks the instance list against the existing names. """ - self.mem = getattr(self.op, "mem", None) - self.vcpus = getattr(self.op, "vcpus", None) - self.ip = getattr(self.op, "ip", None) - self.mac = getattr(self.op, "mac", None) - self.bridge = getattr(self.op, "bridge", None) - self.kernel_path = getattr(self.op, "kernel_path", None) - self.initrd_path = getattr(self.op, "initrd_path", None) - self.hvm_boot_order = getattr(self.op, "hvm_boot_order", None) - self.hvm_acpi = getattr(self.op, "hvm_acpi", None) - self.hvm_pae = getattr(self.op, "hvm_pae", None) - self.hvm_cdrom_image_path = getattr(self.op, "hvm_cdrom_image_path", None) - self.vnc_bind_address = getattr(self.op, "vnc_bind_address", None) - all_parms = [self.mem, self.vcpus, self.ip, self.bridge, self.mac, - self.kernel_path, self.initrd_path, self.hvm_boot_order, - self.hvm_acpi, self.hvm_pae, self.hvm_cdrom_image_path, - self.vnc_bind_address] - if all_parms.count(None) == len(all_parms): - raise errors.OpPrereqError("No changes submitted") - if self.mem is not None: - try: - self.mem = int(self.mem) - except ValueError, err: - raise errors.OpPrereqError("Invalid memory size: %s" % str(err)) - if self.vcpus is not None: - try: - self.vcpus = int(self.vcpus) - except ValueError, err: - raise errors.OpPrereqError("Invalid vcpus number: %s" % str(err)) - if self.ip is not None: - self.do_ip = True - if self.ip.lower() == "none": - self.ip = None - else: - if not utils.IsValidIP(self.ip): - raise errors.OpPrereqError("Invalid IP address '%s'." % self.ip) - else: - self.do_ip = False - self.do_bridge = (self.bridge is not None) - if self.mac is not None: - if self.cfg.IsMacInUse(self.mac): - raise errors.OpPrereqError('MAC address %s already in use in cluster' % - self.mac) - if not utils.IsValidMac(self.mac): - raise errors.OpPrereqError('Invalid MAC address %s' % self.mac) - - if self.kernel_path is not None: - self.do_kernel_path = True - if self.kernel_path == constants.VALUE_NONE: - raise errors.OpPrereqError("Can't set instance to no kernel") - - if self.kernel_path != constants.VALUE_DEFAULT: - if not os.path.isabs(self.kernel_path): - raise errors.OpPrereqError("The kernel path must be an absolute" - " filename") + self.force = self.op.force + + # checking the new params on the primary/secondary nodes + + instance = self.instance = self.cfg.GetInstanceInfo(self.op.instance_name) + cluster = self.cluster = self.cfg.GetClusterInfo() + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + pnode = instance.primary_node + nodelist = list(instance.all_nodes) + + # hvparams processing + if self.op.hvparams: + i_hvdict, hv_new = self._GetUpdatedParams( + instance.hvparams, self.op.hvparams, + cluster.hvparams[instance.hypervisor], + constants.HVS_PARAMETER_TYPES) + # local check + hypervisor.GetHypervisor( + instance.hypervisor).CheckParameterSyntax(hv_new) + _CheckHVParams(self, nodelist, instance.hypervisor, hv_new) + self.hv_new = hv_new # the new actual values + self.hv_inst = i_hvdict # the new dict (without defaults) else: - self.do_kernel_path = False - - if self.initrd_path is not None: - self.do_initrd_path = True - if self.initrd_path not in (constants.VALUE_NONE, - constants.VALUE_DEFAULT): - if not os.path.isabs(self.initrd_path): - raise errors.OpPrereqError("The initrd path must be an absolute" - " filename") + self.hv_new = self.hv_inst = {} + + # beparams processing + if self.op.beparams: + i_bedict, be_new = self._GetUpdatedParams( + instance.beparams, self.op.beparams, + cluster.beparams[constants.PP_DEFAULT], + constants.BES_PARAMETER_TYPES) + self.be_new = be_new # the new actual values + self.be_inst = i_bedict # the new dict (without defaults) else: - self.do_initrd_path = False - - # boot order verification - if self.hvm_boot_order is not None: - if self.hvm_boot_order != constants.VALUE_DEFAULT: - if len(self.hvm_boot_order.strip("acdn")) != 0: - raise errors.OpPrereqError("invalid boot order specified," - " must be one or more of [acdn]" - " or 'default'") - - # hvm_cdrom_image_path verification - if self.op.hvm_cdrom_image_path is not None: - if not os.path.isabs(self.op.hvm_cdrom_image_path): - raise errors.OpPrereqError("The path to the HVM CDROM image must" - " be an absolute path or None, not %s" % - self.op.hvm_cdrom_image_path) - if not os.path.isfile(self.op.hvm_cdrom_image_path): - raise errors.OpPrereqError("The HVM CDROM image must either be a" - " regular file or a symlink pointing to" - " an existing regular file, not %s" % - self.op.hvm_cdrom_image_path) - - # vnc_bind_address verification - if self.op.vnc_bind_address is not None: - if not utils.IsValidIP(self.op.vnc_bind_address): - raise errors.OpPrereqError("given VNC bind address '%s' doesn't look" - " like a valid IP address" % - self.op.vnc_bind_address) + self.be_new = self.be_inst = {} + + self.warn = [] + + if constants.BE_MEMORY in self.op.beparams and not self.force: + mem_check_list = [pnode] + if be_new[constants.BE_AUTO_BALANCE]: + # either we changed auto_balance to yes or it was from before + mem_check_list.extend(instance.secondary_nodes) + instance_info = self.rpc.call_instance_info(pnode, instance.name, + instance.hypervisor) + nodeinfo = self.rpc.call_node_info(mem_check_list, self.cfg.GetVGName(), + instance.hypervisor) + pninfo = nodeinfo[pnode] + msg = pninfo.fail_msg + if msg: + # Assume the primary node is unreachable and go ahead + self.warn.append("Can't get info from primary node %s: %s" % + (pnode, msg)) + elif not isinstance(pninfo.payload.get('memory_free', None), int): + self.warn.append("Node data from primary node %s doesn't contain" + " free memory information" % pnode) + elif instance_info.fail_msg: + self.warn.append("Can't get instance runtime information: %s" % + instance_info.fail_msg) + else: + if instance_info.payload: + current_mem = int(instance_info.payload['memory']) + else: + # Assume instance not running + # (there is a slight race condition here, but it's not very probable, + # and we have no other way to check) + current_mem = 0 + miss_mem = (be_new[constants.BE_MEMORY] - current_mem - + pninfo.payload['memory_free']) + if miss_mem > 0: + raise errors.OpPrereqError("This change will prevent the instance" + " from starting, due to %d MB of memory" + " missing on its primary node" % miss_mem) + + if be_new[constants.BE_AUTO_BALANCE]: + for node, nres in nodeinfo.items(): + if node not in instance.secondary_nodes: + continue + msg = nres.fail_msg + if msg: + self.warn.append("Can't get info from secondary node %s: %s" % + (node, msg)) + elif not isinstance(nres.payload.get('memory_free', None), int): + self.warn.append("Secondary node %s didn't return free" + " memory information" % node) + elif be_new[constants.BE_MEMORY] > nres.payload['memory_free']: + self.warn.append("Not enough memory to failover instance to" + " secondary node %s" % node) + + # NIC processing + self.nic_pnew = {} + self.nic_pinst = {} + for nic_op, nic_dict in self.op.nics: + if nic_op == constants.DDM_REMOVE: + if not instance.nics: + raise errors.OpPrereqError("Instance has no NICs, cannot remove") + continue + if nic_op != constants.DDM_ADD: + # an existing nic + if nic_op < 0 or nic_op >= len(instance.nics): + raise errors.OpPrereqError("Invalid NIC index %s, valid values" + " are 0 to %d" % + (nic_op, len(instance.nics))) + old_nic_params = instance.nics[nic_op].nicparams + old_nic_ip = instance.nics[nic_op].ip + else: + old_nic_params = {} + old_nic_ip = None + + update_params_dict = dict([(key, nic_dict[key]) + for key in constants.NICS_PARAMETERS + if key in nic_dict]) + + if 'bridge' in nic_dict: + update_params_dict[constants.NIC_LINK] = nic_dict['bridge'] + + new_nic_params, new_filled_nic_params = \ + self._GetUpdatedParams(old_nic_params, update_params_dict, + cluster.nicparams[constants.PP_DEFAULT], + constants.NICS_PARAMETER_TYPES) + objects.NIC.CheckParameterSyntax(new_filled_nic_params) + self.nic_pinst[nic_op] = new_nic_params + self.nic_pnew[nic_op] = new_filled_nic_params + new_nic_mode = new_filled_nic_params[constants.NIC_MODE] + + if new_nic_mode == constants.NIC_MODE_BRIDGED: + nic_bridge = new_filled_nic_params[constants.NIC_LINK] + msg = self.rpc.call_bridges_exist(pnode, [nic_bridge]).fail_msg + if msg: + msg = "Error checking bridges on node %s: %s" % (pnode, msg) + if self.force: + self.warn.append(msg) + else: + raise errors.OpPrereqError(msg) + if new_nic_mode == constants.NIC_MODE_ROUTED: + if 'ip' in nic_dict: + nic_ip = nic_dict['ip'] + else: + nic_ip = old_nic_ip + if nic_ip is None: + raise errors.OpPrereqError('Cannot set the nic ip to None' + ' on a routed nic') + if 'mac' in nic_dict: + nic_mac = nic_dict['mac'] + if nic_mac is None: + raise errors.OpPrereqError('Cannot set the nic mac to None') + elif nic_mac in (constants.VALUE_AUTO, constants.VALUE_GENERATE): + # otherwise generate the mac + nic_dict['mac'] = self.cfg.GenerateMAC() + else: + # or validate/reserve the current one + if self.cfg.IsMacInUse(nic_mac): + raise errors.OpPrereqError("MAC address %s already in use" + " in cluster" % nic_mac) + + # DISK processing + if self.op.disks and instance.disk_template == constants.DT_DISKLESS: + raise errors.OpPrereqError("Disk operations not supported for" + " diskless instances") + for disk_op, disk_dict in self.op.disks: + if disk_op == constants.DDM_REMOVE: + if len(instance.disks) == 1: + raise errors.OpPrereqError("Cannot remove the last disk of" + " an instance") + ins_l = self.rpc.call_instance_list([pnode], [instance.hypervisor]) + ins_l = ins_l[pnode] + msg = ins_l.fail_msg + if msg: + raise errors.OpPrereqError("Can't contact node %s: %s" % + (pnode, msg)) + if instance.name in ins_l.payload: + raise errors.OpPrereqError("Instance is running, can't remove" + " disks.") + + if (disk_op == constants.DDM_ADD and + len(instance.nics) >= constants.MAX_DISKS): + raise errors.OpPrereqError("Instance has too many disks (%d), cannot" + " add more" % constants.MAX_DISKS) + if disk_op not in (constants.DDM_ADD, constants.DDM_REMOVE): + # an existing disk + if disk_op < 0 or disk_op >= len(instance.disks): + raise errors.OpPrereqError("Invalid disk index %s, valid values" + " are 0 to %d" % + (disk_op, len(instance.disks))) - instance = self.cfg.GetInstanceInfo( - self.cfg.ExpandInstanceName(self.op.instance_name)) - if instance is None: - raise errors.OpPrereqError("No such instance name '%s'" % - self.op.instance_name) - self.op.instance_name = instance.name - self.instance = instance return def Exec(self, feedback_fn): """Modifies an instance. All parameters take effect only at the next restart of the instance. + """ + # Process here the warnings from CheckPrereq, as we don't have a + # feedback_fn there. + for warn in self.warn: + feedback_fn("WARNING: %s" % warn) + result = [] instance = self.instance - if self.mem: - instance.memory = self.mem - result.append(("mem", self.mem)) - if self.vcpus: - instance.vcpus = self.vcpus - result.append(("vcpus", self.vcpus)) - if self.do_ip: - instance.nics[0].ip = self.ip - result.append(("ip", self.ip)) - if self.bridge: - instance.nics[0].bridge = self.bridge - result.append(("bridge", self.bridge)) - if self.mac: - instance.nics[0].mac = self.mac - result.append(("mac", self.mac)) - if self.do_kernel_path: - instance.kernel_path = self.kernel_path - result.append(("kernel_path", self.kernel_path)) - if self.do_initrd_path: - instance.initrd_path = self.initrd_path - result.append(("initrd_path", self.initrd_path)) - if self.hvm_boot_order: - if self.hvm_boot_order == constants.VALUE_DEFAULT: - instance.hvm_boot_order = None + cluster = self.cluster + # disk changes + for disk_op, disk_dict in self.op.disks: + if disk_op == constants.DDM_REMOVE: + # remove the last disk + device = instance.disks.pop() + device_idx = len(instance.disks) + for node, disk in device.ComputeNodeTree(instance.primary_node): + self.cfg.SetDiskID(disk, node) + msg = self.rpc.call_blockdev_remove(node, disk).fail_msg + if msg: + self.LogWarning("Could not remove disk/%d on node %s: %s," + " continuing anyway", device_idx, node, msg) + result.append(("disk/%d" % device_idx, "remove")) + elif disk_op == constants.DDM_ADD: + # add a new disk + if instance.disk_template == constants.DT_FILE: + file_driver, file_path = instance.disks[0].logical_id + file_path = os.path.dirname(file_path) + else: + file_driver = file_path = None + disk_idx_base = len(instance.disks) + new_disk = _GenerateDiskTemplate(self, + instance.disk_template, + instance.name, instance.primary_node, + instance.secondary_nodes, + [disk_dict], + file_path, + file_driver, + disk_idx_base)[0] + instance.disks.append(new_disk) + info = _GetInstanceInfoText(instance) + + logging.info("Creating volume %s for instance %s", + new_disk.iv_name, instance.name) + # Note: this needs to be kept in sync with _CreateDisks + #HARDCODE + for node in instance.all_nodes: + f_create = node == instance.primary_node + try: + _CreateBlockDev(self, node, instance, new_disk, + f_create, info, f_create) + except errors.OpExecError, err: + self.LogWarning("Failed to create volume %s (%s) on" + " node %s: %s", + new_disk.iv_name, new_disk, node, err) + result.append(("disk/%d" % disk_idx_base, "add:size=%s,mode=%s" % + (new_disk.size, new_disk.mode))) else: - instance.hvm_boot_order = self.hvm_boot_order - result.append(("hvm_boot_order", self.hvm_boot_order)) - if self.hvm_acpi: - instance.hvm_acpi = self.hvm_acpi - result.append(("hvm_acpi", self.hvm_acpi)) - if self.hvm_pae: - instance.hvm_pae = self.hvm_pae - result.append(("hvm_pae", self.hvm_pae)) - if self.hvm_cdrom_image_path: - instance.hvm_cdrom_image_path = self.hvm_cdrom_image_path - result.append(("hvm_cdrom_image_path", self.hvm_cdrom_image_path)) - if self.vnc_bind_address: - instance.vnc_bind_address = self.vnc_bind_address - result.append(("vnc_bind_address", self.vnc_bind_address)) - - self.cfg.AddInstance(instance) + # change a given disk + instance.disks[disk_op].mode = disk_dict['mode'] + result.append(("disk.mode/%d" % disk_op, disk_dict['mode'])) + # NIC changes + for nic_op, nic_dict in self.op.nics: + if nic_op == constants.DDM_REMOVE: + # remove the last nic + del instance.nics[-1] + result.append(("nic.%d" % len(instance.nics), "remove")) + elif nic_op == constants.DDM_ADD: + # mac and bridge should be set, by now + mac = nic_dict['mac'] + ip = nic_dict.get('ip', None) + nicparams = self.nic_pinst[constants.DDM_ADD] + new_nic = objects.NIC(mac=mac, ip=ip, nicparams=nicparams) + instance.nics.append(new_nic) + result.append(("nic.%d" % (len(instance.nics) - 1), + "add:mac=%s,ip=%s,mode=%s,link=%s" % + (new_nic.mac, new_nic.ip, + self.nic_pnew[constants.DDM_ADD][constants.NIC_MODE], + self.nic_pnew[constants.DDM_ADD][constants.NIC_LINK] + ))) + else: + for key in 'mac', 'ip': + if key in nic_dict: + setattr(instance.nics[nic_op], key, nic_dict[key]) + if nic_op in self.nic_pnew: + instance.nics[nic_op].nicparams = self.nic_pnew[nic_op] + for key, val in nic_dict.iteritems(): + result.append(("nic.%s/%d" % (key, nic_op), val)) + + # hvparams changes + if self.op.hvparams: + instance.hvparams = self.hv_inst + for key, val in self.op.hvparams.iteritems(): + result.append(("hv/%s" % key, val)) + + # beparams changes + if self.op.beparams: + instance.beparams = self.be_inst + for key, val in self.op.beparams.iteritems(): + result.append(("be/%s" % key, val)) + + self.cfg.Update(instance) return result @@ -4270,24 +7529,42 @@ class LUQueryExports(NoHooksLU): """Query the exports list """ - _OP_REQP = [] + _OP_REQP = ['nodes'] + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = {} + self.share_locks[locking.LEVEL_NODE] = 1 + if not self.op.nodes: + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + else: + self.needed_locks[locking.LEVEL_NODE] = \ + _GetWantedNodes(self, self.op.nodes) def CheckPrereq(self): - """Check that the nodelist contains only existing nodes. + """Check prerequisites. """ - self.nodes = _GetWantedNodes(self, getattr(self.op, "nodes", None)) + self.nodes = self.acquired_locks[locking.LEVEL_NODE] def Exec(self, feedback_fn): """Compute the list of all the exported system images. - Returns: - a dictionary with the structure node->(export-list) - where export-list is a list of the instances exported on - that node. + @rtype: dict + @return: a dictionary with the structure node->(export-list) + where export-list is a list of the instances exported on + that node. """ - return rpc.call_export_list(self.nodes) + rpcresult = self.rpc.call_export_list(self.nodes) + result = {} + for node in rpcresult: + if rpcresult[node].fail_msg: + result[node] = False + else: + result[node] = rpcresult[node].payload + + return result class LUExportInstance(LogicalUnit): @@ -4297,6 +7574,23 @@ class LUExportInstance(LogicalUnit): HPATH = "instance-export" HTYPE = constants.HTYPE_INSTANCE _OP_REQP = ["instance_name", "target_node", "shutdown"] + REQ_BGL = False + + def ExpandNames(self): + self._ExpandAndLockInstance() + # FIXME: lock only instance primary and destination node + # + # Sad but true, for now we have do lock all nodes, as we don't know where + # the previous export might be, and and in this LU we search for it and + # remove it from its current node. In the future we could fix this by: + # - making a tasklet to search (share-lock all), then create the new one, + # then one to remove, after + # - removing the removal operation altogether + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET + + def DeclareLocks(self, level): + """Last minute lock declaration.""" + # All nodes are locked anyway, so nothing to do here. def BuildHooksEnv(self): """Build hooks env. @@ -4308,8 +7602,8 @@ class LUExportInstance(LogicalUnit): "EXPORT_NODE": self.op.target_node, "EXPORT_DO_SHUTDOWN": self.op.shutdown, } - env.update(_BuildInstanceHookEnvByObject(self.instance)) - nl = [self.sstore.GetMasterNode(), self.instance.primary_node, + env.update(_BuildInstanceHookEnvByObject(self, self.instance)) + nl = [self.cfg.GetMasterNode(), self.instance.primary_node, self.op.target_node] return env, nl, nl @@ -4319,20 +7613,20 @@ class LUExportInstance(LogicalUnit): This checks that the instance and node names are valid. """ - instance_name = self.cfg.ExpandInstanceName(self.op.instance_name) + instance_name = self.op.instance_name self.instance = self.cfg.GetInstanceInfo(instance_name) - if self.instance is None: - raise errors.OpPrereqError("Instance '%s' not found" % - self.op.instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + _CheckNodeOnline(self, self.instance.primary_node) - # node verification - dst_node_short = self.cfg.ExpandNodeName(self.op.target_node) - self.dst_node = self.cfg.GetNodeInfo(dst_node_short) + self.dst_node = self.cfg.GetNodeInfo( + self.cfg.ExpandNodeName(self.op.target_node)) if self.dst_node is None: - raise errors.OpPrereqError("Destination node '%s' is unknown." % - self.op.target_node) - self.op.target_node = self.dst_node.name + # This is wrong node name, not a non-locked node + raise errors.OpPrereqError("Wrong node name %s" % self.op.target_node) + _CheckNodeOnline(self, self.dst_node.name) + _CheckNodeNotDrained(self, self.dst_node.name) # instance disk type verification for disk in self.instance.disks: @@ -4347,51 +7641,84 @@ class LUExportInstance(LogicalUnit): instance = self.instance dst_node = self.dst_node src_node = instance.primary_node + if self.op.shutdown: # shutdown the instance, but not the disks - if not rpc.call_instance_shutdown(src_node, instance): - raise errors.OpExecError("Could not shutdown instance %s on node %s" % - (instance.name, src_node)) + feedback_fn("Shutting down instance %s" % instance.name) + result = self.rpc.call_instance_shutdown(src_node, instance) + result.Raise("Could not shutdown instance %s on" + " node %s" % (instance.name, src_node)) vgname = self.cfg.GetVGName() snap_disks = [] - try: - for disk in instance.disks: - if disk.iv_name == "sda": - # new_dev_name will be a snapshot of an lvm leaf of the one we passed - new_dev_name = rpc.call_blockdev_snapshot(src_node, disk) + # set the disks ID correctly since call_instance_start needs the + # correct drbd minor to create the symlinks + for disk in instance.disks: + self.cfg.SetDiskID(disk, src_node) - if not new_dev_name: - logger.Error("could not snapshot block device %s on node %s" % - (disk.logical_id[1], src_node)) - else: - new_dev = objects.Disk(dev_type=constants.LD_LV, size=disk.size, - logical_id=(vgname, new_dev_name), - physical_id=(vgname, new_dev_name), - iv_name=disk.iv_name) - snap_disks.append(new_dev) + # per-disk results + dresults = [] + try: + for idx, disk in enumerate(instance.disks): + feedback_fn("Creating a snapshot of disk/%s on node %s" % + (idx, src_node)) + + # result.payload will be a snapshot of an lvm leaf of the one we passed + result = self.rpc.call_blockdev_snapshot(src_node, disk) + msg = result.fail_msg + if msg: + self.LogWarning("Could not snapshot disk/%s on node %s: %s", + idx, src_node, msg) + snap_disks.append(False) + else: + disk_id = (vgname, result.payload) + new_dev = objects.Disk(dev_type=constants.LD_LV, size=disk.size, + logical_id=disk_id, physical_id=disk_id, + iv_name=disk.iv_name) + snap_disks.append(new_dev) finally: - if self.op.shutdown and instance.status == "up": - if not rpc.call_instance_start(src_node, instance, None): - _ShutdownInstanceDisks(instance, self.cfg) - raise errors.OpExecError("Could not start instance") + if self.op.shutdown and instance.admin_up: + feedback_fn("Starting instance %s" % instance.name) + result = self.rpc.call_instance_start(src_node, instance, None, None) + msg = result.fail_msg + if msg: + _ShutdownInstanceDisks(self, instance) + raise errors.OpExecError("Could not start instance: %s" % msg) # TODO: check for size - for dev in snap_disks: - if not rpc.call_snapshot_export(src_node, dev, dst_node.name, instance): - logger.Error("could not export block device %s from node %s to node %s" - % (dev.logical_id[1], src_node, dst_node.name)) - if not rpc.call_blockdev_remove(src_node, dev): - logger.Error("could not remove snapshot block device %s from node %s" % - (dev.logical_id[1], src_node)) + cluster_name = self.cfg.GetClusterName() + for idx, dev in enumerate(snap_disks): + feedback_fn("Exporting snapshot %s from %s to %s" % + (idx, src_node, dst_node.name)) + if dev: + result = self.rpc.call_snapshot_export(src_node, dev, dst_node.name, + instance, cluster_name, idx) + msg = result.fail_msg + if msg: + self.LogWarning("Could not export disk/%s from node %s to" + " node %s: %s", idx, src_node, dst_node.name, msg) + dresults.append(False) + else: + dresults.append(True) + msg = self.rpc.call_blockdev_remove(src_node, dev).fail_msg + if msg: + self.LogWarning("Could not remove snapshot for disk/%d from node" + " %s: %s", idx, src_node, msg) + else: + dresults.append(False) - if not rpc.call_finalize_export(dst_node.name, instance, snap_disks): - logger.Error("could not finalize export for instance %s on node %s" % - (instance.name, dst_node.name)) + feedback_fn("Finalizing export on %s" % dst_node.name) + result = self.rpc.call_finalize_export(dst_node.name, instance, snap_disks) + fin_resu = True + msg = result.fail_msg + if msg: + self.LogWarning("Could not finalize export for instance %s" + " on node %s: %s", instance.name, dst_node.name, msg) + fin_resu = False nodelist = self.cfg.GetNodeList() nodelist.remove(dst_node.name) @@ -4399,14 +7726,19 @@ class LUExportInstance(LogicalUnit): # on one-node clusters nodelist will be empty after the removal # if we proceed the backup would be removed because OpQueryExports # substitutes an empty list with the full cluster node list. + iname = instance.name if nodelist: - op = opcodes.OpQueryExports(nodes=nodelist) - exportlist = self.proc.ChainOpCode(op) + feedback_fn("Removing old exports for instance %s" % iname) + exportlist = self.rpc.call_export_list(nodelist) for node in exportlist: - if instance.name in exportlist[node]: - if not rpc.call_export_remove(node, instance.name): - logger.Error("could not remove older export for instance %s" - " on node %s" % (instance.name, node)) + if exportlist[node].fail_msg: + continue + if iname in exportlist[node].payload: + msg = self.rpc.call_export_remove(node, iname).fail_msg + if msg: + self.LogWarning("Could not remove older export for instance %s" + " on node %s: %s", iname, node, msg) + return fin_resu, dresults class LURemoveExport(NoHooksLU): @@ -4414,6 +7746,14 @@ class LURemoveExport(NoHooksLU): """ _OP_REQP = ["instance_name"] + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = {} + # We need all nodes to be locked in order for RemoveExport to work, but we + # don't need to lock the instance itself, as nothing will happen to it (and + # we can remove exports also for a removed instance) + self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET def CheckPrereq(self): """Check prerequisites. @@ -4432,15 +7772,21 @@ class LURemoveExport(NoHooksLU): fqdn_warn = True instance_name = self.op.instance_name - op = opcodes.OpQueryExports(nodes=[]) - exportlist = self.proc.ChainOpCode(op) + locked_nodes = self.acquired_locks[locking.LEVEL_NODE] + exportlist = self.rpc.call_export_list(locked_nodes) found = False for node in exportlist: - if instance_name in exportlist[node]: + msg = exportlist[node].fail_msg + if msg: + self.LogWarning("Failed to query node %s (continuing): %s", node, msg) + continue + if instance_name in exportlist[node].payload: found = True - if not rpc.call_export_remove(node, instance_name): - logger.Error("could not remove export for instance %s" - " on node %s" % (instance_name, node)) + result = self.rpc.call_export_remove(node, instance_name) + msg = result.fail_msg + if msg: + logging.error("Could not remove export for instance %s" + " on node %s: %s", instance_name, node, msg) if fqdn_warn and not found: feedback_fn("Export not found. If trying to remove an export belonging" @@ -4454,26 +7800,34 @@ class TagsLU(NoHooksLU): This is an abstract class which is the parent of all the other tags LUs. """ - def CheckPrereq(self): - """Check prerequisites. - """ - if self.op.kind == constants.TAG_CLUSTER: - self.target = self.cfg.GetClusterInfo() - elif self.op.kind == constants.TAG_NODE: + def ExpandNames(self): + self.needed_locks = {} + if self.op.kind == constants.TAG_NODE: name = self.cfg.ExpandNodeName(self.op.name) if name is None: raise errors.OpPrereqError("Invalid node name (%s)" % (self.op.name,)) self.op.name = name - self.target = self.cfg.GetNodeInfo(name) + self.needed_locks[locking.LEVEL_NODE] = name elif self.op.kind == constants.TAG_INSTANCE: name = self.cfg.ExpandInstanceName(self.op.name) if name is None: raise errors.OpPrereqError("Invalid instance name (%s)" % (self.op.name,)) self.op.name = name - self.target = self.cfg.GetInstanceInfo(name) + self.needed_locks[locking.LEVEL_INSTANCE] = name + + def CheckPrereq(self): + """Check prerequisites. + + """ + if self.op.kind == constants.TAG_CLUSTER: + self.target = self.cfg.GetClusterInfo() + elif self.op.kind == constants.TAG_NODE: + self.target = self.cfg.GetNodeInfo(self.op.name) + elif self.op.kind == constants.TAG_INSTANCE: + self.target = self.cfg.GetInstanceInfo(self.op.name) else: raise errors.OpPrereqError("Wrong tag type requested (%s)" % str(self.op.kind)) @@ -4484,12 +7838,13 @@ class LUGetTags(TagsLU): """ _OP_REQP = ["kind", "name"] + REQ_BGL = False def Exec(self, feedback_fn): """Returns the tag list. """ - return self.target.GetTags() + return list(self.target.GetTags()) class LUSearchTags(NoHooksLU): @@ -4497,6 +7852,10 @@ class LUSearchTags(NoHooksLU): """ _OP_REQP = ["pattern"] + REQ_BGL = False + + def ExpandNames(self): + self.needed_locks = {} def CheckPrereq(self): """Check prerequisites. @@ -4516,9 +7875,9 @@ class LUSearchTags(NoHooksLU): """ cfg = self.cfg tgts = [("/cluster", cfg.GetClusterInfo())] - ilist = [cfg.GetInstanceInfo(name) for name in cfg.GetInstanceList()] + ilist = cfg.GetAllInstancesInfo().values() tgts.extend([("/instances/%s" % i.name, i) for i in ilist]) - nlist = [cfg.GetNodeInfo(name) for name in cfg.GetNodeList()] + nlist = cfg.GetAllNodesInfo().values() tgts.extend([("/nodes/%s" % n.name, n) for n in nlist]) results = [] for path, target in tgts: @@ -4533,6 +7892,7 @@ class LUAddTags(TagsLU): """ _OP_REQP = ["kind", "name", "tags"] + REQ_BGL = False def CheckPrereq(self): """Check prerequisites. @@ -4566,6 +7926,7 @@ class LUDelTags(TagsLU): """ _OP_REQP = ["kind", "name", "tags"] + REQ_BGL = False def CheckPrereq(self): """Check prerequisites. @@ -4598,6 +7959,7 @@ class LUDelTags(TagsLU): " config file and the operation has been" " aborted. Please retry.") + class LUTestDelay(NoHooksLU): """Sleep for a specified amount of time. @@ -4635,20 +7997,16 @@ class LUTestDelay(NoHooksLU): if not utils.TestDelay(self.op.duration): raise errors.OpExecError("Error during master delay test") if self.op.on_nodes: - result = rpc.call_test_delay(self.op.on_nodes, self.op.duration) - if not result: - raise errors.OpExecError("Complete failure from rpc call") + result = self.rpc.call_test_delay(self.op.on_nodes, self.op.duration) for node, node_result in result.items(): - if not node_result: - raise errors.OpExecError("Failure during rpc call to node %s," - " result: %s" % (node, node_result)) + node_result.Raise("Failure during rpc call to node %s" % node) class IAllocator(object): """IAllocator framework. An IAllocator instance has three sets of attributes: - - cfg/sstore that are needed to query the cluster + - cfg that is needed to query the cluster - input data (all members of the _KEYS class attribute are required) - four buffer attributes (in|out_data|text), that represent the input (to the external script) in text and data structure format, @@ -4659,15 +8017,15 @@ class IAllocator(object): """ _ALLO_KEYS = [ "mem_size", "disks", "disk_template", - "os", "tags", "nics", "vcpus", + "os", "tags", "nics", "vcpus", "hypervisor", ] _RELO_KEYS = [ "relocate_from", ] - def __init__(self, cfg, sstore, mode, name, **kwargs): + def __init__(self, cfg, rpc, mode, name, **kwargs): self.cfg = cfg - self.sstore = sstore + self.rpc = rpc # init buffer variables self.in_text = self.out_text = self.in_data = self.out_data = None # init all input fields so that pylint is happy @@ -4675,6 +8033,7 @@ class IAllocator(object): self.name = name self.mem_size = self.disks = self.disk_template = None self.os = self.tags = self.nics = self.vcpus = None + self.hypervisor = None self.relocate_from = None # computed fields self.required_nodes = None @@ -4705,77 +8064,120 @@ class IAllocator(object): """ cfg = self.cfg + cluster_info = cfg.GetClusterInfo() # cluster data data = { - "version": 1, - "cluster_name": self.sstore.GetClusterName(), - "cluster_tags": list(cfg.GetClusterInfo().GetTags()), - "hypervisor_type": self.sstore.GetHypervisorType(), + "version": constants.IALLOCATOR_VERSION, + "cluster_name": cfg.GetClusterName(), + "cluster_tags": list(cluster_info.GetTags()), + "enabled_hypervisors": list(cluster_info.enabled_hypervisors), # we don't have job IDs } - - i_list = [cfg.GetInstanceInfo(iname) for iname in cfg.GetInstanceList()] + iinfo = cfg.GetAllInstancesInfo().values() + i_list = [(inst, cluster_info.FillBE(inst)) for inst in iinfo] # node data node_results = {} node_list = cfg.GetNodeList() - node_data = rpc.call_node_info(node_list, cfg.GetVGName()) - for nname in node_list: + + if self.mode == constants.IALLOCATOR_MODE_ALLOC: + hypervisor_name = self.hypervisor + elif self.mode == constants.IALLOCATOR_MODE_RELOC: + hypervisor_name = cfg.GetInstanceInfo(self.name).hypervisor + + node_data = self.rpc.call_node_info(node_list, cfg.GetVGName(), + hypervisor_name) + node_iinfo = \ + self.rpc.call_all_instances_info(node_list, + cluster_info.enabled_hypervisors) + for nname, nresult in node_data.items(): + # first fill in static (config-based) values ninfo = cfg.GetNodeInfo(nname) - if nname not in node_data or not isinstance(node_data[nname], dict): - raise errors.OpExecError("Can't get data for node %s" % nname) - remote_info = node_data[nname] - for attr in ['memory_total', 'memory_free', 'memory_dom0', - 'vg_size', 'vg_free', 'cpu_total']: - if attr not in remote_info: - raise errors.OpExecError("Node '%s' didn't return attribute '%s'" % - (nname, attr)) - try: - remote_info[attr] = int(remote_info[attr]) - except ValueError, err: - raise errors.OpExecError("Node '%s' returned invalid value for '%s':" - " %s" % (nname, attr, str(err))) - # compute memory used by primary instances - i_p_mem = i_p_up_mem = 0 - for iinfo in i_list: - if iinfo.primary_node == nname: - i_p_mem += iinfo.memory - if iinfo.status == "up": - i_p_up_mem += iinfo.memory - - # compute memory used by instances pnr = { "tags": list(ninfo.GetTags()), - "total_memory": remote_info['memory_total'], - "reserved_memory": remote_info['memory_dom0'], - "free_memory": remote_info['memory_free'], - "i_pri_memory": i_p_mem, - "i_pri_up_memory": i_p_up_mem, - "total_disk": remote_info['vg_size'], - "free_disk": remote_info['vg_free'], "primary_ip": ninfo.primary_ip, "secondary_ip": ninfo.secondary_ip, - "total_cpus": remote_info['cpu_total'], + "offline": ninfo.offline, + "drained": ninfo.drained, + "master_candidate": ninfo.master_candidate, } + + if not (ninfo.offline or ninfo.drained): + nresult.Raise("Can't get data for node %s" % nname) + node_iinfo[nname].Raise("Can't get node instance info from node %s" % + nname) + remote_info = nresult.payload + + for attr in ['memory_total', 'memory_free', 'memory_dom0', + 'vg_size', 'vg_free', 'cpu_total']: + if attr not in remote_info: + raise errors.OpExecError("Node '%s' didn't return attribute" + " '%s'" % (nname, attr)) + if not isinstance(remote_info[attr], int): + raise errors.OpExecError("Node '%s' returned invalid value" + " for '%s': %s" % + (nname, attr, remote_info[attr])) + # compute memory used by primary instances + i_p_mem = i_p_up_mem = 0 + for iinfo, beinfo in i_list: + if iinfo.primary_node == nname: + i_p_mem += beinfo[constants.BE_MEMORY] + if iinfo.name not in node_iinfo[nname].payload: + i_used_mem = 0 + else: + i_used_mem = int(node_iinfo[nname].payload[iinfo.name]['memory']) + i_mem_diff = beinfo[constants.BE_MEMORY] - i_used_mem + remote_info['memory_free'] -= max(0, i_mem_diff) + + if iinfo.admin_up: + i_p_up_mem += beinfo[constants.BE_MEMORY] + + # compute memory used by instances + pnr_dyn = { + "total_memory": remote_info['memory_total'], + "reserved_memory": remote_info['memory_dom0'], + "free_memory": remote_info['memory_free'], + "total_disk": remote_info['vg_size'], + "free_disk": remote_info['vg_free'], + "total_cpus": remote_info['cpu_total'], + "i_pri_memory": i_p_mem, + "i_pri_up_memory": i_p_up_mem, + } + pnr.update(pnr_dyn) + node_results[nname] = pnr data["nodes"] = node_results # instance data instance_data = {} - for iinfo in i_list: - nic_data = [{"mac": n.mac, "ip": n.ip, "bridge": n.bridge} - for n in iinfo.nics] + for iinfo, beinfo in i_list: + nic_data = [] + for nic in iinfo.nics: + filled_params = objects.FillDict( + cluster_info.nicparams[constants.PP_DEFAULT], + nic.nicparams) + nic_dict = {"mac": nic.mac, + "ip": nic.ip, + "mode": filled_params[constants.NIC_MODE], + "link": filled_params[constants.NIC_LINK], + } + if filled_params[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED: + nic_dict["bridge"] = filled_params[constants.NIC_LINK] + nic_data.append(nic_dict) pir = { "tags": list(iinfo.GetTags()), - "should_run": iinfo.status == "up", - "vcpus": iinfo.vcpus, - "memory": iinfo.memory, + "admin_up": iinfo.admin_up, + "vcpus": beinfo[constants.BE_VCPUS], + "memory": beinfo[constants.BE_MEMORY], "os": iinfo.os, "nodes": [iinfo.primary_node] + list(iinfo.secondary_nodes), "nics": nic_data, - "disks": [{"size": dsk.size, "mode": "w"} for dsk in iinfo.disks], + "disks": [{"size": dsk.size, "mode": dsk.mode} for dsk in iinfo.disks], "disk_template": iinfo.disk_template, + "hypervisor": iinfo.hypervisor, } + pir["disk_space_total"] = _ComputeDiskSize(iinfo.disk_template, + pir["disks"]) instance_data[iinfo.name] = pir data["instances"] = instance_data @@ -4793,11 +8195,8 @@ class IAllocator(object): """ data = self.in_data - if len(self.disks) != 2: - raise errors.OpExecError("Only two-disk configurations supported") - disk_space = _ComputeDiskSize(self.disk_template, - self.disks[0]["size"], self.disks[1]["size"]) + disk_space = _ComputeDiskSize(self.disk_template, self.disks) if self.disk_template in constants.DTS_NET_MIRROR: self.required_nodes = 2 @@ -4840,10 +8239,8 @@ class IAllocator(object): raise errors.OpPrereqError("Instance has not exactly one secondary node") self.required_nodes = 1 - - disk_space = _ComputeDiskSize(instance.disk_template, - instance.disks[0].size, - instance.disks[1].size) + disk_sizes = [{'size': disk.size} for disk in instance.disks] + disk_space = _ComputeDiskSize(instance.disk_template, disk_sizes) request = { "type": "relocate", @@ -4867,26 +8264,17 @@ class IAllocator(object): self.in_text = serializer.Dump(self.in_data) - def Run(self, name, validate=True, call_fn=rpc.call_iallocator_runner): + def Run(self, name, validate=True, call_fn=None): """Run an instance allocator and return the results. """ - data = self.in_text - - result = call_fn(self.sstore.GetMasterNode(), name, self.in_text) - - if not isinstance(result, tuple) or len(result) != 4: - raise errors.OpExecError("Invalid result from master iallocator runner") + if call_fn is None: + call_fn = self.rpc.call_iallocator_runner - rcode, stdout, stderr, fail = result + result = call_fn(self.cfg.GetMasterNode(), name, self.in_text) + result.Raise("Failure while running the iallocator script") - if rcode == constants.IARUN_NOTFOUND: - raise errors.OpExecError("Can't find allocator '%s'" % name) - elif rcode == constants.IARUN_FAILURE: - raise errors.OpExecError("Instance allocator call failed: %s," - " output: %s" % - (fail, stdout+stderr)) - self.out_text = stdout + self.out_text = result.payload if validate: self._ValidateResult() @@ -4952,8 +8340,6 @@ class LUTestAllocator(NoHooksLU): " 'nics' parameter") if not isinstance(self.op.disks, list): raise errors.OpPrereqError("Invalid parameter 'disks'") - if len(self.op.disks) != 2: - raise errors.OpPrereqError("Only two-disk configurations supported") for row in self.op.disks: if (not isinstance(row, dict) or "size" not in row or @@ -4962,6 +8348,8 @@ class LUTestAllocator(NoHooksLU): row["mode"] not in ['r', 'w']): raise errors.OpPrereqError("Invalid contents of the" " 'disks' parameter") + if not hasattr(self.op, "hypervisor") or self.op.hypervisor is None: + self.op.hypervisor = self.cfg.GetHypervisorType() elif self.op.mode == constants.IALLOCATOR_MODE_RELOC: if not hasattr(self.op, "name"): raise errors.OpPrereqError("Missing attribute 'name' on opcode input") @@ -4987,7 +8375,7 @@ class LUTestAllocator(NoHooksLU): """ if self.op.mode == constants.IALLOCATOR_MODE_ALLOC: - ial = IAllocator(self.cfg, self.sstore, + ial = IAllocator(self.cfg, self.rpc, mode=self.op.mode, name=self.op.name, mem_size=self.op.mem_size, @@ -4997,9 +8385,10 @@ class LUTestAllocator(NoHooksLU): tags=self.op.tags, nics=self.op.nics, vcpus=self.op.vcpus, + hypervisor=self.op.hypervisor, ) else: - ial = IAllocator(self.cfg, self.sstore, + ial = IAllocator(self.cfg, self.rpc, mode=self.op.mode, name=self.op.name, relocate_from=list(self.relocate_from),