Add etcd db driver

This commit add etcd db driver code and related
test cases for etcd database.

Note that there still some code are needed to
make etcd database backend work appropriately,
those code will be added in follow up patches.

Part of blueprint etcd-db-driver
Change-Id: Ie6b80cb2c3e51808d122241c0f55a1029b8622de
This commit is contained in:
Wenzhi Yu 2016-08-31 16:07:47 +08:00
parent d1977c2aec
commit f26dabdbe4
12 changed files with 712 additions and 46 deletions

View File

@ -9,6 +9,7 @@ greenlet>=0.3.2 # MIT
jsonpatch>=1.1 # BSD
pbr>=1.6 # Apache-2.0
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
python-etcd>=0.4.3 # MIT License
python-glanceclient>=2.5.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0

23
zun/common/singleton.py Normal file
View File

@ -0,0 +1,23 @@
# Copyright 2016 IBM, Corp.
#
# 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.
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(
Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]

View File

@ -14,16 +14,42 @@
from oslo_config import cfg
from zun.common.i18n import _
db_opts = [
# TODO(yuywz): Change to etcd after all etcd db driver code is landed
cfg.StrOpt('db_type',
default='sql',
help=_('Defines which db type to use for storing container. '
'Possible Values: sql, etcd'))
]
sql_opts = [
cfg.StrOpt('mysql_engine',
default='InnoDB',
help='MySQL engine to use.')
help=_('MySQL engine to use.'))
]
etcd_opts = [
cfg.StrOpt('etcd_host',
default='127.0.0.1',
help=_("Host IP address on which etcd service running.")),
cfg.PortOpt('etcd_port',
default=2379,
help=_("Port on which etcd listen client request."))
]
etcd_group = cfg.OptGroup(name='etcd', title='Options for etcd connection')
ALL_OPTS = (db_opts + sql_opts + etcd_opts)
def register_opts(conf):
conf.register_opts(db_opts)
conf.register_opts(sql_opts, 'database')
conf.register_group(etcd_group)
conf.register_opts(etcd_opts, etcd_group)
def list_opts():
return {"DEFAULT": sql_opts}
return {"DEFAULT": ALL_OPTS}

View File

