Extract cluster upgrade to a separate repository

cluster_upgrade extension will be developed by octane team in a separate
repository

Change-Id: Id0bc78478cf3f40767fed760cd54e487a934fa10
This commit is contained in:
Nikita Zubkov 2016-07-13 15:29:16 +03:00
parent ea6b83a15d
commit c3294d60ef
24 changed files with 0 additions and 1988 deletions

View File

@ -1,54 +0,0 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
sqlalchemy.url = driver://user:pass@localhost/dbname
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1,90 +0,0 @@
# Copyright 2015 Mirantis, Inc.
#
# 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 __future__ import with_statement
from alembic import context
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure(
url=config.get_main_option('sqlalchemy.url'),
version_table=config.get_main_option('version_table'))
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata,
version_table=config.get_main_option('version_table'))
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -1,24 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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.
"""cluster_upgrade
Revision ID: 3b5d115d7e49
Revises: None
Create Date: 2015-07-17 19:46:59.579553
"""
# revision identifiers, used by Alembic.
revision = '3b5d115d7e49'
down_revision = None
from alembic import context
from alembic import op
import sqlalchemy as sa
table_prefix = context.config.get_main_option('table_prefix')
table_upgrade_relation_name = '{0}relations'.format(table_prefix)
def upgrade():
op.create_table(
table_upgrade_relation_name,
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('orig_cluster_id', sa.Integer(), nullable=False),
sa.Column('seed_cluster_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('orig_cluster_id'),
sa.UniqueConstraint('seed_cluster_id'))
def downgrade():
op.drop_table(table_upgrade_relation_name)

View File

@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 os
from nailgun import extensions
from . import handlers
class ClusterUpgradeExtension(extensions.BaseExtension):
name = 'cluster_upgrade'
version = '0.0.1'
description = "Cluster Upgrade Extension"
urls = [
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/clone/?$',
'handler': handlers.ClusterUpgradeCloneHandler},
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/assign/?$',
'handler': handlers.NodeReassignHandler},
{'uri': r'/clusters/(?P<cluster_id>\d+)/upgrade/vips/?$',
'handler': handlers.CopyVIPsHandler},
]
@classmethod
def alembic_migrations_path(cls):
return os.path.join(os.path.dirname(__file__),
'alembic_migrations', 'migrations')
@classmethod
def on_cluster_delete(cls, cluster):
from .objects import relations
relations.UpgradeRelationObject.delete_relation(cluster.id)

View File

@ -1,141 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 six
from nailgun.api.v1.handlers import base
from nailgun import objects
from nailgun.task import manager
from . import upgrade
from . import validators
from .objects import adapters
class ClusterUpgradeCloneHandler(base.BaseHandler):
single = objects.Cluster
validator = validators.ClusterUpgradeValidator
@base.content
def POST(self, cluster_id):
"""Initialize the upgrade of the cluster.
Creates a new cluster with specified name and release_id. The
new cluster is created with parameters that are copied from the
cluster with the given cluster_id. The values of the generated
and editable attributes are just copied from one to the other.
:param cluster_id: ID of the cluster from which parameters would
be copied
:returns: JSON representation of the created cluster
:http: * 200 (OK)
* 400 (upgrade parameters are invalid)
* 404 (node or release not found in db)
"""
orig_cluster = adapters.NailgunClusterAdapter(
self.get_object_or_404(self.single, cluster_id))
request_data = self.checked_data(cluster=orig_cluster)
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 the given cluster.
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))
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', [])
roles, pending_roles = upgrade.UpgradeHelper.get_node_roles(
reprovision, node.roles, given_roles)
upgrade.UpgradeHelper.assign_node_to_cluster(
node, cluster, roles, pending_roles)
if reprovision:
self.handle_task(cluster_id, [node.node])
class CopyVIPsHandler(base.BaseHandler):
single = objects.Cluster
validator = validators.CopyVIPsValidator
@base.content
def POST(self, cluster_id):
"""Copy VIPs from original cluster to new one
Original cluster object is obtained from existing relation between
clusters that is created on cluster clone operation
:param cluster_id: id of cluster that VIPs must be copied to
:http: * 200 (OK)
* 400 (validation failed)
* 404 (seed cluster is not found)
"""
from .objects import relations
cluster = self.get_object_or_404(self.single, cluster_id)
relation = relations.UpgradeRelationObject.get_cluster_relation(
cluster.id)
self.checked_data(cluster=cluster, relation=relation)
# get original cluster object and create adapter with it
orig_cluster_adapter = \
adapters.NailgunClusterAdapter(
adapters.NailgunClusterAdapter.get_by_uid(
relation.orig_cluster_id)
)
seed_cluster_adapter = adapters.NailgunClusterAdapter(cluster)
upgrade.UpgradeHelper.copy_vips(orig_cluster_adapter,
seed_cluster_adapter)

View File

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 sqlalchemy import Column
from sqlalchemy import Integer
from nailgun.db.sqlalchemy.models.base import Base
from . import extension
class UpgradeRelation(Base):
__tablename__ = '{0}relations'.format(
extension.ClusterUpgradeExtension.table_prefix())
id = Column(Integer, primary_key=True)
orig_cluster_id = Column(Integer, unique=True, nullable=False)
seed_cluster_id = Column(Integer, unique=True, nullable=False)

View File

@ -1,211 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun import objects
class NailgunClusterAdapter(object):
def __init__(self, cluster):
self.cluster = cluster
@classmethod
def create(cls, data):
cluster = objects.Cluster.create(data)
return cls(cluster)
@property
def id(self):
return self.cluster.id
@property
def name(self):
return self.cluster.name
@property
def net_provider(self):
return self.cluster.net_provider
@property
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
@generated_attrs.setter
def generated_attrs(self, attrs):
self.cluster.attributes.generated = attrs
@property
def editable_attrs(self):
return self.cluster.attributes.editable
@property
def network_template(self):
return self.cluster.network_config.configuration_template
@editable_attrs.setter
def editable_attrs(self, attrs):
self.cluster.attributes.editable = attrs
def get_create_data(self):
return objects.Cluster.get_create_data(self.cluster)
def get_network_manager(self):
net_manager = objects.Cluster.get_network_manager(
instance=self.cluster)
return NailgunNetworkManager(self.cluster, net_manager)
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):
return objects.NetworkGroup.get_admin_network_group()
class NailgunReleaseAdapter(object):
def __init__(self, release):
self.release = release
@classmethod
def get_by_uid(cls, uid, fail_if_not_found=False):
release = objects.Release.get_by_uid(
uid, fail_if_not_found=fail_if_not_found)
return release
@property
def is_deployable(self):
return objects.Release.is_deployable(self.release)
@property
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
return self.release.__cmp__(other)
class NailgunNetworkManager(object):
def __init__(self, cluster, net_manager):
self.cluster = cluster
self.net_manager = net_manager
def update(self, network_configuration):
self.net_manager.update(self.cluster, network_configuration)
def get_assigned_vips(self):
return self.net_manager.get_assigned_vips(self.cluster)
def assign_vips_for_net_groups(self):
return self.net_manager.assign_vips_for_net_groups(self.cluster)
def assign_given_vips_for_net_groups(self, vips):
self.net_manager.assign_given_vips_for_net_groups(self.cluster, vips)
def set_node_netgroups_ids(self, node, mapping):
return objects.Node.set_netgroups_ids(node.node, mapping)
def set_nic_assignment_netgroups_ids(self, node, mapping):
return objects.Node.set_nic_assignment_netgroups_ids(
node.node, mapping)
def set_bond_assignment_netgroups_ids(self, node, mapping):
return objects.Node.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, 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)
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

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun.db import db
from .. import models
class UpgradeRelationObject(object):
@staticmethod
def _query_cluster_relations(cluster_id):
return db.query(models.UpgradeRelation).filter(
(models.UpgradeRelation.orig_cluster_id == cluster_id) |
(models.UpgradeRelation.seed_cluster_id == cluster_id))
@classmethod
def get_cluster_relation(cls, cluster_id):
return cls._query_cluster_relations(cluster_id).first()
@classmethod
def delete_relation(cls, cluster_id):
cls._query_cluster_relations(cluster_id).delete()
@classmethod
def is_cluster_in_upgrade(cls, cluster_id):
query = cls._query_cluster_relations(cluster_id).exists()
return db.query(query).scalar()
@classmethod
def create_relation(cls, orig_cluster_id, seed_cluster_id):
relation = models.UpgradeRelation(
orig_cluster_id=orig_cluster_id,
seed_cluster_id=seed_cluster_id)
db.add(relation)
db.flush()

View File

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2013 Mirantis, Inc.
#
# 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.
EXTENSION = "nailgun.extensions.cluster_upgrade."

View File

@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun import consts
from nailgun.test import base as nailgun_test_base
from .. import upgrade
from ..objects import adapters
class BaseCloneClusterTest(nailgun_test_base.BaseIntegrationTest):
helper = upgrade.UpgradeHelper
def setUp(self):
super(BaseCloneClusterTest, self).setUp()
self.src_release = self.env.create_release(
operating_system=consts.RELEASE_OS.ubuntu,
version="2014.2.2-6.1",
state=consts.RELEASE_STATES.manageonly
)
self.dst_release = self.env.create_release(
operating_system=consts.RELEASE_OS.ubuntu,
version="liberty-9.0",
)
self.src_cluster_db = self.env.create(
cluster_kwargs={
'api': False,
'release_id': self.src_release.id,
'net_provider': consts.CLUSTER_NET_PROVIDERS.neutron,
'net_l23_provider': consts.NEUTRON_L23_PROVIDERS.ovs,
},
nodes_kwargs=[{'roles': ['controller']}]
)
self.src_cluster = adapters.NailgunClusterAdapter(
self.src_cluster_db)
self.data = {
"name": "cluster-clone-{0}".format(self.src_cluster.id),
"release_id": self.dst_release.id,
}

View File

@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 alembic
from nailgun import db
from nailgun.db.migration import make_alembic_config_from_extension
from nailgun.test import base
from .. import extension
_test_revision = '3b5d115d7e49'
def setup_module(module):
alembic_config = make_alembic_config_from_extension(
extension.ClusterUpgradeExtension)
db.dropdb()
alembic.command.upgrade(alembic_config, _test_revision)
class TestAddRelations(base.BaseAlembicMigrationTest):
def test_works_without_core_migrations(self):
columns = [
t.name for t in
self.meta.tables['cluster_upgrade_relations'].columns]
self.assertItemsEqual(columns, [
'id',
'orig_cluster_id',
'seed_cluster_id',
])

View File

@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 mock
from nailgun.test.base import BaseTestCase
from .. import extension
from ..objects import relations
class TestExtension(BaseTestCase):
@mock.patch.object(relations.UpgradeRelationObject, "delete_relation")
def test_on_cluster_delete(self, mock_on_cluster_delete):
cluster = mock.Mock(id=42)
extension.ClusterUpgradeExtension.on_cluster_delete(cluster)
mock_on_cluster_delete.assert_called_once_with(42)

View File

@ -1,249 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 mock
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
class TestClusterUpgradeCloneHandler(tests_base.BaseCloneClusterTest):
def test_clone(self):
resp = self.app.post(
reverse("ClusterUpgradeCloneHandler",
kwargs={"cluster_id": self.src_cluster.id}),
jsonutils.dumps(self.data),
headers=self.default_headers)
body = resp.json_body
self.assertEqual(resp.status_code, 200)
self.assertEqual(body["name"],
"cluster-clone-{0}".format(self.src_cluster.id))
self.assertEqual(body["release_id"], self.dst_release.id)
def test_clone_cluster_not_found_error(self):
resp = self.app.post(
reverse("ClusterUpgradeCloneHandler",
kwargs={"cluster_id": 42}),
jsonutils.dumps(self.data),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 404)
self.assertEqual(resp.json_body["message"], "Cluster not found")
def test_clone_cluster_already_in_upgrade_error(self):
self.app.post(
reverse("ClusterUpgradeCloneHandler",
kwargs={"cluster_id": self.src_cluster.id}),
jsonutils.dumps(self.data),
headers=self.default_headers)
resp = self.app.post(
reverse("ClusterUpgradeCloneHandler",
kwargs={"cluster_id": self.src_cluster.id}),
jsonutils.dumps(self.data),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 400)
def test_clone_cluster_name_already_exists_error(self):
data = dict(self.data, name=self.src_cluster.name)
resp = self.app.post(
reverse("ClusterUpgradeCloneHandler",
kwargs={"cluster_id": self.src_cluster.id}),
jsonutils.dumps(data),
headers=self.default_headers,
expect_errors=True)
self.assertEqual(resp.status_code, 409)
class TestNodeReassignHandler(base.BaseIntegrationTest):
@mock.patch('nailgun.task.task.rpc.cast')
def test_node_reassign_handler(self, mcast):
cluster = self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': consts.NODE_STATUSES.ready}])
seed_cluster = self.env.create_cluster()
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)
@mock.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)
@mock.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):
cluster = self.env.create_cluster()
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):
cluster = self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': 'discover'}])
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):
cluster = self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': 'error',
'error_type': 'provision'}])
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):
cluster = self.env.create(
cluster_kwargs={'api': False},
nodes_kwargs=[{'status': 'ready'}])
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'])
def test_node_reassign_handler_with_empty_data(self):
cluster = self.env.create_cluster(api=False)
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster.id}),
"{}",
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)
def test_node_reassign_handler_with_empty_body(self):
cluster = self.env.create_cluster(api=False)
resp = self.app.post(
reverse('NodeReassignHandler',
kwargs={'cluster_id': cluster.id}),
"",
headers=self.default_headers,
expect_errors=True)
self.assertEqual(400, resp.status_code)
class TestCopyVipsHandler(base.BaseIntegrationTest):
def test_copy_vips_called(self):
from ..objects import relations
orig_cluster = self.env.create_cluster(api=False)
new_cluster = self.env.create_cluster(api=False)
relations.UpgradeRelationObject.create_relation(
orig_cluster.id, new_cluster.id)
with mock.patch('nailgun.extensions.cluster_upgrade.handlers'
'.upgrade.UpgradeHelper.copy_vips') as copy_vips_mc:
resp = self.app.post(
reverse(
'CopyVIPsHandler',
kwargs={'cluster_id': new_cluster.id}
),
headers=self.default_headers,
)
self.assertEqual(resp.status_code, 200)
self.assertTrue(copy_vips_mc.called)

