Directly assign node to an upgrade cluster

The patch adds method that assigns a node to an upgrade cluster without
deleting it from DB. This allows to keep ID of the node and IP addresses
assigned to it. The node is booted into the bootstrap image as soon as
it moves to an upgrade cluster.

Implements blueprint: nailgun-api-env-upgrade-extensions
Co-Authored-By: Artur Svechnikov <asvechnikov@mirantis.com>
Change-Id: If10fadd149a32317420778607146d9d12108d3f9
This commit is contained in:
Ilya Kharin 2015-07-19 23:50:40 +03:00 committed by Nikita Zubkov
parent 29bbd90608
commit 6782ddfe16
7 changed files with 347 additions and 0 deletions

View File

@ -28,6 +28,8 @@ class ClusterUpgradeExtension(extensions.BaseExtension):
urls = [
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/clone/?$',
'handler': handlers.ClusterUpgradeHandler},
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/assign/?$',
'handler': handlers.NodeReassignHandler},
]
@classmethod

View File

@ -14,9 +14,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from nailgun.api.v1.handlers import base
from nailgun import objects
from nailgun.task import manager
from . import upgrade
from . import validators
@ -49,3 +51,40 @@ class ClusterUpgradeHandler(base.BaseHandler):
new_cluster = upgrade.UpgradeHelper.clone_cluster(orig_cluster,
request_data)
return new_cluster.to_json()
class NodeReassignHandler(base.BaseHandler):
single = objects.Cluster
validator = validators.NodeReassignValidator
task_manager = manager.ProvisioningTaskManager
def handle_task(self, cluster_id, nodes):
try:
task_manager = self.task_manager(cluster_id=cluster_id)
task = task_manager.execute(nodes)
except Exception as exc:
raise self.http(400, msg=six.text_type(exc))
self.raise_task(task)
@base.content
def POST(self, cluster_id):
"""Reassign node to cluster via reinstallation
: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)
"""
cluster = adapters.NailgunClusterAdapter(
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']))
upgrade.UpgradeHelper.assign_node_to_cluster(node, cluster)
self.handle_task(cluster_id, [node.node, ])

View File

@ -69,6 +69,19 @@ class NailgunClusterAdapter(object):
def to_json(self):
return objects.Cluster.to_json(self.cluster)
@classmethod
def get_by_uid(cls, cluster_id):
cluster = objects.Cluster.get_by_uid(cluster_id)
return cls(cluster)
def get_network_groups(self):
return (NailgunNetworkGroupAdapter(ng)
for ng in self.cluster.network_groups)
def get_admin_network_group(self):
manager = self.get_network_manager()
return manager.get_admin_network_group()
class NailgunReleaseAdapter(object):
def __init__(self, release):
@ -106,3 +119,81 @@ class NailgunNetworkManager(object):
def assign_given_vips_for_net_groups(self, vips):
self.net_manager.assign_given_vips_for_net_groups(self.cluster, vips)
def get_admin_network_group(self, node_id=None):
ng = self.net_manager.get_admin_network_group(node_id)
return NailgunNetworkGroupAdapter(ng)
def set_node_netgroups_ids(self, node, mapping):
return self.net_manager.set_node_netgroups_ids(node.node, mapping)
def set_nic_assignment_netgroups_ids(self, node, mapping):
return self.net_manager.set_nic_assignment_netgroups_ids(
node.node, mapping)
def set_bond_assignment_netgroups_ids(self, node, mapping):
return self.net_manager.set_bond_assignment_netgroups_ids(
node.node, mapping)
class NailgunNodeAdapter(object):
def __new__(cls, node=None):
if not node:
return None
return super(NailgunNodeAdapter, cls).__new__(cls, node)
def __init__(self, node):
self.node = node
@property
def id(self):
return self.node.id
@property
def cluster_id(self):
return self.node.cluster_id
@property
def hostname(self):
return self.node.hostname
@hostname.setter
def hostname(self, hostname):
self.node.hostname = hostname
@property
def status(self):
return self.node.status
@property
def error_type(self):
return self.node.error_type
@classmethod
def get_by_uid(cls, node_id):
return cls(objects.Node.get_by_uid(node_id))
@property
def roles(self):
return self.node.roles
def update_cluster_assignment(self, cluster):
objects.Node.update_cluster_assignment(self.node, cluster)
def add_pending_change(self, change):
objects.Node.add_pending_change(self.node, change)
class NailgunNetworkGroupAdapter(object):
def __init__(self, network_group):
self.network_group = network_group
@property
def id(self):
return self.network_group.id
@property
def name(self):
return self.network_group.name

View File

@ -14,8 +14,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
from oslo_serialization import jsonutils
from nailgun import consts
from nailgun.test import base
from nailgun.utils import reverse
from . import base as tests_base
@ -67,3 +71,95 @@ class TestClusterUpgradeHandler(tests_base.BaseCloneClusterTest):
headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 409)
class TestNodeReassignHandler(base.BaseIntegrationTest):
@patch('nailgun.task.task.rpc.cast')
def test_node_reassign_handler(self, mcast):
self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': consts.NODE_STATUSES.ready}])
self.env.create_cluster()
cluster = self.env.clusters[0]
seed_cluster = self.env.clusters[1]
node_id = cluster.nodes[0]['id']
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': seed_cluster['id']}),
jsonutils.dumps({'node_id': 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)
def test_node_reassign_handler_no_node(self):
self.env.create_cluster()
cluster = self.env.clusters[0]
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster['id']}),
jsonutils.dumps({'node_id': 42}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(404, resp.status_code)
self.assertEqual("Node with id 42 was not found.",
resp.json_body['message'])
def test_node_reassing_handler_wrong_status(self):
self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': 'discover'}])
cluster = self.env.clusters[0]
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster['id']}),
jsonutils.dumps({'node_id': cluster.nodes[0]['id']}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)
self.assertRegexpMatches(resp.json_body['message'],
"^Node should be in one of statuses:")
def test_node_reassing_handler_wrong_error_type(self):
self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': 'error',
'error_type': 'provision'}])
cluster = self.env.clusters[0]
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster['id']}),
jsonutils.dumps({'node_id': cluster.nodes[0]['id']}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)
self.assertRegexpMatches(resp.json_body['message'],
"^Node should be in error state")
def test_node_reassign_handler_to_the_same_cluster(self):
self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': 'ready'}])
cluster = self.env.clusters[0]
cluster_id = cluster['id']
node_id = cluster.nodes[0]['id']
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster_id}),
jsonutils.dumps({'node_id': node_id}),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)
self.assertEqual("Node {0} is already assigned to cluster {1}".
format(node_id, cluster_id),
resp.json_body['message'])

