Add compute node to DB and objects

Change-Id: Ic196736ff263220eb83acce72772f0ebcc462b9f
This commit is contained in:
Hongbin Lu 2017-02-21 18:02:38 -06:00
parent 8053efa4cf
commit 7255e44328
13 changed files with 729 additions and 17 deletions

View File

@ -341,6 +341,10 @@ class ContainerNotFound(HTTPNotFound):
message = _("Container %(container)s could not be found.") message = _("Container %(container)s could not be found.")
class ComputeNodeNotFound(HTTPNotFound):
message = _("Compute node %(compute_node)s could not be found.")
class ImageNotFound(HTTPNotFound): class ImageNotFound(HTTPNotFound):
message = _("Image %(image)s could not be found.") 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.") 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): class ImageAlreadyExists(ResourceExists):
message = _("An image with tag %(tag)s and repo %(repo)s already exists.") message = _("An image with tag %(tag)s and repo %(repo)s already exists.")

View File

@ -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 :param marker: the last item of the previous page; we return the next
result set. result set.
:param sort_key: Attribute by which results should be sorted. :param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc) (asc, desc)
:returns: A list of tuples of the specified columns. :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): def create_container(context, values):
"""Create a new container. """Create a new container.
:param context: The security context
:param values: A dict containing several items used to identify :param values: A dict containing several items used to identify
and track the container, and several dicts which are and track the container, and several dicts which are
passed 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 :param marker: the last item of the previous page; we return the next
result set. result set.
:param sort_key: Attribute by which results should be sorted. :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) (asc, desc)
:returns: A list of tuples of the specified columns. :returns: A list of tuples of the specified columns.
""" """
@ -242,6 +244,7 @@ def list_images(context, filters=None,
:param marker: the last item of the previous page; we :param marker: the last item of the previous page; we
return the next return the next
:param sort_key: Attribute by which results should be sorted. :param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc) (asc, desc)
:returns: A list of tuples of the specified columns. :returns: A list of tuples of the specified columns.
""" """
@ -291,6 +294,7 @@ def list_resource_providers(context, filters=None, limit=None, marker=None,
def create_resource_provider(context, values): def create_resource_provider(context, values):
"""Create a new resource provider. """Create a new resource provider.
:param context: The security context
:param values: A dict containing several items used to identify and :param values: A dict containing several items used to identify and
track the resource provider, and several dicts which are track the resource provider, and several dicts which are
passed into the Drivers when managing this resource passed into the Drivers when managing this resource
@ -344,6 +348,7 @@ def list_resource_classes(context, limit=None, marker=None, sort_key=None,
:param marker: the last item of the previous page; we :param marker: the last item of the previous page; we
return the next return the next
:param sort_key: Attribute by which results should be sorted. :param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc) (asc, desc)
:returns: A list of tuples of the specified columns. :returns: A list of tuples of the specified columns.
""" """
@ -354,6 +359,7 @@ def list_resource_classes(context, limit=None, marker=None, sort_key=None,
def create_resource_class(context, values): def create_resource_class(context, values):
"""Create a new resource class. """Create a new resource class.
:param context: The security context
:param values: A dict containing several items used to identify :param values: A dict containing several items used to identify
and track the resource class, and several dicts which are and track the resource class, and several dicts which are
passed into the Drivers when managing this resource class. 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 :param marker: the last item of the previous page; we return the next
result set. result set.
:param sort_key: Attribute by which results should be sorted. :param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc) (asc, desc)
:returns: A list of tuples of the specified columns. :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): def create_inventory(context, provider_id, values):
"""Create a new inventory. """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 :param values: A dict containing several items used to identify
and track the inventory, and several dicts which are and track the inventory, and several dicts which are
passed into the Drivers when managing this inventory. passed into the Drivers when managing this inventory.
:param provider_id: The id of a resource provider.
:returns: An inventory. :returns: An inventory.
""" """
return _get_dbdriver_instance().create_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 :param marker: the last item of the previous page; we return the next
result set. result set.
:param sort_key: Attribute by which results should be sorted. :param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc) (asc, desc)
:returns: A list of tuples of the specified columns. :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): def create_allocation(context, values):
"""Create a new allocation. """Create a new allocation.
:param context: The security context
:param values: A dict containing several items used to identify :param values: A dict containing several items used to identify
and track the allocation, and several dicts which are and track the allocation, and several dicts which are
passed into the Drivers when managing this allocation. passed into the Drivers when managing this allocation.
:param provider_id: The id of a resource provider.
:returns: An allocation. :returns: An allocation.
""" """
return _get_dbdriver_instance().create_allocation(context, values) 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( return _get_dbdriver_instance().update_allocation(
context, allocation_id, values) 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)

