Remove pool group from zaqar
Implement: blueprint remove-pool-group-from-zaqar Change-Id: I18bb20d44f5071f9700fb64c10e9393707086de3
This commit is contained in:
parent
39d5b2bc53
commit
5250d940bc
|
@ -229,6 +229,9 @@ class ResponseSchema(api.Api):
|
|||
'group': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'flavor': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'options': {
|
||||
'type': 'object',
|
||||
'additionalProperties': True
|
||||
|
@ -284,6 +287,9 @@ class ResponseSchema(api.Api):
|
|||
'group': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'flavor': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'weight': {
|
||||
'type': 'number',
|
||||
'minimum': -1
|
||||
|
|
|
@ -229,6 +229,9 @@ class ResponseSchema(api.Api):
|
|||
'group': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'flavor': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'options': {
|
||||
'type': 'object',
|
||||
'additionalProperties': True
|
||||
|
@ -284,6 +287,9 @@ class ResponseSchema(api.Api):
|
|||
'group': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'flavor': {
|
||||
'type': ['string', 'null']
|
||||
},
|
||||
'weight': {
|
||||
'type': 'number',
|
||||
'minimum': -1
|
||||
|
|
|
@ -27,6 +27,7 @@ patch_capabilities = {
|
|||
}
|
||||
}
|
||||
|
||||
# TODO(gengchc2): remove pool_group in R release.
|
||||
# NOTE(flaper87): a string valid
|
||||
patch_pool = {
|
||||
'type': 'object',
|
||||
|
@ -38,6 +39,7 @@ patch_pool = {
|
|||
}
|
||||
}
|
||||
|
||||
# TODO(gengchc2): remove pool_group in R release.
|
||||
patch_pool_group = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@ -48,16 +50,29 @@ patch_pool_group = {
|
|||
}
|
||||
}
|
||||
|
||||
# NOTE(gengchc): Add pool_list in flavor creation for removing pool_group
|
||||
patch_pool_list = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'pool_list': {
|
||||
'type': 'array'
|
||||
},
|
||||
'additionalProperties': False
|
||||
}
|
||||
}
|
||||
|
||||
create = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'pool_group': patch_pool_group['properties']['pool_group'],
|
||||
'pool': patch_pool['properties']['pool'],
|
||||
'pool_list': patch_pool_list['properties']['pool_list'],
|
||||
'capabilities': patch_capabilities['properties']['capabilities']
|
||||
},
|
||||
# NOTE(flaper87): capabilities need not be present. Storage drivers
|
||||
# must provide reasonable defaults.
|
||||
# NOTE(wanghao): remove pool in Newton release.
|
||||
'oneOf': [{'required': ['pool_group']}, {'required': ['pool']}],
|
||||
'oneOf': [{'required': ['pool_group']}, {'required': ['pool']},
|
||||
{'required': ['pool_list']}],
|
||||
'additionalProperties': False
|
||||
}
|
||||
|
|
|
@ -49,6 +49,17 @@ patch_group = {
|
|||
}
|
||||
}
|
||||
|
||||
# NOTE(gengchc): remove pool_group add flavor
|
||||
patch_flavor = {
|
||||
'type': 'object', 'properties': {
|
||||
'flavor': {
|
||||
'type': 'string',
|
||||
'minLength': 0,
|
||||
'maxLength': 255
|
||||
},
|
||||
'additionalProperties': False
|
||||
}
|
||||
}
|
||||
|
||||
patch_weight = {
|
||||
'type': 'object', 'properties': {
|
||||
|
@ -63,6 +74,7 @@ create = {
|
|||
'type': 'object', 'properties': {
|
||||
'weight': patch_weight['properties']['weight'],
|
||||
'group': patch_group['properties']['uri'],
|
||||
'flavor': patch_flavor['properties']['flavor'],
|
||||
'uri': patch_uri['properties']['uri'],
|
||||
'options': patch_options['properties']['options']
|
||||
},
|
||||
|
|
|
@ -735,9 +735,9 @@ class Subscription(ControllerBase):
|
|||
class PoolsBase(ControllerBase):
|
||||
"""A controller for managing pools."""
|
||||
|
||||
def _check_capabilities(self, uri, group=None, name=None):
|
||||
def _check_capabilities(self, uri, flavor=None, name=None):
|
||||
default_store = self.driver.conf.drivers.message_store
|
||||
pool_caps = self.capabilities(group=group, name=name)
|
||||
pool_caps = self.capabilities(flavor=flavor, name=name)
|
||||
|
||||
if not pool_caps:
|
||||
return True
|
||||
|
@ -745,30 +745,32 @@ class PoolsBase(ControllerBase):
|
|||
new_store = utils.load_storage_impl(uri,
|
||||
default_store=default_store)
|
||||
|
||||
# NOTE(flaper87): Since all pools in a pool group
|
||||
# NOTE(flaper87): Since all pools in a pool flavor
|
||||
# are assumed to have the same capabilities, it's
|
||||
# fine to check against just 1
|
||||
return pool_caps == new_store.BASE_CAPABILITIES
|
||||
|
||||
def capabilities(self, group=None, name=None):
|
||||
"""Gets the set of capabilities for this group/name
|
||||
def capabilities(self, flavor=None, name=None):
|
||||
"""Gets the set of capabilities for this flavor/name
|
||||
|
||||
:param group: The pool group to get capabilities for
|
||||
:type group: six.text_type
|
||||
:param flavor: The pool flavor to get capabilities for
|
||||
:type flavor: six.text_type
|
||||
:param name: The pool name to get capabilities for
|
||||
:type name: six.text_type
|
||||
"""
|
||||
pllt = []
|
||||
if name:
|
||||
group = list(self._get_pools_by_group(self._get(name)['group']))
|
||||
pool = self.get(name)
|
||||
pllt.append(pool)
|
||||
else:
|
||||
group = list(self._get_pools_by_group(group))
|
||||
pllt = list(self._get_pools_by_flavor(flavor))
|
||||
|
||||
if not len(group) > 0:
|
||||
if not len(pllt) > 0:
|
||||
return ()
|
||||
|
||||
default_store = self.driver.conf.drivers.message_store
|
||||
|
||||
pool_store = utils.load_storage_impl(group[0]['uri'],
|
||||
pool_store = utils.load_storage_impl(pllt[0]['uri'],
|
||||
default_store=default_store)
|
||||
|
||||
return pool_store.BASE_CAPABILITIES
|
||||
|
@ -791,7 +793,7 @@ class PoolsBase(ControllerBase):
|
|||
|
||||
_list = abc.abstractmethod(lambda x: None)
|
||||
|
||||
def create(self, name, weight, uri, group=None, options=None):
|
||||
def create(self, name, weight, uri, group=None, flavor=None, options=None):
|
||||
"""Registers a pool entry.
|
||||
|
||||
:param name: The name of this pool
|
||||
|
@ -801,33 +803,38 @@ class PoolsBase(ControllerBase):
|
|||
:param uri: A URI that can be used by a storage client
|
||||
(e.g., pymongo) to access this pool.
|
||||
:type uri: six.text_type
|
||||
:param group: The group of this pool
|
||||
:type group: six.text_type
|
||||
:param flavor: The flavor of this pool
|
||||
:type flavor: six.text_type
|
||||
:param options: Options used to configure this pool
|
||||
:type options: dict
|
||||
"""
|
||||
if not self._check_capabilities(uri, group=group):
|
||||
flavor_obj = {}
|
||||
if flavor is not None:
|
||||
flavor_obj["name"] = flavor
|
||||
if group is not None:
|
||||
flavor_obj["pool_group"] = group
|
||||
if not self._check_capabilities(uri, flavor=flavor_obj):
|
||||
raise errors.PoolCapabilitiesMismatch()
|
||||
|
||||
return self._create(name, weight, uri, group, options)
|
||||
return self._create(name, weight, uri, group, flavor, options)
|
||||
|
||||
_create = abc.abstractmethod(lambda x: None)
|
||||
|
||||
def get_pools_by_group(self, group=None, detailed=False):
|
||||
"""Returns a pool list filtered by given pool group.
|
||||
def get_pools_by_flavor(self, flavor=None, detailed=False):
|
||||
"""Returns a pool list filtered by given pool flavor.
|
||||
|
||||
:param group: The group to filter on. `None` returns
|
||||
pools that are not assigned to any pool group.
|
||||
:type group: six.text_type
|
||||
:param flavor: The flavor to filter on. `None` returns
|
||||
pools that are not assigned to any pool flavor.
|
||||
:type flavor: six.text_type
|
||||
:param detailed: Should the options data be included?
|
||||
:type detailed: bool
|
||||
:returns: weight, uri, and options for this pool
|
||||
:rtype: {}
|
||||
:raises PoolDoesNotExist: if not found
|
||||
"""
|
||||
return self._get_pools_by_group(group, detailed)
|
||||
return self._get_pools_by_flavor(flavor, detailed)
|
||||
|
||||
_get_pools_by_group = abc.abstractmethod(lambda x: None)
|
||||
_get_pools_by_flavor = abc.abstractmethod(lambda x: None)
|
||||
|
||||
def get(self, name, detailed=False):
|
||||
"""Returns a single pool entry.
|
||||
|
@ -1008,7 +1015,7 @@ class FlavorsBase(ControllerBase):
|
|||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def create(self, name, pool, project=None, capabilities=None):
|
||||
def create(self, name, pool_group=None, project=None, capabilities=None):
|
||||
"""Registers a flavor entry.
|
||||
|
||||
:param name: The name of this flavor
|
||||
|
|
|
@ -97,14 +97,19 @@ class FlavorsController(base.FlavorsBase):
|
|||
return _normalize(res, detailed)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def create(self, name, pool_group, project=None, capabilities=None):
|
||||
def create(self, name, pool_group=None, project=None, capabilities=None):
|
||||
|
||||
# NOTE(flaper87): Check if there are pools in this group.
|
||||
# Should there be a `group_exists` method?
|
||||
# NOTE(wanghao): Since we didn't pass the group name just pool name,
|
||||
# so we don't need to get the pool by group.
|
||||
if not list(self._pools_ctrl.get_pools_by_group(pool_group)):
|
||||
raise errors.PoolGroupDoesNotExist(pool_group)
|
||||
# NOTE(gengchc2): If you do not use the removal group scheme to
|
||||
# configure flavor, pool_group can be None..
|
||||
if pool_group is not None:
|
||||
flavor_obj = {}
|
||||
flavor_obj["pool_group"] = pool_group
|
||||
if not list(self._pools_ctrl.get_pools_by_flavor(flavor_obj)):
|
||||
raise errors.PoolGroupDoesNotExist(pool_group)
|
||||
|
||||
capabilities = {} if capabilities is None else capabilities
|
||||
self._col.update_one({'n': name, 'p': project},
|
||||
|
@ -124,7 +129,8 @@ class FlavorsController(base.FlavorsBase):
|
|||
|
||||
if pool_group is not None:
|
||||
fields['s'] = pool_group
|
||||
|
||||
# NOTE(gengchc2): If you do not use the removal group scheme to
|
||||
# configure flavor, pool_group can be None, pool_group can be remove.
|
||||
assert fields, '`pool_group` or `capabilities` not found in kwargs'
|
||||
res = self._col.update_one({'n': name, 'p': project},
|
||||
{'$set': fields},
|
||||
|
|
|
@ -20,6 +20,7 @@ Schema:
|
|||
'u': uri :: six.text_type
|
||||
'w': weight :: int
|
||||
'o': options :: dict
|
||||
'f': flavor :: six.text_type
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
@ -90,13 +91,22 @@ class PoolsController(base.PoolsBase):
|
|||
return _normalize(res, detailed)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def _get_pools_by_group(self, group=None, detailed=False):
|
||||
cursor = self._col.find({'g': group}, projection=_field_spec(detailed))
|
||||
def _get_pools_by_flavor(self, flavor=None, detailed=False):
|
||||
query = None
|
||||
if flavor is None:
|
||||
query = {'f': None}
|
||||
elif flavor.get("pool_group") is not None:
|
||||
query = {'g': flavor.get("pool_group")}
|
||||
elif flavor.get('name') is not None:
|
||||
query = {'f': flavor.get('name')}
|
||||
cursor = self._col.find(query,
|
||||
projection=_field_spec(detailed))
|
||||
normalizer = functools.partial(_normalize, detailed=detailed)
|
||||
return utils.HookedCursor(cursor, normalizer)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def _create(self, name, weight, uri, group=None, options=None):
|
||||
def _create(self, name, weight, uri, group=None, flavor=None,
|
||||
options=None):
|
||||
options = {} if options is None else options
|
||||
try:
|
||||
self._col.update_one({'n': name},
|
||||
|
@ -104,6 +114,7 @@ class PoolsController(base.PoolsBase):
|
|||
'w': weight,
|
||||
'u': uri,
|
||||
'g': group,
|
||||
'f': flavor,
|
||||
'o': options}},
|
||||
upsert=True)
|
||||
except mongo_error.DuplicateKeyError:
|
||||
|
@ -115,13 +126,20 @@ class PoolsController(base.PoolsBase):
|
|||
|
||||
@utils.raises_conn_error
|
||||
def _update(self, name, **kwargs):
|
||||
names = ('uri', 'weight', 'group', 'options')
|
||||
names = ('uri', 'weight', 'group', 'flavor', 'options')
|
||||
fields = common_utils.fields(kwargs, names,
|
||||
pred=lambda x: x is not None,
|
||||
key_transform=lambda x: x[0])
|
||||
assert fields, ('`weight`, `uri`, `group`, '
|
||||
'or `options` not found in kwargs')
|
||||
|
||||
flavor = fields.get('f')
|
||||
if flavor is not None and len(flavor) == 0:
|
||||
fields['f'] = None
|
||||
group = fields.get('g')
|
||||
if group is not None and len(group) == 0:
|
||||
fields['g'] = None
|
||||
|
||||
res = self._col.update_one({'n': name},
|
||||
{'$set': fields},
|
||||
upsert=False)
|
||||
|
@ -135,17 +153,31 @@ class PoolsController(base.PoolsBase):
|
|||
# recursion error.
|
||||
try:
|
||||
pool = self.get(name)
|
||||
pools_group = self.get_pools_by_group(pool['group'])
|
||||
flavor_ctl = self.driver.flavors_controller
|
||||
res = list(flavor_ctl._list_by_pool_group(pool['group']))
|
||||
if pool['group'] is not None:
|
||||
flavor = {}
|
||||
flavor['pool_group'] = pool['group']
|
||||
pools_group = self.get_pools_by_flavor(flavor=flavor)
|
||||
flavor_ctl = self.driver.flavors_controller
|
||||
res = list(flavor_ctl._list_by_pool_group(pool['group']))
|
||||
|
||||
# NOTE(flaper87): If this is the only pool in the
|
||||
# group and it's being used by a flavor, don't allow
|
||||
# it to be deleted.
|
||||
if res and len(pools_group) == 1:
|
||||
flavors = ', '.join([x['name'] for x in res])
|
||||
raise errors.PoolInUseByFlavor(name, flavors)
|
||||
# NOTE(flaper87): If this is the only pool in the
|
||||
# group and it's being used by a flavor, don't allow
|
||||
# it to be deleted.
|
||||
if res and len(pools_group) == 1:
|
||||
flavors = ', '.join([x['name'] for x in res])
|
||||
raise errors.PoolInUseByFlavor(name, flavors)
|
||||
|
||||
pools_in_flavor = []
|
||||
flavor = pool.get("flavor", None)
|
||||
if flavor is not None:
|
||||
# NOTE(gengchc2): If this is the only pool in the
|
||||
# flavor and it's being used by a flavor, don't allow
|
||||
# it to be deleted.
|
||||
flavor1 = {}
|
||||
flavor1['name'] = flavor
|
||||
pools_in_flavor = self.get_pools_by_flavor(flavor=flavor1)
|
||||
if len(pools_in_flavor) == 1:
|
||||
raise errors.PoolInUseByFlavor(name, flavor)
|
||||
self._col.delete_one({'n': name})
|
||||
except errors.PoolDoesNotExist:
|
||||
pass
|
||||
|
@ -160,6 +192,7 @@ def _normalize(pool, detailed=False):
|
|||
ret = {
|
||||
'name': pool['n'],
|
||||
'group': pool['g'],
|
||||
'flavor': pool['f'],
|
||||
'uri': pool['u'],
|
||||
'weight': pool['w'],
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ from osprofiler import profiler
|
|||
from zaqar.common import decorators
|
||||
from zaqar.common import errors as cerrors
|
||||
from zaqar.common.storage import select
|
||||
from zaqar.i18n import _
|
||||
from zaqar import storage
|
||||
from zaqar.storage import errors
|
||||
from zaqar.storage import pipeline
|
||||
|
@ -246,6 +247,14 @@ class QueueController(storage.Queue):
|
|||
return self._mgt_queue_ctrl.get_metadata(name, project=project)
|
||||
|
||||
def set_metadata(self, name, metadata, project=None):
|
||||
# NOTE(gengchc2): If flavor metadata is modified in queue,
|
||||
# The queue needs to be re-registered to pools, otherwise
|
||||
# the queue flavor parameter is not consistent with the pool.
|
||||
flavor = None
|
||||
if isinstance(metadata, dict):
|
||||
flavor = metadata.get('_flavor')
|
||||
self._pool_catalog.register(name, project=project, flavor=flavor)
|
||||
|
||||
return self._mgt_queue_ctrl.set_metadata(name, metadata=metadata,
|
||||
project=project)
|
||||
|
||||
|
@ -502,22 +511,40 @@ class Catalog(object):
|
|||
|
||||
"""
|
||||
|
||||
# NOTE(cpp-cabrera): only register a queue if the entry
|
||||
# doesn't exist
|
||||
if not self._catalogue_ctrl.exists(project, queue):
|
||||
# NOTE(gengchc): if exist, get queue's pool.flavor:
|
||||
# if queue's pool.flavor is different, first delete it and add it.
|
||||
# Otherwise, if the flavor in the meteredata of the queue is
|
||||
# modified, the catalog will be inconsistent.
|
||||
if self._catalogue_ctrl.exists(project, queue):
|
||||
catalogue = self._catalogue_ctrl.get(project, queue)
|
||||
oldpoolids = catalogue['pool']
|
||||
oldpool = self._pools_ctrl.get(oldpoolids)
|
||||
oldflavor = oldpool['flavor']
|
||||
msgtmpl = _(u'regiester queue to pool: old flavor: %(oldflavor)s '
|
||||
', new flavor: %(flavor)s')
|
||||
LOG.info(msgtmpl,
|
||||
{'oldflavor': oldflavor, 'flavor': flavor})
|
||||
if oldpool['flavor'] != flavor:
|
||||
self._catalogue_ctrl.delete(project, queue)
|
||||
|
||||
if not self._catalogue_ctrl.exists(project, queue):
|
||||
if flavor is not None:
|
||||
flavor = self._flavor_ctrl.get(flavor, project=project)
|
||||
pools = self._pools_ctrl.get_pools_by_group(
|
||||
group=flavor['pool_group'],
|
||||
pools = self._pools_ctrl.get_pools_by_flavor(
|
||||
flavor=flavor,
|
||||
detailed=True)
|
||||
pool = select.weighted(pools)
|
||||
pool = pool and pool['name'] or None
|
||||
msgtmpl = _(u'regiester queue to pool: new flavor:%(flavor)s'
|
||||
' pool_group:%(pool_group)s')
|
||||
LOG.info(msgtmpl,
|
||||
{'flavor': flavor.get('name', None),
|
||||
'pool_group': flavor.get('pool_group', None)})
|
||||
else:
|
||||
# NOTE(flaper87): Get pools assigned to the default
|
||||
# group `None`. We should consider adding a `default_group`
|
||||
# option in the future.
|
||||
pools = self._pools_ctrl.get_pools_by_group(detailed=True)
|
||||
pools = self._pools_ctrl.get_pools_by_flavor(detailed=True)
|
||||
pool = select.weighted(pools)
|
||||
pool = pool and pool['name'] or None
|
||||
|
||||
|
@ -531,7 +558,15 @@ class Catalog(object):
|
|||
if self.lookup(queue, project) is not None:
|
||||
return
|
||||
raise errors.NoPoolFound()
|
||||
msgtmpl = _(u'regiester queue to pool: new flavor: None')
|
||||
LOG.info(msgtmpl)
|
||||
|
||||
msgtmpl = _(u'regiester queue: project:%(project)s'
|
||||
' queue:%(queue)s pool:%(pool)s')
|
||||
LOG.info(msgtmpl,
|
||||
{'project': project,
|
||||
'queue': queue,
|
||||
'pool': pool})
|
||||
self._catalogue_ctrl.insert(project, queue, pool)
|
||||
|
||||
@_pool_id.purges
|
||||
|
|
|
@ -72,18 +72,29 @@ class FlavorsController(base.FlavorsBase):
|
|||
return _normalize(flavor, detailed)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def create(self, name, pool_group, project=None, capabilities=None):
|
||||
def create(self, name, pool_group=None, project=None, capabilities=None):
|
||||
cap = None if capabilities is None else utils.json_encode(capabilities)
|
||||
|
||||
try:
|
||||
stmt = sa.sql.expression.insert(tables.Flavors).values(
|
||||
name=name, pool_group=pool_group, project=project,
|
||||
capabilities=cap
|
||||
)
|
||||
if pool_group is not None:
|
||||
stmt = sa.sql.expression.insert(tables.Flavors).values(
|
||||
name=name, pool_group=pool_group, project=project,
|
||||
capabilities=cap
|
||||
)
|
||||
else:
|
||||
stmt = sa.sql.expression.insert(tables.Flavors).values(
|
||||
name=name, project=project,
|
||||
capabilities=cap
|
||||
)
|
||||
self.driver.run(stmt)
|
||||
except oslo_db.exception.DBDuplicateEntry:
|
||||
if not self._pools_ctrl.get_pools_by_group(pool_group):
|
||||
raise errors.PoolGroupDoesNotExist(pool_group)
|
||||
# NOTE(gengchc2): If you do not use the removal group scheme to
|
||||
# configure flavor, pool_group can be None..
|
||||
if pool_group is not None:
|
||||
flavor_obj = {}
|
||||
flavor_obj["pool_group"] = pool_group
|
||||
if not list(self._pools_ctrl.get_pools_by_flavor(flavor_obj)):
|
||||
raise errors.PoolGroupDoesNotExist(pool_group)
|
||||
|
||||
# TODO(flaper87): merge update/create into a single
|
||||
# method with introduction of upsert
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright 2017 ZTE Corporation.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Queens release
|
||||
|
||||
Revision ID: 005
|
||||
Revises: 006
|
||||
Create Date: 2017-11-09 11:45:45.928605
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '006'
|
||||
down_revision = '005'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
MYSQL_ENGINE = 'InnoDB'
|
||||
MYSQL_CHARSET = 'utf8'
|
||||
|
||||
|
||||
def upgrade():
|
||||
# NOTE(gengchc2): Add a new flavor column to Pools nodes
|
||||
op.add_column('Pools', sa.Column('flavor', sa.String(64), nullable=True))
|
||||
|
||||
# NOTE(gengchc2): Change pool_group to default null in Flavors table
|
||||
op.execute('alter table Flavors change column pool_group '
|
||||
'pool_group varchar(64) default null')
|
|
@ -56,10 +56,22 @@ class PoolsController(base.PoolsBase):
|
|||
yield marker_name and marker_name['next']
|
||||
|
||||
@utils.raises_conn_error
|
||||
def _get_pools_by_group(self, group=None, detailed=False):
|
||||
stmt = sa.sql.select([tables.Pools]).where(
|
||||
tables.Pools.c.group == group
|
||||
)
|
||||
def _get_pools_by_flavor(self, flavor=None, detailed=False):
|
||||
pool_group = flavor.get("pool_group", None) if flavor is not None\
|
||||
else None
|
||||
flavor_name = flavor.get("name", None) if flavor is not None\
|
||||
else None
|
||||
if pool_group is not None:
|
||||
stmt = sa.sql.select([tables.Pools]).where(
|
||||
tables.Pools.c.group == pool_group
|
||||
)
|
||||
elif flavor_name is not None:
|
||||
stmt = sa.sql.select([tables.Pools]).where(
|
||||
tables.Pools.c.flavor == flavor_name
|
||||
)
|
||||
else:
|
||||
stmt = sa.sql.select([tables.Pools])
|
||||
|
||||
cursor = self.driver.run(stmt)
|
||||
|
||||
normalizer = functools.partial(_normalize, detailed=detailed)
|
||||
|
@ -87,7 +99,8 @@ class PoolsController(base.PoolsBase):
|
|||
|
||||
# TODO(cpp-cabrera): rename to upsert
|
||||
@utils.raises_conn_error
|
||||
def _create(self, name, weight, uri, group=None, options=None):
|
||||
def _create(self, name, weight, uri, group=None, flavor=None,
|
||||
options=None):
|
||||
opts = None if options is None else utils.json_encode(options)
|
||||
|
||||
if group is not None:
|
||||
|
@ -95,7 +108,8 @@ class PoolsController(base.PoolsBase):
|
|||
|
||||
try:
|
||||
stmt = sa.sql.expression.insert(tables.Pools).values(
|
||||
name=name, weight=weight, uri=uri, group=group, options=opts
|
||||
name=name, weight=weight, uri=uri, group=group,
|
||||
flavor=flavor, options=opts
|
||||
)
|
||||
self.driver.run(stmt)
|
||||
|
||||
|
@ -103,7 +117,7 @@ class PoolsController(base.PoolsBase):
|
|||
# TODO(cpp-cabrera): merge update/create into a single
|
||||
# method with introduction of upsert
|
||||
self._update(name, weight=weight, uri=uri,
|
||||
group=group, options=options)
|
||||
group=group, flavor=flavor, options=options)
|
||||
|
||||
@utils.raises_conn_error
|
||||
def _exists(self, name):
|
||||
|
@ -117,11 +131,11 @@ class PoolsController(base.PoolsBase):
|
|||
# NOTE(cpp-cabrera): by pruning None-valued kwargs, we avoid
|
||||
# overwriting the existing options field with None, since that
|
||||
# one can be null.
|
||||
names = ('uri', 'weight', 'group', 'options')
|
||||
names = ('uri', 'weight', 'group', 'flavor', 'options')
|
||||
fields = common_utils.fields(kwargs, names,
|
||||
pred=lambda x: x is not None)
|
||||
|
||||
assert fields, ('`weight`, `uri`, `group`, '
|
||||
assert fields, ('`weight`, `uri`, `group`, `flavor`, '
|
||||
'or `options` not found in kwargs')
|
||||
|
||||
if 'options' in fields:
|
||||
|
@ -158,6 +172,7 @@ def _normalize(pool, detailed=False):
|
|||
'group': pool[1],
|
||||
'uri': pool[2],
|
||||
'weight': pool[3],
|
||||
'flavor': pool[5],
|
||||
}
|
||||
if detailed:
|
||||
opts = pool[4]
|
||||
|
|
|
@ -28,7 +28,6 @@ Queues = sa.Table('Queues', metadata,
|
|||
PoolGroup = sa.Table('PoolGroup', metadata,
|
||||
sa.Column('name', sa.String(64), primary_key=True))
|
||||
|
||||
|
||||
Pools = sa.Table('Pools', metadata,
|
||||
sa.Column('name', sa.String(64), primary_key=True),
|
||||
sa.Column('group', sa.ForeignKey('PoolGroup.name',
|
||||
|
@ -37,18 +36,20 @@ Pools = sa.Table('Pools', metadata,
|
|||
sa.Column('uri', sa.String(255),
|
||||
unique=True, nullable=False),
|
||||
sa.Column('weight', sa.INTEGER, nullable=False),
|
||||
sa.Column('options', sa.Text()))
|
||||
|
||||
sa.Column('options', sa.Text()),
|
||||
sa.Column('flavor', sa.String(64), nullable=True))
|
||||
|
||||
# NOTE(gengchc2): Modify pool_group define: turn NOT NULL into DEFAULT NULL:
|
||||
# [alter table Flavors change column pool_group pool_group varchar(64)
|
||||
# default null;]
|
||||
Flavors = sa.Table('Flavors', metadata,
|
||||
sa.Column('name', sa.String(64), primary_key=True),
|
||||
sa.Column('project', sa.String(64)),
|
||||
sa.Column('pool_group', sa.ForeignKey('PoolGroup.name',
|
||||
ondelete='CASCADE'),
|
||||
nullable=False),
|
||||
nullable=True),
|
||||
sa.Column('capabilities', sa.Text()))
|
||||
|
||||
|
||||
Catalogue = sa.Table('Catalogue', metadata,
|
||||
sa.Column('pool', sa.String(64),
|
||||
sa.ForeignKey('Pools.name',
|
||||
|
|
|
@ -66,7 +66,8 @@ class TestBase(testtools.TestCase):
|
|||
opts.set_defaults(self.conf)
|
||||
self.conf.register_opts(configs._PROFILER_OPTIONS,
|
||||
group=configs._PROFILER_GROUP)
|
||||
|
||||
self.redis_url = os.environ.get('ZAQAR_TEST_REDIS_URL',
|
||||
'redis://127.0.0.1:6379')
|
||||
self.mongodb_url = os.environ.get('ZAQAR_TEST_MONGODB_URL',
|
||||
'mongodb://127.0.0.1:27017')
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import math
|
||||
|
@ -68,16 +70,27 @@ class ControllerBaseTest(testing.TestBase):
|
|||
args.append(self.control)
|
||||
self.driver = self.driver_class(*args)
|
||||
else:
|
||||
uri = self.mongodb_url
|
||||
for i in range(4):
|
||||
db_name = "zaqar_test_pools_" + str(i)
|
||||
testredis = os.environ.get('ZAQAR_TEST_REDIS', 0)
|
||||
if testredis:
|
||||
uri = self.redis_url
|
||||
for i in range(4):
|
||||
db_name = "?dbid = " + str(i)
|
||||
|
||||
# NOTE(dynarro): we need to create a unique uri.
|
||||
new_uri = "%s/%s" % (uri, db_name)
|
||||
options = {'database': db_name}
|
||||
self.control.pools_controller.create(six.text_type(i),
|
||||
100, new_uri,
|
||||
options=options)
|
||||
# NOTE(dynarro): we need to create a unique uri.
|
||||
new_uri = "%s/%s" % (uri, db_name)
|
||||
self.control.pools_controller.create(six.text_type(i),
|
||||
100, new_uri)
|
||||
else:
|
||||
uri = self.mongodb_url
|
||||
for i in range(4):
|
||||
db_name = "zaqar_test_pools_" + str(i)
|
||||
|
||||
# NOTE(dynarro): we need to create a unique uri.
|
||||
new_uri = "%s/%s" % (uri, db_name)
|
||||
options = {'database': db_name}
|
||||
self.control.pools_controller.create(six.text_type(i),
|
||||
100, new_uri,
|
||||
options=options)
|
||||
self.driver = self.driver_class(self.conf, cache, self.control)
|
||||
self.addCleanup(self.control.pools_controller.drop_all)
|
||||
self.addCleanup(self.control.catalogue_controller.drop_all)
|
||||
|
@ -1395,10 +1408,12 @@ class PoolsControllerTest(ControllerBaseTest):
|
|||
|
||||
# Let's create one pool
|
||||
self.pool = str(uuid.uuid1())
|
||||
# NOTE(gengchc2): remove pool_group in Rocky release.
|
||||
self.pool_group = str(uuid.uuid1())
|
||||
self.pools_controller.create(self.pool, 100, 'localhost',
|
||||
group=self.pool_group, options={})
|
||||
|
||||
self.pool1 = str(uuid.uuid1())
|
||||
self.flavor = str(uuid.uuid1())
|
||||
self.pools_controller.create(self.pool1, 100, 'localhost1',
|
||||
flavor=self.flavor, options={})
|
||||
self.flavors_controller = self.driver.flavors_controller
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1464,6 +1479,9 @@ class PoolsControllerTest(ControllerBaseTest):
|
|||
|
||||
def test_delete_works(self):
|
||||
self.pools_controller.delete(self.pool)
|
||||
# (gengchc): Remove the flavor from pool, then testcase cleanup pool
|
||||
self.pools_controller.update(self.pool1, flavor="")
|
||||
self.pools_controller.delete(self.pool1)
|
||||
self.assertFalse(self.pools_controller.exists(self.pool))
|
||||
|
||||
def test_delete_nonexistent_is_silent(self):
|
||||
|
@ -1476,9 +1494,9 @@ class PoolsControllerTest(ControllerBaseTest):
|
|||
self.assertRaises(StopIteration, next, pools)
|
||||
|
||||
def test_listing_simple(self):
|
||||
# NOTE(cpp-cabrera): base entry interferes with listing results
|
||||
self.pools_controller.delete(self.pool)
|
||||
|
||||
# (gengchc): Remove the flavor from pool, then testcase cleanup pool
|
||||
self.pools_controller.update(self.pool1, flavor="")
|
||||
self.pools_controller._drop_all()
|
||||
pools = []
|
||||
marker = ''
|
||||
for i in range(15):
|
||||
|
@ -1541,15 +1559,6 @@ class PoolsControllerTest(ControllerBaseTest):
|
|||
self.assertIn('options', entry)
|
||||
self.assertEqual({}, entry['options'])
|
||||
|
||||
def test_mismatching_capabilities(self):
|
||||
# NOTE(flaper87): This may fail for redis. Create
|
||||
# a dummy store for tests.
|
||||
with testing.expect(errors.PoolCapabilitiesMismatch):
|
||||
self.pools_controller.create(str(uuid.uuid1()),
|
||||
100, 'redis://localhost',
|
||||
group=self.pool_group,
|
||||
options={})
|
||||
|
||||
|
||||
class CatalogueControllerTest(ControllerBaseTest):
|
||||
controller_base_class = storage.CatalogueBase
|
||||
|
@ -1562,10 +1571,13 @@ class CatalogueControllerTest(ControllerBaseTest):
|
|||
self.project = six.text_type(uuid.uuid4())
|
||||
|
||||
self.pool = str(uuid.uuid1())
|
||||
self.pool_group = str(uuid.uuid1())
|
||||
self.pool_ctrl.create(self.pool, 100, 'localhost',
|
||||
group=self.pool_group, options={})
|
||||
self.pool_ctrl.create(self.pool, 100, 'localhost', options={})
|
||||
self.addCleanup(self.pool_ctrl.delete, self.pool)
|
||||
self.pool1 = str(uuid.uuid1())
|
||||
self.flavor = str(uuid.uuid1())
|
||||
self.pool_ctrl.create(self.pool1, 100, 'localhost1',
|
||||
options={})
|
||||
self.addCleanup(self.pool_ctrl.delete, self.pool1)
|
||||
|
||||
def tearDown(self):
|
||||
self.controller.drop_all()
|
||||
|
@ -1620,8 +1632,9 @@ class CatalogueControllerTest(ControllerBaseTest):
|
|||
|
||||
def test_update(self):
|
||||
p2 = u'b'
|
||||
# NOTE(gengchc2): Remove [group=self.pool_group] in
|
||||
# it can be tested for redis as management.
|
||||
self.pool_ctrl.create(p2, 100, '127.0.0.1',
|
||||
group=self.pool_group,
|
||||
options={})
|
||||
self.addCleanup(self.pool_ctrl.delete, p2)
|
||||
|
||||
|
@ -1676,6 +1689,8 @@ class CatalogueControllerTest(ControllerBaseTest):
|
|||
self.controller.insert(self.project, q2, u'a')
|
||||
|
||||
|
||||
# NOTE(gengchc2): remove FlavorsControllerTest in Rocky release
|
||||
# and use FlavorsControllerTest1 instead for pool_group removal.
|
||||
class FlavorsControllerTest(ControllerBaseTest):
|
||||
"""Flavors Controller base tests.
|
||||
|
||||
|
@ -1853,6 +1868,185 @@ class FlavorsControllerTest(ControllerBaseTest):
|
|||
self.assertEqual({}, entry['capabilities'])
|
||||
|
||||
|
||||
# NOTE(gengchc2): Unittest for new flavor configure scenario.
|
||||
class FlavorsControllerTest1(ControllerBaseTest):
|
||||
"""Flavors Controller base tests.
|
||||
|
||||
NOTE(flaper87): Implementations of this class should
|
||||
override the tearDown method in order
|
||||
to clean up storage's state.
|
||||
"""
|
||||
controller_base_class = storage.FlavorsBase
|
||||
|
||||
def setUp(self):
|
||||
super(FlavorsControllerTest1, self).setUp()
|
||||
self.pools_controller = self.driver.pools_controller
|
||||
self.flavors_controller = self.driver.flavors_controller
|
||||
|
||||
# Let's create one pool
|
||||
self.pool = str(uuid.uuid1())
|
||||
self.flavor = 'durable'
|
||||
self.pools_controller.create(self.pool, 100, 'localhost',
|
||||
options={})
|
||||
self.addCleanup(self.pools_controller.delete, self.pool)
|
||||
|
||||
def tearDown(self):
|
||||
self.pools_controller.update(self.pool, flavor="")
|
||||
self.flavors_controller.drop_all()
|
||||
super(FlavorsControllerTest1, self).tearDown()
|
||||
|
||||
def test_create_succeeds(self):
|
||||
self.flavors_controller.create(self.flavor,
|
||||
project=self.project,
|
||||
capabilities={})
|
||||
|
||||
def _flavors_expects(self, flavor, xname, xproject):
|
||||
self.assertIn('name', flavor)
|
||||
self.assertEqual(xname, flavor['name'])
|
||||
self.assertNotIn('project', flavor)
|
||||
|
||||
def test_create_replaces_on_duplicate_insert(self):
|
||||
name = str(uuid.uuid1())
|
||||
self.flavors_controller.create(name,
|
||||
project=self.project,
|
||||
capabilities={})
|
||||
entry = self.flavors_controller.get(name, self.project)
|
||||
self._flavors_expects(entry, name, self.project)
|
||||
new_capabilities = {'fifo': False}
|
||||
self.flavors_controller.create(name,
|
||||
project=self.project,
|
||||
capabilities=new_capabilities)
|
||||
entry = self.flavors_controller.get(name, project=self.project,
|
||||
detailed=True)
|
||||
self._flavors_expects(entry, name, self.project)
|
||||
self.assertEqual(new_capabilities, entry['capabilities'])
|
||||
|
||||
def test_get_returns_expected_content(self):
|
||||
name = 'durable'
|
||||
capabilities = {'fifo': True}
|
||||
self.flavors_controller.create(name,
|
||||
project=self.project,
|
||||
capabilities=capabilities)
|
||||
res = self.flavors_controller.get(name, project=self.project)
|
||||
self._flavors_expects(res, name, self.project)
|
||||
self.assertNotIn('capabilities', res)
|
||||
|
||||
def test_detailed_get_returns_expected_content(self):
|
||||
name = 'durable'
|
||||
capabilities = {'fifo': True}
|
||||
self.flavors_controller.create(name,
|
||||
project=self.project,
|
||||
capabilities=capabilities)
|
||||
res = self.flavors_controller.get(name, project=self.project,
|
||||
detailed=True)
|
||||
self._flavors_expects(res, name, self.project)
|
||||
self.assertIn('capabilities', res)
|
||||
self.assertEqual(capabilities, res['capabilities'])
|
||||
|
||||
def test_get_raises_if_not_found(self):
|
||||
self.assertRaises(errors.FlavorDoesNotExist,
|
||||
self.flavors_controller.get, 'notexists')
|
||||
|
||||
def test_exists(self):
|
||||
self.flavors_controller.create('exists',
|
||||
project=self.project,
|
||||
capabilities={})
|
||||
self.assertTrue(self.flavors_controller.exists('exists',
|
||||
project=self.project))
|
||||
self.assertFalse(self.flavors_controller.exists('notexists',
|
||||
project=self.project))
|
||||
|
||||
def test_update_raises_assertion_error_on_bad_fields(self):
|
||||
self.assertRaises(AssertionError, self.pools_controller.update,
|
||||
self.flavor)
|
||||
|
||||
def test_update_works(self):
|
||||
name = 'yummy'
|
||||
self.flavors_controller.create(name,
|
||||
project=self.project,
|
||||
capabilities={})
|
||||
|
||||
res = self.flavors_controller.get(name, project=self.project,
|
||||
detailed=True)
|
||||
|
||||
p = 'olympic'
|
||||
flavor = name
|
||||
self.pools_controller.create(p, 100, 'localhost2',
|
||||
flavor=flavor, options={})
|
||||
self.addCleanup(self.pools_controller.delete, p)
|
||||
|
||||
new_capabilities = {'fifo': False}
|
||||
self.flavors_controller.update(name, project=self.project,
|
||||
capabilities={'fifo': False})
|
||||
res = self.flavors_controller.get(name, project=self.project,
|
||||
detailed=True)
|
||||
self._flavors_expects(res, name, self.project)
|
||||
self.assertEqual(new_capabilities, res['capabilities'])
|
||||
self.pools_controller.update(p, flavor="")
|
||||
|
||||
def test_delete_works(self):
|
||||
name = 'puke'
|
||||
self.flavors_controller.create(name,
|
||||
project=self.project,
|
||||
capabilities={})
|
||||
self.flavors_controller.delete(name, project=self.project)
|
||||
self.assertFalse(self.flavors_controller.exists(name))
|
||||
|
||||
def test_delete_nonexistent_is_silent(self):
|
||||
self.flavors_controller.delete('nonexisting')
|
||||
|
||||
def test_drop_all_leads_to_empty_listing(self):
|
||||
self.flavors_controller.drop_all()
|
||||
cursor = self.flavors_controller.list()
|
||||
flavors = next(cursor)
|
||||
self.assertRaises(StopIteration, next, flavors)
|
||||
self.assertFalse(next(cursor))
|
||||
|
||||
def test_listing_simple(self):
|
||||
name_gen = lambda i: chr(ord('A') + i)
|
||||
for i in range(15):
|
||||
pool = str(i)
|
||||
flavor = name_gen(i)
|
||||
uri = 'localhost:2701' + pool
|
||||
self.pools_controller.create(pool, 100, uri,
|
||||
flavor=flavor, options={})
|
||||
self.addCleanup(self.pools_controller.delete, pool)
|
||||
|
||||
self.flavors_controller.create(flavor, project=self.project,
|
||||
capabilities={})
|
||||
|
||||
def get_res(**kwargs):
|
||||
cursor = self.flavors_controller.list(project=self.project,
|
||||
**kwargs)
|
||||
res = list(next(cursor))
|
||||
marker = next(cursor)
|
||||
self.assertTrue(marker)
|
||||
return res
|
||||
|
||||
res = get_res()
|
||||
self.assertEqual(10, len(res))
|
||||
for i, entry in enumerate(res):
|
||||
self._flavors_expects(entry, name_gen(i), self.project)
|
||||
self.assertNotIn('capabilities', entry)
|
||||
|
||||
res = get_res(limit=5)
|
||||
self.assertEqual(5, len(res))
|
||||
|
||||
res = get_res(marker=name_gen(3))
|
||||
self._flavors_expects(res[0], name_gen(4), self.project)
|
||||
|
||||
res = get_res(detailed=True)
|
||||
self.assertEqual(10, len(res))
|
||||
for i, entry in enumerate(res):
|
||||
self._flavors_expects(entry, name_gen(i), self.project)
|
||||
self.assertIn('capabilities', entry)
|
||||
self.assertEqual({}, entry['capabilities'])
|
||||
# (gengchc): Remove the flavor from pool, then testcase cleanup pools
|
||||
for i in range(15):
|
||||
pool = str(i)
|
||||
self.pools_controller.update(pool, flavor="")
|
||||
|
||||
|
||||
def _insert_fixtures(controller, queue_name, project=None,
|
||||
client_uuid=None, num=4, ttl=120):
|
||||
def messages():
|
||||
|
|
|
@ -507,10 +507,14 @@ class MongodbPoolsTests(base.PoolsControllerTest):
|
|||
|
||||
def setUp(self):
|
||||
super(MongodbPoolsTests, self).setUp()
|
||||
self.pools_controller.create(self.pool, 100, 'localhost',
|
||||
group=self.pool_group, options={})
|
||||
|
||||
def tearDown(self):
|
||||
super(MongodbPoolsTests, self).tearDown()
|
||||
|
||||
# NOTE(gengchc2): remove test_delete_pool_used_by_flavor in Rocky release
|
||||
# and use test_delete_pool_used_by_flavor1 instead for pool_group removal.
|
||||
def test_delete_pool_used_by_flavor(self):
|
||||
self.flavors_controller.create('durable', self.pool_group,
|
||||
project=self.project,
|
||||
|
@ -519,6 +523,19 @@ class MongodbPoolsTests(base.PoolsControllerTest):
|
|||
with testing.expect(errors.PoolInUseByFlavor):
|
||||
self.pools_controller.delete(self.pool)
|
||||
|
||||
# NOTE(gengchc2): Unittest for new flavor configure scenario.
|
||||
def test_delete_pool_used_by_flavor1(self):
|
||||
self.flavors_controller.create(self.flavor,
|
||||
project=self.project,
|
||||
capabilities={})
|
||||
self.pools_controller.update(self.pool1, flavor=self.flavor)
|
||||
|
||||
with testing.expect(errors.PoolInUseByFlavor):
|
||||
self.pools_controller.delete(self.pool1)
|
||||
|
||||
# NOTE(gengchc2): remove test_mismatching_capabilities_fifo in Rocky
|
||||
# release and use test_mismatching_capabilities_fifo1 instead for
|
||||
# pool_group removal.
|
||||
def test_mismatching_capabilities_fifo(self):
|
||||
with testing.expect(errors.PoolCapabilitiesMismatch):
|
||||
self.pools_controller.create(str(uuid.uuid1()),
|
||||
|
@ -526,6 +543,36 @@ class MongodbPoolsTests(base.PoolsControllerTest):
|
|||
group=self.pool_group,
|
||||
options={})
|
||||
|
||||
# NOTE(gengchc2): Unittest for new flavor configure scenario.
|
||||
def test_mismatching_capabilities_fifo1(self):
|
||||
with testing.expect(errors.PoolCapabilitiesMismatch):
|
||||
self.pools_controller.create(str(uuid.uuid1()),
|
||||
100, 'mongodb.fifo://localhost',
|
||||
flavor=self.flavor,
|
||||
options={})
|
||||
|
||||
# NOTE(gengchc2): remove test_mismatching_capabilities in Rocky release
|
||||
# and use test_mismatching_capabilities1 instead for pool_group removal.
|
||||
def test_mismatching_capabilities(self):
|
||||
# NOTE(gengchc2): This test is used for testing mismatchming
|
||||
# capabilities in pool with group
|
||||
with testing.expect(errors.PoolCapabilitiesMismatch):
|
||||
self.pools_controller.create(str(uuid.uuid1()),
|
||||
100, 'redis://localhost',
|
||||
group=self.pool_group,
|
||||
options={})
|
||||
|
||||
def test_mismatching_capabilities1(self):
|
||||
# NOTE(gengchc2): This test is used for testing mismatchming
|
||||
# capabilities in pool with flavor
|
||||
with testing.expect(errors.PoolCapabilitiesMismatch):
|
||||
self.pools_controller.create(str(uuid.uuid1()),
|
||||
100, 'redis://localhost',
|
||||
flavor=self.flavor,
|
||||
options={})
|
||||
|
||||
# NOTE(gengchc2): remove test_duplicate_uri in Rocky release and
|
||||
# use test_duplicate_uri1 instead for pool_group removal.
|
||||
def test_duplicate_uri(self):
|
||||
with testing.expect(errors.PoolAlreadyExists):
|
||||
# The url 'localhost' is used in setUp(). So reusing the uri
|
||||
|
@ -533,6 +580,14 @@ class MongodbPoolsTests(base.PoolsControllerTest):
|
|||
self.pools_controller.create(str(uuid.uuid1()), 100, 'localhost',
|
||||
group=str(uuid.uuid1()), options={})
|
||||
|
||||
# NOTE(gengchc2): Unittest for new flavor configure scenario.
|
||||
def test_duplicate_uri1(self):
|
||||
with testing.expect(errors.PoolAlreadyExists):
|
||||
# The url 'localhost' is used in setUp(). So reusing the uri
|
||||
# 'localhost' here will raise PoolAlreadyExists.
|
||||
self.pools_controller.create(str(uuid.uuid1()), 100, 'localhost',
|
||||
flavor=str(uuid.uuid1()), options={})
|
||||
|
||||
|
||||
@testing.requires_mongodb
|
||||
class MongodbCatalogueTests(base.CatalogueControllerTest):
|
||||
|
@ -577,6 +632,8 @@ class PooledClaimsTests(base.ClaimControllerTest):
|
|||
self.skip("Fix sqlalchemy driver")
|
||||
|
||||
|
||||
# NOTE(gengchc2): remove MongodbFlavorsTest in Rocky release and
|
||||
# use MongodbFlavorsTest1 instead for pool_group removal.
|
||||
@testing.requires_mongodb
|
||||
class MongodbFlavorsTest(base.FlavorsControllerTest):
|
||||
driver_class = mongodb.ControlDriver
|
||||
|
@ -587,3 +644,16 @@ class MongodbFlavorsTest(base.FlavorsControllerTest):
|
|||
def setUp(self):
|
||||
super(MongodbFlavorsTest, self).setUp()
|
||||
self.addCleanup(self.controller.drop_all)
|
||||
|
||||
|
||||
# NOTE(gengchc2): Unittest for new flavor configure scenario.
|
||||
@testing.requires_mongodb
|
||||
class MongodbFlavorsTest1(base.FlavorsControllerTest1):
|
||||
driver_class = mongodb.ControlDriver
|
||||
controller_class = controllers.FlavorsController
|
||||
control_driver_class = mongodb.ControlDriver
|
||||
config_file = 'wsgi_mongodb.conf'
|
||||
|
||||
def setUp(self):
|
||||
super(MongodbFlavorsTest1, self).setUp()
|
||||
self.addCleanup(self.controller.drop_all)
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
# the License.
|
||||
|
||||
import six
|
||||
import uuid
|
||||
|
||||
from zaqar import storage
|
||||
from zaqar.storage import sqlalchemy
|
||||
from zaqar.storage.sqlalchemy import controllers
|
||||
from zaqar.storage.sqlalchemy import tables
|
||||
|
@ -41,6 +43,31 @@ class SqlalchemyPoolsTest(DBCreateMixin, base.PoolsControllerTest):
|
|||
controller_class = controllers.PoolsController
|
||||
control_driver_class = sqlalchemy.ControlDriver
|
||||
|
||||
def setUp(self):
|
||||
super(SqlalchemyPoolsTest, self).setUp()
|
||||
self.pools_controller.create(self.pool, 100, 'localhost',
|
||||
group=self.pool_group, options={})
|
||||
|
||||
# NOTE(gengchc2): remove test_mismatching_capabilities in Rocky release
|
||||
# and use test_mismatching_capabilities1 instead for pool_group removal.
|
||||
def test_mismatching_capabilities(self):
|
||||
# NOTE(gengchc2): This test is used for testing mismatchming
|
||||
# capabilities in pool with group
|
||||
with testing.expect(storage.errors.PoolCapabilitiesMismatch):
|
||||
self.pools_controller.create(str(uuid.uuid1()),
|
||||
100, 'redis://localhost',
|
||||
group=self.pool_group,
|
||||
options={})
|
||||
|
||||
def test_mismatching_capabilities1(self):
|
||||
# NOTE(gengchc2): This test is used for testing mismatchming
|
||||
# capabilities in pool with flavor
|
||||
with testing.expect(storage.errors.PoolCapabilitiesMismatch):
|
||||
self.pools_controller.create(str(uuid.uuid1()),
|
||||
100, 'redis://localhost',
|
||||
flavor=self.flavor,
|
||||
options={})
|
||||
|
||||
|
||||
class SqlalchemyCatalogueTest(DBCreateMixin, base.CatalogueControllerTest):
|
||||
config_file = 'wsgi_sqlalchemy.conf'
|
||||
|
@ -49,6 +76,8 @@ class SqlalchemyCatalogueTest(DBCreateMixin, base.CatalogueControllerTest):
|
|||
control_driver_class = sqlalchemy.ControlDriver
|
||||
|
||||
|
||||
# NOTE(gengchc2): remove SqlalchemyFlavorsTest in Rocky release and
|
||||
# use SqlalchemyFlavorsTest1 instead for pool_group removal.
|
||||
class SqlalchemyFlavorsTest(DBCreateMixin, base.FlavorsControllerTest):
|
||||
config_file = 'wsgi_sqlalchemy.conf'
|
||||
driver_class = sqlalchemy.ControlDriver
|
||||
|
@ -56,6 +85,14 @@ class SqlalchemyFlavorsTest(DBCreateMixin, base.FlavorsControllerTest):
|
|||
control_driver_class = sqlalchemy.ControlDriver
|
||||
|
||||
|
||||
# NOTE(gengchc2): Unittest for new flavor configure scenario.
|
||||
class SqlalchemyFlavorsTest1(DBCreateMixin, base.FlavorsControllerTest1):
|
||||
config_file = 'wsgi_sqlalchemy.conf'
|
||||
driver_class = sqlalchemy.ControlDriver
|
||||
controller_class = controllers.FlavorsController
|
||||
control_driver_class = sqlalchemy.ControlDriver
|
||||
|
||||
|
||||
class MsgidTests(testing.TestBase):
|
||||
|
||||
def test_encode(self):
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
# Copyright (c) 2017 ZTE Corporation..
|
||||
#
|
||||
# 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
|
||||
import uuid
|
||||
|
||||
from zaqar.common import cache as oslo_cache
|
||||
from zaqar.storage import errors
|
||||
from zaqar.storage import mongodb
|
||||
from zaqar.storage import pooling
|
||||
from zaqar.storage import utils
|
||||
from zaqar import tests as testing
|
||||
|
||||
|
||||
# TODO(cpp-cabrera): it would be wonderful to refactor this unit test
|
||||
# so that it could use multiple control storage backends once those
|
||||
# have pools/catalogue implementations.
|
||||
@testing.requires_mongodb
|
||||
class PoolCatalogTest(testing.TestBase):
|
||||
|
||||
config_file = 'wsgi_mongodb_pooled_disable_virtual_pool.conf'
|
||||
|
||||
def setUp(self):
|
||||
super(PoolCatalogTest, self).setUp()
|
||||
|
||||
oslo_cache.register_config(self.conf)
|
||||
cache = oslo_cache.get_cache(self.conf)
|
||||
control = utils.load_storage_driver(self.conf, cache,
|
||||
control_mode=True)
|
||||
|
||||
self.pools_ctrl = control.pools_controller
|
||||
self.flavors_ctrl = control.flavors_controller
|
||||
self.catalogue_ctrl = control.catalogue_controller
|
||||
|
||||
# NOTE(cpp-cabrera): populate catalogue
|
||||
self.pool = str(uuid.uuid1())
|
||||
self.pool2 = str(uuid.uuid1())
|
||||
self.queue = str(uuid.uuid1())
|
||||
self.flavor = str(uuid.uuid1())
|
||||
self.project = str(uuid.uuid1())
|
||||
|
||||
# FIXME(therve) This is horrible, we need to manage duplication in a
|
||||
# nicer way
|
||||
if 'localhost' in self.mongodb_url:
|
||||
other_url = self.mongodb_url.replace('localhost', '127.0.0.1')
|
||||
elif '127.0.0.1' in self.mongodb_url:
|
||||
other_url = self.mongodb_url.replace('127.0.0.1', 'localhost')
|
||||
else:
|
||||
self.skipTest("Can't build a dummy mongo URL.")
|
||||
|
||||
self.pools_ctrl.create(self.pool, 100, self.mongodb_url)
|
||||
self.pools_ctrl.create(self.pool2, 100,
|
||||
other_url)
|
||||
self.catalogue_ctrl.insert(self.project, self.queue, self.pool)
|
||||
self.catalog = pooling.Catalog(self.conf, cache, control)
|
||||
self.flavors_ctrl.create(self.flavor,
|
||||
project=self.project)
|
||||
self.pools_ctrl.update(self.pool2, flavor=self.flavor)
|
||||
|
||||
def tearDown(self):
|
||||
self.catalogue_ctrl.drop_all()
|
||||
self.pools_ctrl.drop_all()
|
||||
super(PoolCatalogTest, self).tearDown()
|
||||
|
||||
def test_lookup_loads_correct_driver(self):
|
||||
storage = self.catalog.lookup(self.queue, self.project)
|
||||
self.assertIsInstance(storage._storage, mongodb.DataDriver)
|
||||
|
||||
def test_lookup_returns_default_or_none_if_queue_not_mapped(self):
|
||||
# Return default
|
||||
self.assertIsNone(self.catalog.lookup('not', 'mapped'))
|
||||
|
||||
self.config(message_store='faulty', group='drivers')
|
||||
self.config(enable_virtual_pool=True, group='pooling:catalog')
|
||||
self.assertIsNotNone(self.catalog.lookup('not', 'mapped'))
|
||||
|
||||
def test_lookup_returns_none_if_entry_deregistered(self):
|
||||
self.catalog.deregister(self.queue, self.project)
|
||||
self.assertIsNone(self.catalog.lookup(self.queue, self.project))
|
||||
|
||||
def test_register_leads_to_successful_lookup(self):
|
||||
self.catalog.register('not_yet', 'mapped')
|
||||
storage = self.catalog.lookup('not_yet', 'mapped')
|
||||
self.assertIsInstance(storage._storage, mongodb.DataDriver)
|
||||
|
||||
def test_register_with_flavor(self):
|
||||
queue = 'test'
|
||||
self.catalog.register(queue, project=self.project,
|
||||
flavor=self.flavor)
|
||||
storage = self.catalog.lookup(queue, self.project)
|
||||
self.assertIsInstance(storage._storage, mongodb.DataDriver)
|
||||
|
||||
def test_register_with_fake_flavor(self):
|
||||
self.assertRaises(errors.FlavorDoesNotExist,
|
||||
self.catalog.register,
|
||||
'test', project=self.project,
|
||||
flavor='fake')
|
||||
|
||||
def test_queues_list_on_multi_pools(self):
|
||||
def fake_list(project=None, marker=None, limit=10, detailed=False):
|
||||
yield iter([{'name': 'fake_queue'}])
|
||||
|
||||
list_str = 'zaqar.storage.mongodb.queues.QueueController.list'
|
||||
with mock.patch(list_str) as queues_list:
|
||||
queues_list.side_effect = fake_list
|
||||
queue_controller = pooling.QueueController(self.catalog)
|
||||
result = queue_controller.list(project=self.project)
|
||||
queue_list = list(next(result))
|
||||
self.assertEqual(1, len(queue_list))
|
||||
|
||||
def test_queue_create_with_empty_json_body(self):
|
||||
queue_controller = pooling.QueueController(self.catalog)
|
||||
with mock.patch('zaqar.storage.pooling.Catalog.register') as register:
|
||||
queue_controller.create(self.queue, metadata={},
|
||||
project=self.project)
|
||||
register.assert_called_with(self.queue, project=self.project,
|
||||
flavor=None)
|
|
@ -23,6 +23,7 @@ from zaqar import tests as testing
|
|||
from zaqar.tests.unit.transport.wsgi import base
|
||||
|
||||
|
||||
# NOTE(gengchc2): remove pool_group in Rocky release.
|
||||
@contextlib.contextmanager
|
||||
def flavor(test, name, pool_group):
|
||||
"""A context manager for constructing a flavor for use in testing.
|
||||
|
|
|
@ -0,0 +1,354 @@
|
|||
# Copyright (c) 2017 ZTE Corporation.
|
||||
#
|
||||
# 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 contextlib
|
||||
import uuid
|
||||
|
||||
import ddt
|
||||
import falcon
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from zaqar import tests as testing
|
||||
from zaqar.tests.unit.transport.wsgi import base
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def flavor(test, name, pool_list):
|
||||
"""A context manager for constructing a flavor for use in testing.
|
||||
|
||||
Deletes the flavor after exiting the context.
|
||||
|
||||
:param test: Must expose simulate_* methods
|
||||
:param name: Name for this flavor
|
||||
:type name: six.text_type
|
||||
:type pool: six.text_type
|
||||
:returns: (name, uri, capabilities)
|
||||
:rtype: see above
|
||||
|
||||
"""
|
||||
|
||||
doc = {'pool_list': pool_list}
|
||||
path = test.url_prefix + '/flavors/' + name
|
||||
|
||||
test.simulate_put(path, body=jsonutils.dumps(doc))
|
||||
|
||||
try:
|
||||
yield name, pool_list
|
||||
|
||||
finally:
|
||||
test.simulate_delete(path)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def flavors(test, count):
|
||||
"""A context manager for constructing flavors for use in testing.
|
||||
|
||||
Deletes the flavors after exiting the context.
|
||||
|
||||
:param test: Must expose simulate_* methods
|
||||
:param count: Number of pools to create
|
||||
:type count: int
|
||||
:returns: (paths, pool_list capabilities)
|
||||
:rtype: ([six.text_type], [six.text_type], [dict])
|
||||
|
||||
"""
|
||||
|
||||
pool_path_all = []
|
||||
flavor_path_all = []
|
||||
for i in range(count):
|
||||
poolname = 'pool' + str(i)
|
||||
pool_doc = {'weight': 100,
|
||||
'uri': test.mongodb_url + '/test' + str(i)}
|
||||
pool_path = test.url_prefix + '/pools/' + poolname
|
||||
test.simulate_put(pool_path, body=jsonutils.dumps(pool_doc))
|
||||
flavorname = str(i)
|
||||
flavor_path = test.url_prefix + "/flavors/" + flavorname
|
||||
flavor_doc = {'pool_list': [poolname]}
|
||||
test.simulate_put(flavor_path, body=jsonutils.dumps(flavor_doc))
|
||||
pool_path_all.append(pool_path)
|
||||
flavor_path_all.append(flavor_path)
|
||||
|
||||
try:
|
||||
yield flavor_path_all
|
||||
finally:
|
||||
for path in flavor_path_all:
|
||||
test.simulate_delete(path)
|
||||
for path in pool_path_all:
|
||||
test.simulate_delete(path)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestFlavorsMongoDB(base.V2Base):
|
||||
|
||||
config_file = 'wsgi_mongodb_pooled.conf'
|
||||
|
||||
@testing.requires_mongodb
|
||||
def setUp(self):
|
||||
super(TestFlavorsMongoDB, self).setUp()
|
||||
self.queue = 'test-queue'
|
||||
self.queue_path = self.url_prefix + '/queues/' + self.queue
|
||||
|
||||
self.pool = 'mypool'
|
||||
self.pool_path = self.url_prefix + '/pools/' + self.pool
|
||||
self.pool_doc = {'weight': 100,
|
||||
'uri': self.mongodb_url + '/test'}
|
||||
self.simulate_put(self.pool_path, body=jsonutils.dumps(self.pool_doc))
|
||||
|
||||
self.flavor = 'test-flavor'
|
||||
self.doc = {'capabilities': {}}
|
||||
self.doc['pool_list'] = [self.pool]
|
||||
self.flavor_path = self.url_prefix + '/flavors/' + self.flavor
|
||||
self.simulate_put(self.flavor_path, body=jsonutils.dumps(self.doc))
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
def tearDown(self):
|
||||
self.simulate_delete(self.queue_path)
|
||||
self.simulate_delete(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_204, self.srmock.status)
|
||||
self.simulate_delete(self.pool_path)
|
||||
|
||||
super(TestFlavorsMongoDB, self).tearDown()
|
||||
|
||||
def test_put_flavor_works(self):
|
||||
name = str(uuid.uuid1())
|
||||
with flavor(self, name, self.doc['pool_list']):
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
def test_put_raises_if_missing_fields(self):
|
||||
path = self.url_prefix + '/flavors/' + str(uuid.uuid1())
|
||||
self.simulate_put(path, body=jsonutils.dumps({}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
self.simulate_put(path,
|
||||
body=jsonutils.dumps({'capabilities': {}}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
@ddt.data(1, 2**32+1, [])
|
||||
def test_put_raises_if_invalid_pool(self, pool_list):
|
||||
path = self.url_prefix + '/flavors/' + str(uuid.uuid1())
|
||||
self.simulate_put(path,
|
||||
body=jsonutils.dumps({'pool_list': pool_list}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def test_put_auto_get_capabilities(self):
|
||||
path = self.url_prefix + '/flavors/' + str(uuid.uuid1())
|
||||
doc = {'pool_list': self.doc['pool_list']}
|
||||
self.simulate_put(path, body=jsonutils.dumps(doc))
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
# NOTE(gengchc2): Delete it, otherwise exist garbage flavor.
|
||||
self.simulate_delete(path)
|
||||
|
||||
def test_put_existing_overwrites(self):
|
||||
# NOTE(cabrera): setUp creates default flavor
|
||||
expect = self.doc
|
||||
self.simulate_put(self.flavor_path,
|
||||
body=jsonutils.dumps(expect))
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
result = self.simulate_get(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
doc = jsonutils.loads(result[0])
|
||||
self.assertEqual(expect['pool_list'], doc['pool_list'])
|
||||
|
||||
def test_create_flavor_no_pool_list(self):
|
||||
self.simulate_delete(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_204, self.srmock.status)
|
||||
|
||||
self.simulate_delete(self.pool_path)
|
||||
self.assertEqual(falcon.HTTP_204, self.srmock.status)
|
||||
resp = self.simulate_put(self.flavor_path,
|
||||
body=jsonutils.dumps(self.doc))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
self.assertEqual(
|
||||
{'description': 'Flavor test-flavor could not be created, '
|
||||
'error:Pool mypool does not exist',
|
||||
'title': 'Unable to create'},
|
||||
jsonutils.loads(resp[0]))
|
||||
|
||||
def test_delete_works(self):
|
||||
self.simulate_delete(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_204, self.srmock.status)
|
||||
|
||||
self.simulate_get(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_404, self.srmock.status)
|
||||
|
||||
def test_get_nonexisting_raises_404(self):
|
||||
self.simulate_get(self.url_prefix + '/flavors/nonexisting')
|
||||
self.assertEqual(falcon.HTTP_404, self.srmock.status)
|
||||
|
||||
def _flavor_expect(self, flavor, xhref, xpool_list=None):
|
||||
self.assertIn('href', flavor)
|
||||
self.assertIn('name', flavor)
|
||||
self.assertEqual(xhref, flavor['href'])
|
||||
if xpool_list is not None:
|
||||
self.assertIn('pool_list', flavor)
|
||||
self.assertEqual(xpool_list, flavor['pool_list'])
|
||||
|
||||
def test_get_works(self):
|
||||
result = self.simulate_get(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
flavor = jsonutils.loads(result[0])
|
||||
self._flavor_expect(flavor, self.flavor_path, self.doc['pool_list'])
|
||||
|
||||
store_caps = ['FIFO', 'CLAIMS', 'DURABILITY',
|
||||
'AOD', 'HIGH_THROUGHPUT']
|
||||
self.assertEqual(store_caps, flavor['capabilities'])
|
||||
|
||||
def test_patch_raises_if_missing_fields(self):
|
||||
self.simulate_patch(self.flavor_path,
|
||||
body=jsonutils.dumps({'location': 1}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def _patch_test(self, doc):
|
||||
result = self.simulate_patch(self.flavor_path,
|
||||
body=jsonutils.dumps(doc))
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
updated_flavor = jsonutils.loads(result[0])
|
||||
self._flavor_expect(updated_flavor, self.flavor_path)
|
||||
capabilities = ['FIFO', 'CLAIMS', 'DURABILITY', 'AOD',
|
||||
'HIGH_THROUGHPUT']
|
||||
self.assertEqual(capabilities, updated_flavor['capabilities'])
|
||||
result = self.simulate_get(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
flavor = jsonutils.loads(result[0])
|
||||
self._flavor_expect(flavor, self.flavor_path)
|
||||
self.assertEqual(capabilities, flavor['capabilities'])
|
||||
|
||||
def test_patch_works(self):
|
||||
doc = {'pool_list': self.doc['pool_list'], 'capabilities': []}
|
||||
self._patch_test(doc)
|
||||
|
||||
def test_patch_works_with_extra_fields(self):
|
||||
doc = {'pool_list': self.doc['pool_list'], 'capabilities': [],
|
||||
'location': 100, 'partition': 'taco'}
|
||||
self._patch_test(doc)
|
||||
|
||||
@ddt.data(-1, 2**32+1, [])
|
||||
def test_patch_raises_400_on_invalid_pool_list(self, pool_list):
|
||||
self.simulate_patch(self.flavor_path,
|
||||
body=jsonutils.dumps({'pool_list': pool_list}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
@ddt.data(-1, 'wee', [])
|
||||
def test_patch_raises_400_on_invalid_capabilities(self, capabilities):
|
||||
doc = {'capabilities': capabilities}
|
||||
self.simulate_patch(self.flavor_path, body=jsonutils.dumps(doc))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def test_patch_raises_404_if_flavor_not_found(self):
|
||||
self.simulate_patch(self.url_prefix + '/flavors/notexists',
|
||||
body=jsonutils.dumps({'pool_list': ['test']}))
|
||||
self.assertEqual(falcon.HTTP_404, self.srmock.status)
|
||||
|
||||
def test_empty_listing(self):
|
||||
self.simulate_delete(self.flavor_path)
|
||||
result = self.simulate_get(self.url_prefix + '/flavors')
|
||||
results = jsonutils.loads(result[0])
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
self.assertEqual(0, len(results['flavors']))
|
||||
self.assertIn('links', results)
|
||||
|
||||
def _listing_test(self, count=10, limit=10,
|
||||
marker=None, detailed=False):
|
||||
# NOTE(cpp-cabrera): delete initial flavor - it will interfere
|
||||
# with listing tests
|
||||
self.simulate_delete(self.flavor_path)
|
||||
query = 'limit={0}&detailed={1}'.format(limit, detailed)
|
||||
if marker:
|
||||
query += '&marker={2}'.format(marker)
|
||||
|
||||
with flavors(self, count):
|
||||
result = self.simulate_get(self.url_prefix + '/flavors',
|
||||
query_string=query)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
results = jsonutils.loads(result[0])
|
||||
self.assertIsInstance(results, dict)
|
||||
self.assertIn('flavors', results)
|
||||
self.assertIn('links', results)
|
||||
flavors_list = results['flavors']
|
||||
link = results['links'][0]
|
||||
self.assertEqual('next', link['rel'])
|
||||
href = falcon.uri.parse_query_string(link['href'].split('?')[1])
|
||||
self.assertIn('marker', href)
|
||||
self.assertEqual(str(limit), href['limit'])
|
||||
self.assertEqual(str(detailed).lower(), href['detailed'])
|
||||
|
||||
next_query_string = ('marker={marker}&limit={limit}'
|
||||
'&detailed={detailed}').format(**href)
|
||||
next_result = self.simulate_get(link['href'].split('?')[0],
|
||||
query_string=next_query_string)
|
||||
next_flavors = jsonutils.loads(next_result[0])
|
||||
next_flavors_list = next_flavors['flavors']
|
||||
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
self.assertIn('links', next_flavors)
|
||||
if limit < count:
|
||||
self.assertEqual(min(limit, count-limit),
|
||||
len(next_flavors_list))
|
||||
else:
|
||||
self.assertEqual(0, len(next_flavors_list))
|
||||
|
||||
self.assertEqual(min(limit, count), len(flavors_list))
|
||||
for i, s in enumerate(flavors_list + next_flavors_list):
|
||||
capabilities = ['FIFO', 'CLAIMS', 'DURABILITY',
|
||||
'AOD', 'HIGH_THROUGHPUT']
|
||||
if detailed:
|
||||
self.assertIn('capabilities', s)
|
||||
self.assertEqual(s['capabilities'], capabilities)
|
||||
else:
|
||||
self.assertNotIn('capabilities', s)
|
||||
|
||||
def test_listing_works(self):
|
||||
self._listing_test()
|
||||
|
||||
def test_detailed_listing_works(self):
|
||||
self._listing_test(detailed=True)
|
||||
|
||||
@ddt.data(1, 5, 10, 15)
|
||||
def test_listing_works_with_limit(self, limit):
|
||||
self._listing_test(count=15, limit=limit)
|
||||
|
||||
def test_listing_marker_is_respected(self):
|
||||
self.simulate_delete(self.flavor_path)
|
||||
|
||||
with flavors(self, 10) as expected:
|
||||
result = self.simulate_get(self.url_prefix + '/flavors',
|
||||
query_string='marker=3')
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
flavor_list = jsonutils.loads(result[0])['flavors']
|
||||
self.assertEqual(6, len(flavor_list))
|
||||
path = expected[4]
|
||||
self._flavor_expect(flavor_list[0], path)
|
||||
|
||||
def test_listing_error_with_invalid_limit(self):
|
||||
self.simulate_delete(self.flavor_path)
|
||||
query = 'limit={0}&detailed={1}'.format(0, True)
|
||||
|
||||
with flavors(self, 10):
|
||||
self.simulate_get(self.url_prefix + '/flavors', query_string=query)
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def test_queue_create_works(self):
|
||||
metadata = {'_flavor': self.flavor}
|
||||
self.simulate_put(self.queue_path, body=jsonutils.dumps(metadata))
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
def test_queue_create_no_flavor(self):
|
||||
metadata = {'_flavor': self.flavor}
|
||||
|
||||
self.simulate_delete(self.flavor_path)
|
||||
self.assertEqual(falcon.HTTP_204, self.srmock.status)
|
||||
|
||||
self.simulate_put(self.queue_path, body=jsonutils.dumps(metadata))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
|
@ -23,6 +23,7 @@ from zaqar import tests as testing
|
|||
from zaqar.tests.unit.transport.wsgi import base
|
||||
|
||||
|
||||
# NOTE(gengchc2): remove pool_group in Rocky release.
|
||||
@contextlib.contextmanager
|
||||
def pool(test, name, weight, uri, group=None, options={}):
|
||||
"""A context manager for constructing a pool for use in testing.
|
||||
|
|
|
@ -0,0 +1,383 @@
|
|||
# Copyright (c) 2017 ZTE Corporation.
|
||||
#
|
||||
# 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 contextlib
|
||||
|
||||
import ddt
|
||||
import falcon
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from zaqar import tests as testing
|
||||
from zaqar.tests.unit.transport.wsgi import base
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def pool(test, name, weight, uri, flavor=None, options={}):
|
||||
"""A context manager for constructing a pool for use in testing.
|
||||
|
||||
Deletes the pool after exiting the context.
|
||||
|
||||
:param test: Must expose simulate_* methods
|
||||
:param name: Name for this pool
|
||||
:type name: six.text_type
|
||||
:type weight: int
|
||||
:type uri: six.text_type
|
||||
:type options: dict
|
||||
:returns: (name, weight, uri, options)
|
||||
:rtype: see above
|
||||
"""
|
||||
uri = "%s/%s" % (uri, uuidutils.generate_uuid())
|
||||
doc = {'weight': weight, 'uri': uri,
|
||||
'flavor': flavor, 'options': options}
|
||||
path = test.url_prefix + '/pools/' + name
|
||||
|
||||
test.simulate_put(path, body=jsonutils.dumps(doc))
|
||||
|
||||
try:
|
||||
yield name, weight, uri, flavor, options
|
||||
|
||||
finally:
|
||||
test.simulate_delete(path)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def pools(test, count, uri, flavor):
|
||||
"""A context manager for constructing pools for use in testing.
|
||||
|
||||
Deletes the pools after exiting the context.
|
||||
|
||||
:param test: Must expose simulate_* methods
|
||||
:param count: Number of pools to create
|
||||
:type count: int
|
||||
:returns: (paths, weights, uris, options)
|
||||
:rtype: ([six.text_type], [int], [six.text_type], [dict])
|
||||
"""
|
||||
mongo_url = uri
|
||||
base = test.url_prefix + '/pools/'
|
||||
args = [(base + str(i), i,
|
||||
{str(i): i})
|
||||
for i in range(count)]
|
||||
for path, weight, option in args:
|
||||
uri = "%s/%s" % (mongo_url, uuidutils.generate_uuid())
|
||||
doc = {'weight': weight, 'uri': uri,
|
||||
'flavor': flavor, 'options': option}
|
||||
test.simulate_put(path, body=jsonutils.dumps(doc))
|
||||
|
||||
try:
|
||||
yield args
|
||||
finally:
|
||||
for path, _, _ in args:
|
||||
# (gengchc): Remove flavor from the pool,
|
||||
# so we can delete the pool.
|
||||
test.simulate_patch(path,
|
||||
body=jsonutils.dumps({'flavor': ''}))
|
||||
test.simulate_delete(path)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestPoolsMongoDB(base.V2Base):
|
||||
|
||||
config_file = 'wsgi_mongodb_pooled.conf'
|
||||
|
||||
@testing.requires_mongodb
|
||||
def setUp(self):
|
||||
super(TestPoolsMongoDB, self).setUp()
|
||||
self.doc = {'weight': 100,
|
||||
'flavor': 'my-flavor',
|
||||
'uri': self.mongodb_url}
|
||||
self.pool = self.url_prefix + '/pools/' + uuidutils.generate_uuid()
|
||||
self.simulate_put(self.pool, body=jsonutils.dumps(self.doc))
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestPoolsMongoDB, self).tearDown()
|
||||
self.simulate_delete(self.pool)
|
||||
self.assertEqual(falcon.HTTP_204, self.srmock.status)
|
||||
|
||||
def test_put_pool_works(self):
|
||||
name = uuidutils.generate_uuid()
|
||||
weight, uri = self.doc['weight'], self.doc['uri']
|
||||
with pool(self, name, weight, uri, flavor='my-flavor'):
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
def test_put_raises_if_missing_fields(self):
|
||||
path = self.url_prefix + '/pools/' + uuidutils.generate_uuid()
|
||||
self.simulate_put(path, body=jsonutils.dumps({'weight': 100}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
self.simulate_put(path,
|
||||
body=jsonutils.dumps(
|
||||
{'uri': self.mongodb_url}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
@ddt.data(-1, 2**32+1, 'big')
|
||||
def test_put_raises_if_invalid_weight(self, weight):
|
||||
path = self.url_prefix + '/pools/' + uuidutils.generate_uuid()
|
||||
doc = {'weight': weight, 'uri': 'a'}
|
||||
self.simulate_put(path,
|
||||
body=jsonutils.dumps(doc))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
@ddt.data(-1, 2**32+1, [], 'localhost:27017')
|
||||
def test_put_raises_if_invalid_uri(self, uri):
|
||||
path = self.url_prefix + '/pools/' + uuidutils.generate_uuid()
|
||||
self.simulate_put(path,
|
||||
body=jsonutils.dumps({'weight': 1, 'uri': uri}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
@ddt.data(-1, 'wee', [])
|
||||
def test_put_raises_if_invalid_options(self, options):
|
||||
path = self.url_prefix + '/pools/' + uuidutils.generate_uuid()
|
||||
doc = {'weight': 1, 'uri': 'a', 'options': options}
|
||||
self.simulate_put(path, body=jsonutils.dumps(doc))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def test_put_same_database_uri(self):
|
||||
# NOTE(cabrera): setUp creates default pool
|
||||
expect = self.doc
|
||||
path = self.url_prefix + '/pools/' + uuidutils.generate_uuid()
|
||||
self.simulate_put(path, body=jsonutils.dumps(expect))
|
||||
self.assertEqual(falcon.HTTP_409, self.srmock.status)
|
||||
|
||||
def test_put_existing_overwrites(self):
|
||||
# NOTE(cabrera): setUp creates default pool
|
||||
expect = self.doc
|
||||
self.simulate_put(self.pool,
|
||||
body=jsonutils.dumps(expect))
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
result = self.simulate_get(self.pool)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
doc = jsonutils.loads(result[0])
|
||||
self.assertEqual(expect['weight'], doc['weight'])
|
||||
self.assertEqual(expect['uri'], doc['uri'])
|
||||
|
||||
def test_put_capabilities_mismatch_pool(self):
|
||||
mongodb_doc = self.doc
|
||||
self.simulate_put(self.pool,
|
||||
body=jsonutils.dumps(mongodb_doc))
|
||||
self.assertEqual(falcon.HTTP_201, self.srmock.status)
|
||||
|
||||
redis_doc = {'weight': 100,
|
||||
'flavor': 'my-flavor',
|
||||
'uri': 'redis://127.0.0.1:6379'}
|
||||
|
||||
self.simulate_put(self.pool,
|
||||
body=jsonutils.dumps(redis_doc))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def test_delete_works(self):
|
||||
# (gengchc): Remove flavor from the pool, so we can delete the pool.
|
||||
self.simulate_patch(self.pool, body=jsonutils.dumps({'flavor': ''}))
|
||||
self.simulate_delete(self.pool)
|
||||
self.assertEqual(falcon.HTTP_204, self.srmock.status)
|
||||
|
||||
self.simulate_get(self.pool)
|
||||
self.assertEqual(falcon.HTTP_404, self.srmock.status)
|
||||
|
||||
def test_get_nonexisting_raises_404(self):
|
||||
self.simulate_get(self.url_prefix + '/pools/nonexisting')
|
||||
self.assertEqual(falcon.HTTP_404, self.srmock.status)
|
||||
|
||||
def _pool_expect(self, pool, xhref, xweight, xuri):
|
||||
self.assertIn('href', pool)
|
||||
self.assertIn('name', pool)
|
||||
self.assertEqual(xhref, pool['href'])
|
||||
self.assertIn('weight', pool)
|
||||
self.assertEqual(xweight, pool['weight'])
|
||||
self.assertIn('uri', pool)
|
||||
|
||||
# NOTE(dynarro): we are using startwith because we are adding to
|
||||
# pools UUIDs, to avoid dupplications
|
||||
self.assertTrue(pool['uri'].startswith(xuri))
|
||||
|
||||
def test_get_works(self):
|
||||
result = self.simulate_get(self.pool)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
pool = jsonutils.loads(result[0])
|
||||
self._pool_expect(pool, self.pool, self.doc['weight'],
|
||||
self.doc['uri'])
|
||||
|
||||
def test_detailed_get_works(self):
|
||||
result = self.simulate_get(self.pool,
|
||||
query_string='detailed=True')
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
pool = jsonutils.loads(result[0])
|
||||
self._pool_expect(pool, self.pool, self.doc['weight'],
|
||||
self.doc['uri'])
|
||||
self.assertIn('options', pool)
|
||||
self.assertEqual({}, pool['options'])
|
||||
|
||||
def test_patch_raises_if_missing_fields(self):
|
||||
self.simulate_patch(self.pool,
|
||||
body=jsonutils.dumps({'location': 1}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def _patch_test(self, doc):
|
||||
result = self.simulate_patch(self.pool,
|
||||
body=jsonutils.dumps(doc))
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
updated_pool = jsonutils.loads(result[0])
|
||||
self._pool_expect(updated_pool, self.pool, doc['weight'],
|
||||
doc['uri'])
|
||||
|
||||
result = self.simulate_get(self.pool,
|
||||
query_string='detailed=True')
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
pool = jsonutils.loads(result[0])
|
||||
self._pool_expect(pool, self.pool, doc['weight'],
|
||||
doc['uri'])
|
||||
self.assertEqual(doc['options'], pool['options'])
|
||||
|
||||
def test_patch_works(self):
|
||||
doc = {'weight': 101,
|
||||
'uri': self.mongodb_url,
|
||||
'options': {'a': 1}}
|
||||
self._patch_test(doc)
|
||||
|
||||
def test_patch_works_with_extra_fields(self):
|
||||
doc = {'weight': 101,
|
||||
'uri': self.mongodb_url,
|
||||
'options': {'a': 1},
|
||||
'location': 100,
|
||||
'partition': 'taco'}
|
||||
self._patch_test(doc)
|
||||
|
||||
@ddt.data(-1, 2**32+1, 'big')
|
||||
def test_patch_raises_400_on_invalid_weight(self, weight):
|
||||
self.simulate_patch(self.pool,
|
||||
body=jsonutils.dumps({'weight': weight}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
@ddt.data(-1, 2**32+1, [], 'localhost:27017')
|
||||
def test_patch_raises_400_on_invalid_uri(self, uri):
|
||||
self.simulate_patch(self.pool,
|
||||
body=jsonutils.dumps({'uri': uri}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
@ddt.data(-1, 'wee', [])
|
||||
def test_patch_raises_400_on_invalid_options(self, options):
|
||||
self.simulate_patch(self.pool,
|
||||
body=jsonutils.dumps({'options': options}))
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
||||
|
||||
def test_patch_raises_404_if_pool_not_found(self):
|
||||
self.simulate_patch(self.url_prefix + '/pools/notexists',
|
||||
body=jsonutils.dumps({'weight': 1}))
|
||||
self.assertEqual(falcon.HTTP_404, self.srmock.status)
|
||||
|
||||
def test_empty_listing(self):
|
||||
# (gengchc): Remove flavor from the pool, so we can delete the pool.
|
||||
self.simulate_patch(self.pool, body=jsonutils.dumps({'flavor': ''}))
|
||||
self.simulate_delete(self.pool)
|
||||
result = self.simulate_get(self.url_prefix + '/pools')
|
||||
results = jsonutils.loads(result[0])
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
self.assertEqual(0, len(results['pools']))
|
||||
self.assertIn('links', results)
|
||||
|
||||
def _listing_test(self, count=10, limit=10,
|
||||
marker=None, detailed=False):
|
||||
# NOTE(cpp-cabrera): delete initial pool - it will interfere
|
||||
# with listing tests
|
||||
# (gengchc): Remove flavor from the pool, so we can delete the pool.
|
||||
self.simulate_patch(self.pool, body=jsonutils.dumps({'flavor': ''}))
|
||||
self.simulate_delete(self.pool)
|
||||
query = 'limit={0}&detailed={1}'.format(limit, detailed)
|
||||
if marker:
|
||||
query += '&marker={0}'.format(marker)
|
||||
|
||||
with pools(self, count, self.doc['uri'], 'my-flavor') as expected:
|
||||
result = self.simulate_get(self.url_prefix + '/pools',
|
||||
query_string=query)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
results = jsonutils.loads(result[0])
|
||||
self.assertIsInstance(results, dict)
|
||||
self.assertIn('pools', results)
|
||||
self.assertIn('links', results)
|
||||
pool_list = results['pools']
|
||||
|
||||
link = results['links'][0]
|
||||
self.assertEqual('next', link['rel'])
|
||||
href = falcon.uri.parse_query_string(link['href'].split('?')[1])
|
||||
self.assertIn('marker', href)
|
||||
self.assertEqual(str(limit), href['limit'])
|
||||
self.assertEqual(str(detailed).lower(), href['detailed'])
|
||||
|
||||
next_query_string = ('marker={marker}&limit={limit}'
|
||||
'&detailed={detailed}').format(**href)
|
||||
next_result = self.simulate_get(link['href'].split('?')[0],
|
||||
query_string=next_query_string)
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
|
||||
next_pool = jsonutils.loads(next_result[0])
|
||||
next_pool_list = next_pool['pools']
|
||||
|
||||
self.assertIn('links', next_pool)
|
||||
if limit < count:
|
||||
self.assertEqual(min(limit, count-limit),
|
||||
len(next_pool_list))
|
||||
else:
|
||||
# NOTE(jeffrey4l): when limit >= count, there will be no
|
||||
# pools in the 2nd page.
|
||||
self.assertEqual(0, len(next_pool_list))
|
||||
|
||||
self.assertEqual(min(limit, count), len(pool_list))
|
||||
for s in pool_list + next_pool_list:
|
||||
# NOTE(flwang): It can't assumed that both sqlalchemy and
|
||||
# mongodb can return query result with the same order. Just
|
||||
# like the order they're inserted. Actually, sqlalchemy can't
|
||||
# guarantee that. So we're leveraging the relationship between
|
||||
# pool weight and the index of pools fixture to get the
|
||||
# right pool to verify.
|
||||
expect = expected[s['weight']]
|
||||
path, weight, options = expect[:3]
|
||||
self._pool_expect(s, path, weight, self.doc['uri'])
|
||||
if detailed:
|
||||
self.assertIn('options', s)
|
||||
self.assertEqual(s['options'], expect[-1])
|
||||
else:
|
||||
self.assertNotIn('options', s)
|
||||
|
||||
def test_listing_works(self):
|
||||
self._listing_test()
|
||||
|
||||
def test_detailed_listing_works(self):
|
||||
self._listing_test(detailed=True)
|
||||
|
||||
@ddt.data(1, 5, 10, 15)
|
||||
def test_listing_works_with_limit(self, limit):
|
||||
self._listing_test(count=15, limit=limit)
|
||||
|
||||
def test_listing_marker_is_respected(self):
|
||||
# (gengchc): Remove flavor from the pool, so we can delete the pool.
|
||||
self.simulate_patch(self.pool, body=jsonutils.dumps({'flavor': ''}))
|
||||
self.simulate_delete(self.pool)
|
||||
|
||||
with pools(self, 10, self.doc['uri'], 'my-flavor') as expected:
|
||||
result = self.simulate_get(self.url_prefix + '/pools',
|
||||
query_string='marker=3')
|
||||
self.assertEqual(falcon.HTTP_200, self.srmock.status)
|
||||
pool_list = jsonutils.loads(result[0])['pools']
|
||||
self.assertEqual(6, len(pool_list))
|
||||
path, weight = expected[4][:2]
|
||||
self._pool_expect(pool_list[0], path, weight, self.doc['uri'])
|
||||
|
||||
def test_listing_error_with_invalid_limit(self):
|
||||
self.simulate_delete(self.pool)
|
||||
query = 'limit={0}&detailed={1}'.format(0, True)
|
||||
|
||||
with pools(self, 10, self.doc['uri'], 'my-flavor'):
|
||||
self.simulate_get(self.url_prefix + '/pools', query_string=query)
|
||||
self.assertEqual(falcon.HTTP_400, self.srmock.status)
|
|
@ -20,7 +20,6 @@ import six
|
|||
|
||||
from zaqar.common.api.schemas import flavors as schema
|
||||
from zaqar.common import decorators
|
||||
from zaqar.common import utils as common_utils
|
||||
from zaqar.i18n import _
|
||||
from zaqar.storage import errors
|
||||
from zaqar.transport import acl
|
||||
|
@ -52,7 +51,8 @@ class Listing(object):
|
|||
|
||||
{
|
||||
"flavors": [
|
||||
{"href": "", "capabilities": {}, "pool_group": ""},
|
||||
{"href": "", "capabilities": {}, "pool_group": "",
|
||||
"pool_list": ""},
|
||||
...
|
||||
],
|
||||
"links": [
|
||||
|
@ -87,11 +87,10 @@ class Listing(object):
|
|||
|
||||
for entry in flavors:
|
||||
entry['href'] = request.path + '/' + entry['name']
|
||||
pool_group = entry['pool_group']
|
||||
# NOTE(wanghao): remove this in Newton.
|
||||
# NOTE(gengchc): Remove pool_group in Rocky
|
||||
entry['pool'] = entry['pool_group']
|
||||
if detailed:
|
||||
caps = self._pools_ctrl.capabilities(group=pool_group)
|
||||
caps = self._pools_ctrl.capabilities(flavor=entry)
|
||||
entry['capabilities'] = [str(cap).split('.')[-1]
|
||||
for cap in caps]
|
||||
|
||||
|
@ -125,9 +124,10 @@ class Resource(object):
|
|||
validator_type = jsonschema.Draft4Validator
|
||||
self._validators = {
|
||||
'create': validator_type(schema.create),
|
||||
# NOTE(gengchc): Remove pool_group in Rocky.
|
||||
'pool_group': validator_type(schema.patch_pool_group),
|
||||
# NOTE(wanghao): Remove this in Newton.
|
||||
'pool': validator_type(schema.patch_pool),
|
||||
'pool_list': validator_type(schema.patch_pool_list),
|
||||
'capabilities': validator_type(schema.patch_capabilities),
|
||||
}
|
||||
|
||||
|
@ -138,7 +138,7 @@ class Resource(object):
|
|||
|
||||
::
|
||||
|
||||
{"pool": "", capabilities: {...}}
|
||||
{"pool": "", "pool_list": [], capabilities: {...}}
|
||||
|
||||
:returns: HTTP | [200, 404]
|
||||
"""
|
||||
|
@ -148,12 +148,17 @@ class Resource(object):
|
|||
|
||||
try:
|
||||
data = self._ctrl.get(flavor, project=project_id)
|
||||
pool_group = data['pool_group']
|
||||
# NOTE(wanghao): remove this in Newton.
|
||||
data['pool'] = data['pool_group']
|
||||
capabilities = self._pools_ctrl.capabilities(group=pool_group)
|
||||
capabilities = self._pools_ctrl.capabilities(flavor=data)
|
||||
data['capabilities'] = [str(cap).split('.')[-1]
|
||||
for cap in capabilities]
|
||||
# NOTE(gengchc): Remove pool_group in Rocky.
|
||||
data['pool'] = data['pool_group']
|
||||
pool_list =\
|
||||
list(self._pools_ctrl.get_pools_by_flavor(flavor=data))
|
||||
pool_name_list = []
|
||||
if len(pool_list) > 0:
|
||||
pool_name_list = [x['name'] for x in pool_list]
|
||||
data['pool_list'] = pool_name_list
|
||||
|
||||
except errors.FlavorDoesNotExist as ex:
|
||||
LOG.debug(ex)
|
||||
|
@ -163,26 +168,75 @@ class Resource(object):
|
|||
|
||||
response.body = transport_utils.to_json(data)
|
||||
|
||||
@decorators.TransportLog("Flavors item")
|
||||
@acl.enforce("flavors:create")
|
||||
def on_put(self, request, response, project_id, flavor):
|
||||
"""Registers a new flavor. Expects the following input:
|
||||
def _check_pools_exists(self, pool_list):
|
||||
if pool_list is not None:
|
||||
for pool in pool_list:
|
||||
if not self._pools_ctrl.exists(pool):
|
||||
raise errors.PoolDoesNotExist(pool)
|
||||
|
||||
::
|
||||
def _update_pools_by_flavor(self, flavor, pool_list):
|
||||
if pool_list is not None:
|
||||
for pool in pool_list:
|
||||
self._pools_ctrl.update(pool, flavor=flavor)
|
||||
|
||||
{"pool_group": "my-pool-group", "capabilities": {}}
|
||||
def _clean_pools_by_flavor(self, flavor, pool_list=None):
|
||||
if pool_list is None:
|
||||
flavor_obj = {}
|
||||
flavor_obj['name'] = flavor
|
||||
pllt = list(self._pools_ctrl.get_pools_by_flavor(
|
||||
flavor=flavor_obj))
|
||||
pool_list = [x['name'] for x in pllt]
|
||||
if pool_list is not None:
|
||||
for pool in pool_list:
|
||||
self._pools_ctrl.update(pool, flavor="")
|
||||
|
||||
A capabilities object may also be provided.
|
||||
def _on_put_by_pool_list(self, request, response, project_id,
|
||||
flavor, pool_list):
|
||||
LOG.debug(u'PUT flavor - name by flavor: %s', flavor)
|
||||
# NOTE(gengchc2): If configuration flavor is used by the new schema,
|
||||
# a list of pools is required.
|
||||
if len(pool_list) == 0:
|
||||
response.status = falcon.HTTP_400
|
||||
response.location = request.path
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), 'Bad Request')
|
||||
# NOTE(gengchc2): Check if pools in the pool_list exist.
|
||||
try:
|
||||
self._check_pools_exists(pool_list)
|
||||
except errors.PoolDoesNotExist as ex:
|
||||
LOG.exception(ex)
|
||||
description = (_(u'Flavor %(flavor)s could not be created, '
|
||||
'error:%(msg)s') %
|
||||
dict(flavor=flavor, msg=str(ex)))
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), description)
|
||||
capabilities = self._pools_ctrl.capabilities(name=pool_list[0])
|
||||
try:
|
||||
self._ctrl.create(flavor,
|
||||
project=project_id,
|
||||
capabilities=capabilities)
|
||||
response.status = falcon.HTTP_201
|
||||
response.location = request.path
|
||||
except errors.ConnectionError as ex:
|
||||
LOG.exception(ex)
|
||||
description = (_(u'Flavor %(flavor)s could not be created, '
|
||||
'error:%(msg)s') %
|
||||
dict(flavor=flavor, msg=str(ex)))
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), description)
|
||||
# NOTE(gengchc2): Update the 'flavor' field in pools tables.
|
||||
try:
|
||||
self._update_pools_by_flavor(flavor, pool_list)
|
||||
except errors.ConnectionError as ex:
|
||||
LOG.exception(ex)
|
||||
description = (_(u'Flavor %(flavor)s could not be created, '
|
||||
'error:%(msg)s') %
|
||||
dict(flavor=flavor, msg=str(ex)))
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), description)
|
||||
|
||||
:returns: HTTP | [201, 400]
|
||||
"""
|
||||
|
||||
LOG.debug(u'PUT flavor - name: %s', flavor)
|
||||
|
||||
data = wsgi_utils.load(request)
|
||||
wsgi_utils.validate(self._validators['create'], data)
|
||||
pool_group = data.get('pool_group') or data.get('pool')
|
||||
capabilities = self._pools_ctrl.capabilities(pool_group)
|
||||
def _on_put_by_group(self, request, response, project_id,
|
||||
flavor, pool_group):
|
||||
LOG.debug(u'PUT flavor - name: %s by group', flavor)
|
||||
flavor_obj = {}
|
||||
flavor_obj["pool_group"] = pool_group
|
||||
capabilities = self._pools_ctrl.capabilities(flavor_obj)
|
||||
try:
|
||||
self._ctrl.create(flavor,
|
||||
pool_group=pool_group,
|
||||
|
@ -197,6 +251,35 @@ class Resource(object):
|
|||
dict(flavor=flavor, pool_group=pool_group))
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), description)
|
||||
|
||||
@decorators.TransportLog("Flavors item")
|
||||
@acl.enforce("flavors:create")
|
||||
def on_put(self, request, response, project_id, flavor):
|
||||
"""Registers a new flavor. Expects the following input:
|
||||
|
||||
::
|
||||
|
||||
{"pool_group": "my-pool-group",
|
||||
"pool_list": [], "capabilities": {}}
|
||||
|
||||
A capabilities object may also be provided.
|
||||
|
||||
:returns: HTTP | [201, 400]
|
||||
"""
|
||||
|
||||
LOG.debug(u'PUT flavor - name: %s', flavor)
|
||||
|
||||
data = wsgi_utils.load(request)
|
||||
wsgi_utils.validate(self._validators['create'], data)
|
||||
LOG.debug(u'The pool_group will be removed in Rocky release.')
|
||||
pool_group = data.get('pool_group') or data.get('pool')
|
||||
pool_list = data.get('pool_list')
|
||||
if pool_list is not None:
|
||||
self._on_put_by_pool_list(request, response, project_id,
|
||||
flavor, pool_list)
|
||||
else:
|
||||
self._on_put_by_group(request, response, project_id,
|
||||
flavor, pool_group)
|
||||
|
||||
@decorators.TransportLog("Flavors item")
|
||||
@acl.enforce("flavors:delete")
|
||||
def on_delete(self, request, response, project_id, flavor):
|
||||
|
@ -206,49 +289,96 @@ class Resource(object):
|
|||
"""
|
||||
|
||||
LOG.debug(u'DELETE flavor - name: %s', flavor)
|
||||
# NOTE(gengchc2): If configuration flavor is
|
||||
# used by the new schema, the flavor field in pools
|
||||
# need to be cleaned.
|
||||
try:
|
||||
self._clean_pools_by_flavor(flavor)
|
||||
except errors.ConnectionError as ex:
|
||||
LOG.exception(ex)
|
||||
description = (_(u'Flavor %(flavor)s could not be deleted.') %
|
||||
dict(flavor=flavor))
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), description)
|
||||
self._ctrl.delete(flavor, project=project_id)
|
||||
response.status = falcon.HTTP_204
|
||||
|
||||
@decorators.TransportLog("Flavors item")
|
||||
@acl.enforce("flavors:update")
|
||||
def on_patch(self, request, response, project_id, flavor):
|
||||
"""Allows one to update a flavors's pool_group.
|
||||
def _on_patch_by_pool_list(self, request, response, project_id,
|
||||
flavor, pool_list):
|
||||
|
||||
This method expects the user to submit a JSON object
|
||||
containing 'pool_group'. If none is found, the request is flagged
|
||||
as bad. There is also strict format checking through the use of
|
||||
jsonschema. Appropriate errors are returned in each case for
|
||||
badly formatted input.
|
||||
if len(pool_list) == 0:
|
||||
response.status = falcon.HTTP_400
|
||||
response.location = request.path
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), 'Bad Request')
|
||||
# NOTE(gengchc2): If the flavor does not exist, return
|
||||
try:
|
||||
self._ctrl.get(flavor, project=project_id)
|
||||
except errors.FlavorDoesNotExist as ex:
|
||||
LOG.debug(ex)
|
||||
raise wsgi_errors.HTTPNotFound(six.text_type(ex))
|
||||
|
||||
:returns: HTTP | [200, 400]
|
||||
"""
|
||||
flavor_obj = {}
|
||||
flavor_obj['name'] = flavor
|
||||
# NOTE(gengchc2): Get the pools list with flavor.
|
||||
pool_list_old = list(self._pools_ctrl.get_pools_by_flavor(
|
||||
flavor=flavor_obj))
|
||||
# NOTE(gengchc2): Check if the new pool in the pool_list exist.
|
||||
try:
|
||||
self._check_pools_exists(pool_list)
|
||||
except errors.PoolDoesNotExist as ex:
|
||||
LOG.exception(ex)
|
||||
description = (_(u'Flavor %(flavor)s cant be updated, '
|
||||
'error:%(msg)s') %
|
||||
dict(flavor=flavor, msg=str(ex)))
|
||||
raise falcon.HTTPBadRequest(_('updatefail'), description)
|
||||
capabilities = self._pools_ctrl.capabilities(name=pool_list[0])
|
||||
try:
|
||||
self._ctrl.update(flavor, project=project_id,
|
||||
capabilities=capabilities)
|
||||
resp_data = self._ctrl.get(flavor, project=project_id)
|
||||
resp_data['capabilities'] = [str(cap).split('.')[-1]
|
||||
for cap in capabilities]
|
||||
except errors.FlavorDoesNotExist as ex:
|
||||
LOG.exception(ex)
|
||||
raise wsgi_errors.HTTPNotFound(six.text_type(ex))
|
||||
|
||||
LOG.debug(u'PATCH flavor - name: %s', flavor)
|
||||
data = wsgi_utils.load(request)
|
||||
|
||||
EXPECT = ('pool_group', 'pool')
|
||||
if not any([(field in data) for field in EXPECT]):
|
||||
LOG.debug(u'PATCH flavor, bad params')
|
||||
raise wsgi_errors.HTTPBadRequestBody(
|
||||
'`pool_group` or `pool` needs to be specified'
|
||||
)
|
||||
|
||||
for field in EXPECT:
|
||||
wsgi_utils.validate(self._validators[field], data)
|
||||
|
||||
fields = common_utils.fields(data, EXPECT,
|
||||
pred=lambda v: v is not None)
|
||||
# NOTE(wanghao): remove this in Newton.
|
||||
if fields.get('pool') and fields.get('pool_group') is None:
|
||||
fields['pool_group'] = fields.get('pool')
|
||||
fields.pop('pool')
|
||||
# (gengchc) Update flavor field in new pool list.
|
||||
try:
|
||||
self._update_pools_by_flavor(flavor, pool_list)
|
||||
except errors.ConnectionError as ex:
|
||||
LOG.exception(ex)
|
||||
description = (_(u'Flavor %(flavor)s could not be updated, '
|
||||
'error:%(msg)s') %
|
||||
dict(flavor=flavor, msg=str(ex)))
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), description)
|
||||
# (gengchc) Remove flavor from old pool list.
|
||||
try:
|
||||
pool_list_removed = []
|
||||
for pool_old in pool_list_old:
|
||||
if pool_old['name'] not in pool_list:
|
||||
pool_list_removed.append(pool_old['name'])
|
||||
self._clean_pools_by_flavor(flavor, pool_list_removed)
|
||||
except errors.ConnectionError as ex:
|
||||
LOG.exception(ex)
|
||||
description = (_(u'Flavor %(flavor)s could not be updated, '
|
||||
'error:%(msg)s') %
|
||||
dict(flavor=flavor, msg=str(ex)))
|
||||
raise falcon.HTTPBadRequest(_('Unable to create'), description)
|
||||
resp_data['pool_list'] = pool_list
|
||||
resp_data['href'] = request.path
|
||||
response.body = transport_utils.to_json(resp_data)
|
||||
|
||||
def _on_patch_by_group(self, request, response, project_id,
|
||||
flavor, pool_group):
|
||||
LOG.debug(u'PATCH flavor - name: %s by group', flavor)
|
||||
resp_data = None
|
||||
try:
|
||||
self._ctrl.update(flavor, project=project_id, **fields)
|
||||
flvor_obj = {}
|
||||
flvor_obj['pool_group'] = pool_group
|
||||
capabilities = self._pools_ctrl.capabilities(flavor=flvor_obj)
|
||||
self._ctrl.update(flavor, project=project_id,
|
||||
pool_group=pool_group,
|
||||
capabilities=capabilities)
|
||||
resp_data = self._ctrl.get(flavor, project=project_id)
|
||||
capabilities = self._pools_ctrl.capabilities(
|
||||
group=resp_data['pool_group'])
|
||||
resp_data['capabilities'] = [str(cap).split('.')[-1]
|
||||
for cap in capabilities]
|
||||
except errors.FlavorDoesNotExist as ex:
|
||||
|
@ -256,3 +386,42 @@ class Resource(object):
|
|||
raise wsgi_errors.HTTPNotFound(six.text_type(ex))
|
||||
resp_data['href'] = request.path
|
||||
response.body = transport_utils.to_json(resp_data)
|
||||
|
||||
@decorators.TransportLog("Flavors item")
|
||||
@acl.enforce("flavors:update")
|
||||
def on_patch(self, request, response, project_id, flavor):
|
||||
"""Allows one to update a flavors'pool list.
|
||||
|
||||
This method expects the user to submit a JSON object
|
||||
containing 'pool_group' or 'pool list'. If none is found,
|
||||
the request is flagged as bad. There is also strict format
|
||||
checking through the use of jsonschema. Appropriate errors
|
||||
are returned in each case for badly formatted input.
|
||||
|
||||
:returns: HTTP | [200, 400]
|
||||
"""
|
||||
|
||||
LOG.debug(u'PATCH flavor - name: %s', flavor)
|
||||
data = wsgi_utils.load(request)
|
||||
# NOTE(gengchc2): remove pool_group in R release.
|
||||
EXPECT = ('pool_group', 'pool', 'pool_list')
|
||||
if not any([(field in data) for field in EXPECT]):
|
||||
LOG.debug(u'PATCH flavor, bad params')
|
||||
raise wsgi_errors.HTTPBadRequestBody(
|
||||
'`pool_group` or `pool` or `pool_list` needs to be specified'
|
||||
)
|
||||
|
||||
for field in EXPECT:
|
||||
wsgi_utils.validate(self._validators[field], data)
|
||||
LOG.debug(u'The pool_group will be removed in Rocky release.')
|
||||
pool_group = data.get('pool_group') or data.get('pool')
|
||||
pool_list = data.get('pool_list')
|
||||
# NOTE(gengchc2): If pool_list is not None, configuration flavor is
|
||||
# used by the new schema.
|
||||
# a list of pools is required.
|
||||
if pool_list is not None:
|
||||
self._on_patch_by_pool_list(request, response, project_id,
|
||||
flavor, pool_list)
|
||||
else:
|
||||
self._on_patch_by_group(request, response, project_id,
|
||||
flavor, pool_group)
|
||||
|
|
|
@ -136,7 +136,8 @@ class Resource(object):
|
|||
self._validators = {
|
||||
'weight': validator_type(schema.patch_weight),
|
||||
'uri': validator_type(schema.patch_uri),
|
||||
'group': validator_type(schema.patch_uri),
|
||||
'group': validator_type(schema.patch_group),
|
||||
'flavor': validator_type(schema.patch_flavor),
|
||||
'options': validator_type(schema.patch_options),
|
||||
'create': validator_type(schema.create)
|
||||
}
|
||||
|
@ -194,7 +195,8 @@ class Resource(object):
|
|||
try:
|
||||
self._ctrl.create(pool, weight=data['weight'],
|
||||
uri=data['uri'],
|
||||
group=data.get('group'),
|
||||
group=data.get('group', None),
|
||||
flavor=data.get('flavor', None),
|
||||
options=data.get('options', {}))
|
||||
response.status = falcon.HTTP_201
|
||||
response.location = request.path
|
||||
|
@ -234,9 +236,9 @@ class Resource(object):
|
|||
"""Allows one to update a pool's weight, uri, and/or options.
|
||||
|
||||
This method expects the user to submit a JSON object
|
||||
containing at least one of: 'uri', 'weight', 'group', 'options'. If
|
||||
none are found, the request is flagged as bad. There is also
|
||||
strict format checking through the use of
|
||||
containing at least one of: 'uri', 'weight', 'group', 'flavor',
|
||||
'options'.If none are found, the request is flagged as bad.
|
||||
There is also strict format checking through the use of
|
||||
jsonschema. Appropriate errors are returned in each case for
|
||||
badly formatted input.
|
||||
|
||||
|
@ -246,11 +248,12 @@ class Resource(object):
|
|||
LOG.debug(u'PATCH pool - name: %s', pool)
|
||||
data = wsgi_utils.load(request)
|
||||
|
||||
EXPECT = ('weight', 'uri', 'group', 'options')
|
||||
EXPECT = ('weight', 'uri', 'group', 'flavor', 'options')
|
||||
if not any([(field in data) for field in EXPECT]):
|
||||
LOG.debug(u'PATCH pool, bad params')
|
||||
raise wsgi_errors.HTTPBadRequestBody(
|
||||
'One of `uri`, `weight`, `group`, or `options` needs '
|
||||
'One of `uri`, `weight`, `group`, `flavor`,'
|
||||
' or `options` needs '
|
||||
'to be specified'
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue