Merge "Add support to re-assign a set of nodes" into stable/mitaka

This commit is contained in:
Jenkins 2016-09-07 15:02:53 +00:00 committed by Gerrit Code Review
commit cc5764751e
4 changed files with 112 additions and 30 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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: