diff --git a/magnum/common/exception.py b/magnum/common/exception.py index 2979ab82af..33b2c36c48 100755 --- a/magnum/common/exception.py +++ b/magnum/common/exception.py @@ -397,3 +397,12 @@ class MemberAlreadyExists(Conflict): class PreDeletionFailed(Conflict): message = _("Failed to pre-delete resources for cluster %(cluster_uuid)s, " "error: %(msg)s.") + + +class NodeGroupAlreadyExists(Conflict): + message = _("A node group with name %(name)s already exists in the " + "cluster %(cluster_id)s.") + + +class NodeGroupNotFound(ResourceNotFound): + message = _("Nodegroup %(nodegroup)s could not be found.") diff --git a/magnum/db/api.py b/magnum/db/api.py index 0fc014617f..07e1c61f16 100644 --- a/magnum/db/api.py +++ b/magnum/db/api.py @@ -540,3 +540,118 @@ class Connection(object): :returns: A federation. :raises: FederationNotFound """ + + @abc.abstractmethod + def create_nodegroup(self, values): + """Create a new nodegroup in cluster. + + :param values: A dict containing several items used to identify + and track the nodegroup. + For example: + :: + + { + 'uuid': uuidutils.generate_uuid(), + 'name': 'example', + ... + } + + :returns: A nodegroup record. + """ + + @abc.abstractmethod + def destroy_nodegroup(self, cluster_id, nodegroup_id): + """Destroy a nodegroup. + + :param cluster_id: The uuid of the cluster where the nodegroup + belongs to. + :param nodegroup_id: The id or uuid of the nodegroup + """ + + @abc.abstractmethod + def update_nodegroup(self, cluster_id, nodegroup_id, values): + """Update properties of a nodegroup. + + :param cluster_id: The uuid of the cluster where the nodegroup + belongs to. + :param nodegroup_id: The id or uuid of a nodegroup. + :param values: A dict containing several items used to identify + and track the nodegroup. + For example: + :: + + { + 'uuid': uuidutils.generate_uuid(), + 'name': 'example', + ... + } + + :returns: A nodegroup record. + :raises: NodeGroupNotFound + """ + + @abc.abstractmethod + def get_nodegroup_by_id(self, context, cluster_id, nodegroup_id): + """Return a nodegroup for a given cluster uuid and nodegroup id. + + :param cluster_id: The uuid of the cluster where the nodegroup + belongs to. + :param nodegroup_id: The id of a nodegroup. + + :returns: A nodegroup record. + :raises: NodeGroupNotFound + """ + + @abc.abstractmethod + def get_nodegroup_by_uuid(self, context, cluster_id, nodegroup_uuid): + """Return a nodegroup for a given cluster uuid and nodegroup uuid. + + :param cluster_id: The uuid of the cluster where the nodegroup + belongs to. + :param nodegroup_uuid: The uuid of a nodegroup. + + :returns: A nodegroup record. + :raises: NodeGroupNotFound + """ + + @abc.abstractmethod + def get_nodegroup_by_name(self, context, cluster_id, nodegroup_name): + """Return a nodegroup for a given cluster uuid and nodegroup name. + + :param cluster_id: The uuid of the cluster where the nodegroup + belongs to. + :param nodegroup_name: The name of a nodegroup. + + :returns: A nodegroup record. + :raises: NodeGroupNotFound + """ + + @abc.abstractmethod + def list_cluster_nodegroups(self, context, cluster_id, filters=None, + limit=None, marker=None, sort_key=None, + sort_dir=None): + """Get matching nodegroups in a given cluster. + + :param context: The security context + :param cluster_id: The uuid of the cluster where the nodegroup + belongs to. + :param filters: Filters to apply. Defaults to None. + + :param limit: Maximum number of nodegroups to return. + :param marker: the last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted. + :param sort_dir: direction in which results should be sorted. + (asc, desc) + + :returns: A list of nodegroup records. + """ + + @abc.abstractmethod + def get_cluster_nodegroup_count(self, context, cluster_id): + """Get count of nodegroups in a given cluster. + + :param cluster_id: The uuid of the cluster where the nodegroup + belongs to. + :returns: Count of matching clusters. + """ diff --git a/magnum/db/sqlalchemy/alembic/versions/ac92cbae311c_add_nodegoup_table.py b/magnum/db/sqlalchemy/alembic/versions/ac92cbae311c_add_nodegoup_table.py new file mode 100644 index 0000000000..3cbc08a6dc --- /dev/null +++ b/magnum/db/sqlalchemy/alembic/versions/ac92cbae311c_add_nodegoup_table.py @@ -0,0 +1,61 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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. + +"""add nodegoup table + +Revision ID: ac92cbae311c +Revises: cbbc65a86986 +Create Date: 2018-09-20 15:26:00.869885 + +""" + +# revision identifiers, used by Alembic. +revision = 'ac92cbae311c' +down_revision = '87e62e3c7abc' + +from alembic import op + +import sqlalchemy as sa + +from oslo_db.sqlalchemy.types import String + +from magnum.db.sqlalchemy import models + + +def upgrade(): + op.create_table( + 'nodegroup', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', String(length=36), nullable=False), + sa.Column('name', String(length=255), nullable=False), + sa.Column('cluster_id', String(length=255), nullable=False), + sa.Column('project_id', String(length=255), nullable=False), + sa.Column('docker_volume_size', sa.Integer(), nullable=True), + sa.Column('labels', models.JSONEncodedDict, nullable=True), + sa.Column('flavor_id', String(length=255), nullable=True), + sa.Column('image_id', String(length=255), nullable=True), + sa.Column('node_addresses', models.JSONEncodedList(), nullable=True), + sa.Column('node_count', sa.Integer(), nullable=True), + sa.Column('max_node_count', sa.Integer(), nullable=True), + sa.Column('min_node_count', sa.Integer(), nullable=True), + sa.Column('role', String(length=255), nullable=True), + sa.Column('is_default', sa.Boolean(), default=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('uuid', name='uniq_nodegroup0uuid'), + sa.UniqueConstraint('cluster_id', 'name', + name='uniq_nodegroup0cluster_id0name'), + ) diff --git a/magnum/db/sqlalchemy/api.py b/magnum/db/sqlalchemy/api.py index cf8886be19..a0a28522f6 100644 --- a/magnum/db/sqlalchemy/api.py +++ b/magnum/db/sqlalchemy/api.py @@ -762,3 +762,115 @@ class Connection(api.Connection): ref.update(values) return ref + + def _add_nodegoup_filters(self, query, filters): + if filters is None: + filters = {} + + possible_filters = ["name", "node_count", "node_addresses", + "role", "is_default"] + + filter_names = set(filters).intersection(possible_filters) + filter_dict = {filter_name: filters[filter_name] + for filter_name in filter_names} + + query = query.filter_by(**filter_dict) + + return query + + def create_nodegroup(self, values): + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + nodegroup = models.NodeGroup() + nodegroup.update(values) + try: + nodegroup.save() + except db_exc.DBDuplicateEntry: + raise exception.NodeGroupAlreadyExists( + cluster_id=values['cluster_id'], name=values['name']) + return nodegroup + + def destroy_nodegroup(self, cluster_id, nodegroup_id): + session = get_session() + with session.begin(): + query = model_query(models.NodeGroup, session=session) + query = add_identity_filter(query, nodegroup_id) + query = query.filter_by(cluster_id=cluster_id) + try: + query.one() + except NoResultFound: + raise exception.NodeGroupNotFound(nodegroup=nodegroup_id) + query.delete() + + def update_nodegroup(self, cluster_id, nodegroup_id, values): + return self._do_update_nodegroup(cluster_id, nodegroup_id, values) + + def _do_update_nodegroup(self, cluster_id, nodegroup_id, values): + session = get_session() + with session.begin(): + query = model_query(models.NodeGroup, session=session) + query = add_identity_filter(query, nodegroup_id) + query = query.filter_by(cluster_id=cluster_id) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.NodeGroupNotFound(nodegroup=nodegroup_id) + + ref.update(values) + return ref + + def get_nodegroup_by_id(self, context, cluster_id, nodegroup_id): + query = model_query(models.NodeGroup) + if not context.is_admin: + query = query.filter_by(project_id=context.project_id) + query = query.filter_by(cluster_id=cluster_id) + query = query.filter_by(id=nodegroup_id) + try: + return query.one() + except NoResultFound: + raise exception.NodeGroupNotFound(nodegroup=nodegroup_id) + + def get_nodegroup_by_uuid(self, context, cluster_id, nodegroup_uuid): + query = model_query(models.NodeGroup) + if not context.is_admin: + query = query.filter_by(project_id=context.project_id) + query = query.filter_by(cluster_id=cluster_id) + query = query.filter_by(uuid=nodegroup_uuid) + try: + return query.one() + except NoResultFound: + raise exception.NodeGroupNotFound(nodegroup=nodegroup_uuid) + + def get_nodegroup_by_name(self, context, cluster_id, nodegroup_name): + query = model_query(models.NodeGroup) + if not context.is_admin: + query = query.filter_by(project_id=context.project_id) + query = query.filter_by(cluster_id=cluster_id) + query = query.filter_by(name=nodegroup_name) + try: + return query.one() + except MultipleResultsFound: + raise exception.Conflict('Multiple nodegroups exist with same ' + 'name. Please use the nodegroup uuid ' + 'instead.') + except NoResultFound: + raise exception.NodeGroupNotFound(nodegroup=nodegroup_name) + + def list_cluster_nodegroups(self, context, cluster_id, filters=None, + limit=None, marker=None, sort_key=None, + sort_dir=None): + query = model_query(models.NodeGroup) + if not context.is_admin: + query = query.filter_by(project_id=context.project_id) + query = self._add_nodegoup_filters(query, filters) + query = query.filter_by(cluster_id=cluster_id) + return _paginate_query(models.NodeGroup, limit, marker, + sort_key, sort_dir, query) + + def get_cluster_nodegroup_count(self, context, cluster_id): + query = model_query(models.NodeGroup) + if not context.is_admin: + query = query.filter_by(project_id=context.project_id) + query = query.filter_by(cluster_id=cluster_id) + return query.count() diff --git a/magnum/db/sqlalchemy/models.py b/magnum/db/sqlalchemy/models.py index 382e2a6cef..e43e0e30b9 100644 --- a/magnum/db/sqlalchemy/models.py +++ b/magnum/db/sqlalchemy/models.py @@ -260,3 +260,33 @@ class Federation(Base): status = Column(String(20)) status_reason = Column(Text) properties = Column(JSONEncodedDict) + + +class NodeGroup(Base): + """Represents a NodeGroup.""" + + __tablename__ = 'nodegroup' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_nodegroup0uuid'), + schema.UniqueConstraint( + 'cluster_id', 'name', + name='uniq_nodegroup0cluster_id0name'), + table_args() + ) + id = Column(Integer, primary_key=True) + uuid = Column(String(36)) + name = Column(String(255)) + cluster_id = Column(String(255)) + project_id = Column(String(255)) + docker_volume_size = Column(Integer(), nullable=True) + labels = Column(JSONEncodedDict, nullable=True) + flavor_id = Column(String(255), nullable=True) + image_id = Column(String(255), nullable=True) + node_addresses = Column(JSONEncodedList, nullable=True) + node_count = Column(Integer()) + role = Column(String(255)) + # NOTE(ttsiouts) We have to define the min and + # max number of nodes for each nodegroup + max_node_count = Column(Integer()) + min_node_count = Column(Integer()) + is_default = Column(Boolean, default=False) diff --git a/magnum/objects/__init__.py b/magnum/objects/__init__.py index 9371d3d67a..bcfce248d9 100644 --- a/magnum/objects/__init__.py +++ b/magnum/objects/__init__.py @@ -17,6 +17,7 @@ from magnum.objects import cluster from magnum.objects import cluster_template from magnum.objects import federation from magnum.objects import magnum_service +from magnum.objects import nodegroup from magnum.objects import quota from magnum.objects import stats from magnum.objects import x509keypair @@ -30,6 +31,7 @@ X509KeyPair = x509keypair.X509KeyPair Certificate = certificate.Certificate Stats = stats.Stats Federation = federation.Federation +NodeGroup = nodegroup.NodeGroup __all__ = (Cluster, ClusterTemplate, MagnumService, @@ -37,5 +39,6 @@ __all__ = (Cluster, Certificate, Stats, Quota, - Federation + Federation, + NodeGroup ) diff --git a/magnum/objects/nodegroup.py b/magnum/objects/nodegroup.py new file mode 100644 index 0000000000..0c3e1ddc72 --- /dev/null +++ b/magnum/objects/nodegroup.py @@ -0,0 +1,218 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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 oslo_utils import strutils +from oslo_utils import uuidutils +from oslo_versionedobjects import fields + +from magnum.db import api as dbapi +from magnum.objects import base + + +@base.MagnumObjectRegistry.register +class NodeGroup(base.MagnumPersistentObject, base.MagnumObject, + base.MagnumObjectDictCompat): + # Version 1.0: Initial version + + VERSION = '1.0' + + dbapi = dbapi.get_instance() + + fields = { + 'id': fields.IntegerField(), + 'uuid': fields.UUIDField(), + 'name': fields.StringField(), + 'cluster_id': fields.StringField(), + 'project_id': fields.StringField(), + 'docker_volume_size': fields.IntegerField(nullable=True), + 'labels': fields.DictOfStringsField(nullable=True), + 'flavor_id': fields.StringField(nullable=True), + 'image_id': fields.StringField(nullable=True), + 'node_addresses': fields.ListOfStringsField(nullable=True), + 'node_count': fields.IntegerField(nullable=False, default=1), + 'role': fields.StringField(), + 'max_node_count': fields.IntegerField(nullable=True), + 'min_node_count': fields.IntegerField(nullable=False, default=1), + 'is_default': fields.BooleanField(default=False) + } + + @staticmethod + def _from_db_object(nodegroup, db_nodegroup): + """Converts a database entity to a formal object.""" + for field in nodegroup.fields: + nodegroup[field] = db_nodegroup[field] + + nodegroup.obj_reset_changes() + return nodegroup + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Converts a list of database entities to a list of formal objects.""" + return [NodeGroup._from_db_object(cls(context), obj) + for obj in db_objects] + + @base.remotable_classmethod + def get(cls, context, cluster_id, nodegroup_id): + """Find a nodegroup based on its id or uuid and return a NodeGroup. + + :param cluster_id: the of id a cluster. + :param nodegroup_id: the id of a nodegroup. + :param context: Security context + :returns: a :class:`NodeGroup` object. + """ + if strutils.is_int_like(nodegroup_id): + return cls.get_by_id(context, cluster_id, nodegroup_id) + elif uuidutils.is_uuid_like(nodegroup_id): + return cls.get_by_uuid(context, cluster_id, nodegroup_id) + else: + return cls.get_by_name(context, cluster_id, nodegroup_id) + + @base.remotable_classmethod + def get_by_id(cls, context, cluster, id_): + """Find a nodegroup based on its integer id and return a NodeGroup. + + :param cluster: the id of a cluster. + :param id_: the id of a nodegroup. + :param context: Security context + :returns: a :class:`NodeGroup` object. + """ + db_nodegroup = cls.dbapi.get_nodegroup_by_id(context, cluster, id_) + nodegroup = NodeGroup._from_db_object(cls(context), db_nodegroup) + return nodegroup + + @base.remotable_classmethod + def get_by_uuid(cls, context, cluster, uuid): + """Find a nodegroup based on uuid and return a :class:`NodeGroup`. + + :param cluster: the id of a cluster. + :param uuid: the uuid of a nodegroup. + :param context: Security context + :returns: a :class:`NodeGroup` object. + """ + db_nodegroup = cls.dbapi.get_nodegroup_by_uuid(context, cluster, uuid) + nodegroup = NodeGroup._from_db_object(cls(context), db_nodegroup) + return nodegroup + + @base.remotable_classmethod + def get_by_name(cls, context, cluster, name): + """Find a nodegroup based on name and return a NodeGroup object. + + :param cluster: the id of a cluster. + :param name: the logical name of a nodegroup. + :param context: Security context + :returns: a :class:`NodeGroup` object. + """ + db_nodegroup = cls.dbapi.get_nodegroup_by_name(context, cluster, name) + nodegroup = NodeGroup._from_db_object(cls(context), db_nodegroup) + return nodegroup + + @base.remotable_classmethod + def get_count_all(cls, context, cluster_id): + """Get count of nodegroups in cluster. + + :param context: The security context + :param cluster_id: The uuid of the cluster + :returns: Count of nodegroups in the cluster. + """ + return cls.dbapi.get_cluster_nodegroup_count(context, cluster_id) + + @base.remotable_classmethod + def list(cls, context, cluster, limit=None, marker=None, + sort_key=None, sort_dir=None, filters=None): + """Return a list of NodeGroup objects. + + :param context: Security context. + :param cluster: The cluster uuid or name + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :param filters: filter dict, can includes 'name', 'node_count', + 'stack_id', 'node_addresses', + 'status'(should be a status list). + :returns: a list of :class:`NodeGroup` objects. + + """ + db_nodegroups = cls.dbapi.list_cluster_nodegroups(context, cluster, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir, + filters=filters) + return NodeGroup._from_db_object_list(db_nodegroups, cls, context) + + @base.remotable + def create(self, context=None): + """Create a nodegroup record in the DB. + + :param context: Security context + """ + values = self.obj_get_changes() + db_nodegroup = self.dbapi.create_nodegroup(values) + self._from_db_object(self, db_nodegroup) + + @base.remotable + def destroy(self, context=None): + """Delete the NodeGroup from the DB. + + :param context: Security context. + """ + self.dbapi.destroy_nodegroup(self.cluster_id, self.uuid) + self.obj_reset_changes() + + @base.remotable + def save(self, context=None): + """Save updates to this NodeGroup. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: Security context. + """ + updates = self.obj_get_changes() + self.dbapi.update_nodegroup(self.cluster_id, self.uuid, updates) + + self.obj_reset_changes() + + @base.remotable + def refresh(self, context=None): + """Loads updates for this NodeGroup. + + Loads a NodeGroup with the same uuid from the database and + checks for updated attributes. Updates are applied from + the loaded NogeGroup column by column, if there are any updates. + + :param context: Security context. + """ + current = self.__class__.get_by_uuid(self._context, + cluster=self.cluster_id, + uuid=self.uuid) + for field in self.fields: + if self.obj_attr_is_set(field) and self[field] != current[field]: + self[field] = current[field] + + @base.remotable_classmethod + def update_nodegroup(cls, context, cluster_id, nodegroup_id, values): + """Updates a NodeGroup. + + :param context: Security context. + :param cluster_id: + :param nodegroup_id: + :param values: a dictionary with the changed values + """ + current = cls.get(context, cluster_id, nodegroup_id) + db_nodegroup = cls.dbapi.update_nodegroup(cluster_id, current.uuid, + values) + return NodeGroup._from_db_object(cls(context), db_nodegroup) diff --git a/magnum/tests/unit/db/test_nodegroup.py b/magnum/tests/unit/db/test_nodegroup.py new file mode 100644 index 0000000000..627ed88c72 --- /dev/null +++ b/magnum/tests/unit/db/test_nodegroup.py @@ -0,0 +1,228 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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. + +"""Tests for manipulating NodeGroups via the DB API""" +from oslo_utils import uuidutils + +from magnum.common import exception +from magnum.tests.unit.db import base +from magnum.tests.unit.db import utils + + +class DbNodeGroupTestCase(base.DbTestCase): + + def test_create_nodegroup(self): + utils.create_test_nodegroup() + + def test_create_nodegroup_already_exists(self): + utils.create_test_nodegroup() + self.assertRaises(exception.NodeGroupAlreadyExists, + utils.create_test_nodegroup) + + def test_create_nodegroup_same_name_same_cluster(self): + # NOTE(ttsiouts): Don't allow the same name for nodegroups + # in the same cluster. + nodegroup = utils.create_test_nodegroup() + new = { + 'name': nodegroup.name, + 'id': nodegroup.id + 8, + 'cluster_id': nodegroup.cluster_id + } + self.assertRaises(exception.NodeGroupAlreadyExists, + utils.create_test_nodegroup, **new) + + def test_create_nodegroup_same_name_different_cluster(self): + # NOTE(ttsiouts): Verify nodegroups with the same name + # but in different clusters are allowed. + nodegroup = utils.create_test_nodegroup() + new = { + 'name': nodegroup.name, + 'id': nodegroup.id + 8, + 'cluster_id': 'fake-cluster-uuid', + 'uuid': 'fake-nodegroup-uuid', + 'project_id': nodegroup.project_id, + } + try: + utils.create_test_nodegroup(**new) + except Exception: + # Something went wrong, just fail the testcase + self.assertTrue(False) + + def test_get_nodegroup_by_id(self): + nodegroup = utils.create_test_nodegroup() + res = self.dbapi.get_nodegroup_by_id(self.context, + nodegroup.cluster_id, + nodegroup.id) + self.assertEqual(nodegroup.id, res.id) + self.assertEqual(nodegroup.uuid, res.uuid) + + def test_get_nodegroup_by_name(self): + nodegroup = utils.create_test_nodegroup() + res = self.dbapi.get_nodegroup_by_name(self.context, + nodegroup.cluster_id, + nodegroup.name) + self.assertEqual(nodegroup.name, res.name) + self.assertEqual(nodegroup.uuid, res.uuid) + + def test_get_cluster_by_uuid(self): + nodegroup = utils.create_test_nodegroup() + res = self.dbapi.get_nodegroup_by_uuid(self.context, + nodegroup.cluster_id, + nodegroup.uuid) + self.assertEqual(nodegroup.id, res.id) + self.assertEqual(nodegroup.uuid, res.uuid) + + def test_get_nodegroup_that_does_not_exist(self): + # Create a cluster with no nodegroups + cluster = utils.create_test_cluster() + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.get_nodegroup_by_id, + self.context, cluster.uuid, 100) + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.get_nodegroup_by_uuid, + self.context, cluster.uuid, + '12345678-9999-0000-aaaa-123456789012') + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.get_nodegroup_by_name, + self.context, cluster.uuid, 'not_found') + + def test_get_nodegroups_in_cluster(self): + uuids_in_cluster = [] + uuids_not_in_cluster = [] + cluster = utils.create_test_cluster(uuid=uuidutils.generate_uuid()) + for i in range(2): + ng = utils.create_test_nodegroup(uuid=uuidutils.generate_uuid(), + name='test%(id)s' % {'id': i}, + cluster_id=cluster.uuid) + uuids_in_cluster.append(ng.uuid) + for i in range(2): + ng = utils.create_test_nodegroup(uuid=uuidutils.generate_uuid(), + name='test%(id)s' % {'id': i}, + cluster_id='fake_cluster') + uuids_not_in_cluster.append(ng.uuid) + res = self.dbapi.list_cluster_nodegroups(self.context, cluster.uuid) + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids_in_cluster), sorted(res_uuids)) + for uuid in uuids_not_in_cluster: + self.assertNotIn(uuid, res_uuids) + + def test_get_cluster_list_sorted(self): + uuids = [] + cluster = utils.create_test_cluster(uuid=uuidutils.generate_uuid()) + for i in range(5): + ng = utils.create_test_nodegroup(uuid=uuidutils.generate_uuid(), + name='test%(id)s' % {'id': i}, + cluster_id=cluster.uuid) + uuids.append(ng.uuid) + res = self.dbapi.list_cluster_nodegroups(self.context, cluster.uuid, + sort_key='uuid') + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), res_uuids) + + self.assertRaises(exception.InvalidParameterValue, + self.dbapi.list_cluster_nodegroups, + self.context, + cluster.uuid, + sort_key='not-there') + + def test_get_nodegroup_list_with_filters(self): + cluster_dict = utils.get_test_cluster( + id=1, uuid=uuidutils.generate_uuid()) + cluster = self.dbapi.create_cluster(cluster_dict) + + group1 = utils.create_test_nodegroup( + name='group-one', + cluster_id=cluster.uuid, + flavor_id=1, + uuid=uuidutils.generate_uuid(), + node_count=1) + group2 = utils.create_test_nodegroup( + name='group-two', + cluster_id=cluster.uuid, + flavor_id=1, + uuid=uuidutils.generate_uuid(), + node_count=1) + group3 = utils.create_test_nodegroup( + name='group-four', + cluster_id=cluster.uuid, + flavor_id=2, + uuid=uuidutils.generate_uuid(), + node_count=3) + + filters = {'name': 'group-one'} + res = self.dbapi.list_cluster_nodegroups( + self.context, cluster.uuid, filters=filters) + self.assertEqual([group1.id], [r.id for r in res]) + + filters = {'node_count': 1} + res = self.dbapi.list_cluster_nodegroups( + self.context, cluster.uuid, filters=filters) + self.assertEqual([group1.id, group2.id], [r.id for r in res]) + + filters = {'flavor_id': 2, 'node_count': 3} + res = self.dbapi.list_cluster_nodegroups( + self.context, cluster.uuid, filters=filters) + self.assertEqual([group3.id], [r.id for r in res]) + + filters = {'name': 'group-five'} + res = self.dbapi.list_cluster_nodegroups( + self.context, cluster.uuid, filters=filters) + self.assertEqual([], [r.id for r in res]) + + def test_destroy_nodegroup(self): + cluster = utils.create_test_cluster() + nodegroup = utils.create_test_nodegroup() + self.assertEqual(nodegroup.uuid, self.dbapi.get_nodegroup_by_uuid( + self.context, cluster.uuid, nodegroup.uuid).uuid) + self.dbapi.destroy_nodegroup(cluster.uuid, nodegroup.uuid) + + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.get_nodegroup_by_uuid, + self.context, cluster.uuid, nodegroup.uuid) + + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.destroy_nodegroup, cluster.uuid, + nodegroup.uuid) + + def test_destroy_nodegroup_by_uuid(self): + cluster = utils.create_test_cluster() + nodegroup = utils.create_test_nodegroup() + self.assertIsNotNone(self.dbapi.get_nodegroup_by_uuid(self.context, + cluster.uuid, + nodegroup.uuid)) + self.dbapi.destroy_nodegroup(cluster.uuid, nodegroup.uuid) + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.get_nodegroup_by_uuid, self.context, + cluster.uuid, nodegroup.uuid) + + def test_destroy_cluster_by_uuid_that_does_not_exist(self): + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.destroy_nodegroup, 'c_uuid', + '12345678-9999-0000-aaaa-123456789012') + + def test_update_cluster(self): + nodegroup = utils.create_test_nodegroup() + old_flavor = nodegroup.flavor_id + new_flavor = 5 + self.assertNotEqual(old_flavor, new_flavor) + res = self.dbapi.update_nodegroup(nodegroup.cluster_id, nodegroup.id, + {'flavor_id': new_flavor}) + self.assertEqual(new_flavor, res.flavor_id) + + def test_update_nodegroup_not_found(self): + uuid = uuidutils.generate_uuid() + self.assertRaises(exception.NodeGroupNotFound, + self.dbapi.update_nodegroup, "c_uuid", uuid, + {'node_count': 5}) diff --git a/magnum/tests/unit/db/utils.py b/magnum/tests/unit/db/utils.py index 8db8b3ad16..ecef9106a5 100644 --- a/magnum/tests/unit/db/utils.py +++ b/magnum/tests/unit/db/utils.py @@ -267,3 +267,40 @@ def create_test_federation(**kw): del federation['id'] dbapi = db_api.get_instance() return dbapi.create_federation(federation) + + +def get_test_nodegroup(**kw): + return { + 'id': kw.get('id', 12), + 'uuid': kw.get('uuid', '483203a3-dbee-4a9c-9d65-9820512f4df8'), + 'name': kw.get('name', 'nodegroup1'), + 'cluster_id': kw.get('cluster_id', + '5d12f6fd-a196-4bf0-ae4c-1f639a523a52'), + 'project_id': kw.get('project_id', 'fake_project'), + 'docker_volume_size': kw.get('docker_volume_size'), + 'labels': kw.get('labels'), + 'flavor_id': kw.get('flavor_id', None), + 'image_id': kw.get('image_id', None), + 'node_addresses': kw.get('node_addresses', ['172.17.2.4']), + 'node_count': kw.get('node_count', 3), + 'role': kw.get('role', 'worker'), + 'max_node_count': kw.get('max_node_count', None), + 'min_node_count': kw.get('min_node_count', 1), + 'is_default': kw.get('is_default', True), + 'created_at': kw.get('created_at'), + 'updated_at': kw.get('updated_at') + } + + +def create_test_nodegroup(**kw): + """Create test nodegroup entry in DB and return federation DB object. + + :param kw: kwargs with overriding values for nodegroup attributes. + :return: Test nodegroup DB object. + """ + nodegroup = get_test_nodegroup(**kw) + # Let DB generate ID if it isn't specified explicitly + if 'id' not in kw: + del nodegroup['id'] + dbapi = db_api.get_instance() + return dbapi.create_nodegroup(nodegroup) diff --git a/magnum/tests/unit/objects/test_nodegroup.py b/magnum/tests/unit/objects/test_nodegroup.py new file mode 100644 index 0000000000..e1aedbbb64 --- /dev/null +++ b/magnum/tests/unit/objects/test_nodegroup.py @@ -0,0 +1,168 @@ +# Copyright (c) 2018 European Organization for Nuclear Research. +# All Rights Reserved. +# +# 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_utils import uuidutils +from testtools.matchers import HasLength + +from magnum import objects +from magnum.tests.unit.db import base +from magnum.tests.unit.db import utils + + +class TestNodeGroupObject(base.DbTestCase): + + def setUp(self): + super(TestNodeGroupObject, self).setUp() + self.fake_nodegroup = utils.get_test_nodegroup() + self.fake_nodegroup['docker_volume_size'] = 3 + self.fake_nodegroup['labels'] = {} + + def test_get_by_id(self): + nodegroup_id = self.fake_nodegroup['id'] + cluster_id = self.fake_nodegroup['cluster_id'] + with mock.patch.object(self.dbapi, 'get_nodegroup_by_id', + autospec=True) as mock_get_nodegroup: + mock_get_nodegroup.return_value = self.fake_nodegroup + nodegroup = objects.NodeGroup.get(self.context, cluster_id, + nodegroup_id) + mock_get_nodegroup.assert_called_once_with(self.context, + cluster_id, + nodegroup_id) + self.assertEqual(self.context, nodegroup._context) + + def test_get_by_uuid(self): + uuid = self.fake_nodegroup['uuid'] + cluster_id = self.fake_nodegroup['cluster_id'] + with mock.patch.object(self.dbapi, 'get_nodegroup_by_uuid', + autospec=True) as mock_get_nodegroup: + mock_get_nodegroup.return_value = self.fake_nodegroup + nodegroup = objects.NodeGroup.get(self.context, cluster_id, uuid) + mock_get_nodegroup.assert_called_once_with(self.context, + cluster_id, uuid) + self.assertEqual(self.context, nodegroup._context) + + def test_get_by_name(self): + name = self.fake_nodegroup['name'] + cluster_id = self.fake_nodegroup['cluster_id'] + with mock.patch.object(self.dbapi, 'get_nodegroup_by_name', + autospec=True) as mock_get_nodegroup: + mock_get_nodegroup.return_value = self.fake_nodegroup + nodegroup = objects.NodeGroup.get(self.context, cluster_id, name) + mock_get_nodegroup.assert_called_once_with(self.context, + cluster_id, name) + self.assertEqual(self.context, nodegroup._context) + + def test_list(self): + cluster_id = self.fake_nodegroup['cluster_id'] + with mock.patch.object(self.dbapi, 'list_cluster_nodegroups', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_nodegroup] + nodegroups = objects.NodeGroup.list(self.context, cluster_id) + self.assertEqual(1, mock_get_list.call_count) + mock_get_list.assert_called_once_with( + self.context, cluster_id, limit=None, marker=None, + filters=None, sort_dir=None, sort_key=None) + self.assertThat(nodegroups, HasLength(1)) + self.assertIsInstance(nodegroups[0], objects.NodeGroup) + self.assertEqual(self.context, nodegroups[0]._context) + + def test_list_with_filters(self): + cluster_id = self.fake_nodegroup['cluster_id'] + with mock.patch.object(self.dbapi, 'list_cluster_nodegroups', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_nodegroup] + filters = {'name': self.fake_nodegroup['name']} + nodegroups = objects.NodeGroup.list(self.context, cluster_id, + filters=filters) + self.assertEqual(1, mock_get_list.call_count) + mock_get_list.assert_called_once_with( + self.context, cluster_id, limit=None, marker=None, + filters=filters, sort_dir=None, sort_key=None) + self.assertThat(nodegroups, HasLength(1)) + self.assertIsInstance(nodegroups[0], objects.NodeGroup) + self.assertEqual(self.context, nodegroups[0]._context) + + def test_create(self): + with mock.patch.object(self.dbapi, 'create_nodegroup', + autospec=True) as mock_create_nodegroup: + mock_create_nodegroup.return_value = self.fake_nodegroup + nodegroup = objects.NodeGroup(self.context, **self.fake_nodegroup) + nodegroup.create() + mock_create_nodegroup.assert_called_once_with(self.fake_nodegroup) + self.assertEqual(self.context, nodegroup._context) + + def test_destroy(self): + uuid = self.fake_nodegroup['uuid'] + cluster_id = self.fake_nodegroup['cluster_id'] + with mock.patch.object(self.dbapi, 'get_nodegroup_by_uuid', + autospec=True) as mock_get_nodegroup: + mock_get_nodegroup.return_value = self.fake_nodegroup + with mock.patch.object(self.dbapi, 'destroy_nodegroup', + autospec=True) as mock_destroy_nodegroup: + nodegroup = objects.NodeGroup.get_by_uuid(self.context, + cluster_id, + uuid) + nodegroup.destroy() + mock_get_nodegroup.assert_called_once_with(self.context, + cluster_id, + uuid) + mock_destroy_nodegroup.assert_called_once_with(cluster_id, + uuid) + self.assertEqual(self.context, nodegroup._context) + + def test_save(self): + uuid = self.fake_nodegroup['uuid'] + cluster_id = self.fake_nodegroup['cluster_id'] + with mock.patch.object(self.dbapi, 'get_nodegroup_by_uuid', + autospec=True) as mock_get_nodegroup: + mock_get_nodegroup.return_value = self.fake_nodegroup + with mock.patch.object(self.dbapi, 'update_nodegroup', + autospec=True) as mock_update_nodegroup: + nodegroup = objects.NodeGroup.get_by_uuid(self.context, + cluster_id, + uuid) + nodegroup.node_count = 10 + nodegroup.save() + + mock_get_nodegroup.assert_called_once_with(self.context, + cluster_id, + uuid) + expected_changes = { + 'node_count': 10, + } + mock_update_nodegroup.assert_called_once_with( + cluster_id, uuid, expected_changes) + self.assertEqual(self.context, nodegroup._context) + + def test_refresh(self): + uuid = self.fake_nodegroup['uuid'] + cluster_id = self.fake_nodegroup['cluster_id'] + new_uuid = uuidutils.generate_uuid() + returns = [dict(self.fake_nodegroup, uuid=uuid), + dict(self.fake_nodegroup, uuid=new_uuid)] + expected = [mock.call(self.context, cluster_id, uuid), + mock.call(self.context, cluster_id, uuid)] + with mock.patch.object(self.dbapi, 'get_nodegroup_by_uuid', + side_effect=returns, + autospec=True) as mock_get_nodegroup: + nodegroup = objects.NodeGroup.get_by_uuid(self.context, + cluster_id, + uuid) + self.assertEqual(uuid, nodegroup.uuid) + nodegroup.refresh() + self.assertEqual(new_uuid, nodegroup.uuid) + self.assertEqual(expected, mock_get_nodegroup.call_args_list) + self.assertEqual(self.context, nodegroup._context) diff --git a/magnum/tests/unit/objects/test_objects.py b/magnum/tests/unit/objects/test_objects.py index b64dfc92de..63237833fb 100644 --- a/magnum/tests/unit/objects/test_objects.py +++ b/magnum/tests/unit/objects/test_objects.py @@ -363,7 +363,8 @@ object_data = { 'MagnumService': '1.0-2d397ec59b0046bd5ec35cd3e06efeca', 'Stats': '1.0-73a1cd6e3c0294c932a66547faba216c', 'Quota': '1.0-94e100aebfa88f7d8428e007f2049c18', - 'Federation': '1.0-166da281432b083f0e4b851336e12e20' + 'Federation': '1.0-166da281432b083f0e4b851336e12e20', + 'NodeGroup': '1.0-75e1378a800040312c59f89546a51d74' }