View File

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun.test.base import BaseIntegrationTest
from .. import models
from ..objects import relations as objects
class TestUpgradeRelationObject(BaseIntegrationTest):
def test_get_and_create_relation(self):
objects.UpgradeRelationObject.create_relation(1, 2)
rel0 = objects.UpgradeRelationObject.get_cluster_relation(1)
self.assertEqual(rel0.orig_cluster_id, 1)
self.assertEqual(rel0.seed_cluster_id, 2)
rel1 = objects.UpgradeRelationObject.get_cluster_relation(2)
self.assertEqual(rel1.orig_cluster_id, 1)
self.assertEqual(rel1.seed_cluster_id, 2)
def test_is_cluster_in_upgrade(self):
objects.UpgradeRelationObject.create_relation(1, 2)
in_upgrade = objects.UpgradeRelationObject.is_cluster_in_upgrade
self.assertTrue(in_upgrade(1))
self.assertTrue(in_upgrade(2))
def test_is_cluster_not_in_upgrade(self):
self.assertEqual(self.db.query(models.UpgradeRelation).count(), 0)
in_upgrade = objects.UpgradeRelationObject.is_cluster_in_upgrade
self.assertFalse(in_upgrade(1))
self.assertFalse(in_upgrade(2))
def test_delete_relation(self):
objects.UpgradeRelationObject.create_relation(1, 2)
objects.UpgradeRelationObject.delete_relation(1)
self.assertEqual(self.db.query(models.UpgradeRelation).count(), 0)

View File

