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:
parent
29bbd90608
commit
6782ddfe16
|
@ -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
|
||||
|
|
|
@ -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, ])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue