diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 3208ebf52aef7bf3f88b470824c544e01c645979..e1d049fd8e12732c8b0c2a0a75994a796bc7bb28 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -4379,7 +4379,7 @@ class LUNodeRemove(LogicalUnit): raise errors.OpPrereqError("Node is the master node, failover to another" " node is required", errors.ECODE_INVAL) - for instance_name, instance in self.cfg.GetAllInstancesInfo(): + for instance_name, instance in self.cfg.GetAllInstancesInfo().items(): if node.name in instance.all_nodes: raise errors.OpPrereqError("Instance %s is still running on the node," " please remove first" % instance_name, @@ -10181,6 +10181,14 @@ class LUNodeEvacuate(NoHooksLU): locking.LEVEL_NODE: [], } + # Determine nodes (via group) optimistically, needs verification once locks + # have been acquired + self.lock_nodes = self._DetermineNodes() + + def _DetermineNodes(self): + """Gets the list of nodes to operate on. + + """ if self.op.remote_node is None: # Iallocator will choose any node(s) in the same group group_nodes = self.cfg.GetNodeGroupMembersByNodes([self.op.node_name]) @@ -10188,7 +10196,7 @@ class LUNodeEvacuate(NoHooksLU): group_nodes = frozenset([self.op.remote_node]) # Determine nodes to be locked - self.lock_nodes = set([self.op.node_name]) | group_nodes + return set([self.op.node_name]) | group_nodes def _DetermineInstances(self): """Builds list of instances to operate on. @@ -10208,6 +10216,14 @@ class LUNodeEvacuate(NoHooksLU): # All instances assert self.op.mode == constants.NODE_EVAC_ALL inst_fn = _GetNodeInstances + # TODO: In 2.6, change the iallocator interface to take an evacuation mode + # per instance + raise errors.OpPrereqError("Due to an issue with the iallocator" + " interface it is not possible to evacuate" + " all instances at once; specify explicitly" + " whether to evacuate primary or secondary" + " instances", + errors.ECODE_INVAL) return inst_fn(self.cfg, self.op.node_name) @@ -10219,8 +10235,8 @@ class LUNodeEvacuate(NoHooksLU): set(i.name for i in self._DetermineInstances()) elif level == locking.LEVEL_NODEGROUP: - # Lock node groups optimistically, needs verification once nodes have - # been acquired + # Lock node groups for all potential target nodes optimistically, needs + # verification once nodes have been acquired self.needed_locks[locking.LEVEL_NODEGROUP] = \ self.cfg.GetNodeGroupsFromNodes(self.lock_nodes) @@ -10233,12 +10249,23 @@ class LUNodeEvacuate(NoHooksLU): owned_nodes = self.owned_locks(locking.LEVEL_NODE) owned_groups = self.owned_locks(locking.LEVEL_NODEGROUP) - assert owned_nodes == self.lock_nodes + need_nodes = self._DetermineNodes() + + if not owned_nodes.issuperset(need_nodes): + raise errors.OpPrereqError("Nodes in same group as '%s' changed since" + " locks were acquired, current nodes are" + " are '%s', used to be '%s'; retry the" + " operation" % + (self.op.node_name, + utils.CommaJoin(need_nodes), + utils.CommaJoin(owned_nodes)), + errors.ECODE_STATE) wanted_groups = self.cfg.GetNodeGroupsFromNodes(owned_nodes) if owned_groups != wanted_groups: raise errors.OpExecError("Node groups changed since locks were acquired," - " current groups are '%s', used to be '%s'" % + " current groups are '%s', used to be '%s';" + " retry the operation" % (utils.CommaJoin(wanted_groups), utils.CommaJoin(owned_groups))) @@ -10249,7 +10276,7 @@ class LUNodeEvacuate(NoHooksLU): if set(self.instance_names) != owned_instances: raise errors.OpExecError("Instances on node '%s' changed since locks" " were acquired, current instances are '%s'," - " used to be '%s'" % + " used to be '%s'; retry the operation" % (self.op.node_name, utils.CommaJoin(self.instance_names), utils.CommaJoin(owned_instances))) @@ -12042,13 +12069,9 @@ class LUGroupAssignNodes(NoHooksLU): """Assign nodes to a new group. """ - for node in self.op.nodes: - self.node_data[node].group = self.group_uuid - - # FIXME: Depends on side-effects of modifying the result of - # C{cfg.GetAllNodesInfo} + mods = [(node_name, self.group_uuid) for node_name in self.op.nodes] - self.cfg.Update(self.group, feedback_fn) # Saves all modified nodes. + self.cfg.AssignGroupNodes(mods) @staticmethod def CheckAssignmentForSplitInstances(changes, node_data, instance_data): diff --git a/lib/config.py b/lib/config.py index 7a8faa19a36cb9bb5caa79b6e3ebcacb3882a089..0941e824b3d6c9794b14a782141837e7c0981864 100644 --- a/lib/config.py +++ b/lib/config.py @@ -38,6 +38,7 @@ import os import random import logging import time +import itertools from ganeti import errors from ganeti import locking @@ -1602,6 +1603,79 @@ class ConfigWriter: else: nodegroup_obj.members.remove(node.name) + @locking.ssynchronized(_config_lock) + def AssignGroupNodes(self, mods): + """Changes the group of a number of nodes. + + @type mods: list of tuples; (node name, new group UUID) + @param modes: Node membership modifications + + """ + groups = self._config_data.nodegroups + nodes = self._config_data.nodes + + resmod = [] + + # Try to resolve names/UUIDs first + for (node_name, new_group_uuid) in mods: + try: + node = nodes[node_name] + except KeyError: + raise errors.ConfigurationError("Unable to find node '%s'" % node_name) + + if node.group == new_group_uuid: + # Node is being assigned to its current group + logging.debug("Node '%s' was assigned to its current group (%s)", + node_name, node.group) + continue + + # Try to find current group of node + try: + old_group = groups[node.group] + except KeyError: + raise errors.ConfigurationError("Unable to find old group '%s'" % + node.group) + + # Try to find new group for node + try: + new_group = groups[new_group_uuid] + except KeyError: + raise errors.ConfigurationError("Unable to find new group '%s'" % + new_group_uuid) + + assert node.name in old_group.members, \ + ("Inconsistent configuration: node '%s' not listed in members for its" + " old group '%s'" % (node.name, old_group.uuid)) + assert node.name not in new_group.members, \ + ("Inconsistent configuration: node '%s' already listed in members for" + " its new group '%s'" % (node.name, new_group.uuid)) + + resmod.append((node, old_group, new_group)) + + # Apply changes + for (node, old_group, new_group) in resmod: + assert node.uuid != new_group.uuid and old_group.uuid != new_group.uuid, \ + "Assigning to current group is not possible" + + node.group = new_group.uuid + + # Update members of involved groups + if node.name in old_group.members: + old_group.members.remove(node.name) + if node.name not in new_group.members: + new_group.members.append(node.name) + + # Update timestamps and serials (only once per node/group object) + now = time.time() + for obj in frozenset(itertools.chain(*resmod)): # pylint: disable-msg=W0142 + obj.serial_no += 1 + obj.mtime = now + + # Force ssconf update + self._config_data.cluster.serial_no += 1 + + self._WriteConfig() + def _BumpSerialNo(self): """Bump up the serial number of the config. diff --git a/test/ganeti.config_unittest.py b/test/ganeti.config_unittest.py index a21bfa6da85568676b4d728d9d15cc4feb88af4b..e82872c6b0e1ff40fc5302924d65970dd8e83b7b 100755 --- a/test/ganeti.config_unittest.py +++ b/test/ganeti.config_unittest.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -# Copyright (C) 2006, 2007, 2010 Google Inc. +# Copyright (C) 2006, 2007, 2010, 2011 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -28,6 +28,8 @@ import time import tempfile import os.path import socket +import operator +import itertools from ganeti import bootstrap from ganeti import config @@ -36,6 +38,7 @@ from ganeti import errors from ganeti import objects from ganeti import utils from ganeti import netutils +from ganeti import compat from ganeti.config import TemporaryReservationManager @@ -239,6 +242,127 @@ class TestConfigRunner(unittest.TestCase): cfg.AddNodeGroup(group, "my-job", check_uuid=False) # Does not raise. self.assertEqual(uuid, group.uuid) + def testAssignGroupNodes(self): + me = netutils.Hostname() + cfg = self._get_object() + + # Create two groups + grp1 = objects.NodeGroup(name="grp1", members=[], + uuid="2f2fadf7-2a70-4a23-9ab5-2568c252032c") + grp1_serial = 1 + cfg.AddNodeGroup(grp1, "job") + + grp2 = objects.NodeGroup(name="grp2", members=[], + uuid="798d0de3-680f-4a0e-b29a-0f54f693b3f1") + grp2_serial = 1 + cfg.AddNodeGroup(grp2, "job") + self.assertEqual(set(map(operator.attrgetter("name"), + cfg.GetAllNodeGroupsInfo().values())), + set(["grp1", "grp2", constants.INITIAL_NODE_GROUP_NAME])) + + # No-op + cluster_serial = cfg.GetClusterInfo().serial_no + cfg.AssignGroupNodes([]) + cluster_serial += 1 + + # Create two nodes + node1 = objects.Node(name="node1", group=grp1.uuid, ndparams={}) + node1_serial = 1 + node2 = objects.Node(name="node2", group=grp2.uuid, ndparams={}) + node2_serial = 1 + cfg.AddNode(node1, "job") + cfg.AddNode(node2, "job") + cluster_serial += 2 + self.assertEqual(set(cfg.GetNodeList()), set(["node1", "node2", me.name])) + + def _VerifySerials(): + self.assertEqual(cfg.GetClusterInfo().serial_no, cluster_serial) + self.assertEqual(node1.serial_no, node1_serial) + self.assertEqual(node2.serial_no, node2_serial) + self.assertEqual(grp1.serial_no, grp1_serial) + self.assertEqual(grp2.serial_no, grp2_serial) + + _VerifySerials() + + self.assertEqual(set(grp1.members), set(["node1"])) + self.assertEqual(set(grp2.members), set(["node2"])) + + # Check invalid nodes and groups + self.assertRaises(errors.ConfigurationError, cfg.AssignGroupNodes, [ + ("unknown.node.example.com", grp2.uuid), + ]) + self.assertRaises(errors.ConfigurationError, cfg.AssignGroupNodes, [ + (node1.name, "unknown-uuid"), + ]) + + self.assertEqual(node1.group, grp1.uuid) + self.assertEqual(node2.group, grp2.uuid) + self.assertEqual(set(grp1.members), set(["node1"])) + self.assertEqual(set(grp2.members), set(["node2"])) + + # Another no-op + cfg.AssignGroupNodes([]) + cluster_serial += 1 + _VerifySerials() + + # Assign to the same group (should be a no-op) + self.assertEqual(node2.group, grp2.uuid) + cfg.AssignGroupNodes([ + (node2.name, grp2.uuid), + ]) + cluster_serial += 1 + self.assertEqual(node2.group, grp2.uuid) + _VerifySerials() + self.assertEqual(set(grp1.members), set(["node1"])) + self.assertEqual(set(grp2.members), set(["node2"])) + + # Assign node 2 to group 1 + self.assertEqual(node2.group, grp2.uuid) + cfg.AssignGroupNodes([ + (node2.name, grp1.uuid), + ]) + cluster_serial += 1 + node2_serial += 1 + grp1_serial += 1 + grp2_serial += 1 + self.assertEqual(node2.group, grp1.uuid) + _VerifySerials() + self.assertEqual(set(grp1.members), set(["node1", "node2"])) + self.assertFalse(grp2.members) + + # And assign both nodes to group 2 + self.assertEqual(node1.group, grp1.uuid) + self.assertEqual(node2.group, grp1.uuid) + self.assertNotEqual(grp1.uuid, grp2.uuid) + cfg.AssignGroupNodes([ + (node1.name, grp2.uuid), + (node2.name, grp2.uuid), + ]) + cluster_serial += 1 + node1_serial += 1 + node2_serial += 1 + grp1_serial += 1 + grp2_serial += 1 + self.assertEqual(node1.group, grp2.uuid) + self.assertEqual(node2.group, grp2.uuid) + _VerifySerials() + self.assertFalse(grp1.members) + self.assertEqual(set(grp2.members), set(["node1", "node2"])) + + # Destructive tests + orig_group = node2.group + try: + other_uuid = "68b3d087-6ea5-491c-b81f-0a47d90228c5" + assert compat.all(node.group != other_uuid + for node in cfg.GetAllNodesInfo().values()) + node2.group = "68b3d087-6ea5-491c-b81f-0a47d90228c5" + self.assertRaises(errors.ConfigurationError, cfg.AssignGroupNodes, [ + ("node2", grp2.uuid), + ]) + _VerifySerials() + finally: + node2.group = orig_group + class TestTRM(unittest.TestCase): EC_ID = 1