@ -1,212 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 six
from nailgun import consts
from nailgun.extensions.cluster_upgrade.upgrade import merge_attributes
from nailgun.extensions.network_manager.objects.serializers import \
network_configuration
from . import base as base_tests
from ..objects import adapters
from ..objects import relations
class TestUpgradeHelperCloneCluster(base_tests.BaseCloneClusterTest):
def setUp(self):
super(TestUpgradeHelperCloneCluster, self).setUp()
self.orig_net_manager = self.src_cluster.get_network_manager()
self.serialize_nets = network_configuration.\
NeutronNetworkConfigurationSerializer.\
serialize_for_cluster
self.public_net_data = {
"cidr": "192.168.42.0/24",
"gateway": "192.168.42.1",
"ip_ranges": [["192.168.42.5", "192.168.42.11"]],
}
def test_merge_attributes(self):
src_editable_attrs = {
"test":
{"metadata": "src_fake",
"key":
{"type": "text",
"value": "fake1, fake2,fake3 , fake4"},
"src_key": "src_data"
},
"repo_setup": "src_data"
}
new_editable_attrs = {
"test":
{"metadata": "new_fake",
"key":
{"type": "text_list",
"value": "fake"},
"new_key": "new_data"
},
"repo_setup": "new_data"
}
result = merge_attributes(src_editable_attrs, new_editable_attrs)
new_editable_attrs["test"]["key"]["value"] = \
[value.strip() for value in
src_editable_attrs["test"]["key"]["value"].split(',')]
self.assertEqual(result, new_editable_attrs)
def test_create_cluster_clone(self):
new_cluster = self.helper.create_cluster_clone(self.src_cluster,
self.data)
src_cluster_data = self.src_cluster.get_create_data()
new_cluster_data = new_cluster.get_create_data()
for key, value in src_cluster_data.items():
if key in ("name", "release_id"):
continue
self.assertEqual(value, new_cluster_data[key])
def test_copy_attributes(self):
new_cluster = self.helper.create_cluster_clone(self.src_cluster,
self.data)
self.assertNotEqual(self.src_cluster.generated_attrs,
new_cluster.generated_attrs)
# Do some unordinary changes
attrs = copy.deepcopy(self.src_cluster.editable_attrs)
attrs["access"]["user"]["value"] = "operator"
attrs["access"]["password"]["value"] = "secrete"
self.src_cluster.editable_attrs = attrs
self.helper.copy_attributes(self.src_cluster, new_cluster)
self.assertEqual(self.src_cluster.generated_attrs,
new_cluster.generated_attrs)
editable_attrs = self.src_cluster.editable_attrs
for section, params in six.iteritems(new_cluster.editable_attrs):
if section == "repo_setup":
continue
for key, value in six.iteritems(params):
if key == "metadata":
continue
self.assertEqual(editable_attrs[section][key]["value"],
value["value"])
def update_public_net_params(self, networks):
pub_net = self._get_pub_net(networks)
pub_net.update(self.public_net_data)
self.orig_net_manager.update(networks)
def _get_pub_net(self, networks):
return next(net for net in networks['networks'] if
net['name'] == consts.NETWORKS.public)
def test_copy_network_config(self):
new_cluster = self.helper.create_cluster_clone(self.src_cluster,
self.data)
# Do some unordinary changes to public network
nets = self.serialize_nets(self.src_cluster.cluster)
self.update_public_net_params(nets)
self.helper.copy_network_config(self.src_cluster, new_cluster)
new_nets = self.serialize_nets(new_cluster.cluster)
public_net = self._get_pub_net(new_nets)
self.assertEqual(public_net['cidr'], self.public_net_data['cidr'])
self.assertEqual(public_net['gateway'],
self.public_net_data['gateway'])
self.assertEqual(public_net['ip_ranges'],
self.public_net_data['ip_ranges'])
def test_copy_vips(self):
# save network information before node reassignment to seed cluster
# as after that no VIP will be allocated/serialized due to
# absence of assigned nodes for the source cluster
orig_nets = self.serialize_nets(self.src_cluster.cluster)
new_cluster = self.helper.clone_cluster(self.src_cluster, self.data)
# we have to move node to new cluster before VIP assignment
# because there is no point in the operation for a cluster
# w/o nodes
node = adapters.NailgunNodeAdapter(self.src_cluster.cluster.nodes[0])
self.helper.assign_node_to_cluster(node, new_cluster, node.roles, [])
self.helper.copy_vips(self.src_cluster, new_cluster)
new_nets = self.serialize_nets(new_cluster.cluster)
self.assertEqual(orig_nets["management_vip"],
new_nets["management_vip"])
self.assertEqual(orig_nets["management_vrouter_vip"],
new_nets["management_vrouter_vip"])
self.assertEqual(orig_nets["public_vip"],
new_nets["public_vip"])
self.assertEqual(orig_nets["public_vrouter_vip"],
new_nets["public_vrouter_vip"])
def test_clone_cluster(self):
self.orig_net_manager.assign_vips_for_net_groups()
new_cluster = self.helper.clone_cluster(self.src_cluster, self.data)
relation = relations.UpgradeRelationObject.get_cluster_relation(
self.src_cluster.id)
self.assertEqual(relation.orig_cluster_id, self.src_cluster.id)
self.assertEqual(relation.seed_cluster_id, new_cluster.id)
def _check_dns_and_ntp_list_values(self, new_cluster, dns_list, ntp_list):
self.assertEqual(
new_cluster.editable_attrs["external_ntp"]["ntp_list"]["value"],
ntp_list)
self.assertEqual(
new_cluster.editable_attrs["external_dns"]["dns_list"]["value"],
dns_list)
self.assertEqual(
new_cluster.editable_attrs["external_ntp"]["ntp_list"]["type"],
"text_list")
self.assertEqual(
new_cluster.editable_attrs["external_dns"]["dns_list"]["type"],
"text_list")
def test_cluster_copy_attrs_with_different_types_dns_and_ntp_lists(self):
attrs = copy.deepcopy(self.src_cluster.editable_attrs)
attrs["external_ntp"]["ntp_list"]["type"] = "text"
attrs["external_ntp"]["ntp_list"]["value"] = "1,2,3"
attrs["external_dns"]["dns_list"]["type"] = "text"
attrs["external_dns"]["dns_list"]["value"] = "4,5,6"
self.src_cluster.editable_attrs = attrs
new_cluster = self.helper.create_cluster_clone(
self.src_cluster, self.data)
self.helper.copy_attributes(self.src_cluster, new_cluster)
self._check_dns_and_ntp_list_values(
new_cluster, ["4", "5", "6"], ["1", "2", "3"])
def test_cluster_copy_attrs_with_same_types_dns_and_ntp_lists(self):
attrs = copy.deepcopy(self.src_cluster.editable_attrs)
attrs["external_ntp"]["ntp_list"]["type"] = "text_list"
attrs["external_ntp"]["ntp_list"]["value"] = ["1", "2", "3"]
attrs["external_dns"]["dns_list"]["type"] = "text_list"
attrs["external_dns"]["dns_list"]["value"] = ["4", "5", "6"]
self.src_cluster.editable_attrs = attrs
new_cluster = self.helper.create_cluster_clone(
self.src_cluster, self.data)
self.helper.copy_attributes(self.src_cluster, new_cluster)
self._check_dns_and_ntp_list_values(
new_cluster, ["4", "5", "6"], ["1", "2", "3"])

View File

@ -1,230 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 mock
from oslo_serialization import jsonutils
from nailgun import consts
from nailgun import errors
from nailgun.settings import settings
from nailgun.test import base
from .. import validators
from . import base as tests_base
from . import EXTENSION
from ..objects import relations
class TestClusterUpgradeValidator(tests_base.BaseCloneClusterTest):
validator = validators.ClusterUpgradeValidator
def test_validate_release_upgrade(self):
self.validator.validate_release_upgrade(self.src_release,
self.dst_release)
@mock.patch.dict(settings.VERSION, {'feature_groups': []})
def test_validate_release_upgrade_deprecated_release(self):
release_511 = self.env.create_release(
operating_system=consts.RELEASE_OS.ubuntu,
version="2014.1.3-5.1.1",
state=consts.RELEASE_STATES.manageonly
)
msg = "^Upgrade to the given release \({0}\).*is deprecated and " \
"cannot be installed\.$".format(self.src_release.id)
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate_release_upgrade(release_511,
self.src_release)
def test_validate_release_upgrade_to_older_release(self):
self.src_release.state = consts.RELEASE_STATES.available
msg = "^Upgrade to the given release \({0}\).*release is equal or " \
"lower than the release of the original cluster\.$" \
.format(self.src_release.id)
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate_release_upgrade(self.dst_release,
self.src_release)
def test_validate_cluster_name(self):
self.validator.validate_cluster_name("cluster-42")
def test_validate_cluster_name_already_exists(self):
msg = "^Environment with this name '{0}' already exists\.$"\
.format(self.src_cluster.name)
with self.assertRaisesRegexp(errors.AlreadyExists, msg):
self.validator.validate_cluster_name(self.src_cluster.name)
def test_validate_cluster_status(self):
self.validator.validate_cluster_status(self.src_cluster)
def test_validate_cluster_status_invalid(self):
dst_cluster = self.env.create_cluster(
api=False,
release_id=self.dst_release.id,
)
relations.UpgradeRelationObject.create_relation(self.src_cluster.id,
dst_cluster.id)
msg = "^Upgrade is not possible because of the original cluster " \
"\({0}\) is already involved in the upgrade routine\.$" \
.format(self.src_cluster.id)
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate_cluster_status(self.src_cluster)
def test_validate(self):
data = jsonutils.dumps(self.data)
self.validator.validate(data, self.src_cluster)
def test_validate_invalid_data(self):
data = "{}"
with self.assertRaises(errors.InvalidData):
self.validator.validate(data, self.src_cluster)
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)
def test_validate_empty_data(self):
cluster = self.env.create_cluster(api=False)
node = self.env.create_node(cluster_id=cluster.id,
roles=["compute"],
status="ready")
msg = "^'node_id' is a required property"
with self.assertRaisesRegexp(errors.InvalidData, msg):
self.validator.validate("{}", node)
def test_validate_empty_body(self):
cluster = self.env.create_cluster(api=False)
node = self.env.create_node(cluster_id=cluster.id,
roles=["compute"],
status="ready")
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'],
})
with self.assertRaises(errors.InvalidData) as exc:
self.validator.validate(data, self.dst_cluster)
self.assertEqual(
exc.exception.message,
"Role 'controller' in conflict with role 'compute'."
)
class TestCopyVIPsValidator(base.BaseTestCase):
validator = validators.CopyVIPsValidator
def test_non_existing_relation_fail(self):
with self.assertRaises(errors.InvalidData) as cm:
self.validator.validate(data=None, cluster=None, relation=None)
self.assertEqual(
cm.exception.message,
"Relation for given cluster does not exist"
)
def test_cluster_is_not_seed(self):
cluster = self.env.create_cluster(api=False)
seed_cluster = self.env.create_cluster(api=False)
relations.UpgradeRelationObject.create_relation(
orig_cluster_id=cluster.id,
seed_cluster_id=cluster.id,
)
relation = relations.UpgradeRelationObject.get_cluster_relation(
cluster.id)
with self.assertRaises(errors.InvalidData) as cm:
self.validator.validate(data=None, cluster=seed_cluster,
relation=relation)
self.assertEqual(
cm.exception.message,
"Given cluster is not seed cluster"
)

View File

@ -1,238 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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
import copy
from distutils import version
import six
from nailgun import consts
from nailgun.extensions.network_manager.objects.serializers import \
network_configuration
from nailgun import objects
from nailgun import utils
from .objects import adapters
def merge_attributes(a, b):
"""Merge values of editable attributes.
The values of the b attributes have precedence over the values
of the a attributes.
"""
attrs = copy.deepcopy(b)
for section, pairs in six.iteritems(attrs):
if section == "repo_setup" or section not in a:
continue
a_values = a[section]
for key, values in six.iteritems(pairs):
if key != "metadata" and key in a_values:
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
def merge_nets(a, b):
new_settings = copy.deepcopy(b)
source_networks = dict((n["name"], n) for n in a["networks"])
for net in new_settings["networks"]:
if net["name"] not in source_networks:
continue
source_net = source_networks[net["name"]]
for key, value in six.iteritems(net):
if (key not in ("cluster_id", "id", "meta", "group_id") and
key in source_net):
net[key] = source_net[key]
networking_params = new_settings["networking_parameters"]
source_params = a["networking_parameters"]
for key, value in six.iteritems(networking_params):
if key not in source_params:
continue
networking_params[key] = source_params[key]
return new_settings
class UpgradeHelper(object):
network_serializers = {
consts.CLUSTER_NET_PROVIDERS.neutron:
network_configuration.NeutronNetworkConfigurationSerializer,
consts.CLUSTER_NET_PROVIDERS.nova_network:
network_configuration.NovaNetworkConfigurationSerializer,
}
@classmethod
def clone_cluster(cls, orig_cluster, data):
from .objects import relations
new_cluster = cls.create_cluster_clone(orig_cluster, data)
cls.copy_attributes(orig_cluster, new_cluster)
cls.copy_network_config(orig_cluster, new_cluster)
relations.UpgradeRelationObject.create_relation(orig_cluster.id,
new_cluster.id)
return new_cluster
@classmethod
def create_cluster_clone(cls, orig_cluster, data):
create_data = orig_cluster.get_create_data()
create_data["name"] = data["name"]
create_data["release_id"] = data["release_id"]
new_cluster = adapters.NailgunClusterAdapter.create(create_data)
return new_cluster
@classmethod
def copy_attributes(cls, orig_cluster, new_cluster):
# TODO(akscram): Attributes should be copied including
# borderline cases when some parameters are
# renamed or moved into plugins. Also, we should
# to keep special steps in copying of parameters
# that know how to translate parameters from one
# version to another. A set of this kind of steps
# should define an upgrade path of a particular
# cluster.
new_cluster.generated_attrs = utils.dict_merge(
new_cluster.generated_attrs,
orig_cluster.generated_attrs)
new_cluster.editable_attrs = merge_attributes(
orig_cluster.editable_attrs,
new_cluster.editable_attrs)
@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
def copy_network_config(cls, orig_cluster, new_cluster):
nets_serializer = cls.network_serializers[orig_cluster.net_provider]
nets = merge_nets(
nets_serializer.serialize_for_cluster(orig_cluster.cluster),
nets_serializer.serialize_for_cluster(new_cluster.cluster))
new_net_manager = new_cluster.get_network_manager()
new_net_manager.update(nets)
@classmethod
def copy_vips(cls, orig_cluster, new_cluster):
orig_net_manager = orig_cluster.get_network_manager()
new_net_manager = new_cluster.get_network_manager()
vips = {}
assigned_vips = orig_net_manager.get_assigned_vips()
for ng_name in (consts.NETWORKS.public, consts.NETWORKS.management):
vips[ng_name] = assigned_vips[ng_name]
# NOTE(akscram): In the 7.0 release was introduced networking
# templates that use the vip_name column as
# unique names of VIPs.
if version.LooseVersion(orig_cluster.release.environment_version) < \
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_vips_for_net_groups()
@classmethod
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)
orig_manager = orig_cluster.get_network_manager()
netgroups_id_mapping = cls.get_netgroups_id_mapping(
orig_cluster, seed_cluster)
node.update_cluster_assignment(seed_cluster, roles, pending_roles)
objects.Node.set_netgroups_ids(node, netgroups_id_mapping)
if not seed_cluster.network_template:
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

