Merge branch stable/mitaka into master

Change-Id: I9075cbe428013f4b5ee140c0b784712b4ff0fa56
This commit is contained in:
Ilya Kharin 2016-09-08 01:32:35 +03:00
commit e38d48cbc5
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: