Reassign nodes without reinstallation

In some upgrade scenarios when shadow environments are used some of
nodes should not be reprovisioned during this procedure. It is useful in
combination when control plane nodes are reprovisioned and data plane
nodes are updated in place.

The update_cluster_assignment method of the objects.Node class was
changed to accept roles and pending_roles for node during the
reassignment. It allows to specify proper roles by the upgrade
extention.

The NodeReassignHandler handler accepts two additional parameters in the
request body:
  - reprovision = True (default) - allows to skip the reprovision step
  - roles = [] (default) - allows to specify new roles or preserve the
                           current roles if empty

Two additional methods were added to NailgunClusterAdapter and
NailgunReleaseAdapter respectively.

Change-Id: Iedb20a904e58f5b9a86eb47de8d8d317dc3cc61b
Blueprint: upgrade-major-openstack-environment
Closes-Bug: #1558655
This commit is contained in:
Ilya Kharin 2016-02-14 23:15:32 -06:00 committed by Nikita Zubkov
parent 3522118cce
commit 715da75c3b
6 changed files with 171 additions and 18 deletions

View File

@ -69,14 +69,21 @@ class NodeReassignHandler(base.BaseHandler):
@base.content
def POST(self, cluster_id):
"""Reassign node to cluster via reinstallation
"""Reassign node to the given cluster.
:param cluster_id: ID of the cluster which node should be
assigned to.
:returns: None
:http: * 202 (OK)
* 400 (Incorrect node state or problem with task execution)
* 404 (Cluster or node not found)
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
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.
:returns: None
:http: * 202 (OK)
* 400 (Incorrect node state, problem with task execution,
conflicting or incorrect roles)
* 404 (Cluster or node not found)
"""
cluster = adapters.NailgunClusterAdapter(
self.get_object_or_404(self.single, cluster_id))
@ -84,7 +91,13 @@ class NodeReassignHandler(base.BaseHandler):
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', [])
upgrade.UpgradeHelper.assign_node_to_cluster(node, cluster)
roles, pending_roles = upgrade.UpgradeHelper.get_node_roles(
reprovision, node.roles, given_roles)
upgrade.UpgradeHelper.assign_node_to_cluster(
node, cluster, roles, pending_roles)
self.handle_task(cluster_id, [node.node, ])
if reprovision:
self.handle_task(cluster_id, [node.node])

View File

@ -42,6 +42,10 @@ class NailgunClusterAdapter(object):
def release(self):
return NailgunReleaseAdapter(self.cluster.release)
@property
def attributes(self):
return self.cluster.attributes
@property
def generated_attrs(self):
return self.cluster.attributes.generated
@ -100,6 +104,10 @@ class NailgunReleaseAdapter(object):
def environment_version(self):
return self.release.environment_version
@property
def roles_metadata(self):
return self.release.roles_metadata
def __cmp__(self, other):
if isinstance(other, NailgunReleaseAdapter):
other = other.release
@ -181,8 +189,9 @@ class NailgunNodeAdapter(object):
def roles(self):
return self.node.roles
def update_cluster_assignment(self, cluster):
objects.Node.update_cluster_assignment(self.node, cluster)
def update_cluster_assignment(self, cluster, roles, pending_roles):
objects.Node.update_cluster_assignment(self.node, cluster, roles,
pending_roles)
def add_pending_change(self, change):
objects.Node.add_pending_change(self.node, change)

View File

@ -97,6 +97,52 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
provisioned_uids = [int(n['uid']) for n in nodes]
self.assertEqual([node_id, ], provisioned_uids)
@patch('nailgun.task.task.rpc.cast')
def test_node_reassign_handler_with_roles(self, mcast):
cluster = self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': consts.NODE_STATUSES.ready,
'roles': ['controller']}])
node = cluster.nodes[0]
seed_cluster = self.env.create_cluster(api=False)
# 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,
'reprovision': True,
'roles': ['compute']}
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': seed_cluster.id}),
jsonutils.dumps(data),
headers=self.default_headers)
self.assertEqual(202, resp.status_code)
self.assertEqual(node.roles, [])
self.assertEqual(node.pending_roles, ['compute'])
self.assertTrue(mcast.called)
@patch('nailgun.task.task.rpc.cast')
def test_node_reassign_handler_without_reprovisioning(self, mcast):
cluster = self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': consts.NODE_STATUSES.ready,
'roles': ['controller']}])
node = cluster.nodes[0]
seed_cluster = self.env.create_cluster(api=False)
data = {'node_id': node.id,
'reprovision': False,
'roles': ['compute']}
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': seed_cluster.id}),
jsonutils.dumps(data),
headers=self.default_headers)
self.assertEqual(200, resp.status_code)
self.assertFalse(mcast.called)
self.assertEqual(node.roles, ['compute'])
def test_node_reassign_handler_no_node(self):
self.env.create_cluster()

View File

@ -141,3 +141,52 @@ class TestNodeReassignValidator(base.BaseTestCase):
msg = "^Empty request received$"
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate("", node)
class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
validator = validators.NodeReassignValidator
def setUp(self):
super(TestNodeReassignNoReinstallValidator, self).setUp()
self.dst_cluster = self.env.create_cluster(
api=False,
release_id=self.dst_release.id,
)
self.node = self.env.create_node(cluster_id=self.src_cluster.id,
roles=["compute"], status="ready")
def test_validate_defaults(self):
request = {"node_id": self.node.id}
data = jsonutils.dumps(request)
parsed = self.validator.validate(data, self.dst_cluster)
self.assertEqual(parsed, request)
self.assertEqual(self.node.roles, ['compute'])
def test_validate_with_roles(self):
request = {
"node_id": self.node.id,
"reprovision": True,
"roles": ['controller'],
}
data = jsonutils.dumps(request)
parsed = self.validator.validate(data, self.dst_cluster)
self.assertEqual(parsed, request)
def test_validate_not_unique_roles(self):
data = jsonutils.dumps({
"node_id": self.node.id,
"roles": ['compute', 'compute'],
})
msg = "has non-unique elements"
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate(data, self.dst_cluster)
def test_validate_no_reprovision_with_conflicts(self):
data = jsonutils.dumps({
"node_id": self.node.id,
"reprovision": False,
"roles": ['controller', 'compute'],
})
msg = '^Role "controller" in conflict with role compute$'
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate(data, self.dst_cluster)

View File

@ -169,7 +169,31 @@ class UpgradeHelper(object):
new_net_manager.assign_vips_for_net_groups()
@classmethod
def assign_node_to_cluster(cls, node, seed_cluster):
def get_node_roles(cls, reprovision, current_roles, given_roles):
"""Return roles depending on the reprovisioning status.
In case the node should be re-provisioned, only pending roles
should be set, otherwise for an already provisioned and deployed
node only actual roles should be set. In the both case the
given roles will have precedence over the existing.
:param reprovision: boolean, if set to True then the node should
be re-provisioned
:param current_roles: a list of current roles of the node
:param given_roles: a list of roles that should be assigned to
the node
:returns: a tuple of a list of roles and a list of pending roles
that will be assigned to the node
"""
roles_to_assign = given_roles if given_roles else current_roles
if reprovision:
roles, pending_roles = [], roles_to_assign
else:
roles, pending_roles = roles_to_assign, []
return roles, pending_roles
@classmethod
def assign_node_to_cluster(cls, node, seed_cluster, roles, pending_roles):
orig_cluster = adapters.NailgunClusterAdapter.get_by_uid(
node.cluster_id)
@ -178,7 +202,7 @@ class UpgradeHelper(object):
netgroups_id_mapping = cls.get_netgroups_id_mapping(
orig_cluster, seed_cluster)
node.update_cluster_assignment(seed_cluster)
node.update_cluster_assignment(seed_cluster, roles, pending_roles)
objects.Node.set_netgroups_ids(node, netgroups_id_mapping)
orig_manager.set_nic_assignment_netgroups_ids(
node, netgroups_id_mapping)

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from nailgun.api.v1.validators import assignment
from nailgun.api.v1.validators import base
from nailgun import consts
from nailgun.errors import errors
@ -84,7 +85,7 @@ class ClusterUpgradeValidator(base.BasicValidator):
log_message=True)
class NodeReassignValidator(base.BasicValidator):
class NodeReassignValidator(assignment.NodeAssignmentValidator):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Assign Node Parameters",
@ -92,17 +93,28 @@ class NodeReassignValidator(base.BasicValidator):
"type": "object",
"properties": {
"node_id": {"type": "number"},
"reprovision": {"type": "boolean", "default": True},
"roles": {"type": "array",
"items": {"type": "string"},
"uniqueItems": True},
},
"required": ["node_id"],
}
@classmethod
def validate(cls, data, cluster):
data = super(NodeReassignValidator, cls).validate(data)
cls.validate_schema(data, cls.schema)
node = cls.validate_node(data['node_id'])
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)
return data
roles = parsed.get('roles', [])
if roles:
cls.validate_roles(cluster, roles)
else:
cls.validate_roles(cluster, node.roles)
return parsed
@classmethod
def validate_node(cls, node_id):