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:
parent
d1977c2aec
commit
f26dabdbe4
@ -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
23
zun/common/singleton.py
Normal 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]
|
@ -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}
|
||||
|
@ -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
0
zun/db/etcd/__init__.py
Normal file
265
zun/db/etcd/api.py
Normal file
265
zun/db/etcd/api.py
Normal 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
108
zun/db/etcd/models.py
Normal 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
|
@ -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.")
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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': ''})
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user