@ -1,164 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# 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 nailgun.api.v1.validators import assignment
from nailgun.api.v1.validators import base
from nailgun import consts
from nailgun import errors
from nailgun import objects
from .objects import adapters
class ClusterUpgradeValidator(base.BasicValidator):
schema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Start upgrade procedure for a cluster",
"description": "Serialized parameters to upgrade a cluster.",
"type": "object",
"properties": {
"name": {"type": "string"},
"release_id": {"type": "number"},
},
"required": ["name", "release_id"],
}
@classmethod
def validate(cls, data, cluster):
cluster = adapters.NailgunClusterAdapter(cluster)
data = super(ClusterUpgradeValidator, cls).validate(data)
cls.validate_schema(data, cls.schema)
cls.validate_cluster_status(cluster)
cls.validate_cluster_name(data["name"])
release = adapters.NailgunReleaseAdapter.get_by_uid(
data["release_id"], fail_if_not_found=True)
cls.validate_release_upgrade(cluster.release, release)
return data
@classmethod
def validate_release_upgrade(cls, orig_release, new_release):
if not objects.Release.is_deployable(new_release):
raise errors.InvalidData(
"Upgrade to the given release ({0}) is not possible because "
"this release is deprecated and cannot be installed."
.format(new_release.id),
log_message=True)
if orig_release >= new_release:
raise errors.InvalidData(
"Upgrade to the given release ({0}) is not possible because "
"this release is equal or lower than the release of the "
"original cluster.".format(new_release.id),
log_message=True)
@classmethod
def validate_cluster_name(cls, cluster_name):
clusters = objects.ClusterCollection.filter_by(None,
name=cluster_name)
if clusters.first():
raise errors.AlreadyExists(
"Environment with this name '{0}' already exists."
.format(cluster_name),
log_message=True)
@classmethod
def validate_cluster_status(cls, cluster):
from .objects.relations import UpgradeRelationObject
if UpgradeRelationObject.is_cluster_in_upgrade(cluster.id):
raise errors.InvalidData(
"Upgrade is not possible because of the original cluster ({0})"
" is already involved in the upgrade routine."
.format(cluster.id),
log_message=True)
class NodeReassignValidator(assignment.NodeAssignmentValidator):
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"},
"reprovision": {"type": "boolean", "default": True},
"roles": {"type": "array",
"items": {"type": "string"},
"uniqueItems": True},
},
"required": ["node_id"],
}
@classmethod
def validate(cls, data, cluster):
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)
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):
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)
class CopyVIPsValidator(base.BasicValidator):
@classmethod
def validate(cls, data, cluster, relation):
if relation is None:
raise errors.InvalidData(
"Relation for given cluster does not exist"
)
if cluster.id != relation.seed_cluster_id:
raise errors.InvalidData("Given cluster is not seed cluster")
return data

View File

@ -88,8 +88,6 @@ if __name__ == "__main__":
'delete_expired_oswl_entries'),
],
'nailgun.extensions': [
('cluster_upgrade = nailgun.extensions.cluster_upgrade'
'.extension:ClusterUpgradeExtension'),
('volume_manager = nailgun.extensions.volume_manager'
'.extension:VolumeManagerExtension'),
('network_manager = nailgun.extensions.network_manager'