From 7255e4432818614e9fbbfdaaa47f9fdd396e6919 Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Tue, 21 Feb 2017 18:02:38 -0600 Subject: [PATCH] Add compute node to DB and objects Change-Id: Ic196736ff263220eb83acce72772f0ebcc462b9f --- zun/common/exception.py | 8 + zun/db/api.py | 94 +++++++++- .../eeac0d191f5a_add_compute_node_table.py | 42 +++++ zun/db/sqlalchemy/api.py | 91 +++++++++- zun/db/sqlalchemy/models.py | 12 ++ zun/objects/__init__.py | 4 +- zun/objects/compute_node.py | 142 +++++++++++++++ zun/objects/numa.py | 17 +- .../container/docker/test_docker_driver.py | 4 +- zun/tests/unit/db/test_compute_host.py | 161 ++++++++++++++++++ zun/tests/unit/db/utils.py | 33 ++++ zun/tests/unit/objects/test_compute_node.py | 137 +++++++++++++++ zun/tests/unit/objects/test_objects.py | 1 + 13 files changed, 729 insertions(+), 17 deletions(-) create mode 100644 zun/db/sqlalchemy/alembic/versions/eeac0d191f5a_add_compute_node_table.py create mode 100644 zun/objects/compute_node.py create mode 100644 zun/tests/unit/db/test_compute_host.py create mode 100644 zun/tests/unit/objects/test_compute_node.py diff --git a/zun/common/exception.py b/zun/common/exception.py index bfa9e19e4..ce0b1a644 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -341,6 +341,10 @@ class ContainerNotFound(HTTPNotFound): message = _("Container %(container)s could not be found.") +class ComputeNodeNotFound(HTTPNotFound): + message = _("Compute node %(compute_node)s could not be found.") + + class ImageNotFound(HTTPNotFound): message = _("Image %(image)s could not be found.") @@ -369,6 +373,10 @@ class ContainerAlreadyExists(ResourceExists): message = _("A container with %(field)s %(value)s already exists.") +class ComputeNodeAlreadyExists(ResourceExists): + message = _("A compute node with %(field)s %(value)s already exists.") + + class ImageAlreadyExists(ResourceExists): message = _("An image with tag %(tag)s and repo %(repo)s already exists.") diff --git a/zun/db/api.py b/zun/db/api.py index 21f96e5ce..def05e029 100644 --- a/zun/db/api.py +++ b/zun/db/api.py @@ -55,6 +55,7 @@ def list_containers(context, filters=None, limit=None, marker=None, :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 tuples of the specified columns. """ @@ -65,6 +66,7 @@ def list_containers(context, filters=None, limit=None, marker=None, def create_container(context, values): """Create a new container. + :param context: The security context :param values: A dict containing several items used to identify and track the container, and several dicts which are passed @@ -180,7 +182,7 @@ def list_zun_services(context, filters=None, limit=None, :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. + :param sort_dir: Direction in which results should be sorted. (asc, desc) :returns: A list of tuples of the specified columns. """ @@ -242,7 +244,8 @@ def list_images(context, filters=None, :param marker: the last item of the previous page; we return the next :param sort_key: Attribute by which results should be sorted. - (asc, desc) + :param sort_dir: Direction in which results should be sorted. + (asc, desc) :returns: A list of tuples of the specified columns. """ return _get_dbdriver_instance().list_images( @@ -291,6 +294,7 @@ def list_resource_providers(context, filters=None, limit=None, marker=None, def create_resource_provider(context, values): """Create a new resource provider. + :param context: The security context :param values: A dict containing several items used to identify and track the resource provider, and several dicts which are passed into the Drivers when managing this resource @@ -344,7 +348,8 @@ def list_resource_classes(context, limit=None, marker=None, sort_key=None, :param marker: the last item of the previous page; we return the next :param sort_key: Attribute by which results should be sorted. - (asc, desc) + :param sort_dir: Direction in which results should be sorted. + (asc, desc) :returns: A list of tuples of the specified columns. """ return _get_dbdriver_instance().list_resource_classes( @@ -354,6 +359,7 @@ def list_resource_classes(context, limit=None, marker=None, sort_key=None, def create_resource_class(context, values): """Create a new resource class. + :param context: The security context :param values: A dict containing several items used to identify and track the resource class, and several dicts which are passed into the Drivers when managing this resource class. @@ -408,6 +414,7 @@ def list_inventories(context, filters=None, limit=None, marker=None, :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 tuples of the specified columns. """ @@ -418,10 +425,11 @@ def list_inventories(context, filters=None, limit=None, marker=None, def create_inventory(context, provider_id, values): """Create a new inventory. + :param context: The security context + :param provider_id: The id of a resource provider. :param values: A dict containing several items used to identify and track the inventory, and several dicts which are passed into the Drivers when managing this inventory. - :param provider_id: The id of a resource provider. :returns: An inventory. """ return _get_dbdriver_instance().create_inventory( @@ -473,6 +481,7 @@ def list_allocations(context, filters=None, limit=None, marker=None, :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 tuples of the specified columns. """ @@ -483,10 +492,10 @@ def list_allocations(context, filters=None, limit=None, marker=None, def create_allocation(context, values): """Create a new allocation. + :param context: The security context :param values: A dict containing several items used to identify and track the allocation, and several dicts which are passed into the Drivers when managing this allocation. - :param provider_id: The id of a resource provider. :returns: An allocation. """ return _get_dbdriver_instance().create_allocation(context, values) @@ -522,3 +531,78 @@ def update_allocation(context, allocation_id, values): """ return _get_dbdriver_instance().update_allocation( context, allocation_id, values) + + +def list_compute_nodes(context, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + """List matching compute nodes. + + Return a list of the specified columns for all compute nodes that match + the specified filters. + :param context: The security context + :param filters: Filters to apply. Defaults to None. + :param limit: Maximum number of compute nodes 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 tuples of the specified columns. + """ + return _get_dbdriver_instance().list_compute_nodes( + context, filters, limit, marker, sort_key, sort_dir) + + +def create_compute_node(context, values): + """Create a new compute node. + + :param context: The security context + :param values: A dict containing several items used to identify + and track the compute node, and several dicts which are + passed into the Drivers when managing this compute node. + :returns: A compute node. + """ + return _get_dbdriver_instance().create_compute_node(context, values) + + +def get_compute_node(context, node_uuid): + """Return a compute node. + + :param context: The security context + :param node_uuid: The uuid of a compute node. + :returns: A compute node. + """ + return _get_dbdriver_instance().get_compute_node(context, node_uuid) + + +def get_compute_node_by_hostname(context, hostname): + """Return a compute node. + + :param context: The security context + :param hostname: The hostname of a compute node. + :returns: A compute node. + """ + return _get_dbdriver_instance().get_compute_node_by_hostname( + context, hostname) + + +def destroy_compute_node(context, node_uuid): + """Destroy a compute node and all associated interfaces. + + :param context: Request context + :param node_uuid: The uuid of a compute node. + """ + return _get_dbdriver_instance().destroy_compute_node(context, node_uuid) + + +def update_compute_node(context, node_uuid, values): + """Update properties of a compute node. + + :context: Request context + :param node_uuid: The uuid of a compute node. + :values: The properties to be updated + :returns: A compute node. + :raises: ComputeNodeNotFound + """ + return _get_dbdriver_instance().update_compute_node( + context, node_uuid, values) diff --git a/zun/db/sqlalchemy/alembic/versions/eeac0d191f5a_add_compute_node_table.py b/zun/db/sqlalchemy/alembic/versions/eeac0d191f5a_add_compute_node_table.py new file mode 100644 index 000000000..ee7f893d4 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/eeac0d191f5a_add_compute_node_table.py @@ -0,0 +1,42 @@ +# 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 compute node table + +Revision ID: eeac0d191f5a +Revises: 8192905fd835 +Create Date: 2017-02-28 21:32:58.122924 + +""" + +# revision identifiers, used by Alembic. +revision = 'eeac0d191f5a' +down_revision = '8192905fd835' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + +from zun.db.sqlalchemy import models + + +def upgrade(): + op.create_table( + 'compute_node', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('uuid', sa.String(length=36), nullable=False), + sa.Column('hostname', sa.String(length=255), nullable=False), + sa.Column('numa_topology', models.JSONEncodedDict(), nullable=True), + sa.PrimaryKeyConstraint('uuid'), + ) diff --git a/zun/db/sqlalchemy/api.py b/zun/db/sqlalchemy/api.py index d7f9ae7fc..a4c11825d 100644 --- a/zun/db/sqlalchemy/api.py +++ b/zun/db/sqlalchemy/api.py @@ -87,10 +87,10 @@ def add_identity_filter(query, value): def _paginate_query(model, limit=None, marker=None, sort_key=None, - sort_dir=None, query=None): + sort_dir=None, query=None, default_sort_key='id'): if not query: query = model_query(model) - sort_keys = ['id'] + sort_keys = [default_sort_key] if sort_key and sort_key not in sort_keys: sort_keys.insert(0, sort_key) try: @@ -300,7 +300,7 @@ class Connection(object): return _paginate_query(models.ZunService, query=query) def pull_image(self, context, values): - # ensure defaults are present for new containers + # ensure defaults are present for new images if not values.get('uuid'): values['uuid'] = uuidutils.generate_uuid() image = models.Image() @@ -652,3 +652,88 @@ class Connection(object): ref.update(values) return ref + + def _add_compute_nodes_filters(self, query, filters): + if filters is None: + filters = {} + + filter_names = ['hostname'] + for name in filter_names: + if name in filters: + query = query.filter_by(**{name: filters[name]}) + + return query + + def list_compute_nodes(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + query = model_query(models.ComputeNode) + query = self._add_compute_nodes_filters(query, filters) + return _paginate_query(models.ComputeNode, limit, marker, + sort_key, sort_dir, query, + default_sort_key='uuid') + + def create_compute_node(self, context, values): + # ensure defaults are present for new compute nodes + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + compute_node = models.ComputeNode() + compute_node.update(values) + try: + compute_node.save() + except db_exc.DBDuplicateEntry: + raise exception.ComputeNodeAlreadyExists( + field='UUID', value=values['uuid']) + return compute_node + + def get_compute_node(self, context, node_uuid): + query = model_query(models.ComputeNode) + query = query.filter_by(uuid=node_uuid) + try: + return query.one() + except NoResultFound: + raise exception.ComputeNodeNotFound( + compute_node=node_uuid) + + def get_compute_node_by_hostname(self, context, hostname): + query = model_query(models.ComputeNode) + query = query.filter_by(hostname=hostname) + try: + return query.one() + except NoResultFound: + raise exception.ComputeNodeNotFound( + compute_node=hostname) + except MultipleResultsFound: + raise exception.Conflict('Multiple compute nodes exist with same ' + 'hostname. Please use the uuid instead.') + + def destroy_compute_node(self, context, node_uuid): + session = get_session() + with session.begin(): + query = model_query(models.ComputeNode, session=session) + query = query.filter_by(uuid=node_uuid) + count = query.delete() + if count != 1: + raise exception.ComputeNodeNotFound( + compute_node=node_uuid) + + def update_compute_node(self, context, node_uuid, values): + if 'uuid' in values: + msg = _("Cannot overwrite UUID for an existing ComputeNode.") + raise exception.InvalidParameterValue(err=msg) + + return self._do_update_compute_node(node_uuid, values) + + def _do_update_compute_node(self, node_uuid, values): + session = get_session() + with session.begin(): + query = model_query(models.ComputeNode, session=session) + query = query.filter_by(uuid=node_uuid) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.ComputeNodeNotFound( + compute_node=node_uuid) + + ref.update(values) + return ref diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index 57a97e817..097b81d39 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -259,3 +259,15 @@ class Allocation(Base): primaryjoin=('and_(Allocation.resource_provider_id == ' 'ResourceProvider.id)'), foreign_keys=resource_provider_id) + + +class ComputeNode(Base): + """Represents a compute node. """ + + __tablename__ = 'compute_node' + __table_args__ = ( + table_args() + ) + uuid = Column(String(36), primary_key=True, nullable=False) + hostname = Column(String(255), nullable=False) + numa_topology = Column(JSONEncodedDict, nullable=True) diff --git a/zun/objects/__init__.py b/zun/objects/__init__.py index 26d57cc92..6f1fa56f8 100644 --- a/zun/objects/__init__.py +++ b/zun/objects/__init__.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. - +from zun.objects import compute_node from zun.objects import container from zun.objects import image from zun.objects import numa @@ -25,6 +25,7 @@ NUMANode = numa.NUMANode NUMATopology = numa.NUMATopology ResourceProvider = resource_provider.ResourceProvider ResourceClass = resource_class.ResourceClass +ComputeNode = compute_node.ComputeNode __all__ = ( Container, @@ -34,4 +35,5 @@ __all__ = ( ResourceClass, NUMANode, NUMATopology, + ComputeNode, ) diff --git a/zun/objects/compute_node.py b/zun/objects/compute_node.py new file mode 100644 index 000000000..d081b759d --- /dev/null +++ b/zun/objects/compute_node.py @@ -0,0 +1,142 @@ +# 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_versionedobjects import fields + +from zun.db import api as dbapi +from zun.objects import base +from zun.objects.numa import NUMATopology + + +@base.ZunObjectRegistry.register +class ComputeNode(base.ZunPersistentObject, base.ZunObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'uuid': fields.UUIDField(read_only=True, nullable=False), + 'numa_topology': fields.ObjectField('NUMATopology', nullable=True), + 'hostname': fields.StringField(nullable=False), + } + + @staticmethod + def _from_db_object(context, compute_node, db_compute_node): + """Converts a database entity to a formal object.""" + for field in compute_node.fields: + if field == 'numa_topology': + numa_obj = NUMATopology._from_dict( + db_compute_node['numa_topology']) + compute_node.numa_topology = numa_obj + else: + setattr(compute_node, field, db_compute_node[field]) + + compute_node.obj_reset_changes(recursive=True) + return compute_node + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Converts a list of database entities to a list of formal objects.""" + return [ComputeNode._from_db_object(context, cls(context), obj) + for obj in db_objects] + + @base.remotable + def create(self, context): + """Create a compute node record in the DB. + + :param context: Security context. + + """ + values = self.obj_get_changes() + numa_obj = values.pop('numa_topology', None) + if numa_obj is not None: + values['numa_topology'] = numa_obj._to_dict() + + db_compute_node = dbapi.create_compute_node(context, values) + self._from_db_object(context, self, db_compute_node) + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + """Find a compute node based on uuid. + + :param uuid: the uuid of a compute node. + :param context: Security context + :returns: a :class:`ComputeNode` object. + """ + db_compute_node = dbapi.get_compute_node(context, uuid) + compute_node = ComputeNode._from_db_object( + context, cls(context), db_compute_node) + return compute_node + + @base.remotable_classmethod + def get_by_hostname(cls, context, hostname): + db_compute_node = dbapi.get_compute_node_by_hostname( + context, hostname) + return cls._from_db_object(context, cls(), db_compute_node) + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, + sort_key=None, sort_dir=None, filters=None): + """Return a list of ComputeNode objects. + + :param context: Security context. + :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: filters when list resource providers. + :returns: a list of :class:`ComputeNode` object. + + """ + db_compute_nodes = dbapi.list_compute_nodes( + context, limit=limit, marker=marker, sort_key=sort_key, + sort_dir=sort_dir, filters=filters) + return ComputeNode._from_db_object_list( + db_compute_nodes, cls, context) + + @base.remotable + def destroy(self, context=None): + """Delete the ComputeNode from the DB. + + :param context: Security context. + """ + dbapi.destroy_compute_node(context, self.uuid) + self.obj_reset_changes(recursive=True) + + @base.remotable + def save(self, context=None): + """Save updates to this ComputeNode. + + Updates will be made column by column based on the result + of self.what_changed(). + + :param context: Security context. + """ + updates = self.obj_get_changes() + dbapi.update_compute_node(context, self.uuid, updates) + self.obj_reset_changes(recursive=True) + + @base.remotable + def refresh(self, context=None): + """Loads updates for this ComputeNode. + + Loads a compute node with the same uuid from the database and + checks for updated attributes. Updates are applied from + the loaded compute node column by column, if there are any + updates. + + :param context: Security context. + """ + current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) + for field in self.fields: + if self.obj_attr_is_set(field) and \ + getattr(self, field) != getattr(current, field): + setattr(self, field, getattr(current, field)) diff --git a/zun/objects/numa.py b/zun/objects/numa.py index 8ee0c94b2..bf902ad60 100644 --- a/zun/objects/numa.py +++ b/zun/objects/numa.py @@ -60,16 +60,16 @@ class NUMANode(base.ZunObject): def _to_dict(self): return { 'id': self.id, - 'cpus': self.cpuset, - 'pinned_cpus': self.pinned_cpus + 'cpuset': list(self.cpuset), + 'pinned_cpus': list(self.pinned_cpus) } @classmethod def _from_dict(cls, data_dict): - cpuset = data_dict.get('cpus', '') - cell_id = data_dict.get('id') - pinned_cpus = data_dict.get('pinned_cpus') - return cls(id=cell_id, cpuset=cpuset, + cpuset = set(data_dict.get('cpuset', '')) + node_id = data_dict.get('id') + pinned_cpus = set(data_dict.get('pinned_cpus')) + return cls(id=node_id, cpuset=cpuset, pinned_cpus=pinned_cpus) @@ -88,5 +88,10 @@ class NUMATopology(base.ZunObject): NUMANode._from_dict(node_dict) for node_dict in data_dict.get('nodes', [])]) + def _to_dict(self): + return { + 'nodes': [n._to_dict() for n in self.nodes], + } + def to_list(self): return [n._to_dict() for n in self.nodes] diff --git a/zun/tests/unit/container/docker/test_docker_driver.py b/zun/tests/unit/container/docker/test_docker_driver.py index 0a686dfce..9e670dc23 100644 --- a/zun/tests/unit/container/docker/test_docker_driver.py +++ b/zun/tests/unit/container/docker/test_docker_driver.py @@ -32,8 +32,8 @@ CONF = conf.CONF _numa_node = { 'id': 0, - 'cpus': set([8]), - 'pinned_cpus': set([]) + 'cpuset': [8], + 'pinned_cpus': [] } _numa_topo_spec = [_numa_node] diff --git a/zun/tests/unit/db/test_compute_host.py b/zun/tests/unit/db/test_compute_host.py new file mode 100644 index 000000000..49baaa6a5 --- /dev/null +++ b/zun/tests/unit/db/test_compute_host.py @@ -0,0 +1,161 @@ +# 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 compute nodes via the DB API""" + +from oslo_config import cfg +from oslo_utils import uuidutils +import six + +from zun.common import exception +import zun.conf +from zun.db import api as dbapi +from zun.tests.unit.db import base +from zun.tests.unit.db import utils + +CONF = zun.conf.CONF + + +class DbComputeNodeTestCase(base.DbTestCase): + + def setUp(self): + cfg.CONF.set_override('db_type', 'sql') + super(DbComputeNodeTestCase, self).setUp() + + def test_create_compute_node(self): + utils.create_test_compute_node(context=self.context) + + def test_create_compute_node_already_exists(self): + utils.create_test_compute_node( + context=self.context, uuid='123') + with self.assertRaisesRegexp(exception.ComputeNodeAlreadyExists, + 'A compute node with UUID 123.*'): + utils.create_test_compute_node( + context=self.context, uuid='123') + + def test_get_compute_node_by_uuid(self): + node = utils.create_test_compute_node(context=self.context) + res = dbapi.get_compute_node( + self.context, node.uuid) + self.assertEqual(node.uuid, res.uuid) + self.assertEqual(node.hostname, res.hostname) + + def test_get_compute_node_by_hostname(self): + node = utils.create_test_compute_node(context=self.context) + res = dbapi.get_compute_node_by_hostname( + self.context, node.hostname) + self.assertEqual(node.uuid, res.uuid) + self.assertEqual(node.hostname, res.hostname) + + def test_get_compute_node_that_does_not_exist(self): + self.assertRaises(exception.ComputeNodeNotFound, + dbapi.get_compute_node, + self.context, + uuidutils.generate_uuid()) + + def test_list_compute_nodes(self): + uuids = [] + for i in range(1, 6): + node = utils.create_test_compute_node( + uuid=uuidutils.generate_uuid(), + context=self.context, + hostname='node'+str(i)) + uuids.append(six.text_type(node['uuid'])) + res = dbapi.list_compute_nodes(self.context) + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), sorted(res_uuids)) + + def test_list_compute_nodes_sorted(self): + uuids = [] + for i in range(5): + node = utils.create_test_compute_node( + uuid=uuidutils.generate_uuid(), + context=self.context, + hostname='node'+str(i)) + uuids.append(six.text_type(node.uuid)) + res = dbapi.list_compute_nodes(self.context, sort_key='uuid') + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), res_uuids) + + self.assertRaises(exception.InvalidParameterValue, + dbapi.list_compute_nodes, + self.context, + sort_key='foo') + + def test_list_compute_nodes_with_filters(self): + node1 = utils.create_test_compute_node( + hostname='node-one', + uuid=uuidutils.generate_uuid(), + context=self.context) + node2 = utils.create_test_compute_node( + hostname='node-two', + uuid=uuidutils.generate_uuid(), + context=self.context) + + res = dbapi.list_compute_nodes( + self.context, filters={'hostname': 'node-one'}) + self.assertEqual([node1.uuid], [r.uuid for r in res]) + + res = dbapi.list_compute_nodes( + self.context, filters={'hostname': 'node-two'}) + self.assertEqual([node2.uuid], [r.uuid for r in res]) + + res = dbapi.list_compute_nodes( + self.context, filters={'hostname': 'bad-node'}) + self.assertEqual([], [r.uuid for r in res]) + + res = dbapi.list_compute_nodes( + self.context, + filters={'hostname': node1.hostname}) + self.assertEqual([node1.uuid], [r.uuid for r in res]) + + def test_destroy_compute_node(self): + node = utils.create_test_compute_node(context=self.context) + dbapi.destroy_compute_node(self.context, node.uuid) + self.assertRaises(exception.ComputeNodeNotFound, + dbapi.get_compute_node, + self.context, node.uuid) + + def test_destroy_compute_node_by_uuid(self): + node = utils.create_test_compute_node(context=self.context) + dbapi.destroy_compute_node(self.context, node.uuid) + self.assertRaises(exception.ComputeNodeNotFound, + dbapi.get_compute_node, + self.context, node.uuid) + + def test_destroy_compute_node_that_does_not_exist(self): + self.assertRaises(exception.ComputeNodeNotFound, + dbapi.destroy_compute_node, self.context, + uuidutils.generate_uuid()) + + def test_update_compute_node(self): + node = utils.create_test_compute_node(context=self.context) + old_hostname = node.hostname + new_hostname = 'new-hostname' + self.assertNotEqual(old_hostname, new_hostname) + + res = dbapi.update_compute_node( + self.context, node.uuid, {'hostname': new_hostname}) + self.assertEqual(new_hostname, res.hostname) + + def test_update_compute_node_not_found(self): + node_uuid = uuidutils.generate_uuid() + new_hostname = 'new-hostname' + self.assertRaises(exception.ComputeNodeNotFound, + dbapi.update_compute_node, self.context, + node_uuid, {'hostname': new_hostname}) + + def test_update_compute_node_uuid(self): + node = utils.create_test_compute_node(context=self.context) + self.assertRaises(exception.InvalidParameterValue, + dbapi.update_compute_node, self.context, + node.uuid, {'uuid': ''}) diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index 9d10e0406..64c524a93 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -240,6 +240,39 @@ def create_test_allocation(**kw): return dbapi.create_allocation(kw['context'], allocation) +def get_test_numa_topology(**kw): + return { + "nodes": [ + { + "id": 0, + "cpuset": [1, 2], + "pinned_cpus": [] + }, + { + "id": 1, + "cpuset": [3, 4], + "pinned_cpus": [3, 4] + } + ] + } + + +def get_test_compute_node(**kw): + return { + 'uuid': kw.get('uuid', '24a5b17a-f2eb-4556-89db-5f4169d13982'), + 'hostname': kw.get('hostname', 'localhost'), + 'numa_topology': kw.get('numa_topology', get_test_numa_topology()), + 'created_at': kw.get('created_at'), + 'updated_at': kw.get('updated_at'), + } + + +def create_test_compute_node(**kw): + compute_host = get_test_compute_node(**kw) + dbapi = db_api._get_dbdriver_instance() + return dbapi.create_compute_node(kw['context'], compute_host) + + class FakeEtcdMultipleResult(object): def __init__(self, value): diff --git a/zun/tests/unit/objects/test_compute_node.py b/zun/tests/unit/objects/test_compute_node.py new file mode 100644 index 000000000..2ba9bf9be --- /dev/null +++ b/zun/tests/unit/objects/test_compute_node.py @@ -0,0 +1,137 @@ +# 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 testtools.matchers import HasLength + +from zun import objects +from zun.tests.unit.db import base +from zun.tests.unit.db import utils + + +class TestComputeNodeObject(base.DbTestCase): + + def setUp(self): + super(TestComputeNodeObject, self).setUp() + self.fake_numa_topology = utils.get_test_numa_topology() + self.fake_compute_node = utils.get_test_compute_node( + numa_topology=self.fake_numa_topology) + + def test_get_by_uuid(self): + uuid = self.fake_compute_node['uuid'] + with mock.patch.object(self.dbapi, 'get_compute_node', + autospec=True) as mock_get_compute_node: + mock_get_compute_node.return_value = self.fake_compute_node + compute_node = objects.ComputeNode.get_by_uuid(self.context, uuid) + mock_get_compute_node.assert_called_once_with( + self.context, uuid) + self.assertEqual(self.context, compute_node._context) + + def test_get_by_hostname(self): + hostname = self.fake_compute_node['hostname'] + with mock.patch.object(self.dbapi, 'get_compute_node_by_hostname', + autospec=True) as mock_get: + mock_get.return_value = self.fake_compute_node + compute_node = objects.ComputeNode.get_by_hostname( + self.context, hostname) + mock_get.assert_called_once_with(self.context, hostname) + self.assertEqual(self.context, compute_node._context) + + def test_list(self): + with mock.patch.object(self.dbapi, 'list_compute_nodes', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_compute_node] + compute_nodes = objects.ComputeNode.list(self.context) + self.assertEqual(1, mock_get_list.call_count) + self.assertThat(compute_nodes, HasLength(1)) + self.assertIsInstance(compute_nodes[0], objects.ComputeNode) + self.assertEqual(self.context, compute_nodes[0]._context) + + def test_list_with_filters(self): + with mock.patch.object(self.dbapi, 'list_compute_nodes', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_compute_node] + filt = {'hostname': 'test'} + compute_nodes = objects.ComputeNode.list( + self.context, filters=filt) + self.assertEqual(1, mock_get_list.call_count) + self.assertThat(compute_nodes, HasLength(1)) + self.assertIsInstance(compute_nodes[0], objects.ComputeNode) + self.assertEqual(self.context, compute_nodes[0]._context) + mock_get_list.assert_called_once_with( + self.context, filters=filt, limit=None, marker=None, + sort_key=None, sort_dir=None) + + def test_create(self): + with mock.patch.object(self.dbapi, 'create_compute_node', + autospec=True) as mock_create: + mock_create.return_value = self.fake_compute_node + compute_node_dict = dict(self.fake_compute_node) + compute_node_dict['numa_topology'] = objects.NUMATopology\ + ._from_dict(compute_node_dict['numa_topology']) + compute_node = objects.ComputeNode( + self.context, **compute_node_dict) + compute_node.create(self.context) + mock_create.assert_called_once_with( + self.context, self.fake_compute_node) + self.assertEqual(self.context, compute_node._context) + + def test_destroy(self): + uuid = self.fake_compute_node['uuid'] + with mock.patch.object(self.dbapi, 'get_compute_node', + autospec=True) as mock_get: + mock_get.return_value = self.fake_compute_node + with mock.patch.object(self.dbapi, 'destroy_compute_node', + autospec=True) as mock_destroy: + compute_node = objects.ComputeNode.get_by_uuid( + self.context, uuid) + compute_node.destroy() + mock_get.assert_called_once_with(self.context, uuid) + mock_destroy.assert_called_once_with(None, uuid) + self.assertEqual(self.context, compute_node._context) + + def test_save(self): + uuid = self.fake_compute_node['uuid'] + with mock.patch.object(self.dbapi, 'get_compute_node', + autospec=True) as mock_get: + mock_get.return_value = self.fake_compute_node + with mock.patch.object(self.dbapi, 'update_compute_node', + autospec=True) as mock_update: + compute_node = objects.ComputeNode.get_by_uuid( + self.context, uuid) + compute_node.hostname = 'myhostname' + compute_node.save() + + mock_get.assert_called_once_with(self.context, uuid) + mock_update.assert_called_once_with( + None, uuid, + {'hostname': 'myhostname'}) + self.assertEqual(self.context, compute_node._context) + + def test_refresh(self): + uuid = self.fake_compute_node['uuid'] + hostname = self.fake_compute_node['hostname'] + new_hostname = 'myhostname' + returns = [dict(self.fake_compute_node, hostname=hostname), + dict(self.fake_compute_node, hostname=new_hostname)] + expected = [mock.call(self.context, uuid), + mock.call(self.context, uuid)] + with mock.patch.object(self.dbapi, 'get_compute_node', + side_effect=returns, + autospec=True) as mock_get: + compute_node = objects.ComputeNode.get_by_uuid(self.context, uuid) + self.assertEqual(hostname, compute_node.hostname) + compute_node.refresh() + self.assertEqual(new_hostname, compute_node.hostname) + self.assertEqual(expected, mock_get.call_args_list) + self.assertEqual(self.context, compute_node._context) diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index fcc8606d8..67df077ca 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -362,6 +362,7 @@ object_data = { 'ResourceClass': '1.1-d661c7675b3cd5b8c3618b68ba64324e', 'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24', 'ZunService': '1.0-2a19ab9987a746621b2ada02d8aadf22', + 'ComputeNode': '1.0-790d40d5af2d05dc6302bd2e4aa4d80b', }