Merge master into stable/mitaka
Change-Id: I62b4f8d1a0a75337d617959b0d7cbc104279018b
This commit is contained in:
commit
40dd411fe4
|
@ -0,0 +1,11 @@
|
||||||
|
Fuel nailgun extenstion for cluster upgrade
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
This extension for Nailgun provides API handlers and logic for
|
||||||
|
cluster upgrading. This extension used by the fuel-octane project.
|
||||||
|
|
||||||
|
Instalation
|
||||||
|
-----------
|
||||||
|
After installing `fuel-nailgun-extension-cluster-upgrade` package run:
|
||||||
|
1) `nailgun_syncdb` - migrate database
|
||||||
|
2) restart nailgun service
|
|
@ -0,0 +1,6 @@
|
||||||
|
libpq-dev
|
||||||
|
postgresql
|
||||||
|
postgresql-client
|
||||||
|
# We don't use these, but mysql-prep step is in template job
|
||||||
|
mysql-client
|
||||||
|
mysql-server
|
|
@ -33,6 +33,9 @@ class ClusterUpgradeExtension(extensions.BaseExtension):
|
||||||
'handler': handlers.NodeReassignHandler},
|
'handler': handlers.NodeReassignHandler},
|
||||||
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/vips/?$',
|
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/vips/?$',
|
||||||
'handler': handlers.CopyVIPsHandler},
|
'handler': handlers.CopyVIPsHandler},
|
||||||
|
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/clone_release/'
|
||||||
|
r'(?P<release_id>\d+)/?$',
|
||||||
|
'handler': handlers.CreateUpgradeReleaseHandler},
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -29,7 +29,9 @@ class ClusterUpgradeCloneHandler(base.BaseHandler):
|
||||||
single = objects.Cluster
|
single = objects.Cluster
|
||||||
validator = validators.ClusterUpgradeValidator
|
validator = validators.ClusterUpgradeValidator
|
||||||
|
|
||||||
@base.content
|
@base.handle_errors
|
||||||
|
@base.validate
|
||||||
|
@base.serialize
|
||||||
def POST(self, cluster_id):
|
def POST(self, cluster_id):
|
||||||
"""Initialize the upgrade of the cluster.
|
"""Initialize the upgrade of the cluster.
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ class ClusterUpgradeCloneHandler(base.BaseHandler):
|
||||||
request_data = self.checked_data(cluster=orig_cluster)
|
request_data = self.checked_data(cluster=orig_cluster)
|
||||||
new_cluster = upgrade.UpgradeHelper.clone_cluster(orig_cluster,
|
new_cluster = upgrade.UpgradeHelper.clone_cluster(orig_cluster,
|
||||||
request_data)
|
request_data)
|
||||||
return new_cluster.to_json()
|
return new_cluster.to_dict()
|
||||||
|
|
||||||
|
|
||||||
class NodeReassignHandler(base.BaseHandler):
|
class NodeReassignHandler(base.BaseHandler):
|
||||||
|
@ -67,7 +69,8 @@ class NodeReassignHandler(base.BaseHandler):
|
||||||
|
|
||||||
self.raise_task(task)
|
self.raise_task(task)
|
||||||
|
|
||||||
@base.content
|
@base.handle_errors
|
||||||
|
@base.validate
|
||||||
def POST(self, cluster_id):
|
def POST(self, cluster_id):
|
||||||
"""Reassign node to the given cluster.
|
"""Reassign node to the given cluster.
|
||||||
|
|
||||||
|
@ -107,7 +110,8 @@ class CopyVIPsHandler(base.BaseHandler):
|
||||||
single = objects.Cluster
|
single = objects.Cluster
|
||||||
validator = validators.CopyVIPsValidator
|
validator = validators.CopyVIPsValidator
|
||||||
|
|
||||||
@base.content
|
@base.handle_errors
|
||||||
|
@base.validate
|
||||||
def POST(self, cluster_id):
|
def POST(self, cluster_id):
|
||||||
"""Copy VIPs from original cluster to new one
|
"""Copy VIPs from original cluster to new one
|
||||||
|
|
||||||
|
@ -139,3 +143,45 @@ class CopyVIPsHandler(base.BaseHandler):
|
||||||
|
|
||||||
upgrade.UpgradeHelper.copy_vips(orig_cluster_adapter,
|
upgrade.UpgradeHelper.copy_vips(orig_cluster_adapter,
|
||||||
seed_cluster_adapter)
|
seed_cluster_adapter)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateUpgradeReleaseHandler(base.BaseHandler):
|
||||||
|
@staticmethod
|
||||||
|
def merge_network_roles(base_nets, orig_nets):
|
||||||
|
"""Create network metadata based on two releases.
|
||||||
|
|
||||||
|
Overwrite base default_mapping by orig default_maping values.
|
||||||
|
"""
|
||||||
|
orig_network_dict = {n['id']: n for n in orig_nets}
|
||||||
|
for base_net in base_nets:
|
||||||
|
orig_net = orig_network_dict.get(base_net['id'])
|
||||||
|
if orig_net is None:
|
||||||
|
orig_net = base_net
|
||||||
|
base_net['default_mapping'] = orig_net['default_mapping']
|
||||||
|
return base_net
|
||||||
|
|
||||||
|
@base.serialize
|
||||||
|
def POST(self, cluster_id, release_id):
|
||||||
|
"""Create release for upgrade purposes.
|
||||||
|
|
||||||
|
Creates a new release with network_roles_metadata based the given
|
||||||
|
release and re-use network parameters from the given cluster.
|
||||||
|
|
||||||
|
:returns: JSON representation of the created cluster
|
||||||
|
:http: * 200 (OK)
|
||||||
|
* 404 (Cluster or release not found.)
|
||||||
|
"""
|
||||||
|
base_release = self.get_object_or_404(objects.Release, release_id)
|
||||||
|
orig_cluster = self.get_object_or_404(objects.Cluster, cluster_id)
|
||||||
|
orig_release = orig_cluster.release
|
||||||
|
|
||||||
|
network_metadata = self.merge_network_roles(
|
||||||
|
base_release.network_roles_metadata,
|
||||||
|
orig_release.network_roles_metadata)
|
||||||
|
data = objects.Release.to_dict(base_release)
|
||||||
|
data['network_roles_metadata'] = network_metadata
|
||||||
|
data['name'] = '{0} Upgrade ({1})'.format(
|
||||||
|
base_release.name, orig_release.id)
|
||||||
|
del data['id']
|
||||||
|
new_release = objects.Release.create(data)
|
||||||
|
return new_release.to_dict()
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from nailgun.extensions.volume_manager import extension as volume_ext
|
||||||
from nailgun import objects
|
from nailgun import objects
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +63,14 @@ class NailgunClusterAdapter(object):
|
||||||
def editable_attrs(self, attrs):
|
def editable_attrs(self, attrs):
|
||||||
self.cluster.attributes.editable = attrs
|
self.cluster.attributes.editable = attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network_template(self):
|
||||||
|
return self.cluster.network_config.configuration_template
|
||||||
|
|
||||||
|
@network_template.setter
|
||||||
|
def network_template(self, template):
|
||||||
|
self.cluster.network_config.configuration_template = template
|
||||||
|
|
||||||
def get_create_data(self):
|
def get_create_data(self):
|
||||||
return objects.Cluster.get_create_data(self.cluster)
|
return objects.Cluster.get_create_data(self.cluster)
|
||||||
|
|
||||||
|
@ -70,8 +79,8 @@ class NailgunClusterAdapter(object):
|
||||||
instance=self.cluster)
|
instance=self.cluster)
|
||||||
return NailgunNetworkManager(self.cluster, net_manager)
|
return NailgunNetworkManager(self.cluster, net_manager)
|
||||||
|
|
||||||
def to_json(self):
|
def to_dict(self):
|
||||||
return objects.Cluster.to_json(self.cluster)
|
return objects.Cluster.to_dict(self.cluster)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_uid(cls, cluster_id):
|
def get_by_uid(cls, cluster_id):
|
||||||
|
@ -96,6 +105,10 @@ class NailgunReleaseAdapter(object):
|
||||||
uid, fail_if_not_found=fail_if_not_found)
|
uid, fail_if_not_found=fail_if_not_found)
|
||||||
return release
|
return release
|
||||||
|
|
||||||
|
@property
|
||||||
|
def operating_system(self):
|
||||||
|
return self.release.operating_system
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_deployable(self):
|
def is_deployable(self):
|
||||||
return objects.Release.is_deployable(self.release)
|
return objects.Release.is_deployable(self.release)
|
||||||
|
@ -173,6 +186,10 @@ class NailgunNodeAdapter(object):
|
||||||
def status(self):
|
def status(self):
|
||||||
return self.node.status
|
return self.node.status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nic_interfaces(self):
|
||||||
|
return self.node.nic_interfaces
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error_type(self):
|
def error_type(self):
|
||||||
return self.node.error_type
|
return self.node.error_type
|
||||||
|
@ -192,6 +209,14 @@ class NailgunNodeAdapter(object):
|
||||||
def add_pending_change(self, change):
|
def add_pending_change(self, change):
|
||||||
objects.Node.add_pending_change(self.node, change)
|
objects.Node.add_pending_change(self.node, change)
|
||||||
|
|
||||||
|
def get_volumes(self):
|
||||||
|
return volume_ext.VolumeManagerExtension.get_node_volumes(self.node)
|
||||||
|
|
||||||
|
def set_volumes(self, volumes):
|
||||||
|
return volume_ext.VolumeManagerExtension.set_node_volumes(
|
||||||
|
self.node, volumes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NailgunNetworkGroupAdapter(object):
|
class NailgunNetworkGroupAdapter(object):
|
||||||
|
|
||||||
|
|
|
@ -77,12 +77,10 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
|
|
||||||
@mock.patch('nailgun.task.task.rpc.cast')
|
@mock.patch('nailgun.task.task.rpc.cast')
|
||||||
def test_node_reassign_handler(self, mcast):
|
def test_node_reassign_handler(self, mcast):
|
||||||
self.env.create(
|
cluster = self.env.create(
|
||||||
cluster_kwargs={'api': False},
|
cluster_kwargs={'api': False},
|
||||||
nodes_kwargs=[{'status': consts.NODE_STATUSES.ready}])
|
nodes_kwargs=[{'status': consts.NODE_STATUSES.ready}])
|
||||||
self.env.create_cluster()
|
seed_cluster = self.env.create_cluster()
|
||||||
cluster = self.env.clusters[0]
|
|
||||||
seed_cluster = self.env.clusters[1]
|
|
||||||
node_id = cluster.nodes[0]['id']
|
node_id = cluster.nodes[0]['id']
|
||||||
|
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
|
@ -144,9 +142,7 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
self.assertEqual(node.roles, ['compute'])
|
self.assertEqual(node.roles, ['compute'])
|
||||||
|
|
||||||
def test_node_reassign_handler_no_node(self):
|
def test_node_reassign_handler_no_node(self):
|
||||||
self.env.create_cluster()
|
cluster = self.env.create_cluster()
|
||||||
|
|
||||||
cluster = self.env.clusters[0]
|
|
||||||
|
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
|
@ -159,10 +155,9 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
resp.json_body['message'])
|
resp.json_body['message'])
|
||||||
|
|
||||||
def test_node_reassing_handler_wrong_status(self):
|
def test_node_reassing_handler_wrong_status(self):
|
||||||
self.env.create(
|
cluster = self.env.create(
|
||||||
cluster_kwargs={'api': False},
|
cluster_kwargs={'api': False},
|
||||||
nodes_kwargs=[{'status': 'discover'}])
|
nodes_kwargs=[{'status': 'discover'}])
|
||||||
cluster = self.env.clusters[0]
|
|
||||||
|
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
|
@ -175,11 +170,10 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
"^Node should be in one of statuses:")
|
"^Node should be in one of statuses:")
|
||||||
|
|
||||||
def test_node_reassing_handler_wrong_error_type(self):
|
def test_node_reassing_handler_wrong_error_type(self):
|
||||||
self.env.create(
|
cluster = self.env.create(
|
||||||
cluster_kwargs={'api': False},
|
cluster_kwargs={'api': False},
|
||||||
nodes_kwargs=[{'status': 'error',
|
nodes_kwargs=[{'status': 'error',
|
||||||
'error_type': 'provision'}])
|
'error_type': 'provision'}])
|
||||||
cluster = self.env.clusters[0]
|
|
||||||
|
|
||||||
resp = self.app.post(
|
resp = self.app.post(
|
||||||
reverse('NodeReassignHandler',
|
reverse('NodeReassignHandler',
|
||||||
|
@ -192,10 +186,9 @@ class TestNodeReassignHandler(base.BaseIntegrationTest):
|
||||||
"^Node should be in error state")
|
"^Node should be in error state")
|
||||||
|
|
||||||
def test_node_reassign_handler_to_the_same_cluster(self):
|
def test_node_reassign_handler_to_the_same_cluster(self):
|
||||||
self.env.create(
|
cluster = self.env.create(
|
||||||
cluster_kwargs={'api': False},
|
cluster_kwargs={'api': False},
|
||||||
nodes_kwargs=[{'status': 'ready'}])
|
nodes_kwargs=[{'status': 'ready'}])
|
||||||
cluster = self.env.clusters[0]
|
|
||||||
|
|
||||||
cluster_id = cluster['id']
|
cluster_id = cluster['id']
|
||||||
node_id = cluster.nodes[0]['id']
|
node_id = cluster.nodes[0]['id']
|
||||||
|
|
|
@ -0,0 +1,221 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from distutils import version
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from nailgun.test import base as nailgun_test_base
|
||||||
|
import six
|
||||||
|
|
||||||
|
from .. import transformations
|
||||||
|
from ..transformations import cluster
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransformations(nailgun_test_base.BaseUnitTest):
|
||||||
|
def test_get_config(self):
|
||||||
|
config = object()
|
||||||
|
|
||||||
|
class Manager(transformations.Manager):
|
||||||
|
default_config = config
|
||||||
|
|
||||||
|
self.assertIs(config, Manager.get_config('testname'))
|
||||||
|
|
||||||
|
def setup_extension_manager(self, extensions):
|
||||||
|
p = mock.patch("stevedore.ExtensionManager", spec=['__call__'])
|
||||||
|
mock_extman = p.start()
|
||||||
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
def extman(namespace, *args, **kwargs):
|
||||||
|
instance = mock.MagicMock(name=namespace)
|
||||||
|
ext_results = {}
|
||||||
|
for ver, exts in six.iteritems(extensions):
|
||||||
|
if namespace.endswith(ver):
|
||||||
|
ext_results = {name: mock.Mock(name=name, plugin=ext)
|
||||||
|
for name, ext in six.iteritems(exts)}
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.fail("Called with unexpected version in namespace: {}, "
|
||||||
|
"expected versions: {}".format(
|
||||||
|
namespace, list(extensions)))
|
||||||
|
instance.__getitem__.side_effect = ext_results.__getitem__
|
||||||
|
return instance
|
||||||
|
|
||||||
|
mock_extman.side_effect = extman
|
||||||
|
return mock_extman
|
||||||
|
|
||||||
|
def test_load_transformers(self):
|
||||||
|
config = {'9.0': ['a', 'b']}
|
||||||
|
extensions = {'9.0': {
|
||||||
|
'a': mock.Mock(name='a'),
|
||||||
|
'b': mock.Mock(name='b'),
|
||||||
|
}}
|
||||||
|
mock_extman = self.setup_extension_manager(extensions)
|
||||||
|
|
||||||
|
res = transformations.Manager.load_transformers('testname', config)
|
||||||
|
|
||||||
|
self.assertEqual(res, [(version.StrictVersion('9.0'), [
|
||||||
|
extensions['9.0']['a'],
|
||||||
|
extensions['9.0']['b'],
|
||||||
|
])])
|
||||||
|
callback = transformations.reraise_endpoint_load_failure
|
||||||
|
self.assertEqual(mock_extman.mock_calls, [
|
||||||
|
mock.call(
|
||||||
|
'nailgun.cluster_upgrade.transformations.testname.9.0',
|
||||||
|
on_load_failure_callback=callback,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_load_transformers_empty(self):
|
||||||
|
config = {}
|
||||||
|
extensions = {'9.0': {
|
||||||
|
'a': mock.Mock(name='a'),
|
||||||
|
'b': mock.Mock(name='b'),
|
||||||
|
}}
|
||||||
|
mock_extman = self.setup_extension_manager(extensions)
|
||||||
|
|
||||||
|
res = transformations.Manager.load_transformers('testname', config)
|
||||||
|
|
||||||
|
self.assertEqual(res, [])
|
||||||
|
self.assertEqual(mock_extman.mock_calls, [])
|
||||||
|
|
||||||
|
def test_load_transformers_sorted(self):
|
||||||
|
config = {'9.0': ['a', 'b'], '8.0': ['c']}
|
||||||
|
extensions = {
|
||||||
|
'9.0': {
|
||||||
|
'a': mock.Mock(name='a'),
|
||||||
|
'b': mock.Mock(name='b'),
|
||||||
|
},
|
||||||
|
'8.0': {
|
||||||
|
'c': mock.Mock(name='c'),
|
||||||
|
'd': mock.Mock(name='d'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mock_extman = self.setup_extension_manager(extensions)
|
||||||
|
|
||||||
|
orig_iteritems = six.iteritems
|
||||||
|
iteritems_patch = mock.patch('six.iteritems')
|
||||||
|
mock_iteritems = iteritems_patch.start()
|
||||||
|
self.addCleanup(iteritems_patch.stop)
|
||||||
|
|
||||||
|
def sorted_iteritems(d):
|
||||||
|
return sorted(orig_iteritems(d), reverse=True)
|
||||||
|
|
||||||
|
mock_iteritems.side_effect = sorted_iteritems
|
||||||
|
|
||||||
|
res = transformations.Manager.load_transformers('testname', config)
|
||||||
|
|
||||||
|
self.assertEqual(res, [
|
||||||
|
(version.StrictVersion('8.0'), [
|
||||||
|
extensions['8.0']['c'],
|
||||||
|
]),
|
||||||
|
(version.StrictVersion('9.0'), [
|
||||||
|
extensions['9.0']['a'],
|
||||||
|
extensions['9.0']['b'],
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
callback = transformations.reraise_endpoint_load_failure
|
||||||
|
self.assertItemsEqual(mock_extman.mock_calls, [
|
||||||
|
mock.call(
|
||||||
|
'nailgun.cluster_upgrade.transformations.testname.9.0',
|
||||||
|
on_load_failure_callback=callback,
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
'nailgun.cluster_upgrade.transformations.testname.8.0',
|
||||||
|
on_load_failure_callback=callback,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_load_transformers_keyerror(self):
|
||||||
|
config = {'9.0': ['a', 'b', 'c']}
|
||||||
|
extensions = {'9.0': {
|
||||||
|
'a': mock.Mock(name='a'),
|
||||||
|
'b': mock.Mock(name='b'),
|
||||||
|
}}
|
||||||
|
mock_extman = self.setup_extension_manager(extensions)
|
||||||
|
|
||||||
|
with self.assertRaisesRegexp(KeyError, 'c'):
|
||||||
|
transformations.Manager.load_transformers('testname', config)
|
||||||
|
|
||||||
|
callback = transformations.reraise_endpoint_load_failure
|
||||||
|
self.assertEqual(mock_extman.mock_calls, [
|
||||||
|
mock.call(
|
||||||
|
'nailgun.cluster_upgrade.transformations.testname.9.0',
|
||||||
|
on_load_failure_callback=callback,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
@mock.patch.object(transformations.Manager, 'load_transformers')
|
||||||
|
def test_apply(self, mock_load):
|
||||||
|
mock_trans = mock.Mock()
|
||||||
|
mock_load.return_value = [
|
||||||
|
(version.StrictVersion('7.0'), [mock_trans.a, mock_trans.b]),
|
||||||
|
(version.StrictVersion('8.0'), [mock_trans.c, mock_trans.d]),
|
||||||
|
(version.StrictVersion('9.0'), [mock_trans.e, mock_trans.f]),
|
||||||
|
]
|
||||||
|
man = transformations.Manager()
|
||||||
|
res = man.apply('7.0', '9.0', {})
|
||||||
|
self.assertEqual(res, mock_trans.f.return_value)
|
||||||
|
self.assertEqual(mock_trans.mock_calls, [
|
||||||
|
mock.call.c({}),
|
||||||
|
mock.call.d(mock_trans.c.return_value),
|
||||||
|
mock.call.e(mock_trans.d.return_value),
|
||||||
|
mock.call.f(mock_trans.e.return_value),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class TestLazy(nailgun_test_base.BaseUnitTest):
|
||||||
|
def test_lazy(self):
|
||||||
|
mgr_cls_mock = mock.Mock()
|
||||||
|
lazy_obj = transformations.Lazy(mgr_cls_mock)
|
||||||
|
lazy_obj.apply()
|
||||||
|
self.assertEqual(lazy_obj.apply, mgr_cls_mock.return_value.apply)
|
||||||
|
|
||||||
|
|
||||||
|
class TestClusterTransformers(nailgun_test_base.BaseUnitTest):
|
||||||
|
def setUp(self):
|
||||||
|
self.data = {
|
||||||
|
'editable': {
|
||||||
|
'external_dns': {
|
||||||
|
'dns_list': {'type': 'text', 'value': 'a,b,\nc, d'}},
|
||||||
|
'external_ntp': {
|
||||||
|
'ntp_list': {'type': 'text', 'value': 'a,b,\nc, d'}},
|
||||||
|
},
|
||||||
|
'generated': {
|
||||||
|
'provision': {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_dns_list(self):
|
||||||
|
res = cluster.transform_dns_list(self.data)
|
||||||
|
self.assertEqual(
|
||||||
|
res['editable']['external_dns']['dns_list'],
|
||||||
|
{'type': 'text_list', 'value': ['a', 'b', 'c', 'd']},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ntp_list(self):
|
||||||
|
res = cluster.transform_ntp_list(self.data)
|
||||||
|
self.assertEqual(
|
||||||
|
res['editable']['external_ntp']['ntp_list'],
|
||||||
|
{'type': 'text_list', 'value': ['a', 'b', 'c', 'd']},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_provision(self):
|
||||||
|
res = cluster.drop_generated_provision(self.data)
|
||||||
|
self.assertNotIn('provision', res['generated'])
|
||||||
|
|
||||||
|
def test_manager(self):
|
||||||
|
man = cluster.Manager() # verify default config and entry points
|
||||||
|
self.assertEqual(man.transformers, [(version.StrictVersion('9.0'), [
|
||||||
|
cluster.transform_dns_list,
|
||||||
|
cluster.transform_ntp_list,
|
||||||
|
cluster.drop_generated_provision,
|
||||||
|
])])
|
|
@ -19,6 +19,7 @@ import six
|
||||||
|
|
||||||
from nailgun import consts
|
from nailgun import consts
|
||||||
from nailgun.objects.serializers import network_configuration
|
from nailgun.objects.serializers import network_configuration
|
||||||
|
from nailgun.test.base import fake_tasks
|
||||||
|
|
||||||
from .. import upgrade
|
from .. import upgrade
|
||||||
from . import base as base_tests
|
from . import base as base_tests
|
||||||
|
@ -49,7 +50,7 @@ class TestUpgradeHelperCloneCluster(base_tests.BaseCloneClusterTest):
|
||||||
{"metadata": "src_fake",
|
{"metadata": "src_fake",
|
||||||
"key":
|
"key":
|
||||||
{"type": "text",
|
{"type": "text",
|
||||||
"value": "fake1, fake2,fake3 , fake4"},
|
"value": "fake"},
|
||||||
"src_key": "src_data"
|
"src_key": "src_data"
|
||||||
},
|
},
|
||||||
"repo_setup": "src_data"
|
"repo_setup": "src_data"
|
||||||
|
@ -68,9 +69,6 @@ class TestUpgradeHelperCloneCluster(base_tests.BaseCloneClusterTest):
|
||||||
result = upgrade.merge_attributes(
|
result = upgrade.merge_attributes(
|
||||||
src_editable_attrs, new_editable_attrs
|
src_editable_attrs, new_editable_attrs
|
||||||
)
|
)
|
||||||
new_editable_attrs["test"]["key"]["value"] = [
|
|
||||||
"fake1", "fake2", "fake3", "fake4"
|
|
||||||
]
|
|
||||||
self.assertEqual(result, new_editable_attrs)
|
self.assertEqual(result, new_editable_attrs)
|
||||||
|
|
||||||
def test_create_cluster_clone(self):
|
def test_create_cluster_clone(self):
|
||||||
|
@ -238,3 +236,44 @@ class TestUpgradeHelperCloneCluster(base_tests.BaseCloneClusterTest):
|
||||||
self.helper.change_env_settings(self.src_cluster, new_cluster)
|
self.helper.change_env_settings(self.src_cluster, new_cluster)
|
||||||
self.assertEqual('image',
|
self.assertEqual('image',
|
||||||
attrs['editable']['provision']['method']['value'])
|
attrs['editable']['provision']['method']['value'])
|
||||||
|
|
||||||
|
def get_assigned_nets(self, node):
|
||||||
|
assigned_nets = {}
|
||||||
|
for iface in node.nic_interfaces:
|
||||||
|
nets = [net.name for net in iface.assigned_networks_list]
|
||||||
|
assigned_nets[iface.name] = nets
|
||||||
|
return assigned_nets
|
||||||
|
|
||||||
|
@fake_tasks()
|
||||||
|
def assign_node_to_cluster(self, template=None):
|
||||||
|
new_cluster = self.helper.clone_cluster(self.src_cluster, self.data)
|
||||||
|
node = adapters.NailgunNodeAdapter(self.src_cluster.cluster.nodes[0])
|
||||||
|
|
||||||
|
orig_assigned_nets = self.get_assigned_nets(node)
|
||||||
|
|
||||||
|
if template:
|
||||||
|
net_template = self.env.read_fixtures(['network_template_80'])[0]
|
||||||
|
new_cluster.network_template = net_template
|
||||||
|
orig_assigned_nets = {
|
||||||
|
'eth0': ['fuelweb_admin'], 'eth1': ['public', 'management']
|
||||||
|
}
|
||||||
|
|
||||||
|
self.helper.assign_node_to_cluster(node, new_cluster, node.roles, [])
|
||||||
|
self.db.refresh(new_cluster.cluster)
|
||||||
|
|
||||||
|
self.assertEqual(node.cluster_id, new_cluster.id)
|
||||||
|
|
||||||
|
self.env.clusters.append(new_cluster.cluster)
|
||||||
|
task = self.env.launch_provisioning_selected(cluster_id=new_cluster.id)
|
||||||
|
self.assertEqual(task.status, consts.TASK_STATUSES.ready)
|
||||||
|
for n in new_cluster.cluster.nodes:
|
||||||
|
self.assertEqual(consts.NODE_STATUSES.provisioned, n.status)
|
||||||
|
|
||||||
|
new_assigned_nets = self.get_assigned_nets(node)
|
||||||
|
self.assertEqual(orig_assigned_nets, new_assigned_nets)
|
||||||
|
|
||||||
|
def test_assign_node_to_cluster(self):
|
||||||
|
self.assign_node_to_cluster()
|
||||||
|
|
||||||
|
def test_assign_node_to_cluster_with_template(self):
|
||||||
|
self.assign_node_to_cluster(template=True)
|
||||||
|
|
|
@ -58,6 +58,14 @@ class TestClusterUpgradeValidator(tests_base.BaseCloneClusterTest):
|
||||||
self.validator.validate_release_upgrade(self.dst_release,
|
self.validator.validate_release_upgrade(self.dst_release,
|
||||||
self.src_release)
|
self.src_release)
|
||||||
|
|
||||||
|
def test_validate_release_upgrade_to_different_os(self):
|
||||||
|
self.dst_release.operating_system = consts.RELEASE_OS.centos
|
||||||
|
msg = "^Changing of operating system is not possible during upgrade " \
|
||||||
|
"\(from {0} to {1}\).$".format("Ubuntu", "CentOS")
|
||||||
|
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
||||||
|
self.validator.validate_release_upgrade(self.src_release,
|
||||||
|
self.dst_release)
|
||||||
|
|
||||||
def test_validate_cluster_name(self):
|
def test_validate_cluster_name(self):
|
||||||
self.validator.validate_cluster_name("cluster-42")
|
self.validator.validate_cluster_name("cluster-42")
|
||||||
|
|
||||||
|
@ -187,10 +195,14 @@ class TestNodeReassignNoReinstallValidator(tests_base.BaseCloneClusterTest):
|
||||||
"reprovision": False,
|
"reprovision": False,
|
||||||
"roles": ['controller', 'compute'],
|
"roles": ['controller', 'compute'],
|
||||||
})
|
})
|
||||||
msg = "^Role 'controller' in conflict with role 'compute'.$"
|
with self.assertRaises(errors.InvalidData) as exc:
|
||||||
with self.assertRaisesRegexp(errors.InvalidData, msg):
|
|
||||||
self.validator.validate(data, self.dst_cluster)
|
self.validator.validate(data, self.dst_cluster)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
exc.exception.message,
|
||||||
|
"Role 'controller' in conflict with role 'compute'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestCopyVIPsValidator(base.BaseTestCase):
|
class TestCopyVIPsValidator(base.BaseTestCase):
|
||||||
validator = validators.CopyVIPsValidator
|
validator = validators.CopyVIPsValidator
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import distutils.version
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
import stevedore
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def reraise_endpoint_load_failure(manager, endpoint, exc):
|
||||||
|
LOG.error('Failed to load %s: %s', endpoint.name, exc)
|
||||||
|
raise # Avoid unexpectedly skipped steps
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(object):
|
||||||
|
default_config = None
|
||||||
|
name = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config = self.get_config(self.name)
|
||||||
|
self.transformers = self.load_transformers(self.name, self.config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config(cls, name):
|
||||||
|
# TODO(yorik-sar): merge actual config with defaults
|
||||||
|
return cls.default_config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_transformers(name, config):
|
||||||
|
transformers = []
|
||||||
|
for version, names in six.iteritems(config):
|
||||||
|
extension_manager = stevedore.ExtensionManager(
|
||||||
|
'nailgun.cluster_upgrade.transformations.{}.{}'.format(
|
||||||
|
name, version),
|
||||||
|
on_load_failure_callback=reraise_endpoint_load_failure,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
sorted_extensions = [extension_manager[n].plugin
|
||||||
|
for n in names]
|
||||||
|
except KeyError as exc:
|
||||||
|
LOG.error('%s transformer %s not found for version %s',
|
||||||
|
name, exc, version)
|
||||||
|
raise
|
||||||
|
strict_version = distutils.version.StrictVersion(version)
|
||||||
|
transformers.append((strict_version, sorted_extensions))
|
||||||
|
transformers.sort()
|
||||||
|
return transformers
|
||||||
|
|
||||||
|
def apply(self, from_version, to_version, data):
|
||||||
|
strict_from = distutils.version.StrictVersion(from_version)
|
||||||
|
strict_to = distutils.version.StrictVersion(to_version)
|
||||||
|
assert strict_from <= strict_to, \
|
||||||
|
"from_version must not be greater than to_version"
|
||||||
|
data = copy.deepcopy(data)
|
||||||
|
for version, transformers in self.transformers:
|
||||||
|
if version <= strict_from:
|
||||||
|
continue
|
||||||
|
if version > strict_to:
|
||||||
|
break
|
||||||
|
for transformer in transformers:
|
||||||
|
LOG.debug("Applying %s transformer %s",
|
||||||
|
self.name, transformer)
|
||||||
|
data = transformer(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class Lazy(object):
|
||||||
|
def __init__(self, mgr_cls):
|
||||||
|
self.mgr_cls = mgr_cls
|
||||||
|
self.mgr = None
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def apply(self, *args, **kwargs):
|
||||||
|
if self.mgr is None:
|
||||||
|
with self.lock:
|
||||||
|
if self.mgr is None:
|
||||||
|
self.mgr = self.mgr_cls()
|
||||||
|
self.apply = self.mgr.apply
|
||||||
|
return self.mgr.apply(*args, **kwargs)
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from cluster_upgrade import transformations
|
||||||
|
|
||||||
|
# NOTE: In the mitaka-9.0 release types of values dns_list and
|
||||||
|
# ntp_list were changed from 'text'
|
||||||
|
# (a string of comma-separated IP-addresses)
|
||||||
|
# to 'text_list' (a list of strings of IP-addresses).
|
||||||
|
|
||||||
|
|
||||||
|
def transform_to_text_list(data):
|
||||||
|
if data['type'] == 'text':
|
||||||
|
data['type'] = 'text_list'
|
||||||
|
data['value'] = [
|
||||||
|
part.strip() for part in data['value'].split(',')
|
||||||
|
]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def transform_dns_list(data):
|
||||||
|
dns_list = data['editable']['external_dns']['dns_list']
|
||||||
|
transform_to_text_list(dns_list)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def transform_ntp_list(data):
|
||||||
|
ntp_list = data['editable']['external_ntp']['ntp_list']
|
||||||
|
transform_to_text_list(ntp_list)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def drop_generated_provision(data):
|
||||||
|
data['generated'].pop('provision', None)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(transformations.Manager):
|
||||||
|
default_config = {
|
||||||
|
'9.0': ['dns_list', 'ntp_list', 'drop_provision'],
|
||||||
|
}
|
||||||
|
name = 'cluster'
|
|
@ -0,0 +1,62 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
from cluster_upgrade import transformations
|
||||||
|
|
||||||
|
|
||||||
|
def transform_vips(data):
|
||||||
|
"""Rename or remove types of VIPs for 7.0 network groups.
|
||||||
|
|
||||||
|
This method renames types of VIPs from older releases (<7.0) to
|
||||||
|
be compatible with network groups of the 7.0 release according
|
||||||
|
to the rules:
|
||||||
|
|
||||||
|
management: haproxy -> management
|
||||||
|
public: haproxy -> public
|
||||||
|
public: vrouter -> vrouter_pub
|
||||||
|
|
||||||
|
Note, that in the result VIPs are present only those IPs that
|
||||||
|
correspond to the given rules.
|
||||||
|
"""
|
||||||
|
rename_vip_rules = {
|
||||||
|
"management": {
|
||||||
|
"haproxy": "management",
|
||||||
|
"vrouter": "vrouter",
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"haproxy": "public",
|
||||||
|
"vrouter": "vrouter_pub",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
renamed_vips = collections.defaultdict(dict)
|
||||||
|
for ng_name, vips_obj in data.items():
|
||||||
|
|
||||||
|
ng_vip_rules = rename_vip_rules[ng_name]
|
||||||
|
for vip_name, vip_addr in vips_obj.items():
|
||||||
|
if vip_name not in ng_vip_rules:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_vip_name = ng_vip_rules[vip_name]
|
||||||
|
renamed_vips[ng_name][new_vip_name] = vip_addr
|
||||||
|
|
||||||
|
return renamed_vips
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(transformations.Manager):
|
||||||
|
default_config = {
|
||||||
|
'7.0': ['transform_vips']
|
||||||
|
}
|
||||||
|
name = 'vip'
|
|
@ -0,0 +1,53 @@
|
||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from cluster_upgrade import transformations
|
||||||
|
|
||||||
|
|
||||||
|
def transform_node_volumes(volumes):
|
||||||
|
try:
|
||||||
|
os_vg = next(vol for vol in volumes
|
||||||
|
if 'id' in vol and vol['id'] == 'os')
|
||||||
|
except StopIteration:
|
||||||
|
return volumes
|
||||||
|
|
||||||
|
other_volumes = [vol for vol in volumes
|
||||||
|
if 'id' not in vol or vol['id'] != 'os']
|
||||||
|
|
||||||
|
for disk in other_volumes:
|
||||||
|
disk_volumes = disk['volumes']
|
||||||
|
disk['volumes'] = []
|
||||||
|
|
||||||
|
for v in disk_volumes:
|
||||||
|
if v['type'] == 'pv' and v['vg'] == 'os' and v['size'] > 0:
|
||||||
|
for vv in os_vg['volumes']:
|
||||||
|
partition = {'name': vv['name'],
|
||||||
|
'size': vv['size'],
|
||||||
|
'type': 'partition',
|
||||||
|
'mount': vv['mount'],
|
||||||
|
'file_system': vv['file_system']}
|
||||||
|
disk['volumes'].append(partition)
|
||||||
|
else:
|
||||||
|
if v['type'] == 'lvm_meta_pool' or v['type'] == 'boot':
|
||||||
|
v['size'] = 0
|
||||||
|
disk['volumes'].append(v)
|
||||||
|
|
||||||
|
return volumes
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(transformations.Manager):
|
||||||
|
default_config = {
|
||||||
|
'6.1': ['node_volumes']
|
||||||
|
}
|
||||||
|
name = 'volumes'
|
|
@ -14,9 +14,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import collections
|
|
||||||
import copy
|
import copy
|
||||||
from distutils import version
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from nailgun import consts
|
from nailgun import consts
|
||||||
|
@ -24,7 +22,11 @@ from nailgun import objects
|
||||||
from nailgun.objects.serializers import network_configuration
|
from nailgun.objects.serializers import network_configuration
|
||||||
from nailgun import utils
|
from nailgun import utils
|
||||||
|
|
||||||
|
from . import transformations # That's weird, but that's how hacking likes
|
||||||
from .objects import adapters
|
from .objects import adapters
|
||||||
|
from .transformations import cluster as cluster_trs
|
||||||
|
from .transformations import vip
|
||||||
|
from .transformations import volumes as volumes_trs
|
||||||
|
|
||||||
|
|
||||||
def merge_attributes(a, b):
|
def merge_attributes(a, b):
|
||||||
|
@ -41,25 +43,9 @@ def merge_attributes(a, b):
|
||||||
for key, values in six.iteritems(pairs):
|
for key, values in six.iteritems(pairs):
|
||||||
if key != "metadata" and key in a_values:
|
if key != "metadata" and key in a_values:
|
||||||
values["value"] = a_values[key]["value"]
|
values["value"] = a_values[key]["value"]
|
||||||
# NOTE: In the mitaka-9.0 release types of values dns_list and
|
|
||||||
# ntp_list were changed from 'text'
|
|
||||||
# (a string of comma-separated IP-addresses)
|
|
||||||
# to 'text_list' (a list of strings of IP-addresses).
|
|
||||||
if a_values[key]['type'] == 'text' and \
|
|
||||||
values['type'] == 'text_list':
|
|
||||||
values["value"] = [
|
|
||||||
value.strip() for value in values['value'].split(',')
|
|
||||||
]
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
def merge_generated_attrs(new_attrs, orig_attrs):
|
|
||||||
# skip attributes that should be generated for new cluster
|
|
||||||
attrs = copy.deepcopy(orig_attrs)
|
|
||||||
attrs.pop('provision', None)
|
|
||||||
return utils.dict_merge(new_attrs, attrs)
|
|
||||||
|
|
||||||
|
|
||||||
def merge_nets(a, b):
|
def merge_nets(a, b):
|
||||||
new_settings = copy.deepcopy(b)
|
new_settings = copy.deepcopy(b)
|
||||||
source_networks = dict((n["name"], n) for n in a["networks"])
|
source_networks = dict((n["name"], n) for n in a["networks"])
|
||||||
|
@ -87,6 +73,9 @@ class UpgradeHelper(object):
|
||||||
consts.CLUSTER_NET_PROVIDERS.nova_network:
|
consts.CLUSTER_NET_PROVIDERS.nova_network:
|
||||||
network_configuration.NovaNetworkConfigurationSerializer,
|
network_configuration.NovaNetworkConfigurationSerializer,
|
||||||
}
|
}
|
||||||
|
cluster_transformations = transformations.Lazy(cluster_trs.Manager)
|
||||||
|
vip_transformations = transformations.Lazy(vip.Manager)
|
||||||
|
volumes_transformations = transformations.Lazy(volumes_trs.Manager)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clone_cluster(cls, orig_cluster, data):
|
def clone_cluster(cls, orig_cluster, data):
|
||||||
|
@ -110,61 +99,30 @@ class UpgradeHelper(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy_attributes(cls, orig_cluster, new_cluster):
|
def copy_attributes(cls, orig_cluster, new_cluster):
|
||||||
# TODO(akscram): Attributes should be copied including
|
attrs = cls.cluster_transformations.apply(
|
||||||
# borderline cases when some parameters are
|
orig_cluster.release.environment_version,
|
||||||
# renamed or moved into plugins. Also, we should
|
new_cluster.release.environment_version,
|
||||||
# to keep special steps in copying of parameters
|
{
|
||||||
# that know how to translate parameters from one
|
'editable': orig_cluster.editable_attrs,
|
||||||
# version to another. A set of this kind of steps
|
'generated': orig_cluster.generated_attrs,
|
||||||
# should define an upgrade path of a particular
|
},
|
||||||
# cluster.
|
)
|
||||||
new_cluster.generated_attrs = merge_generated_attrs(
|
|
||||||
|
new_cluster.generated_attrs = utils.dict_merge(
|
||||||
new_cluster.generated_attrs,
|
new_cluster.generated_attrs,
|
||||||
orig_cluster.generated_attrs)
|
attrs['generated'],
|
||||||
|
)
|
||||||
|
|
||||||
new_cluster.editable_attrs = merge_attributes(
|
new_cluster.editable_attrs = merge_attributes(
|
||||||
orig_cluster.editable_attrs,
|
attrs['editable'],
|
||||||
new_cluster.editable_attrs)
|
new_cluster.editable_attrs,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def change_env_settings(cls, orig_cluster, new_cluster):
|
def change_env_settings(cls, orig_cluster, new_cluster):
|
||||||
attrs = new_cluster.attributes
|
attrs = new_cluster.attributes
|
||||||
attrs['editable']['provision']['method']['value'] = 'image'
|
attrs['editable']['provision']['method']['value'] = 'image'
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def transform_vips_for_net_groups_70(cls, vips):
|
|
||||||
"""Rename or remove types of VIPs for 7.0 network groups.
|
|
||||||
|
|
||||||
This method renames types of VIPs from older releases (<7.0) to
|
|
||||||
be compatible with network groups of the 7.0 release according
|
|
||||||
to the rules:
|
|
||||||
|
|
||||||
management: haproxy -> management
|
|
||||||
public: haproxy -> public
|
|
||||||
public: vrouter -> vrouter_pub
|
|
||||||
|
|
||||||
Note, that in the result VIPs are present only those IPs that
|
|
||||||
correspond to the given rules.
|
|
||||||
"""
|
|
||||||
rename_vip_rules = {
|
|
||||||
"management": {
|
|
||||||
"haproxy": "management",
|
|
||||||
"vrouter": "vrouter",
|
|
||||||
},
|
|
||||||
"public": {
|
|
||||||
"haproxy": "public",
|
|
||||||
"vrouter": "vrouter_pub",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
renamed_vips = collections.defaultdict(dict)
|
|
||||||
for ng_name, vips in six.iteritems(vips):
|
|
||||||
ng_vip_rules = rename_vip_rules[ng_name]
|
|
||||||
for vip_name, vip_addr in six.iteritems(vips):
|
|
||||||
if vip_name not in ng_vip_rules:
|
|
||||||
continue
|
|
||||||
new_vip_name = ng_vip_rules[vip_name]
|
|
||||||
renamed_vips[ng_name][new_vip_name] = vip_addr
|
|
||||||
return renamed_vips
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy_network_config(cls, orig_cluster, new_cluster):
|
def copy_network_config(cls, orig_cluster, new_cluster):
|
||||||
nets_serializer = cls.network_serializers[orig_cluster.net_provider]
|
nets_serializer = cls.network_serializers[orig_cluster.net_provider]
|
||||||
|
@ -181,17 +139,16 @@ class UpgradeHelper(object):
|
||||||
orig_net_manager = orig_cluster.get_network_manager()
|
orig_net_manager = orig_cluster.get_network_manager()
|
||||||
new_net_manager = new_cluster.get_network_manager()
|
new_net_manager = new_cluster.get_network_manager()
|
||||||
|
|
||||||
vips = orig_net_manager.get_assigned_vips()
|
vips = {}
|
||||||
for ng_name in vips:
|
assigned_vips = orig_net_manager.get_assigned_vips()
|
||||||
if ng_name not in (consts.NETWORKS.public,
|
for ng_name in (consts.NETWORKS.public, consts.NETWORKS.management):
|
||||||
consts.NETWORKS.management):
|
vips[ng_name] = assigned_vips[ng_name]
|
||||||
vips.pop(ng_name)
|
|
||||||
# NOTE(akscram): In the 7.0 release was introduced networking
|
vips = cls.vip_transformations.apply(
|
||||||
# templates that use the vip_name column as
|
orig_cluster.release.environment_version,
|
||||||
# unique names of VIPs.
|
new_cluster.release.environment_version,
|
||||||
if version.LooseVersion(orig_cluster.release.environment_version) < \
|
vips
|
||||||
version.LooseVersion("7.0"):
|
)
|
||||||
vips = cls.transform_vips_for_net_groups_70(vips)
|
|
||||||
new_net_manager.assign_given_vips_for_net_groups(vips)
|
new_net_manager.assign_given_vips_for_net_groups(vips)
|
||||||
new_net_manager.assign_vips_for_net_groups()
|
new_net_manager.assign_vips_for_net_groups()
|
||||||
|
|
||||||
|
@ -224,6 +181,13 @@ class UpgradeHelper(object):
|
||||||
orig_cluster = adapters.NailgunClusterAdapter.get_by_uid(
|
orig_cluster = adapters.NailgunClusterAdapter.get_by_uid(
|
||||||
node.cluster_id)
|
node.cluster_id)
|
||||||
|
|
||||||
|
volumes = cls.volumes_transformations.apply(
|
||||||
|
orig_cluster.release.environment_version,
|
||||||
|
seed_cluster.release.environment_version,
|
||||||
|
node.get_volumes(),
|
||||||
|
)
|
||||||
|
node.set_volumes(volumes)
|
||||||
|
|
||||||
orig_manager = orig_cluster.get_network_manager()
|
orig_manager = orig_cluster.get_network_manager()
|
||||||
|
|
||||||
netgroups_id_mapping = cls.get_netgroups_id_mapping(
|
netgroups_id_mapping = cls.get_netgroups_id_mapping(
|
||||||
|
@ -231,10 +195,13 @@ class UpgradeHelper(object):
|
||||||
|
|
||||||
node.update_cluster_assignment(seed_cluster, roles, pending_roles)
|
node.update_cluster_assignment(seed_cluster, roles, pending_roles)
|
||||||
objects.Node.set_netgroups_ids(node, netgroups_id_mapping)
|
objects.Node.set_netgroups_ids(node, netgroups_id_mapping)
|
||||||
orig_manager.set_nic_assignment_netgroups_ids(
|
|
||||||
node, netgroups_id_mapping)
|
if not seed_cluster.network_template:
|
||||||
orig_manager.set_bond_assignment_netgroups_ids(
|
orig_manager.set_nic_assignment_netgroups_ids(
|
||||||
node, netgroups_id_mapping)
|
node, netgroups_id_mapping)
|
||||||
|
orig_manager.set_bond_assignment_netgroups_ids(
|
||||||
|
node, netgroups_id_mapping)
|
||||||
|
|
||||||
node.add_pending_change(consts.CLUSTER_CHANGES.interfaces)
|
node.add_pending_change(consts.CLUSTER_CHANGES.interfaces)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -62,6 +62,12 @@ class ClusterUpgradeValidator(base.BasicValidator):
|
||||||
"this release is equal or lower than the release of the "
|
"this release is equal or lower than the release of the "
|
||||||
"original cluster.".format(new_release.id),
|
"original cluster.".format(new_release.id),
|
||||||
log_message=True)
|
log_message=True)
|
||||||
|
if orig_release.operating_system != new_release.operating_system:
|
||||||
|
raise errors.InvalidData(
|
||||||
|
"Changing of operating system is not possible during upgrade "
|
||||||
|
"(from {0} to {1}).".format(orig_release.operating_system,
|
||||||
|
new_release.operating_system),
|
||||||
|
log_message=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_cluster_name(cls, cluster_name):
|
def validate_cluster_name(cls, cluster_name):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = fuel-nailgun-extension-cluster-upgrade
|
name = fuel-nailgun-extension-cluster-upgrade
|
||||||
summary = Cluster upgrade extension for Fuel
|
summary = Cluster upgrade extension for Fuel
|
||||||
|
description-file = README.rst
|
||||||
author = Mirantis Inc.
|
author = Mirantis Inc.
|
||||||
author-email = product@mirantis.com
|
author-email = product@mirantis.com
|
||||||
home-page = http://mirantis.com
|
home-page = http://mirantis.com
|
||||||
|
@ -24,3 +25,11 @@ packages =
|
||||||
[entry_points]
|
[entry_points]
|
||||||
nailgun.extensions =
|
nailgun.extensions =
|
||||||
cluster_upgrade = cluster_upgrade.extension:ClusterUpgradeExtension
|
cluster_upgrade = cluster_upgrade.extension:ClusterUpgradeExtension
|
||||||
|
nailgun.cluster_upgrade.transformations.volumes.6.1 =
|
||||||
|
node_volumes = cluster_upgrade.transformations.volumes:transform_node_volumes
|
||||||
|
nailgun.cluster_upgrade.transformations.cluster.9.0 =
|
||||||
|
dns_list = cluster_upgrade.transformations.cluster:transform_dns_list
|
||||||
|
ntp_list = cluster_upgrade.transformations.cluster:transform_ntp_list
|
||||||
|
drop_provision = cluster_upgrade.transformations.cluster:drop_generated_provision
|
||||||
|
nailgun.cluster_upgrade.transformations.vip.7.0 =
|
||||||
|
transform_vips = cluster_upgrade.transformations.vip:transform_vips
|
||||||
|
|
Loading…
Reference in New Issue