View File

@ -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'),
)

View File

@ -87,10 +87,10 @@ def add_identity_filter(query, value):
def _paginate_query(model, limit=None, marker=None, sort_key=None, 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: if not query:
query = model_query(model) query = model_query(model)
sort_keys = ['id'] sort_keys = [default_sort_key]
if sort_key and sort_key not in sort_keys: if sort_key and sort_key not in sort_keys:
sort_keys.insert(0, sort_key) sort_keys.insert(0, sort_key)
try: try:
@ -300,7 +300,7 @@ class Connection(object):
return _paginate_query(models.ZunService, query=query) return _paginate_query(models.ZunService, query=query)
def pull_image(self, context, values): 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'): if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid() values['uuid'] = uuidutils.generate_uuid()
image = models.Image() image = models.Image()
@ -652,3 +652,88 @@ class Connection(object):
ref.update(values) ref.update(values)
return ref 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

View File

@ -259,3 +259,15 @@ class Allocation(Base):
primaryjoin=('and_(Allocation.resource_provider_id == ' primaryjoin=('and_(Allocation.resource_provider_id == '
'ResourceProvider.id)'), 'ResourceProvider.id)'),
foreign_keys=resource_provider_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)

View File

@ -10,7 +10,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from zun.objects import compute_node
from zun.objects import container from zun.objects import container
from zun.objects import image from zun.objects import image
from zun.objects import numa from zun.objects import numa
@ -25,6 +25,7 @@ NUMANode = numa.NUMANode
NUMATopology = numa.NUMATopology NUMATopology = numa.NUMATopology
ResourceProvider = resource_provider.ResourceProvider ResourceProvider = resource_provider.ResourceProvider
ResourceClass = resource_class.ResourceClass ResourceClass = resource_class.ResourceClass
ComputeNode = compute_node.ComputeNode
__all__ = ( __all__ = (
Container, Container,
@ -34,4 +35,5 @@ __all__ = (
ResourceClass, ResourceClass,
NUMANode, NUMANode,
NUMATopology, NUMATopology,
ComputeNode,
) )

142
zun/objects/compute_node.py Normal file
View File

@ -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))

View File

@ -60,16 +60,16 @@ class NUMANode(base.ZunObject):
def _to_dict(self): def _to_dict(self):
return { return {
'id': self.id, 'id': self.id,
'cpus': self.cpuset, 'cpuset': list(self.cpuset),
'pinned_cpus': self.pinned_cpus 'pinned_cpus': list(self.pinned_cpus)
} }
@classmethod @classmethod
def _from_dict(cls, data_dict): def _from_dict(cls, data_dict):
cpuset = data_dict.get('cpus', '') cpuset = set(data_dict.get('cpuset', ''))
cell_id = data_dict.get('id') node_id = data_dict.get('id')
pinned_cpus = data_dict.get('pinned_cpus') pinned_cpus = set(data_dict.get('pinned_cpus'))
return cls(id=cell_id, cpuset=cpuset, return cls(id=node_id, cpuset=cpuset,
pinned_cpus=pinned_cpus) pinned_cpus=pinned_cpus)
@ -88,5 +88,10 @@ class NUMATopology(base.ZunObject):
NUMANode._from_dict(node_dict) NUMANode._from_dict(node_dict)
for node_dict in data_dict.get('nodes', [])]) 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): def to_list(self):
return [n._to_dict() for n in self.nodes] return [n._to_dict() for n in self.nodes]

View File

@ -32,8 +32,8 @@ CONF = conf.CONF
_numa_node = { _numa_node = {
'id': 0, 'id': 0,
'cpus': set([8]), 'cpuset': [8],
'pinned_cpus': set([]) 'pinned_cpus': []
} }
_numa_topo_spec = [_numa_node] _numa_topo_spec = [_numa_node]

View File

@ -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': ''})

View File

@ -240,6 +240,39 @@ def create_test_allocation(**kw):
return dbapi.create_allocation(kw['context'], allocation) 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): class FakeEtcdMultipleResult(object):
def __init__(self, value): def __init__(self, value):

View File

@ -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)

View File

@ -362,6 +362,7 @@ object_data = {
'ResourceClass': '1.1-d661c7675b3cd5b8c3618b68ba64324e', 'ResourceClass': '1.1-d661c7675b3cd5b8c3618b68ba64324e',
'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24', 'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24',
'ZunService': '1.0-2a19ab9987a746621b2ada02d8aadf22', 'ZunService': '1.0-2a19ab9987a746621b2ada02d8aadf22',
'ComputeNode': '1.0-790d40d5af2d05dc6302bd2e4aa4d80b',
} }