View File

@ -14,13 +14,17 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslo_serialization import jsonutils
from nailgun import consts
from nailgun.errors import errors
from nailgun.test import base
from .. import validators
from . import base as tests_base
from . import EXTENSION
from ..objects import relations
@ -85,3 +89,35 @@ class TestClusterUpgradeValidator(tests_base.BaseCloneClusterTest):
data = "{}"
with self.assertRaises(errors.InvalidData):
self.validator.validate(data, self.cluster_61)
class TestNodeReassignValidator(base.BaseTestCase):
validator = validators.NodeReassignValidator
@mock.patch(EXTENSION + "validators.adapters.NailgunNodeAdapter."
"get_by_uid")
def test_validate_node_not_found(self, mock_gbu):
mock_gbu.return_value = None
with self.assertRaises(errors.ObjectNotFound):
self.validator.validate_node(42)
@mock.patch(EXTENSION + "validators.adapters.NailgunNodeAdapter."
"get_by_uid")
def test_validate_node_wrong_status(self, mock_gbu):
mock_gbu.return_value = mock.Mock(status='wrong_state')
with self.assertRaises(errors.InvalidData):
self.validator.validate_node(42)
@mock.patch(EXTENSION + "validators.adapters.NailgunNodeAdapter."
"get_by_uid")
def test_validate_node_wrong_error_type(self, mock_gbu):
mock_gbu.return_value = mock.Mock(status='error',
error_type='wrong')
with self.assertRaises(errors.InvalidData):
self.validator.validate_node(42)
def test_validate_node_cluster(self):
node = mock.Mock(id=42, cluster_id=42)
cluster = mock.Mock(id=42)
with self.assertRaises(errors.InvalidData):
self.validator.validate_node_cluster(node, cluster)

View File

@ -123,3 +123,33 @@ class UpgradeHelper(object):
vips.pop(ng_name)
new_net_manager.assign_given_vips_for_net_groups(vips)
new_net_manager.assign_vips_for_net_groups()
@classmethod
def assign_node_to_cluster(cls, node, seed_cluster):
orig_cluster = adapters.NailgunClusterAdapter.get_by_uid(
node.cluster_id)
orig_manager = orig_cluster.get_network_manager()
seed_manager = seed_cluster.get_network_manager()
netgroups_id_mapping = cls.get_netgroups_id_mapping(
orig_cluster, seed_cluster)
node.update_cluster_assignment(seed_cluster)
seed_manager.set_node_netgroups_ids(node, netgroups_id_mapping)
orig_manager.set_nic_assignment_netgroups_ids(
node, netgroups_id_mapping)
orig_manager.set_bond_assignment_netgroups_ids(
node, netgroups_id_mapping)
node.add_pending_change(consts.CLUSTER_CHANGES.interfaces)
@classmethod
def get_netgroups_id_mapping(self, orig_cluster, seed_cluster):
orig_ng = orig_cluster.get_network_groups()
seed_ng = seed_cluster.get_network_groups()
seed_ng_dict = dict((ng.name, ng.id) for ng in seed_ng)
mapping = dict((ng.id, seed_ng_dict[ng.name]) for ng in orig_ng)
mapping[orig_cluster.get_admin_network_group().id] = \
seed_cluster.get_admin_network_group().id
return mapping

View File

@ -15,6 +15,7 @@
# under the License.
from nailgun.api.v1.validators import base
from nailgun import consts
from nailgun.errors import errors
from nailgun import objects
@ -81,3 +82,55 @@ class ClusterUpgradeValidator(base.BasicValidator):
" is already involved in the upgrade routine."
.format(cluster.id),
log_message=True)
class NodeReassignValidator(base.BasicValidator):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Assign Node Parameters",
"description": "Serialized parameters to assign node",
"type": "object",
"properties": {
"node_id": {"type": "number"},
},
}
@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'])
cls.validate_node_cluster(node, cluster)
return data
@classmethod
def validate_node(cls, node_id):
node = adapters.NailgunNodeAdapter.get_by_uid(node_id)
if not node:
raise errors.ObjectNotFound("Node with id {0} was not found.".
format(node_id), log_message=True)
# node can go to error state while upgrade process
allowed_statuses = (consts.NODE_STATUSES.ready,
consts.NODE_STATUSES.provisioned,
consts.NODE_STATUSES.error)
if node.status not in allowed_statuses:
raise errors.InvalidData("Node should be in one of statuses: {0}."
" Currently node has {1} status.".
format(allowed_statuses, node.status),
log_message=True)
if node.status == consts.NODE_STATUSES.error and\
node.error_type != consts.NODE_ERRORS.deploy:
raise errors.InvalidData("Node should be in error state only with"
"deploy error type. Currently error type"
" of node is {0}".format(node.error_type),
log_message=True)
return node
@classmethod
def validate_node_cluster(cls, node, cluster):
if node.cluster_id == cluster.id:
raise errors.InvalidData("Node {0} is already assigned to cluster"
" {1}".format(node.id, cluster.id),
log_message=True)