@ -20,20 +20,30 @@ import abc
from oslo_db import api as db_api
import six
from zun.common import exception
from zun.common.i18n import _
import zun.conf
"""Add the database backend mapping here"""
CONF = zun.conf.CONF
_BACKEND_MAPPING = {'sqlalchemy': 'zun.db.sqlalchemy.api'}
IMPL = db_api.DBAPI.from_config(zun.conf.CONF,
IMPL = db_api.DBAPI.from_config(CONF,
backend_mapping=_BACKEND_MAPPING,
lazy=True)
def get_instance():
"""Return a DB API instance."""
"""Add more judgement for selecting more database backend"""
return IMPL
if CONF.db_type == 'sql':
return IMPL
elif CONF.db_type == 'etcd':
import zun.db.etcd.api as etcd_api
return etcd_api.get_connection()
else:
raise exception.ConfigInvalid(
_("db_type value of %s is invalid, "
"must be sql or etcd") % CONF.db_type)
@six.add_metaclass(abc.ABCMeta)
@ -119,24 +129,27 @@ class Connection(object):
return dbdriver.get_container_by_name(context, container_name)
@classmethod
def destroy_container(self, container_id):
def destroy_container(self, context, container_id):
"""Destroy a container and all associated interfaces.
:param context: Request context
:param container_id: The id or uuid of a container.
"""
dbdriver = get_instance()
return dbdriver.destroy_container(container_id)
return dbdriver.destroy_container(context, container_id)
@classmethod
def update_container(self, container_id, values):
def update_container(self, context, container_id, values):
"""Update properties of a container.
:context: Request context
:param container_id: The id or uuid of a container.
:values: The properties to be updated
:returns: A container.
:raises: ContainerNotFound
"""
dbdriver = get_instance()
return dbdriver.update_container(container_id, values)
return dbdriver.update_container(context, container_id, values)
@classmethod
def destroy_zun_service(self, zun_service_id):

0
zun/db/etcd/__init__.py Normal file
View File

265
zun/db/etcd/api.py Normal file
View File

@ -0,0 +1,265 @@
# Copyright 2016 IBM, Corp.
#
# 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.
"""etcd storage backend."""
import json
import etcd
from oslo_log import log
from oslo_utils import strutils
from oslo_utils import uuidutils
import six
from zun.common import exception
from zun.common.i18n import _
from zun.common.i18n import _LE
from zun.common import singleton
import zun.conf
from zun.db.etcd import models
LOG = log.getLogger(__name__)
def get_connection():
connection = EtcdAPI(host=zun.conf.CONF.etcd.etcd_host,
port=zun.conf.CONF.etcd.etcd_port)
return connection
def clean_all_data():
conn = get_connection()
conn.clean_all_zun_data()
def add_identity_filter(query, value):
"""Adds an identity filter to a query.
Filters results by ID, if supplied value is a valid integer.
Otherwise attempts to filter results by UUID.
:param query: Initial query to add filter to.
:param value: Value for filtering results by.
:return: Modified query.
"""
if strutils.is_int_like(value):
return query.filter_by(id=value)
elif uuidutils.is_uuid_like(value):
return query.filter_by(uuid=value)
else:
raise exception.InvalidIdentity(identity=value)
def translate_etcd_result(etcd_result):
"""Translate etcd unicode result to etcd.models.Container."""
try:
container_data = json.loads(etcd_result.value)
return models.Container(container_data)
except (ValueError, TypeError) as e:
LOG.error(_LE("Error occurred while translating etcd result: %s"),
six.text_type(e))
raise
@six.add_metaclass(singleton.Singleton)
class EtcdAPI(object):
"""etcd API."""
def __init__(self, host, port):
self.client = etcd.Client(host=host, port=port)
def clean_all_zun_data(self):
try:
for d in self.client.read('/').children:
if d.key in ('/containers',):
self.client.delete(d.key, recursive=True)
except etcd.EtcdKeyNotFound as e:
LOG.error(_LE('Error occurred while cleaning zun data: %s'),
six.text_type(e))
raise
def _add_tenant_filters(self, context, filters):
filters = filters or {}
if context.is_admin and context.all_tenants:
return filters
if context.project_id:
filters['project_id'] = context.project_id
else:
filters['user_id'] = context.user_id
return filters
def _filter_containers(self, containers, filters):
for c in list(containers):
for k, v in six.iteritems(filters):
if c.get(k) != v:
containers.remove(c)
break
return containers
def _process_list_result(self, res_list, limit=None, sort_key=None):
sorted_res_list = res_list
if sort_key:
if not hasattr(res_list[0], sort_key):
raise exception.InvalidParameterValue(
err='Container has no attribute: %s' % sort_key)
sorted_res_list = sorted(res_list, key=lambda k: k.get(sort_key))
if limit:
sorted_res_list = sorted_res_list[0:limit]
return sorted_res_list
def list_container(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
try:
res = getattr(self.client.read('/containers'), 'children', None)
except etcd.EtcdKeyNotFound as e:
LOG.error(_LE("Error occurred while reading from etcd server: %s"),
six.text_type(e))
raise
containers = []
for c in res:
if c.value is not None:
containers.append(translate_etcd_result(c))
filters = self._add_tenant_filters(context, filters)
filtered_containers = self._filter_containers(
containers, filters)
return self._process_list_result(filtered_containers,
limit=limit, sort_key=sort_key)
def create_container(self, container_data):
# ensure defaults are present for new containers
if not container_data.get('uuid'):
container_data['uuid'] = uuidutils.generate_uuid()
container = models.Container(container_data)
try:
container.save()
except Exception:
raise
return container
def get_container_by_id(self, context, container_id):
try:
filters = self._add_tenant_filters(
context, {'id': container_id})
containers = self.list_container(context, filters=filters)
except etcd.EtcdKeyNotFound:
raise exception.ContainerNotFound(container=container_id)
except Exception as e:
LOG.error(_LE('Error occurred while retrieving container: %s'),
six.text_type(e))
if len(containers) == 0:
raise exception.ContainerNotFound(container=container_id)
return containers[0]
def get_container_by_uuid(self, context, container_uuid):
try:
res = self.client.read('/containers/' + container_uuid)
container = translate_etcd_result(res)
if container.get('project_id') == context.project_id or \
container.get('user_id') == context.user_id:
return container
else:
raise exception.ContainerNotFound(container=container_uuid)
except etcd.EtcdKeyNotFound:
raise exception.ContainerNotFound(container=container_uuid)
except Exception as e:
LOG.error(_LE('Error occurred while retrieving container: %s'),
six.text_type(e))
def get_container_by_name(self, context, container_name):
try:
filters = self._add_tenant_filters(
context, {'name': container_name})
containers = self.list_container(context, filters=filters)
except etcd.EtcdKeyNotFound:
raise exception.ContainerNotFound(container=container_name)
except Exception as e:
LOG.error(_LE('Error occurred while retrieving container: %s'),
six.text_type(e))
if len(containers) > 1:
raise exception.Conflict('Multiple containers exist with same '
'name. Please use the container uuid '
'instead.')
elif len(containers) == 0:
raise exception.ContainerNotFound(container=container_name)
return containers[0]
def _get_container_by_ident(self, context, container_ident):
try:
if strutils.is_int_like(container_ident):
container = self.get_container_by_id(context,
container_ident)
elif uuidutils.is_uuid_like(container_ident):
container = self.get_container_by_uuid(context,
container_ident)
else:
raise exception.InvalidIdentity(identity=container_ident)
except Exception:
raise
return container
def destroy_container(self, context, container_ident):
container = self._get_container_by_ident(context, container_ident)
self.client.delete('/containers/' + container.uuid)
def update_container(self, context, container_ident, values):
# NOTE(yuywz): Update would fail if any other client
# write '/containers/$CONTAINER_UUID' in the meanwhile
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Container.")
raise exception.InvalidParameterValue(err=msg)
try:
target_uuid = self._get_container_by_ident(
context, container_ident).uuid
target = self.client.read('/containers/' + target_uuid)
target_value = json.loads(target.value)
target_value.update(values)
target.value = json.dumps(target_value)
self.client.update(target)
except Exception:
raise
return translate_etcd_result(target)
# TODO(yuywz): following method for zun_service will be implemented
# in follow up patch.
def destroy_zun_service(self, zun_service_id):
pass
def update_zun_service(self, zun_service_id, values):
pass
def get_zun_service_by_host_and_binary(self, context, host, binary):
pass
def create_zun_service(self, values):
pass
def get_zun_service_list(self, context, disabled=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
pass

108
zun/db/etcd/models.py Normal file
View File

@ -0,0 +1,108 @@
# Copyright 2016 IBM, Corp.
#
# 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.
"""
etcd models
"""
import etcd
import six
from zun.common import exception
from zun import objects
class Base(object):
def __setitem__(self, key, value):
setattr(self, key, value)
def __getitem__(self, key):
return getattr(self, key)
def get(self, key):
return getattr(self, key)
def etcd_path(self, sub_path):
return self.path + '/' + sub_path
def as_dict(self):
d = {}
for f in self._fields:
d[f] = getattr(self, f, None)
return d
def update(self, values):
"""Make the model object behave like a dict."""
for k, v in six.iteritems(values):
setattr(self, k, v)
def save(self, session=None):
import zun.db.etcd.api as db_api
if session is None:
session = db_api.get_connection()
try:
session.client.read(self.etcd_path(self.uuid))
except etcd.EtcdKeyNotFound:
session.client.write(self.etcd_path(self.uuid), self.as_dict())
return
raise exception.ContainerAlreadyExists(uuid=self.uuid)
class ZunService(Base):
"""Represents health status of various zun services"""
_path = '/zun_service'
_fields = objects.ZunService.fields.keys()
def __init__(self, service_data):
self.path = ZunService.path()
for f in ZunService.fields():
setattr(self, f, None)
self.update(service_data)
@classmethod
def path(cls):
return cls._path
@classmethod
def fields(cls):
return cls._fields
class Container(Base):
"""Represents a container."""
_path = '/containers'
_fields = objects.Container.fields.keys()
def __init__(self, container_data):
self.path = Container.path()
for f in Container.fields():
setattr(self, f, None)
self.update(container_data)
@classmethod
def path(cls):
return cls._path
@classmethod
def fields(cls):
return cls._fields

View File

@ -183,7 +183,7 @@ class Connection(api.Connection):
'name. Please use the container uuid '
'instead.')
def destroy_container(self, container_id):
def destroy_container(self, context, container_id):
session = get_session()
with session.begin():
query = model_query(models.Container, session=session)
@ -192,7 +192,7 @@ class Connection(api.Connection):
if count != 1:
raise exception.ContainerNotFound(container_id)
def update_container(self, container_id, values):
def update_container(self, context, container_id, values):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Container.")

View File

@ -148,7 +148,7 @@ class Container(base.ZunPersistentObject, base.ZunObject,
A context should be set when instantiating the
object, e.g.: Container(context)
"""
dbapi.Connection.destroy_container(self.uuid)
dbapi.Connection.destroy_container(context, self.uuid)
self.obj_reset_changes()
@base.remotable
@ -166,7 +166,7 @@ class Container(base.ZunPersistentObject, base.ZunObject,
object, e.g.: Container(context)
"""
updates = self.obj_get_changes()
dbapi.Connection.update_container(self.uuid, updates)
dbapi.Connection.update_container(context, self.uuid, updates)
self.obj_reset_changes()

View File

@ -11,10 +11,17 @@
# under the License.
"""Tests for manipulating Containers 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
from zun.db import api as dbapi
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
@ -26,34 +33,35 @@ class DbContainerTestCase(base.DbTestCase):
def test_create_container_already_exists(self):
utils.create_test_container()
self.assertRaises(exception.ResourceExists,
self.assertRaises(exception.ContainerAlreadyExists,
utils.create_test_container)
def test_get_container_by_id(self):
container = utils.create_test_container()
res = self.dbapi.get_container_by_id(self.context, container.id)
res = dbapi.Connection.get_container_by_id(self.context, container.id)
self.assertEqual(container.id, res.id)
self.assertEqual(container.uuid, res.uuid)
def test_get_container_by_uuid(self):
container = utils.create_test_container()
res = self.dbapi.get_container_by_uuid(self.context,
container.uuid)
res = dbapi.Connection.get_container_by_uuid(self.context,
container.uuid)
self.assertEqual(container.id, res.id)
self.assertEqual(container.uuid, res.uuid)
def test_get_container_by_name(self):
container = utils.create_test_container()
res = self.dbapi.get_container_by_name(self.context,
container.name)
res = dbapi.Connection.get_container_by_name(
self.context, container.name)
self.assertEqual(container.id, res.id)
self.assertEqual(container.uuid, res.uuid)
def test_get_container_that_does_not_exist(self):
self.assertRaises(exception.ContainerNotFound,
self.dbapi.get_container_by_id, self.context, 99)
dbapi.Connection.get_container_by_id,
self.context, 99)
self.assertRaises(exception.ContainerNotFound,
self.dbapi.get_container_by_uuid,
dbapi.Connection.get_container_by_uuid,
self.context,
uuidutils.generate_uuid())
@ -63,7 +71,7 @@ class DbContainerTestCase(base.DbTestCase):
container = utils.create_test_container(
uuid=uuidutils.generate_uuid())
uuids.append(six.text_type(container['uuid']))
res = self.dbapi.list_container(self.context)
res = dbapi.Connection.list_container(self.context)
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), sorted(res_uuids))
@ -73,12 +81,12 @@ class DbContainerTestCase(base.DbTestCase):
container = utils.create_test_container(
uuid=uuidutils.generate_uuid())
uuids.append(six.text_type(container.uuid))
res = self.dbapi.list_container(self.context, sort_key='uuid')
res = dbapi.Connection.list_container(self.context, sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.list_container,
dbapi.Connection.list_container,
self.context,
sort_key='foo')
@ -90,40 +98,40 @@ class DbContainerTestCase(base.DbTestCase):
name='container-two',
uuid=uuidutils.generate_uuid())
res = self.dbapi.list_container(self.context,
filters={'name': 'container-one'})
res = dbapi.Connection.list_container(
self.context, filters={'name': 'container-one'})
self.assertEqual([container1.id], [r.id for r in res])
res = self.dbapi.list_container(self.context,
filters={'name': 'container-two'})
res = dbapi.Connection.list_container(
self.context, filters={'name': 'container-two'})
self.assertEqual([container2.id], [r.id for r in res])
res = self.dbapi.list_container(self.context,
filters={'name': 'bad-container'})
res = dbapi.Connection.list_container(
self.context, filters={'name': 'bad-container'})
self.assertEqual([], [r.id for r in res])
res = self.dbapi.list_container(
res = dbapi.Connection.list_container(
self.context,
filters={'name': container1.name})
self.assertEqual([container1.id], [r.id for r in res])
def test_destroy_container(self):
container = utils.create_test_container()
self.dbapi.destroy_container(container.id)
dbapi.Connection.destroy_container(self.context, container.id)
self.assertRaises(exception.ContainerNotFound,
self.dbapi.get_container_by_id,
dbapi.Connection.get_container_by_id,
self.context, container.id)
def test_destroy_container_by_uuid(self):
container = utils.create_test_container()
self.dbapi.destroy_container(container.uuid)
dbapi.Connection.destroy_container(self.context, container.uuid)
self.assertRaises(exception.ContainerNotFound,
self.dbapi.get_container_by_uuid,
dbapi.Connection.get_container_by_uuid,
self.context, container.uuid)
def test_destroy_container_that_does_not_exist(self):
self.assertRaises(exception.ContainerNotFound,
self.dbapi.destroy_container,
dbapi.Connection.destroy_container, self.context,
uuidutils.generate_uuid())
def test_update_container(self):
@ -132,19 +140,237 @@ class DbContainerTestCase(base.DbTestCase):
new_image = 'new-image'
self.assertNotEqual(old_image, new_image)
res = self.dbapi.update_container(container.id,
{'image': new_image})
res = dbapi.Connection.update_container(self.context, container.id,
{'image': new_image})
self.assertEqual(new_image, res.image)
def test_update_container_not_found(self):
container_uuid = uuidutils.generate_uuid()
new_image = 'new-image'
self.assertRaises(exception.ContainerNotFound,
self.dbapi.update_container,
dbapi.Connection.update_container, self.context,
container_uuid, {'image': new_image})
def test_update_container_uuid(self):
container = utils.create_test_container()
self.assertRaises(exception.InvalidParameterValue,
self.dbapi.update_container, container.id,
{'uuid': ''})
dbapi.Connection.update_container, self.context,
container.id, {'uuid': ''})
class FakeEtcdMutlipleResult(object):
def __init__(self, value):
self.children = []
for v in value:
res = mock.MagicMock()
res.value = json.dumps(v)
self.children.append(res)
class FakeEtcdResult(object):
def __init__(self, value):
self.value = json.dumps(value)
class EtcdDbContainerTestCase(DbContainerTestCase):
def setUp(self):
cfg.CONF.set_override('db_type', 'etcd')
super(EtcdDbContainerTestCase, self).setUp()
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_create_container(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
utils.create_test_container()
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_create_container_already_exists(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
utils.create_test_container()
mock_read.side_effect = lambda *args: None
self.assertRaises(exception.ContainerAlreadyExists,
utils.create_test_container)
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_get_container_by_id(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container = utils.create_test_container()
mock_read.side_effect = lambda *args: FakeEtcdMutlipleResult(
[container.as_dict()])
res = dbapi.Connection.get_container_by_id(self.context, container.id)
self.assertEqual(container.id, res.id)
self.assertEqual(container.uuid, res.uuid)
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_get_container_by_uuid(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container = utils.create_test_container()
mock_read.side_effect = lambda *args: FakeEtcdResult(
container.as_dict())
res = dbapi.Connection.get_container_by_uuid(self.context,
container.uuid)
self.assertEqual(container.id, res.id)
self.assertEqual(container.uuid, res.uuid)
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_get_container_by_name(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container = utils.create_test_container()
mock_read.side_effect = lambda *args: FakeEtcdMutlipleResult(
[container.as_dict()])
res = dbapi.Connection.get_container_by_name(
self.context, container.name)
self.assertEqual(container.id, res.id)
self.assertEqual(container.uuid, res.uuid)
@mock.patch.object(etcd_client, 'read')
def test_get_container_that_does_not_exist(self, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
self.assertRaises(exception.ContainerNotFound,
dbapi.Connection.get_container_by_id,
self.context, 99)
self.assertRaises(exception.ContainerNotFound,
dbapi.Connection.get_container_by_uuid,
self.context,
uuidutils.generate_uuid())
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_list_container(self, mock_write, mock_read):
uuids = []
containers = []
mock_read.side_effect = etcd.EtcdKeyNotFound
for i in range(1, 6):
container = utils.create_test_container(
uuid=uuidutils.generate_uuid())
containers.append(container.as_dict())
uuids.append(six.text_type(container['uuid']))
mock_read.side_effect = lambda *args: FakeEtcdMutlipleResult(
containers)
res = dbapi.Connection.list_container(self.context)
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), sorted(res_uuids))
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_list_container_sorted(self, mock_write, mock_read):
uuids = []
containers = []
mock_read.side_effect = etcd.EtcdKeyNotFound
for _ in range(5):
container = utils.create_test_container(
uuid=uuidutils.generate_uuid())
containers.append(container.as_dict())
uuids.append(six.text_type(container.uuid))
mock_read.side_effect = lambda *args: FakeEtcdMutlipleResult(
containers)
res = dbapi.Connection.list_container(self.context, sort_key='uuid')
res_uuids = [r.uuid for r in res]
self.assertEqual(sorted(uuids), res_uuids)
self.assertRaises(exception.InvalidParameterValue,
dbapi.Connection.list_container,
self.context,
sort_key='foo')
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_list_container_with_filters(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container1 = utils.create_test_container(
name='container-one',
uuid=uuidutils.generate_uuid())
container2 = utils.create_test_container(
name='container-two',
uuid=uuidutils.generate_uuid())
mock_read.side_effect = lambda *args: FakeEtcdMutlipleResult(
[container1.as_dict(), container2.as_dict()])
res = dbapi.Connection.list_container(
self.context, filters={'name': 'container-one'})
self.assertEqual([container1.id], [r.id for r in res])
res = dbapi.Connection.list_container(
self.context, filters={'name': 'container-two'})
self.assertEqual([container2.id], [r.id for r in res])
res = dbapi.Connection.list_container(
self.context, filters={'name': 'container-three'})
self.assertEqual([], [r.id for r in res])
res = dbapi.Connection.list_container(
self.context,
filters={'name': container1.name})
self.assertEqual([container1.id], [r.id for r in res])
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
@mock.patch.object(etcd_client, 'delete')
def test_destroy_container(self, mock_delete, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container = utils.create_test_container()
mock_read.side_effect = lambda *args: FakeEtcdMutlipleResult(
[container.as_dict()])
dbapi.Connection.destroy_container(self.context, container.id)
mock_delete.assert_called_once_with('/containers/%s' % container.uuid)
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
@mock.patch.object(etcd_client, 'delete')
def test_destroy_container_by_uuid(self, mock_delete,
mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container = utils.create_test_container()
mock_read.side_effect = lambda *args: FakeEtcdResult(
container.as_dict())
dbapi.Connection.destroy_container(self.context, container.uuid)
mock_delete.assert_called_once_with('/containers/%s' % container.uuid)
@mock.patch.object(etcd_client, 'read')
def test_destroy_container_that_does_not_exist(self, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
self.assertRaises(exception.ContainerNotFound,
dbapi.Connection.destroy_container, self.context,
uuidutils.generate_uuid())
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
@mock.patch.object(etcd_client, 'update')
def test_update_container(self, mock_update, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container = utils.create_test_container()
new_image = 'new-image'
mock_read.side_effect = lambda *args: FakeEtcdResult(
container.as_dict())
dbapi.Connection.update_container(self.context, container.uuid,
{'image': new_image})
self.assertEqual(new_image, json.loads(
mock_update.call_args_list[0][0][0].value)['image'])
@mock.patch.object(etcd_client, 'read')
def test_update_container_not_found(self, mock_read):
container_uuid = uuidutils.generate_uuid()
new_image = 'new-image'
mock_read.side_effect = etcd.EtcdKeyNotFound
self.assertRaises(exception.ContainerNotFound,
dbapi.Connection.update_container, self.context,
container_uuid, {'image': new_image})
@mock.patch.object(etcd_client, 'read')
@mock.patch.object(etcd_client, 'write')
def test_update_container_uuid(self, mock_write, mock_read):
mock_read.side_effect = etcd.EtcdKeyNotFound
container = utils.create_test_container()
self.assertRaises(exception.InvalidParameterValue,
dbapi.Connection.update_container, self.context,
container.id, {'uuid': ''})

View File

@ -11,10 +11,13 @@
# under the License.
"""Zun test utilities."""
from oslo_config import cfg
from zun.common import name_generator
from zun.db import api as db_api
CONF = cfg.CONF
def get_test_container(**kw):
return {
@ -50,7 +53,7 @@ def create_test_container(**kw):
"""
container = get_test_container(**kw)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kw:
if CONF.db_type == 'sql' and 'id' not in kw:
del container['id']
dbapi = db_api.get_instance()
return dbapi.create_container(container)

View File

@ -111,7 +111,7 @@ class TestContainerObject(base.DbTestCase):
container = objects.Container.get_by_uuid(self.context, uuid)
container.destroy()
mock_get_container.assert_called_once_with(self.context, uuid)
mock_destroy_container.assert_called_once_with(uuid)
mock_destroy_container.assert_called_once_with(None, uuid)
self.assertEqual(self.context, container._context)
def test_save(self):
@ -129,9 +129,10 @@ class TestContainerObject(base.DbTestCase):
mock_get_container.assert_called_once_with(self.context, uuid)
mock_update_container.assert_called_once_with(
uuid, {'image': 'container.img',
'environment': {"key1": "val", "key2": "val2"},
'memory': '512m'})
None, uuid,
{'image': 'container.img',
'environment': {"key1": "val", "key2": "val2"},
'memory': '512m'})
self.assertEqual(self.context, container._context)
def test_refresh(self):