diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 4a10432c19f623c024f46e78e0d5920bbbdd6f08..b287f331d2bb75dbb1b9405aea4fa1bbda2160ee 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -10395,13 +10395,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 a7d7b77ed2254cdaa20ec1c6cfbad93c250fb875..e77bb3407edb2834d81168fcbb83d4bbf0ea1a7c 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 @@ -1517,6 +1518,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