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:
parent
3cda80e31a
commit
405c7db906
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue