Add support to re-assign a set of nodes

This patch adds an ability to re-assign a set of the given nodes at
once. This feature was technically available but not exposed to the
client. A groupped re-assigning allows to effectively re-provision nodes
by creating an atomic task in Astute.

Change-Id: I4a7c7e35d844683ef73ad7f8459d1892e80e0a64
Related-Bug: #1616925
This commit is contained in:
Ilya Kharin 2016-09-03 02:42:07 +03:00
parent e4d4a0b4b4
commit a4e2a67e3e
4 changed files with 112 additions and 30 deletions

View File

@ -73,16 +73,16 @@ class NodeReassignHandler(base.BaseHandler):
@base.handle_errors
@base.validate
def POST(self, cluster_id):
"""Reassign node to the given cluster.
"""Reassign nodes to the given cluster.
The given node will be assigned from the current cluster to the
given cluster, by default it involves the reprovisioning of this
node. If the 'reprovision' flag is set to False, then the node
The given nodes will be assigned from the current cluster to the
given cluster, by default it involves the reprovisioning of the
nodes. If the 'reprovision' flag is set to False, then the nodes
will be just reassigned. If the 'roles' list is specified, then
the given roles will be used as 'pending_roles' in case of
the reprovisioning or otherwise as 'roles'.
:param cluster_id: ID of the cluster node should be assigned to.
:param cluster_id: ID of the cluster nodes should be assigned to.
:returns: None
:http: * 202 (OK)
* 400 (Incorrect node state, problem with task execution,
@ -93,18 +93,22 @@ class NodeReassignHandler(base.BaseHandler):
self.get_object_or_404(self.single, cluster_id))
data = self.checked_data(cluster=cluster)
node = adapters.NailgunNodeAdapter(
self.get_object_or_404(objects.Node, data['node_id']))
reprovision = data.get('reprovision', True)
given_roles = data.get('roles', [])
roles, pending_roles = upgrade.UpgradeHelper.get_node_roles(
reprovision, node.roles, given_roles)
upgrade.UpgradeHelper.assign_node_to_cluster(
node, cluster, roles, pending_roles)
nodes_to_provision = []
for node_id in data['nodes_ids']:
node = adapters.NailgunNodeAdapter(
self.get_object_or_404(objects.Node, node_id))
nodes_to_provision.append(node.node)
roles, pending_roles = upgrade.UpgradeHelper.get_node_roles(
reprovision, node.roles, given_roles)
upgrade.UpgradeHelper.assign_node_to_cluster(
node, cluster, roles, pending_roles)
if reprovision:
self.handle_task(cluster_id, [node.node])
self.handle_task(cluster_id, nodes_to_provision)
class CopyVIPsHandler(base.BaseHandler):

View File

@ -87,14 +87,14 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': seed_cluster['id']}),
jsonutils.dumps({'node_id': node_id}),
jsonutils.dumps({'nodes_ids': [node_id]}),
headers=self.default_headers)
self.assertEqual(202, resp.status_code)
args, kwargs = mcast.call_args
nodes = args[1]['args']['provisioning_info']['nodes']
provisioned_uids = [int(n['uid']) for n in nodes]
self.assertEqual([node_id, ], provisioned_uids)
self.assertEqual([node_id], provisioned_uids)
@mock.patch('nailgun.task.task.rpc.cast')
def test_node_reassign_handler_with_roles(self, mcast):
@ -108,7 +108,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
# NOTE(akscram): reprovision=True means that the node will be
# re-provisioned during the reassigning. This is
# a default behavior.
data = {'node_id': node.id,
data = {'nodes_ids': [node.id],
'reprovision': True,
'roles': ['compute']}
resp = self.app.post(
@ -130,7 +130,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
node = cluster.nodes[0]
seed_cluster = self.env.create_cluster(api=False)
data = {'node_id': node.id,
data = {'nodes_ids': [node.id],
'reprovision': False,
'roles': ['compute']}
resp = self.app.post(
@ -148,7 +148,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster['id']}),
jsonutils.dumps({'node_id': 42}),
jsonutils.dumps({'nodes_ids': [42]}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(404, resp.status_code)
@ -163,7 +163,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster['id']}),
jsonutils.dumps({'node_id': cluster.nodes[0]['id']}),
jsonutils.dumps({'nodes_ids': [cluster.nodes[0]['id']]}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)
@ -179,7 +179,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster['id']}),
jsonutils.dumps({'node_id': cluster.nodes[0]['id']}),
jsonutils.dumps({'nodes_ids': [cluster.nodes[0]['id']]}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)
@ -196,7 +196,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster_id}),
jsonutils.dumps({'node_id': node_id}),
jsonutils.dumps({'nodes_ids': [node_id]}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)

View File

