Implement etcd API for ResourceClass

Porposed change of this commit is:

1.Add etcd db model and implements etcd API for
  ResourceClass.
2.Add 'uuid' field into ResourceClass.

Part of blueprint expose-host-capabilities

Change-Id: I9e5678e8742fd4c6a26b9ced6d6eccd6b7976363
This commit is contained in:
Wenzhi Yu 2017-02-16 13:18:48 +00:00
parent 3a84375531
commit cf1ad08b0f
11 changed files with 364 additions and 56 deletions

View File

@ -366,34 +366,34 @@ def get_resource_class(context, resource_ident):
"""Return a resource class.
:param context: The security context
:param resource_ident: The id or name of a resource class.
:param resource_ident: The uuid or name of a resource class.
:returns: A resource class.
"""
return _get_dbdriver_instance().get_resource_class(
context, resource_ident)
def destroy_resource_class(context, resource_id):
def destroy_resource_class(context, resource_uuid):
"""Destroy a resource class and all associated interfaces.
:param context: Request context
:param resource_id: The id of a resource class.
:param resource_uuid: The uuid of a resource class.
"""
return _get_dbdriver_instance().destroy_resource_class(
context, resource_id)
context, resource_uuid)
def update_resource_class(context, resource_id, values):
def update_resource_class(context, resource_uuid, values):
"""Update properties of a resource class.
:context: Request context
:param resource_id: The id of a resource class.
:param resource_uuid: The uuid of a resource class.
:values: The properties to be updated
:returns: A resource class.
:raises: ResourceClassNotFound
"""
return _get_dbdriver_instance().update_resource_class(
context, resource_id, values)
context, resource_uuid, values)
def list_inventories(context, filters=None, limit=None, marker=None,

View File

@ -76,6 +76,8 @@ def translate_etcd_result(etcd_result, model_type):
ret = models.ZunService(data)
elif model_type == 'image':
ret = models.Image(data)
elif model_type == 'resource_class':
ret = models.ResourceClass(data)
else:
raise exception.InvalidParameterValue(
_('The model_type value: %s is invalid.'), model_type)
@ -436,3 +438,100 @@ class EtcdAPI(object):
if len(images) == 0:
return None
return images[0]
def list_resource_classes(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
try:
res = getattr(self.client.read('/resource_classes'),
'children', None)
except etcd.EtcdKeyNotFound:
return []
except Exception as e:
LOG.error(
_LE('Error occurred while reading from etcd server: %s'),
six.text_type(e))
raise
resource_classes = []
for r in res:
if r.value is not None:
resource_classes.append(
translate_etcd_result(r, 'resource_class'))
if filters:
resource_classes = self._filter_resources(
resource_classes, filters)
return self._process_list_result(
resource_classes, limit=limit, sort_key=sort_key)
@lockutils.synchronized('etcd_resource_class')
def create_resource_class(self, context, values):
resource_class = models.ResourceClass(values)
resource_class.save()
return resource_class
def get_resource_class(self, context, ident):
if uuidutils.is_uuid_like(ident):
return self._get_resource_class_by_uuid(context, ident)
else:
return self._get_resource_class_by_name(context, ident)
def _get_resource_class_by_uuid(self, context, uuid):
try:
resource_class = None
res = self.client.read('/resource_classes/' + uuid)
resource_class = translate_etcd_result(res, 'resource_class')
except etcd.EtcdKeyNotFound:
raise exception.ResourceClassNotFound(resource_class=uuid)
except Exception as e:
LOG.error(
_LE('Error occurred while retriving resource class: %s'),
six.text_type(e))
raise
return resource_class
def _get_resource_class_by_name(self, context, name):
try:
rcs = self.list_resource_classes(
context, filters={'name': name})
except etcd.EtcdKeyNotFound:
raise exception.ResourceClassNotFound(resource_class=name)
except Exception as e:
LOG.error(
_LE('Error occurred while retriving resource class: %s'),
six.text_type(e))
raise
if len(rcs) > 1:
raise exception.Conflict('Multiple resource classes exist with '
'same name. Please use uuid instead.')
elif len(rcs) == 0:
raise exception.ResourceClassNotFound(resource_class=name)
return rcs[0]
@lockutils.synchronized('etcd_resource_class')
def destroy_resource_class(self, context, uuid):
resource_class = self._get_resource_class_by_uuid(context, uuid)
self.client.delete('/resource_classes/' + resource_class.uuid)
@lockutils.synchronized('etcd_resource_class')
def update_resource_class(self, context, uuid, values):
if 'uuid' in values:
msg = _("Cannot override UUID for an existing resource class.")
raise exception.InvalidParameterValue(err=msg)
try:
target = self.client.read('/resource_classes/' + uuid)
target_value = json.loads(target.value)
target_value.update(values)
target.value = json.dumps(target_value)
self.client.update(target)
except etcd.EtcdKeyNotFound:
raise exception.ResourceClassNotFound(resource_class=uuid)
except Exception as e:
LOG.error(
_LE('Error occurred while updating resource class: %s'),
six.text_type(e))
raise
return translate_etcd_result(target, 'resource_class')

View File

@ -154,3 +154,26 @@ class Image(Base):
@classmethod
def fields(cls):
return cls._fields
class ResourceClass(Base):
"""Represents a resource class."""
_path = '/resource_classes'
_fields = objects.ResourceClass.fields.keys()
def __init__(self, resource_class_data):
self.path = ResourceClass.path()
for f in ResourceClass.fields():
setattr(self, f, None)
self.id = 1
self.update(resource_class_data)
@classmethod
def path(cls):
return cls._path
@classmethod
def fields(cls):
return cls._fields

View File

@ -0,0 +1,36 @@
# 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 uuid_to_resource_class
Revision ID: 8192905fd835
Revises: e4d145e195f4
Create Date: 2017-02-24 07:00:22.344162
"""
# revision identifiers, used by Alembic.
revision = '8192905fd835'
down_revision = 'e4d145e195f4'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('resource_class',
sa.Column('uuid', sa.String(length=36), nullable=False))
op.create_unique_constraint('uniq_resource_class0uuid',
'resource_class', ['uuid'])
op.drop_index('uniq_container0name', table_name='resource_class')

View File

@ -472,23 +472,23 @@ class Connection(object):
resource.save()
except db_exc.DBDuplicateEntry:
raise exception.ResourceClassAlreadyExists(
field='name', value=values['name'])
field='uuid', value=values['uuid'])
return resource
def get_resource_class(self, context, resource_ident):
if strutils.is_int_like(resource_ident):
return self._get_resource_class_by_id(context, resource_ident)
if uuidutils.is_uuid_like(resource_ident):
return self._get_resource_class_by_uuid(context, resource_ident)
else:
return self._get_resource_class_by_name(context, resource_ident)
def _get_resource_class_by_id(self, context, resource_id):
def _get_resource_class_by_uuid(self, context, resource_uuid):
query = model_query(models.ResourceClass)
query = query.filter_by(id=resource_id)
query = query.filter_by(uuid=resource_uuid)
try:
return query.one()
except NoResultFound:
raise exception.ResourceClassNotFound(
resource_class=resource_id)
resource_class=resource_uuid)
def _get_resource_class_by_name(self, context, resource_name):
query = model_query(models.ResourceClass)

View File

@ -194,10 +194,11 @@ class ResourceClass(Base):
__tablename__ = 'resource_class'
__table_args__ = (
schema.UniqueConstraint('name', name='uniq_resource_class0name'),
schema.UniqueConstraint('uuid', name='uniq_resource_class0uuid'),
table_args()
)
id = Column(Integer, primary_key=True, nullable=False)
uuid = Column(String(36), nullable=False)
name = Column(String(255), nullable=False)

View File

@ -20,10 +20,12 @@ from zun.objects import fields as z_fields
@base.ZunObjectRegistry.register
class ResourceClass(base.ZunPersistentObject, base.ZunObject):
# Version 1.0: Initial version
VERSION = '1.0'
# Version 1.1: Add uuid field
VERSION = '1.1'
fields = {
'id': fields.IntegerField(read_only=True),
'uuid': fields.UUIDField(nullable=False),
'name': z_fields.ResourceClassField(nullable=False),
}
@ -43,14 +45,14 @@ class ResourceClass(base.ZunPersistentObject, base.ZunObject):
for obj in db_objects]
@base.remotable_classmethod
def get_by_id(cls, context, rc_id):
"""Find a resource class based on id.
def get_by_uuid(cls, context, uuid):
"""Find a resource class based on uuid.
:param rc_id: the id of a resource class.
:param uuid: the uuid of a resource class.
:param context: Security context
:returns: a :class:`ResourceClass` object.
"""
db_resource = dbapi.get_resource_class(context, rc_id)
db_resource = dbapi.get_resource_class(context, uuid)
resource = ResourceClass._from_db_object(cls(context), db_resource)
return resource
@ -112,7 +114,7 @@ class ResourceClass(base.ZunPersistentObject, base.ZunObject):
A context should be set when instantiating the
object, e.g.: ResourceClass(context)
"""
dbapi.destroy_resource_class(context, self.id)
dbapi.destroy_resource_class(context, self.uuid)
self.obj_reset_changes()
@base.remotable
@ -130,7 +132,7 @@ class ResourceClass(base.ZunPersistentObject, base.ZunObject):
object, e.g.: ResourceClass(context)
"""
updates = self.obj_get_changes()
dbapi.update_resource_class(context, self.id, updates)
dbapi.update_resource_class(context, self.uuid, updates)
self.obj_reset_changes()
@ -150,7 +152,7 @@ class ResourceClass(base.ZunPersistentObject, base.ZunObject):
A context should be set when instantiating the
object, e.g.: ResourceClass(context)
"""
current = self.__class__.get_by_id(self._context, rc_id=self.id)
current = self.__class__.get_by_uuid(self._context, self.uuid)
for field in self.fields:
if self.obj_attr_is_set(field) and \
getattr(self, field) != getattr(current, field):

View File

@ -11,8 +11,13 @@
# under the License.
"""Tests for manipulating resource classes via the DB API"""
import json
import mock
import etcd
from etcd import Client as etcd_client
from oslo_config import cfg
from oslo_utils import uuidutils
import six
from zun.common import exception
@ -20,6 +25,8 @@ import zun.conf
from zun.db import api as dbapi
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
from zun.tests.unit.db.utils import FakeEtcdMultipleResult
from zun.tests.unit.db.utils import FakeEtcdResult
CONF = zun.conf.CONF
@ -35,16 +42,16 @@ class DbResourceClassTestCase(base.DbTestCase):
def test_create_resource_class_already_exists(self):
utils.create_test_resource_class(
context=self.context, name='123')
context=self.context, uuid='123')
with self.assertRaisesRegexp(exception.ResourceClassAlreadyExists,
'A resource class with name 123.*'):
'A resource class with uuid 123.*'):
utils.create_test_resource_class(
context=self.context, name='123')
context=self.context, uuid='123')
def test_get_resource_class_by_id(self):
def test_get_resource_class_by_uuid(self):
resource = utils.create_test_resource_class(context=self.context)
res = dbapi.get_resource_class(self.context, resource.id)
self.assertEqual(resource.id, res.id)
res = dbapi.get_resource_class(self.context, resource.uuid)
self.assertEqual(resource.uuid, res.uuid)
self.assertEqual(resource.name, res.name)
def test_get_resource_class_by_name(self):
@ -54,17 +61,16 @@ class DbResourceClassTestCase(base.DbTestCase):
self.assertEqual(resource.name, res.name)
def test_get_resource_class_that_does_not_exist(self):
bad_id = 1111111
self.assertRaises(exception.ResourceClassNotFound,
dbapi.get_resource_class,
self.context,
bad_id)
self.context, uuidutils.generate_uuid())
def test_list_resource_classes(self):
names = []
for i in range(1, 6):
resource = utils.create_test_resource_class(
context=self.context,
uuid=uuidutils.generate_uuid(),
name='class'+str(i))
names.append(six.text_type(resource['name']))
res = dbapi.list_resource_classes(self.context)
@ -76,6 +82,7 @@ class DbResourceClassTestCase(base.DbTestCase):
for i in range(5):
resource = utils.create_test_resource_class(
context=self.context,
uuid=uuidutils.generate_uuid(),
name='class'+str(i))
names.append(six.text_type(resource.name))
res = dbapi.list_resource_classes(self.context, sort_key='name')
@ -116,3 +123,151 @@ class DbResourceClassTestCase(base.DbTestCase):
self.assertRaises(exception.ResourceClassNotFound,
dbapi.update_resource_class, self.context,
bad_id, {'name': new_name})
class EtcdDbResourceClassTestCase(base.DbTestCase):
def setUp(self):
cfg.CONF.set_override('db_type', 'etcd')
super(EtcdDbResourceClassTestCase, self).setUp()
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_create_resource_class(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
utils.create_test_resource_class(context=self.context)
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_create_resource_class_already_exists(self, mock_write,
mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
utils.create_test_resource_class(context=self.context, name='123')
mock_read.side_effect = lambda *args: None
self.assertRaises(exception.ResourceExists,
utils.create_test_resource_class,
context=self.context, name='123')
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_get_resource_class_by_uuid(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
resource_class = utils.create_test_resource_class(
context=self.context)
mock_read.side_effect = lambda *args: FakeEtcdResult(
resource_class.as_dict())
res = dbapi.get_resource_class(self.context, resource_class.uuid)
self.assertEqual(resource_class.uuid, res.uuid)
self.assertEqual(resource_class.name, res.name)
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_get_resource_class_by_name(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
rcs = utils.create_test_resource_class(context=self.context)
mock_read.side_effect = lambda *args: FakeEtcdMultipleResult(
[rcs.as_dict()])
res = dbapi.get_resource_class(self.context, rcs.name)
self.assertEqual(rcs.uuid, res.uuid)
@mock.patch.object(etcd_client, 'read')
def test_get_resource_class_that_does_not_exist(self, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
self.assertRaises(exception.ResourceClassNotFound,
dbapi.get_resource_class,
self.context, 'fake-ident')
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_list_resource_classes(self, mock_write, mock_read):
names = []
resource_classes = []
mock_read.side_effect = etcd.EtcdKeyNotFound
for i in range(1, 6):
res_class = utils.create_test_resource_class(
context=self.context, name='class'+str(i))
resource_classes.append(res_class.as_dict())
names.append(six.text_type(res_class['name']))
mock_read.side_effect = lambda *args: FakeEtcdMultipleResult(
resource_classes)
res = dbapi.list_resource_classes(self.context)
res_names = [r.name for r in res]
self.assertEqual(sorted(names), sorted(res_names))
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_list_resource_classes_sorted(self, mock_write, mock_read):
names = []
resource_classes = []
mock_read.side_effect = etcd.EtcdKeyNotFound
for i in range(1, 6):
res_class = utils.create_test_resource_class(
context=self.context, name='class'+str(i))
resource_classes.append(res_class.as_dict())
names.append(six.text_type(res_class['name']))
mock_read.side_effect = lambda *args: FakeEtcdMultipleResult(
resource_classes)
res = dbapi.list_resource_classes(self.context, sort_key='name')
res_names = [r.name for r in res]
self.assertEqual(sorted(names), res_names)
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
@mock.patch.object(etcd_client, 'delete')
def test_destroy_resource_class(self, mock_delete,
mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
resource_class = utils.create_test_resource_class(
context=self.context)
mock_read.side_effect = lambda *args: FakeEtcdResult(
resource_class.as_dict())
dbapi.destroy_resource_class(self.context, resource_class.uuid)
mock_delete.assert_called_once_with(
'/resource_classes/%s' % resource_class.uuid)
@mock.patch.object(etcd_client, 'read')
def test_destroy_resource_class_that_does_not_exist(self, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
self.assertRaises(exception.ResourceClassNotFound,
dbapi.destroy_resource_class,
self.context,
'ca3e2a25-2901-438d-8157-de7ffd68d535')
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
@mock.patch.object(etcd_client, 'update')
def test_update_resource_class(self, mock_update,
mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
resource_class = utils.create_test_resource_class(
context=self.context)
old_name = resource_class.name
new_name = 'new-name'
self.assertNotEqual(old_name, new_name)
mock_read.side_effect = lambda *args: FakeEtcdResult(
resource_class.as_dict())
dbapi.update_resource_class(
self.context, resource_class.uuid, {'name': new_name})
self.assertEqual(new_name, json.loads(
mock_update.call_args_list[0][0][0].value)['name'])
@mock.patch.object(etcd_client, 'read')
def test_update_resource_class_not_found(self, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
new_name = 'new-name'
self.assertRaises(exception.ResourceClassNotFound,
dbapi.update_resource_class,
self.context,
'ca3e2a25-2901-438d-8157-de7ffd68d535',
{'name': new_name})
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_update_resource_class_uuid(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
resource_class = utils.create_test_resource_class(
context=self.context)
self.assertRaises(exception.InvalidParameterValue,
dbapi.update_resource_class,
self.context, resource_class.uuid,
{'uuid': ''})

View File

@ -172,6 +172,7 @@ def create_test_resource_provider(**kw):
def get_test_resource_class(**kw):
return {
'id': kw.get('id', 42),
'uuid': kw.get('uuid', '1136bf0e-66db-409d-aa4d-3af94eed8bcc'),
'name': kw.get('name', 'VCPU'),
'created_at': kw.get('created_at'),
'updated_at': kw.get('updated_at'),

View File

@ -359,7 +359,7 @@ object_data = {
'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd',
'NUMANode': '1.0-cba878b70b2f8b52f1e031b41ac13b4e',
'NUMATopology': '1.0-b54086eda7e4b2e6145ecb6ee2c925ab',
'ResourceClass': '1.0-2c41abea55d0f7cb47a97bdb345b37fd',
'ResourceClass': '1.1-d661c7675b3cd5b8c3618b68ba64324e',
'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24',
'ZunService': '1.0-2a19ab9987a746621b2ada02d8aadf22',
}

View File

@ -25,16 +25,6 @@ class TestResourceClassObject(base.DbTestCase):
super(TestResourceClassObject, self).setUp()
self.fake_resource = utils.get_test_resource_class()
def test_get_by_id(self):
rc_id = self.fake_resource['id']
with mock.patch.object(self.dbapi, 'get_resource_class',
autospec=True) as mock_get_resource_class:
mock_get_resource_class.return_value = self.fake_resource
resource = objects.ResourceClass.get_by_id(self.context, rc_id)
mock_get_resource_class.assert_called_once_with(
self.context, rc_id)
self.assertEqual(self.context, resource._context)
def test_get_by_name(self):
name = self.fake_resource['name']
with mock.patch.object(self.dbapi, 'get_resource_class',
@ -67,51 +57,52 @@ class TestResourceClassObject(base.DbTestCase):
self.assertEqual(self.context, resource._context)
def test_destroy(self):
rc_id = self.fake_resource['id']
rc_uuid = self.fake_resource['uuid']
with mock.patch.object(self.dbapi, 'get_resource_class',
autospec=True) as mock_get_resource_class:
mock_get_resource_class.return_value = self.fake_resource
with mock.patch.object(self.dbapi, 'destroy_resource_class',
autospec=True) as mock_destroy:
resource = objects.ResourceClass.get_by_id(
self.context, rc_id)
resource = objects.ResourceClass.get_by_uuid(
self.context, rc_uuid)
resource.destroy()
mock_get_resource_class.assert_called_once_with(
self.context, rc_id)
mock_destroy.assert_called_once_with(None, rc_id)
self.context, rc_uuid)
mock_destroy.assert_called_once_with(None, rc_uuid)
self.assertEqual(self.context, resource._context)
def test_save(self):
rc_id = self.fake_resource['id']
rc_uuid = self.fake_resource['uuid']
with mock.patch.object(self.dbapi, 'get_resource_class',
autospec=True) as mock_get_resource_class:
mock_get_resource_class.return_value = self.fake_resource
with mock.patch.object(self.dbapi, 'update_resource_class',
autospec=True) as mock_update:
resource = objects.ResourceClass.get_by_id(
self.context, rc_id)
resource = objects.ResourceClass.get_by_uuid(
self.context, rc_uuid)
resource.name = 'MEMORY_MB'
resource.save()
mock_get_resource_class.assert_called_once_with(
self.context, rc_id)
self.context, rc_uuid)
mock_update.assert_called_once_with(
None, rc_id,
None, rc_uuid,
{'name': 'MEMORY_MB'})
self.assertEqual(self.context, resource._context)
def test_refresh(self):
rc_id = self.fake_resource['id']
rc_uuid = self.fake_resource['uuid']
name = self.fake_resource['name']
new_name = 'MEMORY_MB'
returns = [dict(self.fake_resource, name=name),
dict(self.fake_resource, name=new_name)]
expected = [mock.call(self.context, rc_id),
mock.call(self.context, rc_id)]
expected = [mock.call(self.context, rc_uuid),
mock.call(self.context, rc_uuid)]
with mock.patch.object(self.dbapi, 'get_resource_class',
side_effect=returns,
autospec=True) as mock_get_resource_class:
resource = objects.ResourceClass.get_by_id(self.context, rc_id)
resource = objects.ResourceClass.get_by_uuid(
self.context, rc_uuid)
self.assertEqual(name, resource.name)
resource.refresh()
self.assertEqual(new_name, resource.name)