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:
parent
e4d4a0b4b4
commit
a4e2a67e3e
|
@ -73,16 +73,16 @@ class NodeReassignHandler(base.BaseHandler):
|
||||||
@base.handle_errors
|
@base.handle_errors
|
||||||
@base.validate
|
@base.validate
|
||||||
def POST(self, cluster_id):
|
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
|
The given nodes will be assigned from the current cluster to the
|
||||||
given cluster, by default it involves the reprovisioning of this
|
given cluster, by default it involves the reprovisioning of the
|
||||||
node. If the 'reprovision' flag is set to False, then the node
|
nodes. If the 'reprovision' flag is set to False, then the nodes
|
||||||
will be just reassigned. If the 'roles' list is specified, then
|
will be just reassigned. If the 'roles' list is specified, then
|
||||||
the given roles will be used as 'pending_roles' in case of
|
the given roles will be used as 'pending_roles' in case of
|
||||||
the reprovisioning or otherwise as 'roles'.
|
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
|
:returns: None
|
||||||
:http: * 202 (OK)
|
:http: * 202 (OK)
|
||||||
* 400 (Incorrect node state, problem with task execution,
|
* 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))
|
self.get_object_or_404(self.single, cluster_id))
|
||||||
|
|
||||||
data = self.checked_data(cluster=cluster)
|
data = self.checked_data(cluster=cluster)
|
||||||
node = adapters.NailgunNodeAdapter(
|
|
||||||
self.get_object_or_404(objects.Node, data['node_id']))
|
|
||||||
reprovision = data.get('reprovision', True)
|
reprovision = data.get('reprovision', True)
|
||||||
given_roles = data.get('roles', [])
|
given_roles = data.get('roles', [])
|
||||||
|
|
||||||
roles, pending_roles = upgrade.UpgradeHelper.get_node_roles(
|
nodes_to_provision = []
|
||||||
reprovision, node.roles, given_roles)
|
for node_id in data['nodes_ids']:
|
||||||
upgrade.UpgradeHelper.assign_node_to_cluster(
|
node = adapters.NailgunNodeAdapter(
|
||||||
node, cluster, roles, pending_roles)
|
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:
|
if reprovision:
|
||||||
self.handle_task(cluster_id, [node.node])
|
self.handle_task(cluster_id, nodes_to_provision)
|
||||||
|
|
||||||
|
|
||||||
class CopyVIPsHandler(base.BaseHandler):
|
class CopyVIPsHandler(base.BaseHandler):
|
||||||
|
|
|
@ -87,14 +87,14 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
kwargs={'cluster_id': seed_cluster['id']}),
|
kwargs={'cluster_id': seed_cluster['id']}),
|
||||||
jsonutils.dumps({'node_id': node_id}),
|
jsonutils.dumps({'nodes_ids': [node_id]}),
|
||||||
headers=self.default_headers)
|
headers=self.default_headers)
|
||||||
self.assertEqual(202, resp.status_code)
|
self.assertEqual(202, resp.status_code)
|
||||||
|
|
||||||
args, kwargs = mcast.call_args
|
args, kwargs = mcast.call_args
|
||||||
nodes = args[1]['args']['provisioning_info']['nodes']
|
nodes = args[1]['args']['provisioning_info']['nodes']
|
||||||
provisioned_uids = [int(n['uid']) for n in 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')
|
@mock.patch('nailgun.task.task.rpc.cast')
|
||||||
def test_node_reassign_handler_with_roles(self, mcast):
|
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
|
# NOTE(akscram): reprovision=True means that the node will be
|
||||||
# re-provisioned during the reassigning. This is
|
# re-provisioned during the reassigning. This is
|
||||||
# a default behavior.
|
# a default behavior.
|
||||||
data = {'node_id': node.id,
|
data = {'nodes_ids': [node.id],
|
||||||
'reprovision': True,
|
'reprovision': True,
|
||||||
'roles': ['compute']}
|
'roles': ['compute']}
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
|
@ -130,7 +130,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
node = cluster.nodes[0]
|
node = cluster.nodes[0]
|
||||||
seed_cluster = self.env.create_cluster(api=False)
|
seed_cluster = self.env.create_cluster(api=False)
|
||||||
|
|
||||||
data = {'node_id': node.id,
|
data = {'nodes_ids': [node.id],
|
||||||
'reprovision': False,
|
'reprovision': False,
|
||||||
'roles': ['compute']}
|
'roles': ['compute']}
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
|
@ -148,7 +148,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
kwargs={'cluster_id': cluster['id']}),
|
kwargs={'cluster_id': cluster['id']}),
|
||||||
jsonutils.dumps({'node_id': 42}),
|
jsonutils.dumps({'nodes_ids': [42]}),
|
||||||
headers=self.default_headers,
|
headers=self.default_headers,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(404, resp.status_code)
|
self.assertEqual(404, resp.status_code)
|
||||||
|
@ -163,7 +163,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
kwargs={'cluster_id': cluster['id']}),
|
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,
|
headers=self.default_headers,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(400, resp.status_code)
|
self.assertEqual(400, resp.status_code)
|
||||||
|
@ -179,7 +179,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
kwargs={'cluster_id': cluster['id']}),
|
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,
|
headers=self.default_headers,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(400, resp.status_code)
|
self.assertEqual(400, resp.status_code)
|
||||||
|
@ -196,7 +196,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
kwargs={'cluster_id': cluster_id}),
|
kwargs={'cluster_id': cluster_id}),
|
||||||
jsonutils.dumps({'node_id': node_id}),
|
jsonutils.dumps({'nodes_ids': [node_id]}),
|
||||||
headers=self.default_headers,
|
headers=self.default_headers,
|
||||||
expect_errors=True)
|
expect_errors=True)
|
||||||
self.assertEqual(400, resp.status_code)
|
self.assertEqual(400, resp.status_code)
|
||||||
|
|
|
@ -137,7 +137,7 @@ class TestNodeReassignValidator(base.BaseTestCase):
|
||||||
node = self.env.create_node(cluster_id=cluster.id,
|
node = self.env.create_node(cluster_id=cluster.id,
|
||||||
roles=["compute"],
|
roles=["compute"],
|
||||||
status="ready")
|
status="ready")
|
||||||
msg = "^'node_id' is a required property"
|
msg = "^'nodes_ids' is a required property"
|
||||||
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
||||||
self.validator.validate("{}", node)
|
self.validator.validate("{}", node)
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
|
||||||
roles=["compute"], status="ready")
|
roles=["compute"], status="ready")
|
||||||
|
|
||||||
def test_validate_defaults(self):
|
def test_validate_defaults(self):
|
||||||
request = {"node_id": self.node.id}
|
request = {"nodes_ids": [self.node.id]}
|
||||||
data = jsonutils.dumps(request)
|
data = jsonutils.dumps(request)
|
||||||
parsed = self.validator.validate(data, self.dst_cluster)
|
parsed = self.validator.validate(data, self.dst_cluster)
|
||||||
self.assertEqual(parsed, request)
|
self.assertEqual(parsed, request)
|
||||||
|
@ -172,7 +172,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
|
||||||
|
|
||||||
def test_validate_with_roles(self):
|
def test_validate_with_roles(self):
|
||||||
request = {
|
request = {
|
||||||
"node_id": self.node.id,
|
"nodes_ids": [self.node.id],
|
||||||
"reprovision": True,
|
"reprovision": True,
|
||||||
"roles": ['controller'],
|
"roles": ['controller'],
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
|
||||||
|
|
||||||
def test_validate_not_unique_roles(self):
|
def test_validate_not_unique_roles(self):
|
||||||
data = jsonutils.dumps({
|
data = jsonutils.dumps({
|
||||||
"node_id": self.node.id,
|
"nodes_ids": [self.node.id],
|
||||||
"roles": ['compute', 'compute'],
|
"roles": ['compute', 'compute'],
|
||||||
})
|
})
|
||||||
msg = "has non-unique elements"
|
msg = "has non-unique elements"
|
||||||
|
@ -191,7 +191,7 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
|
||||||
|
|
||||||
def test_validate_no_reprovision_with_conflicts(self):
|
def test_validate_no_reprovision_with_conflicts(self):
|
||||||
data = jsonutils.dumps({
|
data = jsonutils.dumps({
|
||||||
"node_id": self.node.id,
|
"nodes_ids": [self.node.id],
|
||||||
"reprovision": False,
|
"reprovision": False,
|
||||||
"roles": ['controller', 'compute'],
|
"roles": ['controller', 'compute'],
|
||||||
})
|
})
|
||||||
|
@ -203,6 +203,64 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
|
||||||
"Role 'controller' in conflict with role 'compute'."
|
"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):
|
class TestCopyVIPsValidator(base.BaseTestCase):
|
||||||
validator = validators.CopyVIPsValidator
|
validator = validators.CopyVIPsValidator
|
||||||
|
|
|
@ -98,13 +98,18 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
|
||||||
"description": "Serialized parameters to assign node",
|
"description": "Serialized parameters to assign node",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"node_id": {"type": "number"},
|
"nodes_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "number"},
|
||||||
|
"uniqueItems": True,
|
||||||
|
"minItems": 1,
|
||||||
|
},
|
||||||
"reprovision": {"type": "boolean", "default": True},
|
"reprovision": {"type": "boolean", "default": True},
|
||||||
"roles": {"type": "array",
|
"roles": {"type": "array",
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"uniqueItems": True},
|
"uniqueItems": True},
|
||||||
},
|
},
|
||||||
"required": ["node_id"],
|
"required": ["nodes_ids"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -112,14 +117,19 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
|
||||||
parsed = super(NodeReassignValidator, cls).validate(data)
|
parsed = super(NodeReassignValidator, cls).validate(data)
|
||||||
cls.validate_schema(parsed, cls.schema)
|
cls.validate_schema(parsed, cls.schema)
|
||||||
|
|
||||||
node = cls.validate_node(parsed['node_id'])
|
nodes = []
|
||||||
cls.validate_node_cluster(node, cluster)
|
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', [])
|
roles = parsed.get('roles', [])
|
||||||
if roles:
|
if roles:
|
||||||
|
cls.validate_nodes_roles(nodes)
|
||||||
cls.validate_roles(cluster, roles)
|
cls.validate_roles(cluster, roles)
|
||||||
else:
|
else:
|
||||||
cls.validate_roles(cluster, node.roles)
|
for node in nodes:
|
||||||
|
cls.validate_roles(cluster, node.roles)
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -147,6 +157,16 @@ class NodeReassignValidator(assignment.NodeAssignmentValidator):
|
||||||
log_message=True)
|
log_message=True)
|
||||||
return node
|
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
|
@classmethod
|
||||||
def validate_node_cluster(cls, node, cluster):
|
def validate_node_cluster(cls, node, cluster):
|
||||||
if node.cluster_id == cluster.id:
|
if node.cluster_id == cluster.id:
|
||||||
|
|
Loading…
Reference in New Issue