@ -137,7 +137,7 @@ class TestNodeReassignValidator(base.BaseTestCase):
node = self.env.create_node(cluster_id=cluster.id,
roles=["compute"],
status="ready")
msg = "^'node_id' is a required property"
msg = "^'nodes_ids' is a required property"
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate("{}", node)
@ -164,7 +164,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
roles=["compute"], status="ready")
def test_validate_defaults(self):
request = {"node_id": self.node.id}
request = {"nodes_ids": [self.node.id]}
data = jsonutils.dumps(request)
parsed = self.validator.validate(data, self.dst_cluster)
self.assertEqual(parsed, request)
@ -172,7 +172,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
def test_validate_with_roles(self):
request = {
"node_id": self.node.id,
"nodes_ids": [self.node.id],
"reprovision": True,
"roles": ['controller'],
}
@ -182,7 +182,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
def test_validate_not_unique_roles(self):
data = jsonutils.dumps({
"node_id": self.node.id,
"nodes_ids": [self.node.id],
"roles": ['compute', 'compute'],
})
msg = "has non-unique elements"
@ -191,7 +191,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
def test_validate_no_reprovision_with_conflicts(self):
data = jsonutils.dumps({
"node_id": self.node.id,
"nodes_ids": [self.node.id],
"reprovision": False,
"roles": ['controller', 'compute'],
})
@ -203,6 +203,64 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
"Role 'controller' in conflict with role 'compute'."
)
def test_validate_empty_nodes_ids_error(self):
data = jsonutils.dumps({
"nodes_ids": [],
"roles": ['controller'],
})
msg = "minItems.*nodes_ids"
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate(data, self.dst_cluster)
def test_validate_several_nodes_ids(self):
node = self.env.create_node(cluster_id=self.src_cluster.id,
roles=["compute"], status="ready")
request = {
"nodes_ids": [self.node.id, node.id],
}
data = jsonutils.dumps(request)
parsed = self.validator.validate(data, self.dst_cluster)
self.assertEqual(parsed, request)
def test_validate_mixed_two_not_sorted_roles(self):
self.node.roles = ["compute", "ceph-osd"]
node = self.env.create_node(cluster_id=self.src_cluster.id,
roles=["ceph-osd", "compute"],
status="ready")
request = {
"nodes_ids": [self.node.id, node.id],
"roles": ["compute"],
}
data = jsonutils.dumps(request)
parsed = self.validator.validate(data, self.dst_cluster)
self.assertEqual(parsed, request)
def test_validate_mixed_roles_error(self):
node = self.env.create_node(cluster_id=self.src_cluster.id,
roles=["ceph-osd"], status="ready")
self._assert_validate_nodes_roles([self.node.id, node.id],
["controller"])
def test_validate_mixed_two_roles_error(self):
# Two nodes have two roles each, the first ones are the same
# while the second ones differ.
self.node.roles = ["compute", "ceph-osd"]
node = self.env.create_node(cluster_id=self.src_cluster.id,
roles=["compute", "mongo"],
status="ready")
self._assert_validate_nodes_roles([self.node.id, node.id],
["compute"])
def _assert_validate_nodes_roles(self, nodes_ids, roles):
data = jsonutils.dumps({
"nodes_ids": nodes_ids,
"roles": roles,
})
msg = "Only nodes with the same set of assigned roles are supported " \
"for the operation."
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate(data, self.dst_cluster)
class TestCopyVIPsValidator(base.BaseTestCase):
validator = validators.CopyVIPsValidator

View File

@ -98,13 +98,18 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
"description": "Serialized parameters to assign node",
"type": "object",
"properties": {
"node_id": {"type": "number"},
"nodes_ids": {
"type": "array",
"items": {"type": "number"},
"uniqueItems": True,
"minItems": 1,
},
"reprovision": {"type": "boolean", "default": True},
"roles": {"type": "array",
"items": {"type": "string"},
"uniqueItems": True},
},
"required": ["node_id"],
"required": ["nodes_ids"],
}
@classmethod
@ -112,14 +117,19 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
parsed = super(NodeReassignValidator, cls).validate(data)
cls.validate_schema(parsed, cls.schema)
node = cls.validate_node(parsed['node_id'])
cls.validate_node_cluster(node, cluster)
nodes = []
for node_id in parsed['nodes_ids']:
node = cls.validate_node(node_id)
cls.validate_node_cluster(node, cluster)
nodes.append(node)
roles = parsed.get('roles', [])
if roles:
cls.validate_nodes_roles(nodes)
cls.validate_roles(cluster, roles)
else:
cls.validate_roles(cluster, node.roles)
for node in nodes:
cls.validate_roles(cluster, node.roles)
return parsed
@classmethod
@ -147,6 +157,16 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
log_message=True)
return node
@classmethod
def validate_nodes_roles(cls, nodes):
roles = set(nodes[0].roles)
if all(roles == set(n.roles) for n in nodes[1:]):
return
raise errors.InvalidData(
"Only nodes with the same set of assigned roles are supported "
"for the operation.",
log_message=True)
@classmethod
def validate_node_cluster(cls, node, cluster):
if node.cluster_id == cluster.id: