Merge "Add support to re-assign a set of nodes" into stable/mitaka
This commit is contained in:
commit
cc5764751e
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue