Remove API-GW related code from the Tricricle

1. What is the problem?
Nova API-GW and Cinder API-GW code are still in the repository of
the Tricircle project.

2. What is the solution to the problem?
According to the blueprint for the Tricircle splitting:
https://blueprints.launchpad.net/tricircle/+spec/make-tricircle-dedicated-for-networking-automation-across-neutron
Nova API-GW and Cinder API-GW related code should be removed from
the repository of the Tricircle project.

3. What the features need to be implemented to the Tricircle to realize
the solution?
No new features.

Change-Id: I152bab9c0a7a114f0361a69a3ab2d7037731434f
This commit is contained in:
joehuang 2016-10-09 04:50:11 -04:00
parent ff7f86fa01
commit 467f8fcb47
66 changed files with 0 additions and 10343 deletions

View File

@ -1,63 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved.
#
# 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.
# Much of this module is based on the work of the Ironic team
# see http://git.openstack.org/cgit/openstack/ironic/tree/ironic/cmd/api.py
import logging as std_logging
import sys
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import wsgi
from tricircle.common import config
from tricircle.common.i18n import _LI
from tricircle.common.i18n import _LW
from tricircle.common import restapp
from tricircle.cinder_apigw import app
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def main():
config.init(app.common_opts, sys.argv[1:])
application = app.setup_app()
host = CONF.bind_host
port = CONF.bind_port
workers = CONF.api_workers
if workers < 1:
LOG.warning(_LW("Wrong worker number, worker = %(workers)s"), workers)
workers = 1
LOG.info(_LI("Cinder_APIGW on http://%(host)s:%(port)s with %(workers)s"),
{'host': host, 'port': port, 'workers': workers})
service = wsgi.Server(CONF, 'Tricircle Cinder_APIGW',
application, host, port)
restapp.serve(service, CONF, workers)
LOG.info(_LI("Configuration:"))
CONF.log_opt_values(LOG, std_logging.INFO)
restapp.wait()
if __name__ == '__main__':
main()

View File

@ -1,68 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved.
#
# 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.
# Much of this module is based on the work of the Ironic team
# see http://git.openstack.org/cgit/openstack/ironic/tree/ironic/cmd/api.py
import eventlet
if __name__ == "__main__":
eventlet.monkey_patch()
import logging as std_logging
import sys
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import wsgi
from tricircle.common import config
from tricircle.common.i18n import _LI
from tricircle.common.i18n import _LW
from tricircle.common import restapp
from tricircle.nova_apigw import app
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def main():
config.init(app.common_opts, sys.argv[1:])
application = app.setup_app()
host = CONF.bind_host
port = CONF.bind_port
workers = CONF.api_workers
if workers < 1:
LOG.warning(_LW("Wrong worker number, worker = %(workers)s"), workers)
workers = 1
LOG.info(_LI("Nova_APIGW on http://%(host)s:%(port)s with %(workers)s"),
{'host': host, 'port': port, 'workers': workers})
service = wsgi.Server(CONF, 'Tricircle Nova_APIGW',
application, host, port)
restapp.serve(service, CONF, workers)
LOG.info(_LI("Configuration:"))
CONF.log_opt_values(LOG, std_logging.INFO)
restapp.wait()
if __name__ == '__main__':
main()

View File

@ -1,16 +0,0 @@
[DEFAULT]
output_file = etc/cinder_apigw.conf.sample
wrap_width = 79
namespace = tricircle.cinder_apigw
namespace = tricircle.common
namespace = tricircle.db
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.policy
namespace = oslo.service.periodic_task
namespace = oslo.service.service
namespace = oslo.service.sslutils
namespace = oslo.db
namespace = oslo.middleware
namespace = oslo.concurrency
namespace = keystonemiddleware.auth_token

View File

@ -1,16 +0,0 @@
[DEFAULT]
output_file = etc/nova_apigw.conf.sample
wrap_width = 79
namespace = tricircle.nova_apigw
namespace = tricircle.common
namespace = tricircle.db
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.policy
namespace = oslo.service.periodic_task
namespace = oslo.service.service
namespace = oslo.service.sslutils
namespace = oslo.db
namespace = oslo.middleware
namespace = oslo.concurrency
namespace = keystonemiddleware.auth_token

View File

@ -50,9 +50,6 @@ oslo.config.opts =
tricircle.common = tricircle.common.opts:list_opts
tricircle.db = tricircle.db.opts:list_opts
tricircle.network = tricircle.network.opts:list_opts
tricircle.nova_apigw = tricircle.nova_apigw.opts:list_opts
tricircle.cinder_apigw = tricircle.cinder_apigw.opts:list_opts
tricircle.xjob = tricircle.xjob.opts:list_opts
tempest.test_plugins =
@ -61,8 +58,3 @@ tempest.test_plugins =
tricircle.network.type_drivers =
local = tricircle.network.drivers.type_local:LocalTypeDriver
shared_vlan = tricircle.network.drivers.type_shared_vlan:SharedVLANTypeDriver
tricircle.common.schedulers =
pod_manager = tricircle.common.scheduler.pod_manager:PodManager
bottom_pod_filter = tricircle.common.scheduler.filters.bottom_pod_filter:BottomPodFilter
filter_scheduler = tricircle.common.scheduler.filter_scheduler:FilterScheduler

View File

@ -28,8 +28,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:genconfig]
commands = oslo-config-generator --config-file=etc/api-cfg-gen.conf
oslo-config-generator --config-file=etc/nova_apigw-cfg-gen.conf
oslo-config-generator --config-file=etc/cinder_apigw-cfg-gen.conf
oslo-config-generator --config-file=etc/xjob-cfg-gen.conf
oslo-config-generator --config-file=etc/tricircle_plugin-cfg-gen.conf

View File

@ -1,76 +0,0 @@
# Copyright (c) 2015 Huawei, Tech. Co,. Ltd.
# All Rights Reserved.
#
# 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 pecan
from oslo_config import cfg
from tricircle.common.i18n import _
from tricircle.common import restapp
common_opts = [
cfg.IPOpt('bind_host', default='0.0.0.0',
help=_("The host IP to bind to")),
cfg.PortOpt('bind_port', default=19997,
help=_("The port to bind to")),
cfg.IntOpt('api_workers', default=1,
help=_("number of api workers")),
cfg.StrOpt('api_extensions_path', default="",
help=_("The path for API extensions")),
cfg.StrOpt('auth_strategy', default='keystone',
help=_("The type of authentication to use")),
cfg.BoolOpt('allow_bulk', default=True,
help=_("Allow the usage of the bulk API")),
cfg.BoolOpt('allow_pagination', default=False,
help=_("Allow the usage of the pagination")),
cfg.BoolOpt('allow_sorting', default=False,
help=_("Allow the usage of the sorting")),
cfg.StrOpt('pagination_max_limit', default="-1",
help=_("The maximum number of items returned in a single "
"response, value was 'infinite' or negative integer "
"means no limit")),
]
def setup_app(*args, **kwargs):
config = {
'server': {
'port': cfg.CONF.bind_port,
'host': cfg.CONF.bind_host
},
'app': {
'root': 'tricircle.cinder_apigw.controllers.root.RootController',
'modules': ['tricircle.cinder_apigw'],
'errors': {
400: '/error',
'__force_dict__': True
}
}
}
pecan_config = pecan.configuration.conf_from_dict(config)
# app_hooks = [], hook collection will be put here later
app = pecan.make_app(
pecan_config.app.root,
debug=False,
wrap_app=restapp.auth_app,
force_canonical=False,
hooks=[],
guess_content_type_from_ext=True
)
return app

View File

@ -1,136 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
import oslo_log.log as logging
from tricircle.cinder_apigw.controllers import volume
from tricircle.cinder_apigw.controllers import volume_actions
from tricircle.cinder_apigw.controllers import volume_metadata
from tricircle.cinder_apigw.controllers import volume_type
LOG = logging.getLogger(__name__)
class RootController(object):
@pecan.expose()
def _lookup(self, version, *remainder):
if version == 'v2':
return V2Controller(), remainder
@pecan.expose(generic=True, template='json')
def index(self):
return {
"versions": [
{
"status": "CURRENT",
"updated": "2012-11-21T11:33:21Z",
"id": "v2.0",
"links": [
{
"href": pecan.request.application_url + "/v2/",
"rel": "self"
}
]
}
]
}
@index.when(method='POST')
@index.when(method='PUT')
@index.when(method='DELETE')
@index.when(method='HEAD')
@index.when(method='PATCH')
def not_supported(self):
pecan.abort(405)
class V2Controller(object):
_media_type1 = "application/vnd.openstack.volume+xml;version=1"
_media_type2 = "application/vnd.openstack.volume+json;version=1"
def __init__(self):
self.resource_controller = {
'volumes': volume.VolumeController,
'types': volume_type.VolumeTypeController
}
self.volumes_sub_controller = {
'metadata': volume_metadata.VolumeMetaDataController,
'action': volume_actions.VolumeActionController,
}
@pecan.expose()
def _lookup(self, tenant_id, *remainder):
if not remainder:
pecan.abort(404)
return
resource = remainder[0]
if resource not in self.resource_controller:
pecan.abort(404)
return
if resource == 'volumes' and len(remainder) >= 3:
volume_id = remainder[1]
sub_resource = remainder[2]
if sub_resource not in self.volumes_sub_controller:
pecan.abort(404)
return
return self.volumes_sub_controller[sub_resource](
tenant_id, volume_id), remainder[3:]
return self.resource_controller[resource](tenant_id), remainder[1:]
@pecan.expose(generic=True, template='json')
def index(self):
return {
"version": {
"status": "CURRENT",
"updated": "2012-11-21T11:33:21Z",
"media-types": [
{
"base": "application/xml",
"type": self._media_type1
},
{
"base": "application/json",
"type": self._media_type2
}
],
"id": "v2.0",
"links": [
{
"href": pecan.request.application_url + "/v2/",
"rel": "self"
},
{
"href": "http://docs.openstack.org/",
"type": "text/html",
"rel": "describedby"
}
]
}
}
@index.when(method='POST')
@index.when(method='PUT')
@index.when(method='DELETE')
@index.when(method='HEAD')
@index.when(method='PATCH')
def not_supported(self):
pecan.abort(405)

View File

@ -1,387 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 urlparse
from pecan import expose
from pecan import request
from pecan import response
from pecan import rest
from oslo_log import log as logging
from oslo_serialization import jsonutils
from tricircle.common import az_ag
from tricircle.common import constants as cons
import tricircle.common.context as t_context
from tricircle.common import httpclient as hclient
from tricircle.common.i18n import _
from tricircle.common.i18n import _LE
from tricircle.common.scheduler import filter_scheduler
from tricircle.common import utils
import tricircle.db.api as db_api
from tricircle.db import core
from tricircle.db import models
LOG = logging.getLogger(__name__)
class VolumeController(rest.RestController):
def __init__(self, tenant_id):
self.tenant_id = tenant_id
self.filter_scheduler = filter_scheduler.FilterScheduler()
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if 'volume' not in kw:
return utils.format_cinder_error(
400, _("Missing required element 'volume' in request body."))
az = kw['volume'].get('availability_zone', '')
pod, pod_az = self.filter_scheduler.select_destination(
context, az, self.tenant_id, pod_group='')
if not pod:
LOG.error(_LE("Pod not configured or scheduling failure"))
return utils.format_cinder_error(
500, _('Pod not configured or scheduling failure'))
t_pod = db_api.get_top_pod(context)
if not t_pod:
LOG.error(_LE("Top Pod not configured"))
return utils.format_cinder_error(500, _('Top Pod not configured'))
# TODO(joehuang): get release from pod configuration,
# to convert the content
# b_release = pod['release']
# t_release = t_pod['release']
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
s_ctx = hclient.get_pod_service_ctx(
context,
request.url,
pod['pod_name'],
s_type=cons.ST_CINDER)
if s_ctx['b_url'] == '':
LOG.error(_LE("Bottom Pod endpoint incorrect %s") %
pod['pod_name'])
return utils.format_cinder_error(
500, _('Bottom Pod endpoint incorrect'))
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
t_vol = kw['volume']
# add or remove key-value in the request for diff. version
b_vol_req = hclient.convert_object(t_release, b_release, t_vol,
res_type=cons.RT_VOLUME)
# convert az to the configured one
# remove the AZ parameter to bottom request for default one
b_vol_req['availability_zone'] = pod['pod_az_name']
if b_vol_req['availability_zone'] == '':
b_vol_req.pop("availability_zone", None)
b_body = jsonutils.dumps({'volume': b_vol_req})
resp = hclient.forward_req(
context,
'POST',
b_headers,
s_ctx['b_url'],
b_body)
b_status = resp.status_code
b_ret_body = jsonutils.loads(resp.content)
# build routing and convert response from the bottom pod
# for different version.
response.status = b_status
if b_status == 202:
if b_ret_body.get('volume') is not None:
b_vol_ret = b_ret_body['volume']
try:
with context.session.begin():
core.create_resource(
context, models.ResourceRouting,
{'top_id': b_vol_ret['id'],
'bottom_id': b_vol_ret['id'],
'pod_id': pod['pod_id'],
'project_id': self.tenant_id,
'resource_type': cons.RT_VOLUME})
except Exception as e:
LOG.exception(_LE('Failed to create volume '
'resource routing'
'top_id: %(top_id)s ,'
'bottom_id: %(bottom_id)s ,'
'pod_id: %(pod_id)s ,'
'%(exception)s '),
{'top_id': b_vol_ret['id'],
'bottom_id': b_vol_ret['id'],
'pod_id': pod['pod_id'],
'exception': e})
return utils.format_cinder_error(
500, _('Failed to create volume resource routing'))
ret_vol = hclient.convert_object(b_release, t_release,
b_vol_ret,
res_type=cons.RT_VOLUME)
ret_vol['availability_zone'] = pod['az_name']
return {'volume': ret_vol}
return b_ret_body
@expose(generic=True, template='json')
def get_one(self, _id):
context = t_context.extract_context_from_environ()
if _id == 'detail':
return {'volumes': self._get_all(context)}
# TODO(joehuang): get the release of top and bottom
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
s_ctx = hclient.get_res_routing_ref(context, _id, request.url,
cons.ST_CINDER)
if not s_ctx:
return utils.format_cinder_error(
404, _('Volume %s could not be found.') % _id)
if s_ctx['b_url'] == '':
return utils.format_cinder_error(
404, _('Bottom Pod endpoint incorrect'))
resp = hclient.forward_req(context, 'GET',
b_headers,
s_ctx['b_url'],
request.body)
b_ret_body = jsonutils.loads(resp.content)
b_status = resp.status_code
response.status = b_status
if b_status == 200:
if b_ret_body.get('volume') is not None:
b_vol_ret = b_ret_body['volume']
ret_vol = hclient.convert_object(b_release, t_release,
b_vol_ret,
res_type=cons.RT_VOLUME)
pod = utils.get_pod_by_top_id(context, _id)
if pod:
ret_vol['availability_zone'] = pod['az_name']
return {'volume': ret_vol}
# resource not find but routing exist, remove the routing
if b_status == 404:
filters = [{'key': 'top_id', 'comparator': 'eq', 'value': _id},
{'key': 'resource_type',
'comparator': 'eq',
'value': cons.RT_VOLUME}]
with context.session.begin():
core.delete_resources(context,
models.ResourceRouting,
filters)
return b_ret_body
@expose(generic=True, template='json')
def get_all(self):
# TODO(joehuang): here should return link instead,
# now combined with 'detail'
context = t_context.extract_context_from_environ()
return {'volumes': self._get_all(context)}
def _get_all(self, context):
# TODO(joehuang): query optimization for pagination, sort, etc
ret = []
pods = az_ag.list_pods_by_tenant(context, self.tenant_id)
for pod in pods:
if pod['pod_name'] == '':
continue
query = urlparse.urlsplit(request.url).query
query_filters = urlparse.parse_qsl(query)
skip_pod = False
for k, v in query_filters:
if k == 'availability_zone' and v != pod['az_name']:
skip_pod = True
break
if skip_pod:
continue
s_ctx = hclient.get_pod_service_ctx(
context,
request.url,
pod['pod_name'],
s_type=cons.ST_CINDER)
if s_ctx['b_url'] == '':
LOG.error(_LE("bottom pod endpoint incorrect %s")
% pod['pod_name'])
continue
# TODO(joehuang): get the release of top and bottom
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
resp = hclient.forward_req(context, 'GET',
b_headers,
s_ctx['b_url'],
request.body)
if resp.status_code == 200:
routings = db_api.get_bottom_mappings_by_tenant_pod(
context, self.tenant_id,
pod['pod_id'], cons.RT_VOLUME
)
b_ret_body = jsonutils.loads(resp.content)
if b_ret_body.get('volumes'):
for vol in b_ret_body['volumes']:
if not routings.get(vol['id']):
b_ret_body['volumes'].remove(vol)
continue
vol['availability_zone'] = pod['az_name']
ret.extend(b_ret_body['volumes'])
return ret
@expose(generic=True, template='json')
def put(self, _id, **kw):
context = t_context.extract_context_from_environ()
# TODO(joehuang): Implement API multi-version compatibility
# currently _convert_header and _convert_object are both dummy
# functions and API versions are hard coded. After multi-version
# compatibility is implemented, API versions will be retrieved from
# top and bottom API server, also, _convert_header and _convert_object
# will do the real job to convert the request header and body
# according to the API versions.
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
s_ctx = hclient.get_res_routing_ref(context, _id, request.url,
cons.ST_CINDER)
if not s_ctx:
return utils.format_cinder_error(
404, _('Volume %s could not be found.') % _id)
if s_ctx['b_url'] == '':
return utils.format_cinder_error(
404, _('Bottom Pod endpoint incorrect'))
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
t_vol = kw['volume']
# add or remove key-value in the request for diff. version
b_vol_req = hclient.convert_object(t_release, b_release, t_vol,
res_type=cons.RT_VOLUME)
b_body = jsonutils.dumps({'volume': b_vol_req})
resp = hclient.forward_req(context, 'PUT',
b_headers,
s_ctx['b_url'],
b_body)
b_status = resp.status_code
b_ret_body = jsonutils.loads(resp.content)
response.status = b_status
if b_status == 200:
if b_ret_body.get('volume') is not None:
b_vol_ret = b_ret_body['volume']
ret_vol = hclient.convert_object(b_release, t_release,
b_vol_ret,
res_type=cons.RT_VOLUME)
pod = utils.get_pod_by_top_id(context, _id)
if pod:
ret_vol['availability_zone'] = pod['az_name']
return {'volume': ret_vol}
# resource not found but routing exist, remove the routing
if b_status == 404:
filters = [{'key': 'top_id', 'comparator': 'eq', 'value': _id},
{'key': 'resource_type',
'comparator': 'eq',
'value': cons.RT_VOLUME}]
with context.session.begin():
core.delete_resources(context,
models.ResourceRouting,
filters)
return b_ret_body
@expose(generic=True, template='json')
def delete(self, _id):
context = t_context.extract_context_from_environ()
# TODO(joehuang): get the release of top and bottom
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
s_ctx = hclient.get_res_routing_ref(context, _id, request.url,
cons.ST_CINDER)
if not s_ctx:
return utils.format_cinder_error(
404, _('Volume %s could not be found.') % _id)
if s_ctx['b_url'] == '':
return utils.format_cinder_error(
404, _('Bottom Pod endpoint incorrect'))
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
resp = hclient.forward_req(context, 'DELETE',
b_headers,
s_ctx['b_url'],
request.body)
response.status = resp.status_code
# don't remove the resource routing for delete is async. operation
# remove the routing when query is executed but not find
# No content in the resp actually
return response

View File

@ -1,244 +0,0 @@
# Copyright 2016 OpenStack Foundation.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import rest
from oslo_log import log as logging
import tricircle.common.client as t_client
from tricircle.common import constants
import tricircle.common.context as t_context
from tricircle.common.i18n import _
from tricircle.common.i18n import _LE
from tricircle.common import utils
import tricircle.db.api as db_api
LOG = logging.getLogger(__name__)
class VolumeActionController(rest.RestController):
def __init__(self, project_id, volume_id):
self.project_id = project_id
self.volume_id = volume_id
self.clients = {constants.TOP: t_client.Client()}
self.handle_map = {
'os-attach': self._attach,
'os-extend': self._extend,
'os-reset_status': self._reset_status,
'os-set_image_metadata': self._set_image_metadata,
'os-unset_image_metadata': self._unset_image_metadata,
'os-show_image_metadata': self._show_image_metadata,
'os-force_detach': self._force_detach
}
def _get_client(self, pod_name=constants.TOP):
if pod_name not in self.clients:
self.clients[pod_name] = t_client.Client(pod_name)
return self.clients[pod_name]
def _action(self, context, pod_name, action, info=None, **kwargs):
"""Perform a volume "action".
:param pod_name: the bottom pod name.
:param action: volume action name.
:param info: action parameters body.
"""
body = {action: info}
url = '/volumes/%s/action' % self.volume_id
api = self._get_client(pod_name).get_native_client('volume', context)
return api.client.post(url, body=body)
def _attach(self, context, pod_name, kw):
"""Add attachment metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
mountpoint = None
if 'mountpoint' in kw['os-attach']:
mountpoint = kw['os-attach']['mountpoint']
body = {'mountpoint': mountpoint}
instance_uuid = None
if 'instance_uuid' in kw['os-attach']:
instance_uuid = kw['os-attach']['instance_uuid']
host_name = None
if 'host_name' in kw['os-attach']:
host_name = kw['os-attach']['host_name']
except (KeyError, ValueError, TypeError):
msg = _('The server could not comply with the request since '
'it is either malformed or otherwise incorrect.')
return utils.format_cinder_error(400, msg)
if instance_uuid is not None:
body.update({'instance_uuid': instance_uuid})
if host_name is not None:
body.update({'host_name': host_name})
return self._action(context, pod_name, 'os-attach', body)
def _extend(self, context, pod_name, kw):
"""Extend the size of the specified volume.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
new_size = int(kw['os-extend']['new_size'])
except (KeyError, ValueError, TypeError):
msg = _("New volume size must be specified as an integer.")
return utils.format_cinder_error(400, msg)
return self._action(context, pod_name, 'os-extend',
{'new_size': new_size})
def _force_detach(self, context, pod_name, kw):
"""Forces a volume to detach
:param pod_name: the bottom pod name.
:param kw: request body.
"""
body = kw['os-force_detach']
return self._action(context, pod_name, 'os-force_detach', body)
def _reset_status(self, context, pod_name, kw):
"""Update the provided volume with the provided state.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
status = None
if 'status' in kw['os-reset_status']:
status = kw['os-reset_status']['status']
attach_status = None
if 'attach_status' in kw['os-reset_status']:
attach_status = kw['os-reset_status']['attach_status']
migration_status = None
if 'migration_status' in kw['os-reset_status']:
migration_status = kw['os-reset_status']['migration_status']
except (TypeError, KeyError, ValueError):
msg = _('The server has either erred or is incapable of '
'performing the requested operation.')
return utils.format_cinder_error(500, msg)
body = {'status': status} if status else {}
if attach_status:
body.update({'attach_status': attach_status})
if migration_status:
body.update({'migration_status': migration_status})
return self._action(context, pod_name, 'os-reset_status', body)
def _set_image_metadata(self, context, pod_name, kw):
"""Set a volume's image metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
metadata = kw['os-set_image_metadata']['metadata']
except (KeyError, TypeError):
msg = _("Malformed request body.")
return utils.format_cinder_error(400, msg)
return self._action(context, pod_name, 'os-set_image_metadata',
{'metadata': metadata})
def _unset_image_metadata(self, context, pod_name, kw):
"""Unset specified keys from volume's image metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
try:
key = kw['os-unset_image_metadata']['key']
except (KeyError, TypeError):
msg = _("Malformed request body.")
return utils.format_cinder_error(400, msg)
return self._action(
context, pod_name, 'os-unset_image_metadata', {'key': key})
def _show_image_metadata(self, context, pod_name, kw):
"""Show a volume's image metadata.
:param pod_name: the bottom pod name.
:param kw: request body.
"""
return self._action(context, pod_name, 'os-show_image_metadata')
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
action_handle = None
action_type = None
for _type in self.handle_map:
if _type in kw:
action_handle = self.handle_map[_type]
action_type = _type
if not action_handle:
return utils.format_cinder_error(
400, _('Volume action not supported'))
volume_mappings = db_api.get_bottom_mappings_by_top_id(
context, self.volume_id, constants.RT_VOLUME)
if not volume_mappings:
return utils.format_cinder_error(
404, _('Volume %(volume_id)s could not be found.') % {
'volume_id': self.volume_id
})
pod_name = volume_mappings[0][0]['pod_name']
if action_type == 'os-attach':
instance_uuid = kw['os-attach'].get('instance_uuid')
if instance_uuid is not None:
server_mappings = db_api.get_bottom_mappings_by_top_id(
context, instance_uuid, constants.RT_SERVER)
if not server_mappings:
return utils.format_cinder_error(
404, _('Server not found'))
server_pod_name = server_mappings[0][0]['pod_name']
if server_pod_name != pod_name:
LOG.error(_LE('Server %(server)s is in pod %(server_pod)s'
'and volume %(volume)s is in pod'
'%(volume_pod)s, which '
'are not the same.'),
{'server': instance_uuid,
'server_pod': server_pod_name,
'volume': self.volume_id,
'volume_pod': pod_name})
return utils.format_cinder_error(
400, _('Server and volume not in the same pod'))
try:
resp, body = action_handle(context, pod_name, kw)
pecan.response.status = resp.status_code
if not body:
return pecan.response
else:
return body
except Exception as e:
code = 500
message = _('Action %(action)s on volume %(volume_id)s fails') % {
'action': action_type,
'volume_id': self.volume_id}
if hasattr(e, 'code'):
code = e.code
ex_message = str(e)
if ex_message:
message = ex_message
LOG.error(message)
return utils.format_cinder_error(code, message)

View File

@ -1,287 +0,0 @@
# Copyright 2016 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose
from pecan import request
from pecan import response
from pecan import rest
from oslo_log import log as logging
from oslo_serialization import jsonutils
from tricircle.common import constants as cons
import tricircle.common.context as t_context
from tricircle.common import httpclient as hclient
from tricircle.common.i18n import _
from tricircle.common.i18n import _LE
from tricircle.common import utils
import tricircle.db.api as db_api
LOG = logging.getLogger(__name__)
class VolumeMetaDataController(rest.RestController):
def __init__(self, tenant_id, volume_id):
self.tenant_id = tenant_id
self.volume_id = volume_id
@expose(generic=True, template='json')
def post(self, **kw):
"""Create volume metadata associated with a volume.
:param kw: dictionary of values to be created
:returns: created volume metadata
"""
context = t_context.extract_context_from_environ()
if 'metadata' not in kw:
return utils.format_cinder_error(
400, _("Missing required element 'metadata' in "
"request body."))
try:
pod = utils.get_pod_by_top_id(context, self.volume_id)
if pod is None:
return utils.format_cinder_error(
404, _('Volume %(volume_id)s could not be found.') % {
'volume_id': self.volume_id
})
t_pod = db_api.get_top_pod(context)
if not t_pod:
LOG.error(_LE("Top Pod not configured"))
return utils.format_cinder_error(
500, _('Top Pod not configured'))
except Exception as e:
LOG.exception(_LE('Fail to create metadata for a volume:'
'%(volume_id)s'
'%(exception)s'),
{'volume_id': self.volume_id,
'exception': e})
return utils.format_cinder_error(500, _('Fail to create metadata'))
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
s_ctx = hclient.get_pod_service_ctx(
context,
request.url,
pod['pod_name'],
s_type=cons.ST_CINDER)
if s_ctx['b_url'] == '':
LOG.error(_LE("Bottom pod endpoint incorrect %s") %
pod['pod_name'])
return utils.format_cinder_error(
500, _('Bottom pod endpoint incorrect'))
b_headers = hclient.convert_header(t_release, b_release,
request.headers)
t_metadata = kw['metadata']
# add or remove key-value in the request for diff. version
b_vol_req = hclient.convert_object(t_release, b_release, t_metadata,
res_type=cons.RT_VOl_METADATA)
b_body = jsonutils.dumps({'metadata': b_vol_req})
resp = hclient.forward_req(
context,
'POST',
b_headers,
s_ctx['b_url'],
b_body)
b_status = resp.status_code
b_body_ret = jsonutils.loads(resp.content)
# convert response from the bottom pod
# for different version.
response.status = b_status
if b_status == 200:
if b_body_ret.get('metadata') is not None:
b_metadata_ret = b_body_ret['metadata']
vol_ret = hclient.convert_object(b_release, t_release,
b_metadata_ret,
res_type=cons.
RT_VOl_METADATA)
return {'metadata': vol_ret}
return b_body_ret
@expose(generic=True, template='json')
def get_one(self):
"""Get all metadata associated with a volume."""
context = t_context.extract_context_from_environ()
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
try:
s_ctx = hclient.get_res_routing_ref(context, self.volume_id,
request.url, cons.ST_CINDER)
if not s_ctx:
return utils.format_cinder_error(
500, _('Fail to find resource'))
except Exception as e:
LOG.exception(_LE('Fail to get metadata for a volume:'
'%(volume_id)s'
'%(exception)s'),
{'volume_id': self.volume_id,
'exception': e})
return utils.format_cinder_error(500, _('Fail to get metadata'))
if s_ctx['b_url'] == '':
return utils.format_cinder_error(
500, _('Bottom pod endpoint incorrect'))
resp = hclient.forward_req(context, 'GET',
b_headers,
s_ctx['b_url'],
request.body)
b_body_ret = jsonutils.loads(resp.content)
b_status = resp.status_code
response.status = b_status
if b_status == 200:
if b_body_ret.get('metadata') is not None:
b_metadata_ret = b_body_ret['metadata']
vol_ret = hclient.convert_object(b_release, t_release,
b_metadata_ret,
res_type=cons.
RT_VOl_METADATA)
return {'metadata': vol_ret}
return b_body_ret
@expose(generic=True, template='json')
def put(self, **kw):
"""Update volume metadata.
:param kw: dictionary of values to be updated
:returns: updated volume type
"""
context = t_context.extract_context_from_environ()
if 'metadata' not in kw:
return utils.format_cinder_error(
400, _("Missing required element 'metadata' in "
"request body."))
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
try:
s_ctx = hclient.get_res_routing_ref(context, self.volume_id,
request.url, cons.ST_CINDER)
if not s_ctx:
return utils.format_cinder_error(
404, _('Resource not found'))
except Exception as e:
LOG.exception(_LE('Fail to update metadata for a volume: '
'%(volume_id)s'
'%(exception)s'),
{'volume_id': self.volume_id,
'exception': e})
return utils.format_cinder_error(
500, _('Fail to update metadata'))
if s_ctx['b_url'] == '':
return utils.format_cinder_error(
500, _('Bottom pod endpoint incorrect'))
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
t_metadata = kw['metadata']
# add or remove key/value in the request for diff. version
b_vol_req = hclient.convert_object(t_release, b_release, t_metadata,
res_type=cons.RT_VOl_METADATA)
b_body = jsonutils.dumps({'metadata': b_vol_req})
resp = hclient.forward_req(context, 'PUT',
b_headers,
s_ctx['b_url'],
b_body)
b_status = resp.status_code
b_body_ret = jsonutils.loads(resp.content)
response.status = b_status
if b_status == 200:
if b_body_ret.get('metadata') is not None:
b_metadata_ret = b_body_ret['metadata']
vol_ret = hclient.convert_object(b_release, t_release,
b_metadata_ret,
res_type=cons.
RT_VOl_METADATA)
return {'metadata': vol_ret}
return b_body_ret
@expose(generic=True, template='json')
def delete(self, key):
"""Delete the given metadata item from a volume."""
context = t_context.extract_context_from_environ()
t_release = cons.R_MITAKA
b_release = cons.R_MITAKA
try:
s_ctx = hclient.get_res_routing_ref(context, self.volume_id,
request.url, cons.ST_CINDER)
if not s_ctx:
return utils.format_cinder_error(
404, _('Fail to find resource'))
except Exception as e:
LOG.exception(_LE('Fail to delete metadata from a volume: '
'%(volume_id)s'
'%(exception)s'),
{'volume_id': self.volume_id,
'exception': e})
return utils.format_cinder_error(
500, _('Fail to delete metadata'))
if s_ctx['b_url'] == '':
return utils.format_cinder_error(
500, _('Bottom pod endpoint incorrect'))
b_headers = hclient.convert_header(t_release,
b_release,
request.headers)
resp = hclient.forward_req(context, 'DELETE',
b_headers,
s_ctx['b_url'],
request.body)
response.status = resp.status_code
# don't remove the resource routing for delete is async. operation
# remove the routing when query is executed but not found
# No content in the resp actually
return response

View File

@ -1,286 +0,0 @@
# Copyright 2016 OpenStack Foundation.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import rest
from oslo_log import log as logging
from oslo_utils import uuidutils
import tricircle.common.context as t_context
from tricircle.common import exceptions
from tricircle.common.i18n import _
from tricircle.common.i18n import _LE
from tricircle.common import utils
import tricircle.db.api as db_api
from tricircle.db import core
from tricircle.db import models
LOG = logging.getLogger(__name__)
class VolumeTypeController(rest.RestController):
def __init__(self, tenant_id):
self.tenant_id = tenant_id
def _metadata_refs(self, metadata_dict, meta_class):
metadata_refs = []
if metadata_dict:
for k, v in metadata_dict.items():
metadata_ref = meta_class()
metadata_ref['key'] = k
metadata_ref['value'] = v
metadata_refs.append(metadata_ref)
return metadata_refs
@expose(generic=True, template='json')
def post(self, **kw):
"""Creates volume types."""
context = t_context.extract_context_from_environ()
if not context.is_admin:
return utils.format_cinder_error(
403, _("Policy doesn't allow volume_extension:types_manage "
"to be performed."))
if 'volume_type' not in kw:
return utils.format_cinder_error(
400, _("Missing required element 'volume_type' in "
"request body."))
projects = []
if self.tenant_id is not None:
projects = [self.tenant_id]
vol_type = kw['volume_type']
name = vol_type.get('name', None)
description = vol_type.get('description')
specs = vol_type.get('extra_specs', {})
is_public = vol_type.pop('os-volume-type-access:is_public', True)
if name is None or len(name.strip()) == 0:
return utils.format_cinder_error(
400, _("Volume type name can not be empty."))
try:
utils.check_string_length(name, 'Type name',
min_len=1, max_len=255)
except exceptions.InvalidInput as e:
return utils.format_cinder_error(
400, e.message)
if description is not None:
try:
utils.check_string_length(description, 'Type description',
min_len=0, max_len=255)
except exceptions.InvalidInput as e:
return utils.format_cinder_error(400, e.message)
if not utils.is_valid_boolstr(is_public):
msg = _("Invalid value '%(is_public)s' for is_public. "
"Accepted values: True or False.") % {
'is_public': is_public}
return utils.format_cinder_error(400, msg)
vol_type['extra_specs'] = specs
vol_type['is_public'] = is_public
vol_type['id'] = uuidutils.generate_uuid()
session = core.get_session()
with session.begin():
try:
db_api.volume_type_get_by_name(context, vol_type['name'],
session)
return utils.format_cinder_error(
409, _("Volume Type %(id)s already exists.") % {
'id': vol_type['id']})
except exceptions.VolumeTypeNotFoundByName:
pass
try:
extra_specs = vol_type['extra_specs']
vol_type['extra_specs'] = \
self._metadata_refs(vol_type.get('extra_specs'),
models.VolumeTypeExtraSpecs)
volume_type_ref = models.VolumeTypes()
volume_type_ref.update(vol_type)
session.add(volume_type_ref)
for project in set(projects):
access_ref = models.VolumeTypeProjects()
access_ref.update({"volume_type_id": volume_type_ref.id,
"project_id": project})
access_ref.save(session=session)
except Exception as e:
LOG.exception(_LE('Fail to create volume type: %(name)s,'
'%(exception)s'),
{'name': vol_type['name'],
'exception': e})
return utils.format_cinder_error(
500, _('Fail to create volume type'))
vol_type['extra_specs'] = extra_specs
return {'volume_type': vol_type}
@expose(generic=True, template='json')
def get_one(self, _id):
"""Retrieves single volume type by id.
:param _id: id of volume type to be retrieved
:returns: retrieved volume type
"""
context = t_context.extract_context_from_environ()
try:
result = db_api.volume_type_get(context, _id)
except exceptions.VolumeTypeNotFound as e:
return utils.format_cinder_error(404, e.message)
except Exception as e:
LOG.exception(_LE('Volume type not found: %(id)s,'
'%(exception)s'),
{'id': _id,
'exception': e})
return utils.format_cinder_error(
404, _("Volume type %(id)s could not be found.") % {
'id': _id})
return {'volume_type': result}
@expose(generic=True, template='json')
def get_all(self):
"""Get all non-deleted volume_types."""
filters = {}
context = t_context.extract_context_from_environ()
if not context.is_admin:
# Only admin has query access to all volume types
filters['is_public'] = True
try:
list_result = db_api.volume_type_get_all(context,
list_result=True,
filters=filters)
except Exception as e:
LOG.exception(_LE('Fail to retrieve volume types: %(exception)s'),
{'exception': e})
return utils.format_cinder_error(500, e)
return {'volume_types': list_result}
@expose(generic=True, template='json')
def put(self, _id, **kw):
"""Update volume type by id.
:param _id: id of volume type to be updated
:param kw: dictionary of values to be updated
:returns: updated volume type
"""
context = t_context.extract_context_from_environ()
if not context.is_admin:
return utils.format_cinder_error(
403, _("Policy doesn't allow volume_extension:types_manage "
"to be performed."))
if 'volume_type' not in kw:
return utils.format_cinder_error(
400, _("Missing required element 'volume_type' in "
"request body."))
values = kw['volume_type']
name = values.get('name')
description = values.get('description')
is_public = values.get('os-volume-type-access:is_public')
# Name and description can not be both None.
# If name specified, name can not be empty.
if name and len(name.strip()) == 0:
return utils.format_cinder_error(
400, _("Volume type name can not be empty."))
if name is None and description is None and is_public is None:
msg = _("Specify volume type name, description, is_public or "
"a combination thereof.")
return utils.format_cinder_error(400, msg)
if is_public is not None and not utils.is_valid_boolstr(is_public):
msg = _("Invalid value '%(is_public)s' for is_public. Accepted "
"values: True or False.") % {'is_public': is_public}
return utils.format_cinder_error(400, msg)
if name:
try:
utils.check_string_length(name, 'Type name',
min_len=1, max_len=255)
except exceptions.InvalidInput as e:
return utils.format_cinder_error(400, e.message)
if description is not None:
try:
utils.check_string_length(description, 'Type description',
min_len=0, max_len=255)
except exceptions.InvalidInput as e:
return utils.format_cinder_error(400, e.message)
try:
type_updated = \
db_api.volume_type_update(context, _id,
dict(name=name,
description=description,
is_public=is_public))
except exceptions.VolumeTypeNotFound as e:
return utils.format_cinder_error(404, e.message)
except exceptions.VolumeTypeExists as e:
return utils.format_cinder_error(409, e.message)
except exceptions.VolumeTypeUpdateFailed as e:
return utils.format_cinder_error(500, e.message)
except Exception as e:
LOG.exception(_LE('Fail to update volume type: %(name)s,'
'%(exception)s'),
{'name': values['name'],
'exception': e})
return utils.format_cinder_error(
500, _("Fail to update volume type."))
return {'volume_type': type_updated}
@expose(generic=True, template='json')
def delete(self, _id):
"""Marks volume types as deleted.
:param _id: id of volume type to be deleted
"""
context = t_context.extract_context_from_environ()
if not context.is_admin:
return utils.format_cinder_error(
403, _("Policy doesn't allow volume_extension:types_manage "
"to be performed."))
session = core.get_session()
with session.begin():
try:
db_api.volume_type_get(context, _id, session)
except exceptions.VolumeTypeNotFound as e:
return utils.format_cinder_error(404, e.message)
try:
db_api.volume_type_delete(context, _id, session)
except Exception as e:
LOG.exception(_LE('Fail to update volume type: %(id)s,'
'%(exception)s'),
{'id': _id,
'exception': e})
return utils.format_cinder_error(
500, _('Fail to delete volume type.'))
pecan.response.status = 202
return pecan.response

View File

@ -1,22 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# 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 tricircle.cinder_apigw.app
def list_opts():
return [
('DEFAULT', tricircle.cinder_apigw.app.common_opts),
]

View File

@ -1,31 +0,0 @@
# 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 abc
import six
from stevedore import driver
@six.add_metaclass(abc.ABCMeta)
class Scheduler(object):
def __init__(self):
self.pod_manager = driver.DriverManager(
namespace='tricircle.common.schedulers',
name='pod_manager',
invoke_on_load=True
).driver
@abc.abstractmethod
def select_destination(self, context, az_name, tenant_id, spec_obj):
return None, None

View File

@ -1,58 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from tricircle.common.scheduler import driver
class FilterScheduler(driver.Scheduler):
def __init__(self, *args, **kwargs):
super(FilterScheduler, self).__init__(*args, **kwargs)
def select_destination(self, context, az_name, tenant_id, pod_group):
current_binding, current_pod = \
self.pod_manager.get_current_binding_and_pod(
context, az_name, tenant_id, pod_group)
if current_binding and current_pod:
return current_pod, current_pod['pod_az_name']
else:
pods = self.pod_manager.get_available_pods(
context, az_name, pod_group)
if not pods:
return None, None
# TODO(Yipei): Weigh pods and select one whose weight
# is the maximum. Here we chose one randomly.
is_current = False
best_pod = None
# select the pod by a circle in pods
for pod in pods:
if is_current:
best_pod = pod
break
if current_binding \
and pod['pod_id'] == current_binding['pod_id']:
is_current = True
if is_current and len(pods) == 1:
return None, None
if not best_pod:
best_pod = pods[0]
if current_binding:
is_successful = self.pod_manager.update_binding(
context, current_binding, best_pod['pod_id'])
else:
is_successful = self.pod_manager.create_binding(
context, tenant_id, best_pod['pod_id'])
if not is_successful:
return None, None
return best_pod, best_pod['pod_az_name']

View File

@ -1,31 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class BaseFilter(object):
"""Base class for all pod filter classes."""
def _filter_one(self, obj, pod_group):
return True
def filter_all(self, filter_obj_list, pod_group):
for obj in filter_obj_list:
if self._filter_one(obj, pod_group):
yield obj
class BasePodFilter(BaseFilter):
def _filter_one(self, obj, pod_group):
return self.is_pod_passed(obj, pod_group)
def is_pod_passed(self, pod, pod_group):
raise NotImplementedError()

View File

@ -1,23 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from tricircle.common.scheduler.filters import base_filters
class BottomPodFilter(base_filters.BasePodFilter):
"""Returns all bottom pods."""
def is_pod_passed(self, pod, pod_group):
flag = False
if pod['az_name'] != '':
flag = True
return flag

View File

@ -1,109 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from stevedore import driver
from tricircle.common.i18n import _LE
from tricircle.db import api as db_api
LOG = logging.getLogger(__name__)
class PodManager(object):
def __init__(self):
filter_names = ['bottom_pod_filter']
self.default_filters = self._choose_pod_filters(filter_names)
@staticmethod
def _choose_pod_filters(filter_names):
good_filters = []
for filter_name in filter_names:
filter_ = driver.DriverManager(
'tricircle.common.schedulers',
filter_name,
invoke_on_load=True
).driver
good_filters.append(filter_)
return good_filters
@staticmethod
def get_current_binding_and_pod(context, az_name, tenant_id, pod_group):
filter_b = [{'key': 'tenant_id', 'comparator': 'eq',
'value': tenant_id}]
current_bindings = db_api.get_pod_binding_by_tenant_id(
context, filter_b)
if not current_bindings:
return None, None
has_available_pods = False
for pod_b in current_bindings:
if pod_b['is_binding']:
pod = db_api.get_pod_by_pod_id(context, pod_b['pod_id'])
if az_name and pod['az_name'] == az_name:
has_available_pods = True
elif az_name == '' and pod['az_name'] != '':
# if the az_name is not specified, a default bottom
# pod will be selected
has_available_pods = True
if has_available_pods:
# TODO(Yipei): check resource_affinity_tag
# if the resource utilization of the pod reaches the limit,
# return [], []. Considering the feature of checking
# resource utilization is not implemented, we use
# resource_affinity_tag to test the logic of updating
# a binding relationship.
if pod_group != '':
return pod_b, None
# TODO(Yipei): check resource utilization of the pod
# if the resource utilization of the pod reaches the limit,
# return pod_b, []
# If a pod passes the above checking, both the pod and its
# corresponding binding are returned.
return pod_b, pod
return None, None
@staticmethod
def create_binding(context, tenant_id, pod_id):
try:
db_api.create_pod_binding(context, tenant_id, pod_id)
except Exception as e:
LOG.error(_LE('Fail to create pod binding: %(exception)s'),
{'exception': e})
return False
return True
@staticmethod
def update_binding(context, current_binding, pod_id):
current_binding['is_binding'] = False
try:
db_api.change_pod_binding(
context, current_binding, pod_id)
except Exception as e:
LOG.error(_LE('Fail to update pod binding: %(exception)s'),
{'exception': e})
return False
return True
def get_available_pods(self, context, az_name, pod_group):
if az_name != '':
filter_q = [{'key': 'az_name',
'comparator': 'eq', 'value': az_name}]
else:
filter_q = None
pods = db_api.list_pods(context, filter_q)
for filter_ in self.default_filters:
objs_ = filter_.filter_all(pods, pod_group)
pods = list(objs_)
return pods

View File

@ -1,83 +0,0 @@
# Copyright (c) 2015 Huawei, Tech. Co,. Ltd.
# All Rights Reserved.
#
# 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 pecan
from oslo_config import cfg
from tricircle.common.i18n import _
from tricircle.common import restapp
from tricircle.nova_apigw.controllers import micro_versions
from tricircle.nova_apigw.controllers import root
from tricircle.nova_apigw.controllers import root_versions
common_opts = [
cfg.IPOpt('bind_host', default='0.0.0.0',
help=_("The host IP to bind to")),
cfg.PortOpt('bind_port', default=19998,
help=_("The port to bind to")),
cfg.IntOpt('api_workers', default=1,
help=_("number of api workers")),
cfg.StrOpt('api_extensions_path', default="",
help=_("The path for API extensions")),
cfg.StrOpt('auth_strategy', default='keystone',
help=_("The type of authentication to use")),
cfg.BoolOpt('allow_bulk', default=True,
help=_("Allow the usage of the bulk API")),
cfg.BoolOpt('allow_pagination', default=False,
help=_("Allow the usage of the pagination")),
cfg.BoolOpt('allow_sorting', default=False,
help=_("Allow the usage of the sorting")),
cfg.StrOpt('pagination_max_limit', default="-1",
help=_("The maximum number of items returned in a single "
"response, value was 'infinite' or negative integer "
"means no limit")),
]
def setup_app(*args, **kwargs):
config = {
'server': {
'port': cfg.CONF.bind_port,
'host': cfg.CONF.bind_host
},
'app': {
'root': 'tricircle.nova_apigw.controllers.root.RootController',
'modules': ['tricircle.nova_apigw'],
'errors': {
400: '/error',
'__force_dict__': True
}
}
}
pecan_config = pecan.configuration.conf_from_dict(config)
app_hooks = [root.ErrorHook()]
app = pecan.make_app(
pecan_config.app.root,
debug=False,
wrap_app=restapp.auth_app,
force_canonical=False,
hooks=app_hooks,
guess_content_type_from_ext=True
)
# get nova api version
app = micro_versions.MicroVersion(app)
# version can be unauthenticated so it goes outside of auth
app = root_versions.Versions(app)
return app

View File

@ -1,173 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import rest
from oslo_log import log as logging
import tricircle.common.client as t_client
from tricircle.common import constants
import tricircle.common.context as t_context
from tricircle.common.i18n import _
from tricircle.common import utils
import tricircle.db.api as db_api
LOG = logging.getLogger(__name__)
class ActionController(rest.RestController):
def __init__(self, project_id, server_id):
self.project_id = project_id
self.server_id = server_id
self.clients = {constants.TOP: t_client.Client()}
self.handle_map = {
'os-start': self._handle_start,
'os-stop': self._handle_stop,
'forceDelete': self._handle_force_delete,
'lock': self._handle_lock,
'unlock': self._handle_unlock,
'pause': self._handle_pause,
'unpause': self._handle_unpause,
'resume': self._handle_resume,
'suspend': self._handle_suspend,
'shelve': self._handle_shelve,
'unshelve': self._handle_unshelve,
'shelveOffload': self._handle_shelve_offload,
'migrate': self._handle_migrate,
'trigger_crash_dump': self._handle_trigger_crash_dump,
'reboot': self._handle_action,
'resize': self._handle_action,
'confirmResize': self._handle_action,
'revertResize': self._handle_action,
'os-resetState': self._handle_action
}
def _get_client(self, pod_name=constants.TOP):
if pod_name not in self.clients:
self.clients[pod_name] = t_client.Client(pod_name)
return self.clients[pod_name]
def _handle_start(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'start', self.server_id)
def _handle_stop(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'stop', self.server_id)
def _handle_force_delete(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'force_delete', self.server_id)
def _handle_pause(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'pause', self.server_id)
def _handle_unpause(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'unpause', self.server_id)
def _handle_lock(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'lock', self.server_id)
def _handle_unlock(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'unlock', self.server_id)
def _handle_suspend(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'suspend', self.server_id)
def _handle_resume(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'resume', self.server_id)
def _handle_shelve(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'shelve', self.server_id)
def _handle_shelve_offload(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'shelve_offload', self.server_id)
def _handle_unshelve(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'unshelve', self.server_id)
def _handle_trigger_crash_dump(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'trigger_crash_dump',
self.server_id)
def _handle_migrate(self, context, pod_name, body):
client = self._get_client(pod_name)
return client.action_servers(context, 'migrate', self.server_id)
def _handle_action(self, context, pod_name, body):
"""Perform a server action
:param pod_name: the bottom pod name.
:param body: action parameters body.
"""
url = constants.SERVER_ACTION_URL % self.server_id
api = self._get_client(pod_name).get_native_client(constants.RT_SERVER,
context)
return api.client.post(url, body=body)
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
action_handle = None
action_type = None
for _type in self.handle_map:
if _type in kw:
action_handle = self.handle_map[_type]
action_type = _type
if not action_handle:
return utils.format_nova_error(
400, _('Server action not supported'))
server_mappings = db_api.get_bottom_mappings_by_top_id(
context, self.server_id, constants.RT_SERVER)
if not server_mappings:
return utils.format_nova_error(
404, _('Server %s could not be found') % self.server_id)
pod_name = server_mappings[0][0]['pod_name']
try:
resp, body = action_handle(context, pod_name, kw)
pecan.response.status = resp.status_code
if not body:
return pecan.response
else:
return body
except Exception as e:
code = 500
message = _('Action %(action)s on server %(server_id)s fails') % {
'action': action_type,
'server_id': self.server_id}
if hasattr(e, 'code'):
code = e.code
ex_message = str(e)
if ex_message:
message = ex_message
LOG.error(message)
return utils.format_nova_error(code, message)

View File

@ -1,141 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import rest
import oslo_db.exception as db_exc
from tricircle.common import az_ag
import tricircle.common.context as t_context
import tricircle.common.exceptions as t_exc
from tricircle.common.i18n import _
from tricircle.common import utils
from tricircle.db import core
from tricircle.db import models
class AggregateActionController(rest.RestController):
def __init__(self, project_id, aggregate_id):
self.project_id = project_id
self.aggregate_id = aggregate_id
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if not context.is_admin:
return utils.format_nova_error(
403, _("Policy doesn't allow os_compute_api:os-aggregates:"
"index to be performed."))
try:
with context.session.begin():
core.get_resource(context, models.Aggregate, self.aggregate_id)
except t_exc.ResourceNotFound:
return utils.format_nova_error(
404, _('Aggregate %s could not be found.') % self.aggregate_id)
if 'add_host' in kw or 'remove_host' in kw:
return utils.format_nova_error(
400, _('Add and remove host action not supported'))
# TODO(zhiyuan) handle aggregate metadata updating
try:
aggregate = az_ag.get_one_ag(context, self.aggregate_id)
return {'aggregate': aggregate}
except Exception:
return utils.format_nova_error(
500, _('Aggregate operation on %s failed') % self.aggregate_id)
class AggregateController(rest.RestController):
def __init__(self, project_id):
self.project_id = project_id
@pecan.expose()
def _lookup(self, aggregate_id, action, *remainder):
if action == 'action':
return AggregateActionController(self.project_id,
aggregate_id), remainder
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if not context.is_admin:
return utils.format_nova_error(
403, _("Policy doesn't allow os_compute_api:os-aggregates:"
"index to be performed."))
if 'aggregate' not in kw:
return utils.format_nova_error(
400, _('aggregate is not set'))
host_aggregate = kw['aggregate']
name = host_aggregate['name'].strip()
avail_zone = host_aggregate.get('availability_zone')
if avail_zone:
avail_zone = avail_zone.strip()
try:
with context.session.begin():
aggregate = az_ag.create_ag_az(context,
ag_name=name,
az_name=avail_zone)
except db_exc.DBDuplicateEntry:
return utils.format_nova_error(
409, _('Aggregate %s already exists.') % name)
except Exception:
return utils.format_nova_error(
500, _('Fail to create aggregate'))
return {'aggregate': aggregate}
@expose(generic=True, template='json')
def get_one(self, _id):
context = t_context.extract_context_from_environ()
try:
with context.session.begin():
aggregate = az_ag.get_one_ag(context, _id)
return {'aggregate': aggregate}
except t_exc.ResourceNotFound:
return utils.format_nova_error(
404, _('Aggregate %s could not be found.') % _id)
except Exception:
return utils.format_nova_error(
500, _('Fail to get aggregate %s') % _id)
@expose(generic=True, template='json')
def get_all(self):
context = t_context.extract_context_from_environ()
try:
with context.session.begin():
aggregates = az_ag.get_all_ag(context)
except Exception:
return utils.format_nova_error(500, _('Fail to list aggregates'))
return {'aggregates': aggregates}
@expose(generic=True, template='json')
def delete(self, _id):
context = t_context.extract_context_from_environ()
try:
with context.session.begin():
az_ag.delete_ag(context, _id)
pecan.response.status = 200
except t_exc.ResourceNotFound:
return utils.format_nova_error(
404, _('Aggregate %s could not be found.') % _id)
except Exception:
return utils.format_nova_error(
500, _('Fail to delete aggregate %s') % _id)

View File

@ -1,217 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import rest
import oslo_db.exception as db_exc
import tricircle.common.context as t_context
from tricircle.common.i18n import _
from tricircle.common import utils
from tricircle.db import core
from tricircle.db import models
class FlavorManageController(rest.RestController):
# NOTE(zhiyuan) according to nova API reference, flavor creating and
# deleting should use '/flavors/os-flavor-manage' path, but '/flavors/'
# also supports this two operations to keep compatible with nova client
def __init__(self, project_id):
self.project_id = project_id
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if not context.is_admin:
return utils.format_nova_error(
403, _("Policy doesn't allow os_compute_api:os-flavor-manage "
"to be performed."))
required_fields = ['name', 'ram', 'vcpus', 'disk']
if 'flavor' not in kw:
utils.format_nova_error(400, _('flavor is not set'))
if not utils.validate_required_fields_set(kw['flavor'],
required_fields):
utils.format_nova_error(
400, _('Invalid input for field/attribute flavor.'))
flavor_dict = {
'name': kw['flavor']['name'],
'flavorid': kw['flavor'].get('id'),
'memory_mb': kw['flavor']['ram'],
'vcpus': kw['flavor']['vcpus'],
'root_gb': kw['flavor']['disk'],
'ephemeral_gb': kw['flavor'].get('OS-FLV-EXT-DATA:ephemeral', 0),
'swap': kw['flavor'].get('swap', 0),
'rxtx_factor': kw['flavor'].get('rxtx_factor', 1.0),
'is_public': kw['flavor'].get('os-flavor-access:is_public', True),
}
try:
with context.session.begin():
flavor = core.create_resource(
context, models.InstanceTypes, flavor_dict)
except db_exc.DBDuplicateEntry as e:
if 'flavorid' in e.columns:
return utils.format_nova_error(
409, _('Flavor with ID %s already '
'exists.') % flavor_dict['flavorid'])
else:
return utils.format_nova_error(
409, _('Flavor with name %s already '
'exists.') % flavor_dict['name'])
except Exception:
return utils.format_nova_error(500, _('Failed to create flavor'))
return {'flavor': flavor}
@expose(generic=True, template='json')
def delete(self, _id):
context = t_context.extract_context_from_environ()
try:
with context.session.begin():
flavors = core.query_resource(context, models.InstanceTypes,
[{'key': 'flavorid',
'comparator': 'eq',
'value': _id}], [])
if not flavors:
return utils.format_nova_error(
404, _('Flavor %s could not be found') % _id)
core.delete_resource(context, models.InstanceTypes,
flavors[0]['id'])
except Exception:
return utils.format_nova_error(500, _('Failed to delete flavor'))
pecan.response.status = 202
return
class FlavorController(rest.RestController):
def __init__(self, project_id):
self.project_id = project_id
@pecan.expose()
def _lookup(self, action, *remainder):
if action == 'os-flavor-manage':
return FlavorManageController(self.project_id), remainder
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if not context.is_admin:
return utils.format_nova_error(
403, _("Policy doesn't allow os_compute_api:os-flavor-manage "
"to be performed."))
required_fields = ['name', 'ram', 'vcpus', 'disk']
if 'flavor' not in kw:
utils.format_nova_error(400, _('flavor is not set'))
if not utils.validate_required_fields_set(kw['flavor'],
required_fields):
utils.format_nova_error(
400, _('Invalid input for field/attribute flavor.'))
flavor_dict = {
'name': kw['flavor']['name'],
'flavorid': kw['flavor'].get('id'),
'memory_mb': kw['flavor']['ram'],
'vcpus': kw['flavor']['vcpus'],
'root_gb': kw['flavor']['disk'],
'ephemeral_gb': kw['flavor'].get('OS-FLV-EXT-DATA:ephemeral', 0),
'swap': kw['flavor'].get('swap', 0),
'rxtx_factor': kw['flavor'].get('rxtx_factor', 1.0),
'is_public': kw['flavor'].get('os-flavor-access:is_public', True),
}
try:
with context.session.begin():
flavor = core.create_resource(
context, models.InstanceTypes, flavor_dict)
except db_exc.DBDuplicateEntry as e:
if 'flavorid' in e.columns:
return utils.format_nova_error(
409, _('Flavor with ID %s already '
'exists.') % flavor_dict['flavorid'])
else:
return utils.format_nova_error(
409, _('Flavor with name %s already '
'exists.') % flavor_dict['name'])
except Exception:
utils.format_nova_error(500, _('Failed to create flavor'))
flavor['id'] = flavor['flavorid']
del flavor['flavorid']
return {'flavor': flavor}
@expose(generic=True, template='json')
def get_one(self, _id):
# NOTE(zhiyuan) this function handles two kinds of requests
# GET /flavors/flavor_id
# GET /flavors/detail
context = t_context.extract_context_from_environ()
if _id == 'detail':
with context.session.begin():
flavors = core.query_resource(context, models.InstanceTypes,
[], [])
for flavor in flavors:
flavor['id'] = flavor['flavorid']
del flavor['flavorid']
return {'flavors': flavors}
else:
with context.session.begin():
flavors = core.query_resource(context, models.InstanceTypes,
[{'key': 'flavorid',
'comparator': 'eq',
'value': _id}], [])
if not flavors:
return utils.format_nova_error(
404, _('Flavor %s could not be found') % _id)
flavor = flavors[0]
flavor['id'] = flavor['flavorid']
del flavor['flavorid']
return {'flavor': flavor}
@expose(generic=True, template='json')
def get_all(self):
context = t_context.extract_context_from_environ()
with context.session.begin():
flavors = core.query_resource(context, models.InstanceTypes,
[], [])
return {'flavors': [dict(
[('id', flavor['flavorid']),
('name', flavor['name'])]) for flavor in flavors]}
@expose(generic=True, template='json')
def delete(self, _id):
# TODO(zhiyuan) handle foreign key constraint
context = t_context.extract_context_from_environ()
try:
with context.session.begin():
flavors = core.query_resource(context, models.InstanceTypes,
[{'key': 'flavorid',
'comparator': 'eq',
'value': _id}], [])
if not flavors:
return utils.format_nova_error(
404, _('Flavor %s could not be found') % _id)
core.delete_resource(context,
models.InstanceTypes, flavors[0]['id'])
except Exception:
return utils.format_nova_error(500, _('Failed to delete flavor'))
pecan.response.status = 202
return

View File

@ -1,197 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose
from pecan import rest
import re
import urlparse
import tricircle.common.client as t_client
from tricircle.common import constants
import tricircle.common.context as t_context
from tricircle.common.i18n import _
from tricircle.common import utils
import tricircle.db.api as db_api
SUPPORTED_FILTERS = {
'name': 'name',
'status': 'status',
'changes-since': 'changes-since',
'server': 'property-instance_uuid',
'type': 'property-image_type',
'minRam': 'min_ram',
'minDisk': 'min_disk',
}
def url_join(*parts):
"""Convenience method for joining parts of a URL
Any leading and trailing '/' characters are removed, and the parts joined
together with '/' as a separator. If last element of 'parts' is an empty
string, the returned URL will have a trailing slash.
"""
parts = parts or ['']
clean_parts = [part.strip('/') for part in parts if part]
if not parts[-1]:
# Empty last element should add a trailing slash
clean_parts.append('')
return '/'.join(clean_parts)
def remove_trailing_version_from_href(href):
"""Removes the api version from the href.
Given: 'http://www.nova.com/compute/v1.1'
Returns: 'http://www.nova.com/compute'
Given: 'http://www.nova.com/v1.1'
Returns: 'http://www.nova.com'
"""
parsed_url = urlparse.urlsplit(href)
url_parts = parsed_url.path.rsplit('/', 1)
# NOTE: this should match vX.X or vX
expression = re.compile(r'^v([0-9]+|[0-9]+\.[0-9]+)(/.*|$)')
if not expression.match(url_parts.pop()):
raise ValueError('URL %s does not contain version' % href)
new_path = url_join(*url_parts)
parsed_url = list(parsed_url)
parsed_url[2] = new_path
return urlparse.urlunsplit(parsed_url)
class ImageController(rest.RestController):
def __init__(self, project_id):
self.project_id = project_id
self.client = t_client.Client()
def _get_links(self, context, image):
nova_url = self.client.get_endpoint(
context, db_api.get_top_pod(context)['pod_id'],
constants.ST_NOVA)
nova_url = nova_url.replace('/$(tenant_id)s', '')
self_link = url_join(nova_url, self.project_id, 'images', image['id'])
bookmark_link = url_join(
remove_trailing_version_from_href(nova_url),
self.project_id, 'images', image['id'])
glance_url = self.client.get_endpoint(
context, db_api.get_top_pod(context)['pod_id'],
constants.ST_GLANCE)
alternate_link = '/'.join([glance_url, 'images', image['id']])
return [{'rel': 'self', 'href': self_link},
{'rel': 'bookmark', 'href': bookmark_link},
{'rel': 'alternate',
'type': 'application/vnd.openstack.image',
'href': alternate_link}]
@staticmethod
def _format_date(dt):
"""Return standard format for a given datetime string."""
if dt is not None:
date_string = dt.split('.')[0]
date_string += 'Z'
return date_string
@staticmethod
def _get_status(image):
"""Update the status field to standardize format."""
return {
'active': 'ACTIVE',
'queued': 'SAVING',
'saving': 'SAVING',
'deleted': 'DELETED',
'pending_delete': 'DELETED',
'killed': 'ERROR',
}.get(image.get('status'), 'UNKNOWN')
@staticmethod
def _get_progress(image):
return {
'queued': 25,
'saving': 50,
'active': 100,
}.get(image.get('status'), 0)
def _construct_list_image_entry(self, context, image):
return {'id': image['id'],
'name': image.get('name'),
'links': self._get_links(context, image)}
def _construct_show_image_entry(self, context, image):
return {
'id': image['id'],
'name': image.get('name'),
'minRam': int(image.get('min_ram') or 0),
'minDisk': int(image.get('min_disk') or 0),
'metadata': image.get('properties', {}),
'created': self._format_date(image.get('created_at')),
'updated': self._format_date(image.get('updated_at')),
'status': self._get_status(image),
'progress': self._get_progress(image),
'links': self._get_links(context, image)
}
@expose(generic=True, template='json')
def get_one(self, _id, **kwargs):
context = t_context.extract_context_from_environ()
if _id == 'detail':
return self.get_all(**kwargs)
image = self.client.get_images(context, _id)
if not image:
return utils.format_nova_error(404, _('Image not found'))
return {'image': self._construct_show_image_entry(context, image)}
def _get_filters(self, params):
"""Return a dictionary of query param filters from the request.
:param params: the URI params coming from the wsgi layer
:return a dict of key/value filters
"""
filters = {}
for param in params:
if param in SUPPORTED_FILTERS or param.startswith('property-'):
# map filter name or carry through if property-*
filter_name = SUPPORTED_FILTERS.get(param, param)
filters[filter_name] = params.get(param)
# ensure server filter is the instance uuid
filter_name = 'property-instance_uuid'
try:
filters[filter_name] = filters[filter_name].rsplit('/', 1)[1]
except (AttributeError, IndexError, KeyError):
pass
filter_name = 'status'
if filter_name in filters:
# The Image API expects us to use lowercase strings for status
filters[filter_name] = filters[filter_name].lower()
return filters
@expose(generic=True, template='json')
def get_all(self, **kwargs):
context = t_context.extract_context_from_environ()
filters = self._get_filters(kwargs)
filters = [{'key': key,
'comparator': 'eq',
'value': value} for key, value in filters.iteritems()]
images = self.client.list_images(context, filters=filters)
ret_images = [self._construct_list_image_entry(
context, image) for image in images]
return {'images': ret_images}

View File

@ -1,120 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from novaclient import api_versions
from novaclient import exceptions
from oslo_serialization import jsonutils
from oslo_service import wsgi
from oslo_utils import encodeutils
import webob.dec
from tricircle.common import constants
class MicroVersion(object):
@staticmethod
def _format_error(code, message, error_type='computeFault'):
return {error_type: {'message': message, 'code': code}}
@classmethod
def factory(cls, global_config, **local_config):
return cls(app=None)
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
"""Get the nova micro version number
* If neither "X-OpenStack-Nova-API-Version" nor
"OpenStack-API-Version" (specifying "compute") is provided,
act as if the minimum supported microversion was specified.
* If both headers are provided,
"OpenStack-API-Version" will be preferred.
* If "X-OpenStack-Nova-API-Version" or "OpenStack-API-Version"
is provided, respond with the API at that microversion.
If that's outside of the range of microversions supported,
return 406 Not Acceptable.
* If "X-OpenStack-Nova-API-Version" or "OpenStack-API-Version"
has a value of "latest" (special keyword),
act as if maximum was specified.
"""
version_num = req.environ.get(
constants.HTTP_NOVA_API_VERSION_REQUEST_HEADER)
legacy_version_num = req.environ.get(
constants.HTTP_LEGACY_NOVA_API_VERSION_REQUEST_HEADER)
message = None
api_version = None
if version_num is None and legacy_version_num is None:
micro_version = constants.NOVA_APIGW_MIN_VERSION
elif version_num is not None:
err_msg = ("Invalid format of client version '%s'. "
"Expected format 'compute X.Y',"
"where X is a major part and Y "
"is a minor part of version.") % version_num
try:
nova_version_prefix = version_num.split()[0]
micro_version = ''.join(version_num.split()[1:])
if nova_version_prefix != 'compute':
message = err_msg
except Exception:
message = err_msg
else:
micro_version = legacy_version_num
if message is None:
try:
# Returns checked APIVersion object,
# or raise UnsupportedVersion exceptions.
api_version = api_versions.get_api_version(micro_version)
except exceptions.UnsupportedVersion as e:
message = e.message
if message is None and api_version is not None:
min_minor = int(constants.NOVA_APIGW_MIN_VERSION.split('.')[1])
max_minor = int(constants.NOVA_APIGW_MAX_VERSION.split('.')[1])
if api_version.is_latest():
micro_version = constants.NOVA_APIGW_MAX_VERSION
api_version.ver_minor = max_minor
if api_version.ver_minor < min_minor or \
api_version.ver_minor > max_minor:
message = ("Version %s is not supported by the API. "
"Minimum is %s, and maximum is %s"
% (micro_version, constants.NOVA_APIGW_MIN_VERSION,
constants.NOVA_APIGW_MAX_VERSION))
if message is None:
req.environ[constants.NOVA_API_VERSION_REQUEST_HEADER] = \
micro_version
if self.app:
return req.get_response(self.app)
else:
content_type = 'application/json'
body = jsonutils.dumps(
self._format_error('406', message, 'computeFault'))
response = webob.Response()
response.content_type = content_type
response.body = encodeutils.to_utf8(body)
response.status_code = 406
return response
def __init__(self, app):
self.app = app

View File

@ -1,50 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from pecan import expose
from pecan import rest
import tricircle.common.client as t_client
import tricircle.common.context as t_context
from tricircle.common.i18n import _
from tricircle.common import utils
class NetworkController(rest.RestController):
def __init__(self, project_id):
self.project_id = project_id
self.client = t_client.Client()
@staticmethod
def _construct_network_entry(network):
network['uuid'] = network['id']
network['label'] = network['name']
return network
@expose(generic=True, template='json')
def get_one(self, _id):
context = t_context.extract_context_from_environ()
network = self.client.get_networks(context, _id)
if not network:
return utils.format_nova_error(404, _('Network not found'))
return {'network': self._construct_network_entry(network)}
@expose(generic=True, template='json')
def get_all(self):
context = t_context.extract_context_from_environ()
networks = self.client.list_networks(context)
return {'networks': [self._construct_network_entry(
network) for network in networks]}

View File

@ -1,269 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 six
from pecan import expose
from pecan import request
from pecan import response
from pecan import Response
from pecan import rest
from oslo_config import cfg
from oslo_log import log as logging
import tricircle.common.context as t_context
from tricircle.common import exceptions as t_exceptions
from tricircle.common.i18n import _
from tricircle.common import quota
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class QuotaSetsController(rest.RestController):
def __init__(self, tenant_id):
self.tenant_id = tenant_id
@expose()
def _lookup(self, target_tenant_id, *remainder):
return QuotaController(self.tenant_id, target_tenant_id), remainder
def build_absolute_limits(quotas):
quota_map = {
'maxTotalRAMSize': 'ram',
'maxTotalInstances': 'instances',
'maxTotalCores': 'cores',
'maxTotalKeypairs': 'key_pairs',
'maxTotalFloatingIps': 'floating_ips',
'maxPersonality': 'injected_files',
'maxPersonalitySize': 'injected_file_content_bytes',
'maxSecurityGroups': 'security_groups',
'maxSecurityGroupRules': 'security_group_rules',
'maxServerMeta': 'metadata_items',
'maxServerGroups': 'server_groups',
'maxServerGroupMembers': 'server_group_members',
}
limits = {}
for display_name, key in six.iteritems(quota_map):
if key in quotas:
limits[display_name] = quotas[key]['limit']
return limits
def build_used_limits(quotas):
quota_map = {
'totalRAMUsed': 'ram',
'totalCoresUsed': 'cores',
'totalInstancesUsed': 'instances',
'totalFloatingIpsUsed': 'floating_ips',
'totalSecurityGroupsUsed': 'security_groups',
'totalServerGroupsUsed': 'server_groups',
}
# need to refresh usage from the bottom pods? Now from the data in top
used_limits = {}
for display_name, key in six.iteritems(quota_map):
if key in quotas:
reserved = quotas[key]['reserved']
used_limits[display_name] = quotas[key]['in_use'] + reserved
return used_limits
class LimitsController(rest.RestController):
def __init__(self, tenant_id):
self.tenant_id = tenant_id
@staticmethod
def _reserved(req):
try:
return int(req.GET['reserved'])
except (ValueError, KeyError):
return False
@expose(generic=True, template='json')
def get_all(self):
# TODO(joehuang): add policy controll here
context = t_context.extract_context_from_environ()
context.project_id = self.tenant_id
target_tenant_id = request.params.get('tenant_id', None)
if target_tenant_id:
target_tenant_id.strip()
else:
return Response('tenant_id not given', 400)
qs = quota.QuotaSetOperation(target_tenant_id,
None)
try:
quotas = qs.show_detail_quota(context, show_usage=True)
except t_exceptions.NotFound as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 404)
except (t_exceptions.AdminRequired,
t_exceptions.NotAuthorized,
t_exceptions.HTTPForbiddenError) as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 403)
except Exception as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 400)
# TODO(joehuang): add API rate limits later
ret = {
'limits': {
'rate': {},
'absolute': {},
},
}
ret['limits']['absolute'].update(
build_absolute_limits(quotas['quota_set']))
ret['limits']['absolute'].update(
build_used_limits(quotas['quota_set']))
return ret
class QuotaController(rest.RestController):
def __init__(self, owner_tenant_id, target_tenant_id):
self.owner_tenant_id = owner_tenant_id
self.target_tenant_id = target_tenant_id
@expose(generic=True, template='json')
def put(self, **kw):
context = t_context.extract_context_from_environ()
if not context.is_admin:
# TODO(joahuang): changed to policy control later
# to support reseller admin mode
return Response(_('Admin role required to update quota'), 409)
return self._quota_action('put', **kw)
@expose(generic=True, template='json')
def delete(self):
"""Delete Quota for a particular tenant.
This works for hierarchical and non-hierarchical projects. For
hierarchical projects only immediate parent admin or the
CLOUD admin are able to perform a delete.
:param id: target project id that needs to be deleted
"""
context = t_context.extract_context_from_environ()
if not context.is_admin:
# TODO(joahuang): changed to policy control later
# to support reseller admin mode
return Response(_('Admin role required to delete quota'), 409)
kw = {}
return self._quota_action('delete', **kw)
@expose(generic=True, template='json')
def get_one(self, show_what):
kw = {}
if show_what == 'defaults' or show_what == 'detail':
return self._quota_action(show_what, **kw)
else:
return Response(_('Only show defaults or detail allowed'), 400)
@expose(generic=True, template='json')
def get_all(self):
kw = {}
return self._quota_action('quota-show', **kw)
def _quota_action(self, action, **kw):
context = t_context.extract_context_from_environ()
context.project_id = self.owner_tenant_id
target_tenant_id = self.target_tenant_id
target_user_id = request.params.get('user_id', None)
if target_user_id:
target_user_id.strip()
qs = quota.QuotaSetOperation(target_tenant_id,
target_user_id)
quotas = {}
try:
if action == 'put':
quotas = qs.update(context, **kw)
elif action == 'delete':
qs.delete(context)
response.status = 202
return
elif action == 'defaults':
quotas = qs.show_default_quota(context)
elif action == 'detail':
quotas = qs.show_detail_quota(context, show_usage=True)
# remove the allocated field which is not visible in Nova
for k, v in quotas['quota_set'].iteritems():
if k != 'id':
v.pop('allocated', None)
elif action == 'quota-show':
quotas = qs.show_detail_quota(context, show_usage=False)
else:
return Response('Resource not found', 404)
except t_exceptions.NotFound as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 404)
except (t_exceptions.AdminRequired,
t_exceptions.NotAuthorized,
t_exceptions.HTTPForbiddenError) as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 403)
except Exception as e:
msg = str(e)
LOG.exception(msg=msg)
return Response(msg, 400)
return {'quota_set': self._build_visible_quota(quotas['quota_set'])}
def _build_visible_quota(self, quota_set):
quota_map = [
'id', 'instances', 'ram', 'cores', 'key_pairs',
'floating_ips', 'fixed_ips',
'injected_files', 'injected_file_path_bytes',
'injected_file_content_bytes',
'security_groups', 'security_group_rules',
'metadata_items', 'server_groups', 'server_group_members',
]
ret = {}
# only return Nova visible quota items
for k, v in quota_set.iteritems():
if k in quota_map:
ret[k] = v
return ret

View File

@ -1,166 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import hooks
from pecan import rest
import oslo_log.log as logging
import webob.exc as web_exc
from tricircle.common import constants
from tricircle.common import context as ctx
from tricircle.common import xrpcapi
from tricircle.nova_apigw.controllers import action
from tricircle.nova_apigw.controllers import aggregate
from tricircle.nova_apigw.controllers import flavor
from tricircle.nova_apigw.controllers import image
from tricircle.nova_apigw.controllers import network
from tricircle.nova_apigw.controllers import quota_sets
from tricircle.nova_apigw.controllers import server
from tricircle.nova_apigw.controllers import server_ips
from tricircle.nova_apigw.controllers import volume
LOG = logging.getLogger(__name__)
class ErrorHook(hooks.PecanHook):
# NOTE(zhiyuan) pecan's default error body is not compatible with nova
# client, clear body in this hook
def on_error(self, state, exc):
if isinstance(exc, web_exc.HTTPException):
exc.body = ''
return exc
class RootController(object):
@pecan.expose()
def _lookup(self, version, *remainder):
if version == 'v2.1':
return V21Controller(), remainder
class V21Controller(object):
_media_type = "application/vnd.openstack.compute+json;version=2.1"
def __init__(self):
self.resource_controller = {
'flavors': flavor.FlavorController,
'os-aggregates': aggregate.AggregateController,
'servers': server.ServerController,
'images': image.ImageController,
'os-quota-sets': quota_sets.QuotaSetsController,
'limits': quota_sets.LimitsController,
'os-networks': network.NetworkController
}
self.server_sub_controller = {
'os-volume_attachments': volume.VolumeController,
'action': action.ActionController,
'ips': server_ips.ServerIpsController
}
def _get_resource_controller(self, project_id, remainder):
if not remainder:
pecan.abort(404)
return
resource = remainder[0]
if resource not in self.resource_controller:
pecan.abort(404)
return
if resource == 'servers' and len(remainder) >= 3:
server_id = remainder[1]
sub_resource = remainder[2]
if sub_resource not in self.server_sub_controller:
pecan.abort(404)
return
return self.server_sub_controller[sub_resource](
project_id, server_id), remainder[3:]
return self.resource_controller[resource](project_id), remainder[1:]
@pecan.expose()
def _lookup(self, project_id, *remainder):
if project_id == 'testrpc':
return TestRPCController(), remainder
else:
# If the last charater of the url path is '/', then
# last remainder element is ''. Since pecan can't
# handle the url properly, '' element must be removed.
# Otherwise pecan will crashed for routing the request
# with incorrect parameter
num = len(remainder)
if num >= 1 and remainder[num - 1] == '':
new_remainder = remainder[:num - 1]
else:
new_remainder = remainder
return self._get_resource_controller(project_id, new_remainder)
@pecan.expose(generic=True, template='json')
def index(self):
return {
"version": {
"status": "CURRENT",
"updated": "2013-07-23T11:33:21Z",
"links": [
{
"href": pecan.request.application_url + "/v2.1/",
"rel": "self"
},
{
"href": "http://docs.openstack.org/",
"type": "text/html",
"rel": "describedby"
}
],
"min_version": constants.NOVA_APIGW_MIN_VERSION,
"version": constants.NOVA_APIGW_MAX_VERSION,
"media-types": [
{
"base": "application/json",
"type": self._media_type
}
],
"id": "v2.1"
}
}
@index.when(method='POST')
@index.when(method='PUT')
@index.when(method='DELETE')
@index.when(method='HEAD')
@index.when(method='PATCH')
def not_supported(self):
pecan.abort(404)
class TestRPCController(rest.RestController):
def __init__(self, *args, **kwargs):
super(TestRPCController, self).__init__(*args, **kwargs)
self.xjobapi = xrpcapi.XJobAPI()
@expose(generic=True, template='json')
def index(self):
if pecan.request.method != 'GET':
pecan.abort(405)
context = ctx.extract_context_from_environ()
payload = '#result from xjob rpc'
return self.xjobapi.test_rpc(context, payload)

View File

@ -1,82 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
from oslo_service import wsgi
from oslo_utils import encodeutils
import webob.dec
from tricircle.common import constants
class Versions(object):
@classmethod
def factory(cls, global_config, **local_config):
return cls(app=None)
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
if req.path != '/':
if self.app:
return req.get_response(self.app)
method = req.environ.get('REQUEST_METHOD')
not_allowed_methods = ['POST', 'PUT', 'DELETE', 'HEAD', 'PATCH']
if method in not_allowed_methods:
response = webob.Response()
response.status_code = 404
return response
versions = {
"versions": [
{
"status": "SUPPORTED",
"updated": "2011-01-21T11:33:21Z",
"links": [
{"href": "http://127.0.0.1:8774/v2/",
"rel": "self"}
],
"min_version": "",
"version": "",
"id": "v2.0"
},
{
"status": "CURRENT",
"updated": "2013-07-23T11:33:21Z",
"links": [
{
"href": req.application_url + "/v2.1/",
"rel": "self"
}
],
"min_version": constants.NOVA_APIGW_MIN_VERSION,
"version": constants.NOVA_APIGW_MAX_VERSION,
"id": "v2.1"
}
]
}
content_type = 'application/json'
body = jsonutils.dumps(versions)
response = webob.Response()
response.content_type = content_type
response.body = encodeutils.to_utf8(body)
return response
def __init__(self, app):
self.app = app

View File

@ -1,680 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 netaddr
import pecan
from pecan import expose
from pecan import rest
import six
import oslo_log.log as logging
import neutronclient.common.exceptions as q_exceptions
import tricircle.common.client as t_client
from tricircle.common import constants
import tricircle.common.context as t_context
import tricircle.common.exceptions as t_exceptions
from tricircle.common.i18n import _
from tricircle.common.i18n import _LE
import tricircle.common.lock_handle as t_lock
from tricircle.common.quota import QUOTAS
from tricircle.common.scheduler import filter_scheduler
from tricircle.common import utils
from tricircle.common import xrpcapi
import tricircle.db.api as db_api
from tricircle.db import core
from tricircle.db import models
from tricircle.network import helper
LOG = logging.getLogger(__name__)
MAX_METADATA_KEY_LENGTH = 255
MAX_METADATA_VALUE_LENGTH = 255
class ServerController(rest.RestController):
def __init__(self, project_id):
self.project_id = project_id
self.clients = {constants.TOP: t_client.Client()}
self.helper = helper.NetworkHelper()
self.xjob_handler = xrpcapi.XJobAPI()
self.filter_scheduler = filter_scheduler.FilterScheduler()
def _get_client(self, pod_name=constants.TOP):
if pod_name not in self.clients:
self.clients[pod_name] = t_client.Client(pod_name)
return self.clients[pod_name]
def _get_all(self, context, params):
filters = [{'key': key,
'comparator': 'eq',
'value': value} for key, value in params.iteritems()]
ret = []
pods = db_api.list_pods(context)
for pod in pods:
if not pod['az_name']:
continue
client = self._get_client(pod['pod_name'])
servers = client.list_servers(context, filters=filters)
self._remove_fip_info(servers)
ret.extend(servers)
return ret
@staticmethod
def _construct_brief_server_entry(server):
return {'id': server['id'],
'name': server.get('name'),
'links': server.get('links')}
@staticmethod
def _transform_network_name(server):
if 'addresses' not in server:
return
keys = [key for key in server['addresses'].iterkeys()]
for key in keys:
value = server['addresses'].pop(key)
network_name = key.split('#')[1]
server['addresses'][network_name] = value
return server
@expose(generic=True, template='json')
def get_one(self, _id, **kwargs):
context = t_context.extract_context_from_environ()
if _id == 'detail':
return {'servers': [self._transform_network_name(
server) for server in self._get_all(context, kwargs)]}
mappings = db_api.get_bottom_mappings_by_top_id(
context, _id, constants.RT_SERVER)
if not mappings:
return utils.format_nova_error(
404, _('Instance %s could not be found.') % _id)
pod, bottom_id = mappings[0]
client = self._get_client(pod['pod_name'])
server = client.get_servers(context, bottom_id)
if not server:
return utils.format_nova_error(
404, _('Instance %s could not be found.') % _id)
else:
self._transform_network_name(server)
return {'server': server}
@expose(generic=True, template='json')
def get_all(self, **kwargs):
context = t_context.extract_context_from_environ()
return {'servers': [self._construct_brief_server_entry(
server) for server in self._get_all(context, kwargs)]}
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if 'server' not in kw:
return utils.format_nova_error(
400, _('server is not set'))
az = kw['server'].get('availability_zone', '')
pod, b_az = self.filter_scheduler.select_destination(
context, az, self.project_id, pod_group='')
if not pod:
return utils.format_nova_error(
500, _('Pod not configured or scheduling failure'))
t_server_dict = kw['server']
self._process_metadata_quota(context, t_server_dict)
self._process_injected_file_quota(context, t_server_dict)
server_body = self._get_create_server_body(kw['server'], b_az)
top_client = self._get_client()
sg_filters = [{'key': 'tenant_id', 'comparator': 'eq',
'value': self.project_id}]
top_sgs = top_client.list_security_groups(context, sg_filters)
top_sg_map = dict((sg['name'], sg) for sg in top_sgs)
if 'security_groups' not in kw['server']:
security_groups = ['default']
else:
security_groups = []
for sg in kw['server']['security_groups']:
if 'name' not in sg:
return utils.format_nova_error(
400, _('Invalid input for field/attribute'))
if sg['name'] not in top_sg_map:
return utils.format_nova_error(
400, _('Unable to find security_group with name or id '
'%s') % sg['name'])
security_groups.append(sg['name'])
t_sg_ids, b_sg_ids, is_news = self._handle_security_group(
context, pod, top_sg_map, security_groups)
server_body['networks'] = []
if 'networks' in kw['server']:
for net_info in kw['server']['networks']:
if 'uuid' in net_info:
network = top_client.get_networks(context,
net_info['uuid'])
if not network:
return utils.format_nova_error(
400, _('Network %s could not be '
'found') % net_info['uuid'])
if not self._check_network_server_az_match(
context, network,
kw['server']['availability_zone']):
return utils.format_nova_error(
400, _('Network and server not in the same '
'availability zone'))
subnets = top_client.list_subnets(
context, [{'key': 'network_id',
'comparator': 'eq',
'value': network['id']}])
if not subnets:
return utils.format_nova_error(
400, _('Network does not contain any subnets'))
t_port_id, b_port_id = self._handle_network(
context, pod, network, subnets,
top_sg_ids=t_sg_ids, bottom_sg_ids=b_sg_ids)
elif 'port' in net_info:
port = top_client.get_ports(context, net_info['port'])
if not port:
return utils.format_nova_error(
400, _('Port %s could not be '
'found') % net_info['port'])
t_port_id, b_port_id = self._handle_port(
context, pod, port)
server_body['networks'].append({'port': b_port_id})
# only for security group first created in a pod, we invoke
# _handle_sg_rule_for_new_group to initialize rules in that group, this
# method removes all the rules in the new group then add new rules
top_sg_id_map = dict((sg['id'], sg) for sg in top_sgs)
new_top_sgs = []
new_bottom_sg_ids = []
default_sg = None
for t_id, b_id, is_new in zip(t_sg_ids, b_sg_ids, is_news):
sg_name = top_sg_id_map[t_id]['name']
if sg_name == 'default':
default_sg = top_sg_id_map[t_id]
continue
if not is_new:
continue
new_top_sgs.append(top_sg_id_map[t_id])
new_bottom_sg_ids.append(b_id)
self._handle_sg_rule_for_new_group(context, pod, new_top_sgs,
new_bottom_sg_ids)
if default_sg:
self._handle_sg_rule_for_default_group(
context, pod, default_sg, self.project_id)
client = self._get_client(pod['pod_name'])
nics = [
{'port-id': _port['port']} for _port in server_body['networks']]
server = client.create_servers(context,
name=server_body['name'],
image=server_body['imageRef'],
flavor=server_body['flavorRef'],
nics=nics,
security_groups=b_sg_ids)
with context.session.begin():
core.create_resource(context, models.ResourceRouting,
{'top_id': server['id'],
'bottom_id': server['id'],
'pod_id': pod['pod_id'],
'project_id': self.project_id,
'resource_type': constants.RT_SERVER})
pecan.response.status = 202
return {'server': server}
@expose(generic=True, template='json')
def delete(self, _id):
context = t_context.extract_context_from_environ()
mappings = db_api.get_bottom_mappings_by_top_id(context, _id,
constants.RT_SERVER)
if not mappings:
pecan.response.status = 404
return {'Error': {'message': _('Server not found'), 'code': 404}}
pod, bottom_id = mappings[0]
client = self._get_client(pod['pod_name'])
top_client = self._get_client()
try:
server_ports = top_client.list_ports(
context, filters=[{'key': 'device_id', 'comparator': 'eq',
'value': _id}])
ret = client.delete_servers(context, bottom_id)
# none return value indicates server not found
if ret is None:
self._remove_stale_mapping(context, _id)
pecan.response.status = 404
return {'Error': {'message': _('Server not found'),
'code': 404}}
for server_port in server_ports:
self.xjob_handler.delete_server_port(context,
server_port['id'])
except Exception as e:
code = 500
message = _('Delete server %(server_id)s fails') % {
'server_id': _id}
if hasattr(e, 'code'):
code = e.code
ex_message = str(e)
if ex_message:
message = ex_message
LOG.error(message)
pecan.response.status = code
return {'Error': {'message': message, 'code': code}}
# NOTE(zhiyuan) Security group rules for default security group are
# also kept until subnet is deleted.
pecan.response.status = 204
return pecan.response
def _get_or_create_route(self, context, pod, _id, _type):
def list_resources(t_ctx, q_ctx, pod_, ele, _type_):
client = self._get_client(pod_['pod_name'])
return client.list_resources(_type_, t_ctx, [{'key': 'name',
'comparator': 'eq',
'value': ele['id']}])
return t_lock.get_or_create_route(context, None,
self.project_id, pod, {'id': _id},
_type, list_resources)
def _handle_router(self, context, pod, net):
top_client = self._get_client()
interfaces = top_client.list_ports(
context, filters=[{'key': 'network_id',
'comparator': 'eq',
'value': net['id']},
{'key': 'device_owner',
'comparator': 'eq',
'value': 'network:router_interface'}])
interfaces = [inf for inf in interfaces if inf['device_id']]
if not interfaces:
return
# TODO(zhiyuan) change xjob invoking from "cast" to "call" to guarantee
# the job can be successfully registered
self.xjob_handler.setup_bottom_router(
context, net['id'], interfaces[0]['device_id'], pod['pod_id'])
def _handle_network(self, context, pod, net, subnets, port=None,
top_sg_ids=None, bottom_sg_ids=None):
(bottom_net_id,
subnet_map) = self.helper.prepare_bottom_network_subnets(
context, None, self.project_id, pod, net, subnets)
top_client = self._get_client()
top_port_body = {'port': {'network_id': net['id'],
'admin_state_up': True}}
if top_sg_ids:
top_port_body['port']['security_groups'] = top_sg_ids
# port
if not port:
port = top_client.create_ports(context, top_port_body)
port_body = self.helper.get_create_port_body(
self.project_id, port, subnet_map, bottom_net_id,
bottom_sg_ids)
else:
port_body = self.helper.get_create_port_body(
self.project_id, port, subnet_map, bottom_net_id)
_, bottom_port_id = self.helper.prepare_bottom_element(
context, self.project_id, pod, port, constants.RT_PORT, port_body)
self._handle_router(context, pod, net)
return port['id'], bottom_port_id
def _handle_port(self, context, pod, port):
top_client = self._get_client()
# NOTE(zhiyuan) at this moment, it is possible that the bottom port has
# been created. if user creates a port and associate it with a floating
# ip before booting a vm, tricircle plugin will create the bottom port
# first in order to setup floating ip in bottom pod. but it is still
# safe for us to use network id and subnet id in the returned port dict
# since tricircle plugin will do id mapping and guarantee ids in the
# dict are top id.
net = top_client.get_networks(context, port['network_id'])
subnets = []
for fixed_ip in port['fixed_ips']:
subnets.append(top_client.get_subnets(context,
fixed_ip['subnet_id']))
return self._handle_network(context, pod, net, subnets, port=port)
@staticmethod
def _safe_create_security_group_rule(context, client, body):
try:
client.create_security_group_rules(context, body)
except q_exceptions.Conflict:
return
@staticmethod
def _safe_delete_security_group_rule(context, client, _id):
try:
client.delete_security_group_rules(context, _id)
except q_exceptions.NotFound:
return
def _handle_security_group(self, context, pod, top_sg_map,
security_groups):
t_sg_ids = []
b_sg_ids = []
is_news = []
for sg_name in security_groups:
t_sg = top_sg_map[sg_name]
sg_body = {
'security_group': {
'name': t_sg['id'],
'description': t_sg['description']}}
is_new, b_sg_id = self.helper.prepare_bottom_element(
context, self.project_id, pod, t_sg, constants.RT_SG, sg_body)
t_sg_ids.append(t_sg['id'])
is_news.append(is_new)
b_sg_ids.append(b_sg_id)
return t_sg_ids, b_sg_ids, is_news
@staticmethod
def _construct_bottom_rule(rule, sg_id, ip=None):
ip = ip or rule['remote_ip_prefix']
# if ip is passed, this is a extended rule for remote group
return {'remote_group_id': None,
'direction': rule['direction'],
'remote_ip_prefix': ip,
'protocol': rule.get('protocol'),
'ethertype': rule['ethertype'],
'port_range_max': rule.get('port_range_max'),
'port_range_min': rule.get('port_range_min'),
'security_group_id': sg_id}
@staticmethod
def _compare_rule(rule1, rule2):
for key in ('direction', 'remote_ip_prefix', 'protocol', 'ethertype',
'port_range_max', 'port_range_min'):
if rule1[key] != rule2[key]:
return False
return True
def _handle_sg_rule_for_default_group(self, context, pod, default_sg,
project_id):
top_client = self._get_client()
new_b_rules = []
for t_rule in default_sg['security_group_rules']:
if not t_rule['remote_group_id']:
# leave sg_id empty here
new_b_rules.append(
self._construct_bottom_rule(t_rule, ''))
continue
if t_rule['ethertype'] != 'IPv4':
continue
subnets = top_client.list_subnets(
context, [{'key': 'tenant_id', 'comparator': 'eq',
'value': project_id}])
bridge_ip_net = netaddr.IPNetwork('100.0.0.0/8')
for subnet in subnets:
ip_net = netaddr.IPNetwork(subnet['cidr'])
if ip_net in bridge_ip_net:
continue
# leave sg_id empty here
new_b_rules.append(
self._construct_bottom_rule(t_rule, '',
subnet['cidr']))
mappings = db_api.get_bottom_mappings_by_top_id(
context, default_sg['id'], constants.RT_SG)
for pod, b_sg_id in mappings:
client = self._get_client(pod['pod_name'])
b_sg = client.get_security_groups(context, b_sg_id)
add_rules = []
del_rules = []
match_index = set()
for b_rule in b_sg['security_group_rules']:
match = False
for i, rule in enumerate(new_b_rules):
if self._compare_rule(b_rule, rule):
match = True
match_index.add(i)
break
if not match:
del_rules.append(b_rule)
for i, rule in enumerate(new_b_rules):
if i not in match_index:
add_rules.append(rule)
for del_rule in del_rules:
self._safe_delete_security_group_rule(
context, client, del_rule['id'])
if add_rules:
rule_body = {'security_group_rules': []}
for add_rule in add_rules:
add_rule['security_group_id'] = b_sg_id
rule_body['security_group_rules'].append(add_rule)
self._safe_create_security_group_rule(context,
client, rule_body)
def _handle_sg_rule_for_new_group(self, context, pod, top_sgs,
bottom_sg_ids):
client = self._get_client(pod['pod_name'])
for i, t_sg in enumerate(top_sgs):
b_sg_id = bottom_sg_ids[i]
new_b_rules = []
for t_rule in t_sg['security_group_rules']:
if t_rule['remote_group_id']:
# we do not handle remote group rule for non-default
# security group, actually tricircle plugin in neutron
# will reject such rule
# default security group is not passed with top_sgs so
# t_rule will not belong to default security group
continue
new_b_rules.append(
self._construct_bottom_rule(t_rule, b_sg_id))
try:
b_sg = client.get_security_groups(context, b_sg_id)
for b_rule in b_sg['security_group_rules']:
self._safe_delete_security_group_rule(
context, client, b_rule['id'])
if new_b_rules:
rule_body = {'security_group_rules': new_b_rules}
self._safe_create_security_group_rule(context, client,
rule_body)
except Exception:
# if we fails when operating bottom security group rule, we
# update the security group mapping to set bottom_id to None
# and expire the mapping, so next time the security group rule
# operations can be redone
with context.session.begin():
routes = core.query_resource(
context, models.ResourceRouting,
[{'key': 'top_id', 'comparator': 'eq',
'value': t_sg['id']},
{'key': 'bottom_id', 'comparator': 'eq',
'value': b_sg_id}], [])
update_dict = {'bottom_id': None,
'created_at': constants.expire_time,
'updated_at': constants.expire_time}
core.update_resource(context, models.ResourceRouting,
routes[0]['id'], update_dict)
raise
@staticmethod
def _get_create_server_body(origin, bottom_az):
body = {}
copy_fields = ['name', 'imageRef', 'flavorRef',
'max_count', 'min_count']
if bottom_az:
body['availability_zone'] = bottom_az
for field in copy_fields:
if field in origin:
body[field] = origin[field]
return body
@staticmethod
def _remove_fip_info(servers):
for server in servers:
if 'addresses' not in server:
continue
for addresses in server['addresses'].values():
remove_index = -1
for i, address in enumerate(addresses):
if address.get('OS-EXT-IPS:type') == 'floating':
remove_index = i
break
if remove_index >= 0:
del addresses[remove_index]
@staticmethod
def _remove_stale_mapping(context, server_id):
filters = [{'key': 'top_id', 'comparator': 'eq', 'value': server_id},
{'key': 'resource_type',
'comparator': 'eq',
'value': constants.RT_SERVER}]
with context.session.begin():
core.delete_resources(context,
models.ResourceRouting,
filters)
@staticmethod
def _check_network_server_az_match(context, network, server_az):
az_hints = 'availability_zone_hints'
network_type = 'provider:network_type'
# for local type network, we make sure it's created in only one az
# NOTE(zhiyuan) race condition exists when creating vms in the same
# local type network but different azs at the same time
if network.get(network_type) == constants.NT_LOCAL:
mappings = db_api.get_bottom_mappings_by_top_id(
context, network['id'], constants.RT_NETWORK)
if mappings:
pod, _ = mappings[0]
if pod['az_name'] != server_az:
return False
# if neutron az not assigned, server az is used
if not network.get(az_hints):
return True
if server_az in network[az_hints]:
return True
else:
return False
def _process_injected_file_quota(self, context, t_server_dict):
try:
ctx = context.elevated()
injected_files = t_server_dict.get('injected_files', None)
self._check_injected_file_quota(ctx, injected_files)
except (t_exceptions.OnsetFileLimitExceeded,
t_exceptions.OnsetFilePathLimitExceeded,
t_exceptions.OnsetFileContentLimitExceeded) as e:
msg = str(e)
LOG.exception(_LE('Quota exceeded %(msg)s'),
{'msg': msg})
return utils.format_nova_error(400, _('Quota exceeded %s') % msg)
def _check_injected_file_quota(self, context, injected_files):
"""Enforce quota limits on injected files.
Raises a QuotaError if any limit is exceeded.
"""
if injected_files is None:
return
# Check number of files first
try:
QUOTAS.limit_check(context,
injected_files=len(injected_files))
except t_exceptions.OverQuota:
raise t_exceptions.OnsetFileLimitExceeded()
# OK, now count path and content lengths; we're looking for
# the max...
max_path = 0
max_content = 0
for path, content in injected_files:
max_path = max(max_path, len(path))
max_content = max(max_content, len(content))
try:
QUOTAS.limit_check(context,
injected_file_path_bytes=max_path,
injected_file_content_bytes=max_content)
except t_exceptions.OverQuota as exc:
# Favor path limit over content limit for reporting
# purposes
if 'injected_file_path_bytes' in exc.kwargs['overs']:
raise t_exceptions.OnsetFilePathLimitExceeded()
else:
raise t_exceptions.OnsetFileContentLimitExceeded()
def _process_metadata_quota(self, context, t_server_dict):
try:
ctx = context.elevated()
metadata = t_server_dict.get('metadata', None)
self._check_metadata_properties_quota(ctx, metadata)
except t_exceptions.InvalidMetadata as e1:
LOG.exception(_LE('Invalid metadata %(exception)s'),
{'exception': str(e1)})
return utils.format_nova_error(400, _('Invalid metadata'))
except t_exceptions.InvalidMetadataSize as e2:
LOG.exception(_LE('Invalid metadata size %(exception)s'),
{'exception': str(e2)})
return utils.format_nova_error(400, _('Invalid metadata size'))
except t_exceptions.MetadataLimitExceeded as e3:
LOG.exception(_LE('Quota exceeded %(exception)s'),
{'exception': str(e3)})
return utils.format_nova_error(400,
_('Quota exceeded in metadata'))
def _check_metadata_properties_quota(self, context, metadata=None):
"""Enforce quota limits on metadata properties."""
if not metadata:
metadata = {}
if not isinstance(metadata, dict):
msg = (_("Metadata type should be dict."))
raise t_exceptions.InvalidMetadata(reason=msg)
num_metadata = len(metadata)
try:
QUOTAS.limit_check(context, metadata_items=num_metadata)
except t_exceptions.OverQuota as exc:
quota_metadata = exc.kwargs['quotas']['metadata_items']
raise t_exceptions.MetadataLimitExceeded(allowed=quota_metadata)
# Because metadata is processed in the bottom pod, we just do
# parameter validation here to ensure quota management
for k, v in six.iteritems(metadata):
try:
utils.check_string_length(v)
utils.check_string_length(k, min_len=1)
except t_exceptions.InvalidInput as e:
raise t_exceptions.InvalidMetadata(reason=str(e))
if len(k) > MAX_METADATA_KEY_LENGTH:
msg = _("Metadata property key greater than 255 characters")
raise t_exceptions.InvalidMetadataSize(reason=msg)
if len(v) > MAX_METADATA_VALUE_LENGTH:
msg = _("Metadata property value greater than 255 characters")
raise t_exceptions.InvalidMetadataSize(reason=msg)

View File

@ -1,72 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import rest
from oslo_log import log as logging
import tricircle.common.client as t_client
from tricircle.common import constants
import tricircle.common.context as t_context
from tricircle.common.i18n import _
from tricircle.common import utils
import tricircle.db.api as db_api
LOG = logging.getLogger(__name__)
class ServerIpsController(rest.RestController):
def __init__(self, project_id, server_id):
self.project_id = project_id
self.server_id = server_id
self.clients = {constants.TOP: t_client.Client()}
def _get_client(self, pod_name=constants.TOP):
if pod_name not in self.clients:
self.clients[pod_name] = t_client.Client(pod_name)
return self.clients[pod_name]
@expose(generic=True, template='json')
def get_all(self, **kwargs):
context = t_context.extract_context_from_environ()
server_mappings = db_api.get_server_mappings_by_top_id(
context, self.server_id)
if not server_mappings:
return utils.format_nova_error(
404, _('Server %s could not be found') % self.server_id)
try:
server_pod_name = server_mappings[0][0]['pod_name']
api = self._get_client(server_pod_name).get_native_client(
constants.RT_SERVER, context)
resp, body = api.client.get('/servers/%s/ips' % self.server_id)
pecan.response.status = resp.status_code
if not body:
return pecan.response
else:
return body
except Exception as e:
code = 500
message = _('Fail to lists assigned IP addresses'
'%(server_id)s: %(exception)s') % {
'server_id': self.server_id,
'exception': e}
if hasattr(e, 'code'):
code = e.code
LOG.error(message)
return utils.format_nova_error(code, message)

View File

@ -1,259 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan import expose
from pecan import rest
import re
from oslo_log import log as logging
import tricircle.common.client as t_client
import tricircle.common.context as t_context
from tricircle.common import exceptions
from tricircle.common.i18n import _
from tricircle.common.i18n import _LE
from tricircle.common import utils
import tricircle.db.api as db_api
LOG = logging.getLogger(__name__)
class VolumeController(rest.RestController):
def __init__(self, project_id, server_id):
self.project_id = project_id
self.server_id = server_id
self.clients = {'top': t_client.Client()}
def _get_client(self, pod_name='top'):
if pod_name not in self.clients:
self.clients[pod_name] = t_client.Client(pod_name)
return self.clients[pod_name]
@expose(generic=True, template='json')
def post(self, **kw):
context = t_context.extract_context_from_environ()
if 'volumeAttachment' not in kw:
return utils.format_nova_error(
400, _('volumeAttachment is not set'))
body = kw['volumeAttachment']
if 'volumeId' not in body:
return utils.format_nova_error(
400, _('Invalid input for field/attribute volumeAttachment'))
try:
server_mappings = db_api.get_server_mappings_by_top_id(
context, self.server_id)
volume_mappings = db_api.get_volume_mappings_by_top_id(
context, body['volumeId'])
except exceptions.ServerMappingsNotFound as e:
return utils.format_nova_error(404, e.message)
except exceptions.VolumeMappingsNotFound as e:
return utils.format_nova_error(404, e.message)
except Exception as e:
LOG.exception(_LE('Fail to create volume attachment for given'
'server %(server_id)s:'
'%(exception)s'),
{'server_id': self.server_id,
'exception': e})
return utils.format_nova_error(
500, _('Fail to create volume attachment'))
server_pod_name = server_mappings[0][0]['pod_name']
volume_pod_name = volume_mappings[0][0]['pod_name']
if server_pod_name != volume_pod_name:
LOG.error(_LE('Server %(server)s is in pod %(server_pod)s and '
'volume %(volume)s is in pod %(volume_pod)s, which '
'are not the same.'),
{'server': self.server_id,
'server_pod': server_pod_name,
'volume': body['volumeId'],
'volume_pod': volume_pod_name})
return utils.format_nova_error(
400, _('Server and volume not in the same pod'))
device = None
if 'device' in body:
device = body['device']
# this regular expression is copied from nova/block_device.py
match = re.match('(^/dev/x{0,1}[a-z]{0,1}d{0,1})([a-z]+)[0-9]*$',
device)
if not match:
return utils.format_nova_error(
400, _('The supplied device path (%s) is '
'invalid.') % device)
try:
api = self._get_client(server_pod_name).get_native_client(
'server_volume', context)
resp, body = api.client.post(
"/servers/%s/os-volume_attachments" % self.server_id, body=kw)
pecan.response.status = resp.status_code
if not body:
return pecan.response
else:
return body
except Exception as e:
code = 500
message = _('Fail to create volume attachment for given server '
'%(server_id)s: %(exception)s') % {
'server_id': self.server_id,
'exception': e}
if hasattr(e, 'code'):
code = e.code
LOG.error(message)
return utils.format_nova_error(code, message)
@expose(generic=True, template='json')
def get_one(self, _id):
"""Get the volume attachment identified by the attachment ID.
:param _id: the ID of volume attachment
:returns: the volume attachment
"""
context = t_context.extract_context_from_environ()
try:
server_mappings = db_api.get_server_mappings_by_top_id(
context, self.server_id)
except exceptions.ServerMappingsNotFound as e:
return utils.format_nova_error(404, e.message)
except Exception as e:
LOG.exception(_LE('Fail to get volume attachment'
'%(attachment_id)s from server %(server_id)s:'
'%(exception)s'),
{'attachment_id': _id,
'server_id': self.server_id,
'exception': e})
return utils.format_nova_error(
500, _('Fail to get volume attachment'))
server_pod_name = server_mappings[0][0]['pod_name']
try:
api = self._get_client(server_pod_name).get_native_client(
'server_volume', context)
resp, body = api.client.get(
"/servers/%s/os-volume_attachments/%s" %
(self.server_id, _id,))
pecan.response.status = resp.status_code
if not body:
return pecan.response
else:
return body
except Exception as e:
code = 500
message = _('Fail to get volume attachment %(attachment_id)s'
'from server %(server_id)s: %(exception)s') % {
'attachment_id': _id,
'server_id': self.server_id,
'exception': e}
if hasattr(e, 'code'):
code = e.code
LOG.error(message)
return utils.format_nova_error(code, message)
@expose(generic=True, template='json')
def get_all(self):
"""GET a list of all volume attachments for a server.
:returns: a list of volume attachments
"""
context = t_context.extract_context_from_environ()
try:
server_mappings = db_api.get_server_mappings_by_top_id(
context, self.server_id)
except exceptions.ServerMappingsNotFound as e:
return utils.format_nova_error(404, e.message)
except Exception as e:
LOG.exception(_LE('Fail to get volume attachments of server'
'%(server_id)s: %(exception)s'),
{'server_id': self.server_id,
'exception': e})
return utils.format_nova_error(
500, _('Fail to get volume attachments'))
server_pod_name = server_mappings[0][0]['pod_name']
try:
api = self._get_client(server_pod_name).get_native_client(
'server_volume', context)
resp, body = api.client.get(
"/servers/%s/os-volume_attachments" % self.server_id)
pecan.response.status = resp.status_code
if not body:
return pecan.response
else:
return body
except Exception as e:
code = 500
message = _('Fail to get volume attachments of server'
'%(server_id)s: %(exception)s') % {
'server_id': self.server_id,
'exception': e}
if hasattr(e, 'code'):
code = e.code
LOG.error(message)
return utils.format_nova_error(code, message)
@expose(generic=True, template='json')
def delete(self, _id):
"""Detach a volume identified by the attachment ID from the given server ID.
:param _id: the ID of volume attachment
"""
context = t_context.extract_context_from_environ()
try:
server_mappings = db_api.get_server_mappings_by_top_id(
context, self.server_id)
except exceptions.ServerMappingsNotFound as e:
return utils.format_nova_error(404, e.message)
except Exception as e:
LOG.exception(_LE('Fail to delete volume attachment'
'%(attachment_id)s from server %(server_id)s:'
'%(exception)s'),
{'attachment_id': _id,
'server_id': self.server_id,
'exception': e})
return utils.format_nova_error(
500, _('Fail to delete volume attachment'))
server_pod_name = server_mappings[0][0]['pod_name']
try:
api = self._get_client(server_pod_name).get_native_client(
'server_volume', context)
resp, body = api.client.delete(
"/servers/%s/os-volume_attachments/%s" %
(self.server_id, _id,))
pecan.response.status = resp.status_code
if not body:
return pecan.response
else:
return body
except Exception as e:
code = 500
message = _('Fail to delete volume attachments %(attachment_id)s'
'from server %(server_id)s: %(exception)s') % {
'attachment_id': _id,
'server_id': self.server_id,
'exception': e}
if hasattr(e, 'code'):
code = e.code
LOG.error(message)
return utils.format_nova_error(code, message)

View File

@ -1,22 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# 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 tricircle.nova_apigw.app
def list_opts():
return [
('DEFAULT', tricircle.nova_apigw.app.common_opts),
]

View File

@ -79,13 +79,6 @@ iniset $TEMPEST_CONF volume-feature-enabled api_v1 false
iniset $TEMPEST_CONF validation connect_method fixed
# Run the Compute Tempest tests
# cd $TRICIRCLE_TEMPEST_PLUGIN_DIR
# sudo BASE=$BASE ./tempest_compute.sh
# Run the Volume Tempest tests
# cd $TRICIRCLE_TEMPEST_PLUGIN_DIR
# sudo BASE=$BASE ./tempest_volume.sh
# Run the Network Tempest tests
cd $TRICIRCLE_TEMPEST_PLUGIN_DIR

View File

@ -16,6 +16,4 @@
export localconf=$BASE/new/devstack/local.conf
export TRICIRCLE_API_CONF=/etc/tricircle/api.conf
export TRICIRCLE_CINDER_APIGW_CONF=/etc/tricircle/cinder_apigw.conf
export TRICIRCLE_NOVA_APIGW_CONF=/etc/tricircle/nova_apigw.conf
export TRICIRCLE_XJOB_CONF=/etc/tricircle/xjob.conf

View File

@ -1,601 +0,0 @@
#!/bin/bash -e
#
# 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.
export DEST=$BASE/new
export TEMPEST_DIR=$DEST/tempest
export TEMPEST_CONF=$TEMPEST_DIR/etc/tempest.conf
# preparation for the tests
cd $TEMPEST_DIR
# Run functional test
echo "Running Tricircle functional test suite..."
# all test cases with following prefix
TESTCASES="(tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_stop_start_server"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_lock_unlock_server"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_pause_unpause_server"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_shelve_unshelve_server"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_suspend_resume_server"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_force_delete_nonexistent_server_id"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_resume_non_existent_server"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_resume_server_invalid_state"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_suspend_non_existent_server"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_suspend_server_invalid_state"
TESTCASES="$TESTCASES|tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_migrate_non_existent_server"
TESTCASES="$TESTCASES|tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_migrate_server_invalid_state"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_reboot_server_hard"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_reboot_server_soft"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_resize_server_revert"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_resize_server_confirm"
TESTCASES="$TESTCASES|tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_resize_server_confirm_from_stopped"
TESTCASES="$TESTCASES|tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_reset_state_server"
TESTCASES="$TESTCASES|tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_reset_state_server_invalid_state"
TESTCASES="$TESTCASES|tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_reset_state_server_invalid_type"
TESTCASES="$TESTCASES|tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_reset_state_server_nonexistent_server"
TESTCASES="$TESTCASES|tempest.api.compute.volumes.test_attach_volume.AttachVolumeShelveTestJSON.test_detach_volume_shelved_or_offload_server"
TESTCASES="$TESTCASES|tempest.api.compute.volumes.test_attach_volume.AttachVolumeTestJSON"
TESTCASES="$TESTCASES|tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_get_volume_without_passing_volume_id"
# add new test cases like following line for volume_type test
# TESTCASES="$TESTCASES|tempest.api.volume.admin.test_volumes_type"
TESTCASES="$TESTCASES)"
ostestr --regex $TESTCASES
# --------------------- IMPORTANT begin -------------------- #
# all following test cases are from Cinder tempest test cases,
# the purpose to list them here is to check which test cases
# are still not covered and tested in Cinder-APIGW.
#
# Those test cases which have been covered by ostestr running
# above should be marked with **DONE** after the "#".
# please leave the length of each line > 80 characters in order
# to keep one test case one line.
#
# When you add new feature to Cinder-APIGW, please select
# proper test cases to test against the feature, and marked
# these test cases with **DONE** after the "#". For those test
# cases which are not needed to be tested in Cinder-APIGW, for
# example V1(which has been deprecated) should be marked with
# **SKIP** after "#"
#
# The test cases running through ostestr could be filtered
# by regex expression, for example, for Cinder volume type
# releated test cases could be executed by a single clause:
# ostestr --regex tempest.api.volume.admin.test_volume_types
# --------------------- IMPORTANT end -----------------------#
# tempest.api.compute.admin.test_agents.AgentsAdminTestJSON.test_create_agent[id-1fc6bdc8-0b6d-4cc7-9f30-9b04fabe5b90]
# tempest.api.compute.admin.test_agents.AgentsAdminTestJSON.test_delete_agent[id-470e0b89-386f-407b-91fd-819737d0b335]
# tempest.api.compute.admin.test_agents.AgentsAdminTestJSON.test_list_agents[id-6a326c69-654b-438a-80a3-34bcc454e138]
# tempest.api.compute.admin.test_agents.AgentsAdminTestJSON.test_list_agents_with_filter[id-eabadde4-3cd7-4ec4-a4b5-5a936d2d4408]
# tempest.api.compute.admin.test_agents.AgentsAdminTestJSON.test_update_agent[id-dc9ffd51-1c50-4f0e-a820-ae6d2a568a9e]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_add_host_create_server_with_az[id-96be03c7-570d-409c-90f8-e4db3c646996]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_add_host_get_details[id-eeef473c-7c52-494d-9f09-2ed7fc8fc036]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_add_host_list[id-7f6a1cc5-2446-4cdb-9baa-b6ae0a919b72]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_add_remove_host[id-c8e85064-e79b-4906-9931-c11c24294d02]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_create_delete[id-0d148aa3-d54c-4317-aa8d-42040a475e20]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_create_delete_with_az[id-5873a6f8-671a-43ff-8838-7ce430bb6d0b]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_create_update_metadata_get_details[id-36ec92ca-7a73-43bc-b920-7531809e8540]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_create_update_with_az[id-4d2b2004-40fa-40a1-aab2-66f4dab81beb]
# tempest.api.compute.admin.test_aggregates.AggregatesAdminTestJSON.test_aggregate_create_verify_entry_in_list[id-68089c38-04b1-4758-bdf0-cf0daec4defd]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_add_existent_host[id-19dd44e1-c435-4ee1-a402-88c4f90b5950,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_add_host_as_user[id-7324c334-bd13-4c93-8521-5877322c3d51,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_add_non_exist_host[id-0ef07828-12b4-45ba-87cc-41425faf5711,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_create_aggregate_name_length_exceeds_255[id-4c194563-543b-4e70-a719-557bbe947fac,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_create_aggregate_name_length_less_than_1[id-3b8a1929-3793-4e92-bcb4-dfa572ee6c1d,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_create_as_user[id-86a1cb14-da37-4a70-b056-903fd56dfe29,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_create_with_existent_aggregate_name[id-9c23a291-b0b1-487b-b464-132e061151b3,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_delete_as_user[id-cd6de795-c15d-45f1-8d9e-813c6bb72a3d,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_delete_with_invalid_id[id-c74f4bf1-4708-4ff2-95a0-f49eaca951bd,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_get_details_as_user[id-557cad12-34c9-4ff4-95f0-22f0dfbaf7dc,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_get_details_with_invalid_id[id-3c916244-2c46-49a4-9b55-b20bb0ae512c,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_list_as_user[id-b7d475a6-5dcd-4ff4-b70a-cd9de66a6672,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_remove_host_as_user[id-7a53af20-137a-4e44-a4ae-e19260e626d9,negative]
# tempest.api.compute.admin.test_aggregates_negative.AggregatesAdminNegativeTestJSON.test_aggregate_remove_nonexistent_host[id-95d6a6fa-8da9-4426-84d0-eec0329f2e4d,negative]
# tempest.api.compute.admin.test_availability_zone.AZAdminV2TestJSON.test_get_availability_zone_list[id-d3431479-8a09-4f76-aa2d-26dc580cb27c]
# tempest.api.compute.admin.test_availability_zone.AZAdminV2TestJSON.test_get_availability_zone_list_detail[id-ef726c58-530f-44c2-968c-c7bed22d5b8c]
# tempest.api.compute.admin.test_availability_zone_negative.AZAdminNegativeTestJSON.test_get_availability_zone_list_detail_with_non_admin_user[id-bf34dca2-fdc3-4073-9c02-7648d9eae0d7,negative]
# tempest.api.compute.admin.test_baremetal_nodes.BaremetalNodesAdminTestJSON.test_list_get_baremetal_nodes[baremetal,id-e475aa6e-416d-4fa4-b3af-28d5e84250fb]
# tempest.api.compute.admin.test_fixed_ips.FixedIPsTestJson.test_list_fixed_ip_details[id-16b7d848-2f7c-4709-85a3-2dfb4576cc52,network]
# tempest.api.compute.admin.test_fixed_ips.FixedIPsTestJson.test_set_reserve[id-5485077b-7e46-4cec-b402-91dc3173433b,network]
# tempest.api.compute.admin.test_fixed_ips.FixedIPsTestJson.test_set_unreserve[id-7476e322-b9ff-4710-bf82-49d51bac6e2e,network]
# tempest.api.compute.admin.test_fixed_ips_negative.FixedIPsNegativeTestJson.test_fixed_ip_with_invalid_action[id-fd26ef50-f135-4232-9d32-281aab3f9176,negative,network]
# tempest.api.compute.admin.test_fixed_ips_negative.FixedIPsNegativeTestJson.test_list_fixed_ip_details_with_non_admin_user[id-9f17f47d-daad-4adc-986e-12370c93e407,negative,network]
# tempest.api.compute.admin.test_fixed_ips_negative.FixedIPsNegativeTestJson.test_set_reserve_with_invalid_ip[id-f51cf464-7fc5-4352-bc3e-e75cfa2cb717,negative,network]
# tempest.api.compute.admin.test_fixed_ips_negative.FixedIPsNegativeTestJson.test_set_reserve_with_non_admin_user[id-ce60042c-fa60-4836-8d43-1c8e3359dc47,negative,network]
# tempest.api.compute.admin.test_fixed_ips_negative.FixedIPsNegativeTestJson.test_set_unreserve_with_non_admin_user[id-f1f7a35b-0390-48c5-9803-5f27461439db,negative,network]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_create_flavor_using_string_ram[id-3b541a2e-2ac2-4b42-8b8d-ba6e22fcd4da]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_create_flavor_verify_entry_in_list_details[id-8261d7b0-be58-43ec-a2e5-300573c3f6c5]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_create_flavor_with_int_id[id-8b4330e1-12c4-4554-9390-e6639971f086]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_create_flavor_with_none_id[id-f83fe669-6758-448a-a85e-32d351f36fe0]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_create_flavor_with_uuid_id[id-94c9bb4e-2c2a-4f3c-bb1f-5f0daf918e6d]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_create_list_flavor_without_extra_data[id-63dc64e6-2e79-4fdf-868f-85500d308d66]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_create_server_with_non_public_flavor[id-bcc418ef-799b-47cc-baa1-ce01368b8987]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_is_public_string_variations[id-fb9cbde6-3a0e-41f2-a983-bdb0a823c44e]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_list_non_public_flavor[id-be6cc18c-7c5d-48c0-ac16-17eaf03c54eb]
# tempest.api.compute.admin.test_flavors.FlavorsAdminTestJSON.test_list_public_flavor_with_other_user[id-b345b196-bfbd-4231-8ac1-6d7fe15ff3a3]
# tempest.api.compute.admin.test_flavors_access.FlavorsAccessTestJSON.test_flavor_access_add_remove[id-59e622f6-bdf6-45e3-8ba8-fedad905a6b4]
# tempest.api.compute.admin.test_flavors_access.FlavorsAccessTestJSON.test_flavor_access_list_with_private_flavor[id-ea2c2211-29fa-4db9-97c3-906d36fad3e0]
# tempest.api.compute.admin.test_flavors_access_negative.FlavorsAccessNegativeTestJSON.test_add_flavor_access_duplicate[id-f3592cc0-0306-483c-b210-9a7b5346eddc,negative]
# tempest.api.compute.admin.test_flavors_access_negative.FlavorsAccessNegativeTestJSON.test_flavor_access_list_with_public_flavor[id-0621c53e-d45d-40e7-951d-43e5e257b272,negative]
# tempest.api.compute.admin.test_flavors_access_negative.FlavorsAccessNegativeTestJSON.test_flavor_non_admin_add[id-41eaaade-6d37-4f28-9c74-f21b46ca67bd,negative]
# tempest.api.compute.admin.test_flavors_access_negative.FlavorsAccessNegativeTestJSON.test_flavor_non_admin_remove[id-073e79a6-c311-4525-82dc-6083d919cb3a,negative]
# tempest.api.compute.admin.test_flavors_access_negative.FlavorsAccessNegativeTestJSON.test_remove_flavor_access_not_found[id-1f710927-3bc7-4381-9f82-0ca6e42644b7,negative]
# tempest.api.compute.admin.test_flavors_extra_specs.FlavorsExtraSpecsTestJSON.test_flavor_non_admin_get_all_keys[id-a99dad88-ae1c-4fba-aeb4-32f898218bd0]
# tempest.api.compute.admin.test_flavors_extra_specs.FlavorsExtraSpecsTestJSON.test_flavor_non_admin_get_specific_key[id-12805a7f-39a3-4042-b989-701d5cad9c90]
# tempest.api.compute.admin.test_flavors_extra_specs.FlavorsExtraSpecsTestJSON.test_flavor_set_get_update_show_unset_keys[id-0b2f9d4b-1ca2-4b99-bb40-165d4bb94208]
# tempest.api.compute.admin.test_flavors_extra_specs_negative.FlavorsExtraSpecsNegativeTestJSON.test_flavor_get_nonexistent_key[id-329a7be3-54b2-48be-8052-bf2ce4afd898,negative]
# tempest.api.compute.admin.test_flavors_extra_specs_negative.FlavorsExtraSpecsNegativeTestJSON.test_flavor_non_admin_set_keys[id-a00a3b81-5641-45a8-ab2b-4a8ec41e1d7d,negative]
# tempest.api.compute.admin.test_flavors_extra_specs_negative.FlavorsExtraSpecsNegativeTestJSON.test_flavor_non_admin_unset_keys[id-28f12249-27c7-44c1-8810-1f382f316b11,negative]
# tempest.api.compute.admin.test_flavors_extra_specs_negative.FlavorsExtraSpecsNegativeTestJSON.test_flavor_non_admin_update_specific_key[id-1ebf4ef8-759e-48fe-a801-d451d80476fb,negative]
# tempest.api.compute.admin.test_flavors_extra_specs_negative.FlavorsExtraSpecsNegativeTestJSON.test_flavor_unset_nonexistent_key[id-440b9f3f-3c7f-4293-a106-0ceda350f8de,negative]
# tempest.api.compute.admin.test_flavors_extra_specs_negative.FlavorsExtraSpecsNegativeTestJSON.test_flavor_update_mismatch_key[id-25b822b8-9f49-44f6-80de-d99f0482e5cb,negative]
# tempest.api.compute.admin.test_flavors_extra_specs_negative.FlavorsExtraSpecsNegativeTestJSON.test_flavor_update_more_key[id-f5889590-bf66-41cc-b4b1-6e6370cfd93f,negative]
# tempest.api.compute.admin.test_floating_ips_bulk.FloatingIPsBulkAdminTestJSON.test_create_list_delete_floating_ips_bulk[id-2c8f145f-8012-4cb8-ac7e-95a587f0e4ab,network]
# tempest.api.compute.admin.test_hosts.HostsAdminTestJSON.test_list_hosts[id-9bfaf98d-e2cb-44b0-a07e-2558b2821e4f]
# tempest.api.compute.admin.test_hosts.HostsAdminTestJSON.test_list_hosts_with_a_blank_zone[id-9af3c171-fbf4-4150-a624-22109733c2a6]
# tempest.api.compute.admin.test_hosts.HostsAdminTestJSON.test_list_hosts_with_nonexistent_zone[id-c6ddbadb-c94e-4500-b12f-8ffc43843ff8]
# tempest.api.compute.admin.test_hosts.HostsAdminTestJSON.test_list_hosts_with_zone[id-5dc06f5b-d887-47a2-bb2a-67762ef3c6de]
# tempest.api.compute.admin.test_hosts.HostsAdminTestJSON.test_show_host_detail[id-38adbb12-aee2-4498-8aec-329c72423aa4]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_list_hosts_with_non_admin_user[id-dd032027-0210-4d9c-860e-69b1b8deed5f,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_reboot_host_with_non_admin_user[id-02d79bb9-eb57-4612-abf6-2cb38897d2f8,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_reboot_nonexistent_host[id-f86bfd7b-0b13-4849-ae29-0322e83ee58b,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_show_host_detail_with_non_admin_user[id-19ebe09c-bfd4-4b7c-81a2-e2e0710f59cc,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_show_host_detail_with_nonexistent_hostname[id-e75b0a1a-041f-47a1-8b4a-b72a6ff36d3f,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_shutdown_host_with_non_admin_user[id-a803529c-7e3f-4d3c-a7d6-8e1c203d27f6,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_shutdown_nonexistent_host[id-9e637444-29cf-4244-88c8-831ae82c31b6,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_startup_host_with_non_admin_user[id-9f4ebb7e-b2ae-4e5b-a38f-0fd1bb0ddfca,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_startup_nonexistent_host[id-0d981ac3-4320-4898-b674-82b61fbb60e4,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_update_host_with_invalid_maintenance_mode[id-ab1e230e-5e22-41a9-8699-82b9947915d4,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_update_host_with_invalid_status[id-fbe2bf3e-3246-4a95-a59f-94e4e298ec77,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_update_host_with_non_admin_user[id-e40c72b1-0239-4ed6-ba21-81a184df1f7c,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_update_host_without_param[id-0cd85f75-6992-4a4a-b1bd-d11e37fd0eee,negative]
# tempest.api.compute.admin.test_hosts_negative.HostsAdminNegativeTestJSON.test_update_nonexistent_host[id-23c92146-2100-4d68-b2d6-c7ade970c9c1,negative]
# tempest.api.compute.admin.test_hypervisor.HypervisorAdminTestJSON.test_get_hypervisor_list[id-7f0ceacd-c64d-4e96-b8ee-d02943142cc5]
# tempest.api.compute.admin.test_hypervisor.HypervisorAdminTestJSON.test_get_hypervisor_list_details[id-1e7fdac2-b672-4ad1-97a4-bad0e3030118]
# tempest.api.compute.admin.test_hypervisor.HypervisorAdminTestJSON.test_get_hypervisor_show_details[id-94ff9eae-a183-428e-9cdb-79fde71211cc]
# tempest.api.compute.admin.test_hypervisor.HypervisorAdminTestJSON.test_get_hypervisor_show_servers[id-e81bba3f-6215-4e39-a286-d52d2f906862]
# tempest.api.compute.admin.test_hypervisor.HypervisorAdminTestJSON.test_get_hypervisor_stats[id-797e4f28-b6e0-454d-a548-80cc77c00816]
# tempest.api.compute.admin.test_hypervisor.HypervisorAdminTestJSON.test_get_hypervisor_uptime[id-91a50d7d-1c2b-4f24-b55a-a1fe20efca70]
# tempest.api.compute.admin.test_hypervisor.HypervisorAdminTestJSON.test_search_hypervisor[id-d7e1805b-3b14-4a3b-b6fd-50ec6d9f361f]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_get_hypervisor_list_details_with_non_admin_user[id-dc02db05-e801-4c5f-bc8e-d915290ab345,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_get_hypervisor_list_with_non_admin_user[id-51b3d536-9b14-409c-9bce-c6f7c794994e,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_get_hypervisor_stats_with_non_admin_user[id-e2b061bb-13f9-40d8-9d6e-d5bf17595849,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_get_hypervisor_uptime_with_non_admin_user[id-6c3461f9-c04c-4e2a-bebb-71dc9cb47df2,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_get_nonexistent_hypervisor_uptime[id-f60aa680-9a3a-4c7d-90e1-fae3a4891303,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_search_hypervisor_with_non_admin_user[id-5b6a6c79-5dc1-4fa5-9c58-9c8085948e74,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_search_nonexistent_hypervisor[id-19a45cc1-1000-4055-b6d2-28e8b2ec4faa,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_show_hypervisor_with_non_admin_user[id-51e663d0-6b89-4817-a465-20aca0667d03,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_show_nonexistent_hypervisor[id-c136086a-0f67-4b2b-bc61-8482bd68989f,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_show_servers_with_non_admin_user[id-2a0a3938-832e-4859-95bf-1c57c236b924,negative]
# tempest.api.compute.admin.test_hypervisor_negative.HypervisorAdminNegativeTestJSON.test_show_servers_with_nonexistent_hypervisor[id-02463d69-0ace-4d33-a4a8-93d7883a2bba,negative]
# tempest.api.compute.admin.test_instance_usage_audit_log.InstanceUsageAuditLogTestJSON.test_get_instance_usage_audit_log[id-6e40459d-7c5f-400b-9e83-449fbc8e7feb]
# tempest.api.compute.admin.test_instance_usage_audit_log.InstanceUsageAuditLogTestJSON.test_list_instance_usage_audit_logs[id-25319919-33d9-424f-9f99-2c203ee48b9d]
# tempest.api.compute.admin.test_instance_usage_audit_log_negative.InstanceUsageAuditLogNegativeTestJSON.test_get_instance_usage_audit_logs_with_invalid_time[id-9b952047-3641-41c7-ba91-a809fc5974c8,negative]
# tempest.api.compute.admin.test_instance_usage_audit_log_negative.InstanceUsageAuditLogNegativeTestJSON.test_instance_usage_audit_logs_with_nonadmin_user[id-a9d33178-d2c9-4131-ad3b-f4ca8d0308a2,negative]
# tempest.api.compute.admin.test_keypairs_v210.KeyPairsV210TestJSON.test_admin_manage_keypairs_for_other_users[id-3c8484af-cfb3-48f6-b8ba-d5d58bbf3eac]
# tempest.api.compute.admin.test_live_migration.LiveBlockMigrationTestJSON.test_iscsi_volume[id-e19c0cc6-6720-4ed8-be83-b6603ed5c812]
# tempest.api.compute.admin.test_live_migration.LiveBlockMigrationTestJSON.test_live_block_migration[id-1dce86b8-eb04-4c03-a9d8-9c1dc3ee0c7b]
# tempest.api.compute.admin.test_live_migration.LiveBlockMigrationTestJSON.test_live_block_migration_paused[id-1e107f21-61b2-4988-8f22-b196e938ab88]
# tempest.api.compute.admin.test_live_migration.LiveBlockMigrationTestJSON.test_volume_backed_live_migration[id-5071cf17-3004-4257-ae61-73a84e28badd,volume]
# tempest.api.compute.admin.test_migrations.MigrationsAdminTest.test_list_migrations[id-75c0b83d-72a0-4cf8-a153-631e83e7d53f]
# tempest.api.compute.admin.test_migrations.MigrationsAdminTest.test_list_migrations_in_flavor_resize_situation[id-1b512062-8093-438e-b47a-37d2f597cd64]
# tempest.api.compute.admin.test_migrations.MigrationsAdminTest.test_resize_server_revert_deleted_flavor[id-33f1fec3-ba18-4470-8e4e-1d888e7c3593]
# tempest.api.compute.admin.test_networks.NetworksTest.test_get_network[id-d206d211-8912-486f-86e2-a9d090d1f416]
# tempest.api.compute.admin.test_networks.NetworksTest.test_list_all_networks[id-df3d1046-6fa5-4b2c-ad0c-cfa46a351cb9]
# tempest.api.compute.admin.test_quotas.QuotaClassesAdminTestJSON.test_update_default_quotas[id-7932ab0f-5136-4075-b201-c0e2338df51a]
# tempest.api.compute.admin.test_quotas.QuotasAdminTestJSON.test_delete_quota[id-389d04f0-3a41-405f-9317-e5f86e3c44f0]
# tempest.api.compute.admin.test_quotas.QuotasAdminTestJSON.test_get_default_quotas[id-3b0a7c8f-cf58-46b8-a60c-715a32a8ba7d]
# tempest.api.compute.admin.test_quotas.QuotasAdminTestJSON.test_get_updated_quotas[id-ce9e0815-8091-4abd-8345-7fe5b85faa1d]
# tempest.api.compute.admin.test_quotas.QuotasAdminTestJSON.test_update_all_quota_resources_for_tenant[id-55fbe2bf-21a9-435b-bbd2-4162b0ed799a]
# tempest.api.compute.admin.test_quotas_negative.QuotasAdminNegativeTestJSON.test_create_server_when_cpu_quota_is_full[id-91058876-9947-4807-9f22-f6eb17140d9b,negative]
# tempest.api.compute.admin.test_quotas_negative.QuotasAdminNegativeTestJSON.test_create_server_when_instances_quota_is_full[id-7c6be468-0274-449a-81c3-ac1c32ee0161,negative]
# tempest.api.compute.admin.test_quotas_negative.QuotasAdminNegativeTestJSON.test_create_server_when_memory_quota_is_full[id-6fdd7012-584d-4327-a61c-49122e0d5864,negative]
# tempest.api.compute.admin.test_quotas_negative.QuotasAdminNegativeTestJSON.test_security_groups_exceed_limit[id-7c6c8f3b-2bf6-4918-b240-57b136a66aa0,network]
# tempest.api.compute.admin.test_quotas_negative.QuotasAdminNegativeTestJSON.test_security_groups_rules_exceed_limit[id-6e9f436d-f1ed-4f8e-a493-7275dfaa4b4d,negative,network]
# tempest.api.compute.admin.test_quotas_negative.QuotasAdminNegativeTestJSON.test_update_quota_normal_user[id-733abfe8-166e-47bb-8363-23dbd7ff3476,negative]
# tempest.api.compute.admin.test_security_group_default_rules.SecurityGroupDefaultRulesTest.test_create_delete_security_group_default_rules[id-6d880615-eec3-4d29-97c5-7a074dde239d]
# tempest.api.compute.admin.test_security_group_default_rules.SecurityGroupDefaultRulesTest.test_create_security_group_default_rule_with_blank_cidr[id-29f2d218-69b0-4a95-8f3d-6bd0ef732b3a]
# tempest.api.compute.admin.test_security_group_default_rules.SecurityGroupDefaultRulesTest.test_create_security_group_default_rule_without_cidr[id-4d752e0a-33a1-4c3a-b498-ff8667ca22e5]
# tempest.api.compute.admin.test_security_group_default_rules.SecurityGroupDefaultRulesTest.test_default_security_group_default_rule_show[id-15cbb349-86b4-4f71-a048-04b7ef3f150b]
# tempest.api.compute.admin.test_security_group_default_rules.SecurityGroupDefaultRulesTest.test_security_group_default_rules_list[id-6e6de55e-9146-4ae0-89f2-3569586e0b9b]
# tempest.api.compute.admin.test_security_groups.SecurityGroupsTestAdminJSON.test_list_security_groups_list_all_tenants_filter[id-49667619-5af9-4c63-ab5d-2cfdd1c8f7f1,network]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_create_server_with_scheduling_hint[id-fdcd9b33-0903-4e00-a1f7-b5f6543068d6]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_get_server_diagnostics_by_admin[id-31ff3486-b8a0-4f56-a6c0-aab460531db3]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_list_servers_by_admin[id-51717b38-bdc1-458b-b636-1cf82d99f62f]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_list_servers_by_admin_with_all_tenants[id-9f5579ae-19b4-4985-a091-2a5d56106580]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_list_servers_by_admin_with_specified_tenant[id-7e5d6b8f-454a-4ba1-8ae2-da857af8338b]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_list_servers_filter_by_error_status[id-06f960bb-15bb-48dc-873d-f96e89be7870]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_list_servers_filter_by_exist_host[id-86c7a8f7-50cf-43a9-9bac-5b985317134f]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_rebuild_server_in_error_state[id-682cb127-e5bb-4f53-87ce-cb9003604442]
# tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_reset_network_inject_network_info[id-7a1323b4-a6a2-497a-96cb-76c07b945c71]
# **DONE** tempest.api.compute.admin.test_servers.ServersAdminTestJSON.test_reset_state_server[id-ee8ae470-db70-474d-b752-690b7892cab1]
# tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_get_server_diagnostics_by_non_admin[id-e84e2234-60d2-42fa-8b30-e2d3049724ac,negative]
# **DONE** tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_migrate_non_existent_server[id-46a4e1ca-87ae-4d28-987a-1b6b136a0221,negative]
# **DONE** tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_migrate_server_invalid_state[id-b0b17f83-d14e-4fc4-8f31-bcc9f3cfa629,negative]
# **DONE** tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_reset_state_server_invalid_state[id-b0b4d8af-1256-41ef-9ee7-25f1c19dde80,negative]
# **DONE** tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_reset_state_server_invalid_type[id-4cdcc984-fab0-4577-9a9d-6d558527ee9d,negative]
# **DONE** tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_reset_state_server_nonexistent_server[id-e741298b-8df2-46f0-81cb-8f814ff2504c,negative]
# tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_resize_server_using_overlimit_ram[id-28dcec23-f807-49da-822c-56a92ea3c687,negative]
# tempest.api.compute.admin.test_servers_negative.ServersAdminNegativeTestJSON.test_resize_server_using_overlimit_vcpus[id-7368a427-2f26-4ad9-9ba9-911a0ec2b0db,negative]
# tempest.api.compute.admin.test_servers_on_multinodes.ServersOnMultiNodesTest.test_create_servers_on_different_hosts[id-cc7ca884-6e3e-42a3-a92f-c522fcf25e8e]
# tempest.api.compute.admin.test_servers_on_multinodes.ServersOnMultiNodesTest.test_create_servers_on_different_hosts_with_list_of_servers[id-7869cc84-d661-4e14-9f00-c18cdc89cf57]
# tempest.api.compute.admin.test_servers_on_multinodes.ServersOnMultiNodesTest.test_create_servers_on_same_host[id-26a9d5df-6890-45f2-abc4-a659290cb130]
# tempest.api.compute.admin.test_services.ServicesAdminTestJSON.test_get_service_by_host_name[id-affb42d5-5b4b-43c8-8b0b-6dca054abcca]
# tempest.api.compute.admin.test_services.ServicesAdminTestJSON.test_get_service_by_service_and_host_name[id-39397f6f-37b8-4234-8671-281e44c74025]
# tempest.api.compute.admin.test_services.ServicesAdminTestJSON.test_get_service_by_service_binary_name[id-f345b1ec-bc6e-4c38-a527-3ca2bc00bef5]
# tempest.api.compute.admin.test_services.ServicesAdminTestJSON.test_list_services[id-5be41ef4-53d1-41cc-8839-5c2a48a1b283]
# tempest.api.compute.admin.test_services_negative.ServicesAdminNegativeTestJSON.test_get_service_by_invalid_params[id-d0884a69-f693-4e79-a9af-232d15643bf7,negative]
# tempest.api.compute.admin.test_services_negative.ServicesAdminNegativeTestJSON.test_get_service_by_invalid_service_and_valid_host[id-1e966d4a-226e-47c7-b601-0b18a27add54,negative]
# tempest.api.compute.admin.test_services_negative.ServicesAdminNegativeTestJSON.test_get_service_with_valid_service_and_invalid_host[id-64e7e7fb-69e8-4cb6-a71d-8d5eb0c98655,negative]
# tempest.api.compute.admin.test_services_negative.ServicesAdminNegativeTestJSON.test_list_services_with_non_admin_user[id-1126d1f8-266e-485f-a687-adc547492646,negative]
# tempest.api.compute.admin.test_simple_tenant_usage.TenantUsagesTestJSON.test_get_usage_tenant[id-94135049-a4c5-4934-ad39-08fa7da4f22e]
# tempest.api.compute.admin.test_simple_tenant_usage.TenantUsagesTestJSON.test_get_usage_tenant_with_non_admin_user[id-9d00a412-b40e-4fd9-8eba-97b496316116]
# tempest.api.compute.admin.test_simple_tenant_usage.TenantUsagesTestJSON.test_list_usage_all_tenants[id-062c8ae9-9912-4249-8b51-e38d664e926e]
# tempest.api.compute.admin.test_simple_tenant_usage_negative.TenantUsagesNegativeTestJSON.test_get_usage_tenant_with_empty_tenant_id[id-8b21e135-d94b-4991-b6e9-87059609c8ed,negative]
# tempest.api.compute.admin.test_simple_tenant_usage_negative.TenantUsagesNegativeTestJSON.test_get_usage_tenant_with_invalid_date[id-4079dd2a-9e8d-479f-869d-6fa985ce45b6,negative]
# tempest.api.compute.admin.test_simple_tenant_usage_negative.TenantUsagesNegativeTestJSON.test_list_usage_all_tenants_with_non_admin_user[id-bbe6fe2c-15d8-404c-a0a2-44fad0ad5cc7,negative]
# tempest.api.compute.certificates.test_certificates.CertificatesV2TestJSON.test_create_root_certificate[id-c070a441-b08e-447e-a733-905909535b1b]
# tempest.api.compute.certificates.test_certificates.CertificatesV2TestJSON.test_get_root_certificate[id-3ac273d0-92d2-4632-bdfc-afbc21d4606c]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_get_flavor[id-1f12046b-753d-40d2-abb6-d8eb8b30cb2f,smoke]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors[id-e36c0eaa-dff5-4082-ad1f-3f9a80aa3f59,smoke]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_detailed_filter_by_min_disk[id-3df2743e-3034-4e57-a4cb-b6527f6eac79]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_detailed_filter_by_min_ram[id-09fe7509-b4ee-4b34-bf8b-39532dc47292]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_detailed_limit_results[id-b26f6327-2886-467a-82be-cef7a27709cb]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_detailed_using_marker[id-6db2f0c0-ddee-4162-9c84-0703d3dd1107]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_filter_by_min_disk[id-10645a4d-96f5-443f-831b-730711e11dd4]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_filter_by_min_ram[id-935cf550-e7c8-4da6-8002-00f92d5edfaa]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_limit_results[id-8d7691b3-6ed4-411a-abc9-2839a765adab]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_using_marker[id-e800f879-9828-4bd0-8eae-4f17189951fb]
# tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON.test_list_flavors_with_detail[id-6e85fde4-b3cd-4137-ab72-ed5f418e8c24]
# tempest.api.compute.flavors.test_flavors_negative.FlavorDetailsNegativeTestJSON.test_flavor_details_negative[negative](inv_res_flavor)
# tempest.api.compute.flavors.test_flavors_negative.FlavorsListWithDetailsNegativeTestJSON.test_flavors_list_with_details_negative[negative](minRam_gen_none)
# tempest.api.compute.flavors.test_flavors_negative.FlavorsListWithDetailsNegativeTestJSON.test_flavors_list_with_details_negative[negative](minRam_gen_string)
# tempest.api.compute.flavors.test_flavors_negative.FlavorsListWithDetailsNegativeTestJSON.test_flavors_list_with_details_negative[negative](minDisk_gen_none)
# tempest.api.compute.flavors.test_flavors_negative.FlavorsListWithDetailsNegativeTestJSON.test_flavors_list_with_details_negative[negative](minDisk_gen_string)
# tempest.api.compute.floating_ips.test_floating_ips_actions.FloatingIPsTestJSON.test_allocate_floating_ip[id-f7bfb946-297e-41b8-9e8c-aba8e9bb5194,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions.FloatingIPsTestJSON.test_associate_already_associated_floating_ip[id-6edef4b2-aaf1-4abc-bbe3-993e2561e0fe,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions.FloatingIPsTestJSON.test_associate_disassociate_floating_ip[id-307efa27-dc6f-48a0-8cd2-162ce3ef0b52,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions.FloatingIPsTestJSON.test_delete_floating_ip[id-de45e989-b5ca-4a9b-916b-04a52e7bbb8b,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions_negative.FloatingIPsNegativeTestJSON.test_allocate_floating_ip_from_nonexistent_pool[id-6e0f059b-e4dd-48fb-8207-06e3bba5b074,negative,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions_negative.FloatingIPsNegativeTestJSON.test_associate_ip_to_server_without_passing_floating_ip[id-804b4fcb-bbf5-412f-925d-896672b61eb3,negative,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions_negative.FloatingIPsNegativeTestJSON.test_associate_nonexistent_floating_ip[id-595fa616-1a71-4670-9614-46564ac49a4c,negative,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions_negative.FloatingIPsNegativeTestJSON.test_delete_nonexistent_floating_ip[id-ae1c55a8-552b-44d4-bfb6-2a115a15d0ba,negative,network]
# tempest.api.compute.floating_ips.test_floating_ips_actions_negative.FloatingIPsNegativeTestJSON.test_dissociate_nonexistent_floating_ip[id-0a081a66-e568-4e6b-aa62-9587a876dca8,negative,network]
# tempest.api.compute.floating_ips.test_list_floating_ips.FloatingIPDetailsTestJSON.test_get_floating_ip_details[id-eef497e0-8ff7-43c8-85ef-558440574f84,network]
# tempest.api.compute.floating_ips.test_list_floating_ips.FloatingIPDetailsTestJSON.test_list_floating_ip_pools[id-df389fc8-56f5-43cc-b290-20eda39854d3,network]
# tempest.api.compute.floating_ips.test_list_floating_ips.FloatingIPDetailsTestJSON.test_list_floating_ips[id-16db31c3-fb85-40c9-bbe2-8cf7b67ff99f,network]
# tempest.api.compute.floating_ips.test_list_floating_ips_negative.FloatingIPDetailsNegativeTestJSON.test_get_nonexistent_floating_ip_details[id-7ab18834-4a4b-4f28-a2c5-440579866695,negative,network]
# tempest.api.compute.images.test_image_metadata.ImagesMetadataTestJSON.test_delete_image_metadata_item[id-a013796c-ba37-4bb5-8602-d944511def14]
# tempest.api.compute.images.test_image_metadata.ImagesMetadataTestJSON.test_get_image_metadata_item[id-4f5db52f-6685-4c75-b848-f4bb363f9aa6]
# tempest.api.compute.images.test_image_metadata.ImagesMetadataTestJSON.test_list_image_metadata[id-37ec6edd-cf30-4c53-bd45-ae74db6b0531]
# tempest.api.compute.images.test_image_metadata.ImagesMetadataTestJSON.test_set_image_metadata[id-ece7befc-d3ce-42a4-b4be-c3067a418c29]
# tempest.api.compute.images.test_image_metadata.ImagesMetadataTestJSON.test_set_image_metadata_item[id-f2de776a-4778-4d90-a5da-aae63aee64ae]
# tempest.api.compute.images.test_image_metadata.ImagesMetadataTestJSON.test_update_image_metadata[id-7b491c11-a9d5-40fe-a696-7f7e03d3fea2]
# tempest.api.compute.images.test_image_metadata_negative.ImagesMetadataTestJSON.test_delete_nonexistent_image_metadata_item[id-848e157f-6bcf-4b2e-a5dd-5124025a8518,negative]
# tempest.api.compute.images.test_image_metadata_negative.ImagesMetadataTestJSON.test_get_nonexistent_image_metadata_item[id-41ae052c-6ee6-405c-985e-5712393a620d,negative]
# tempest.api.compute.images.test_image_metadata_negative.ImagesMetadataTestJSON.test_list_nonexistent_image_metadata[id-94069db2-792f-4fa8-8bd3-2271a6e0c095,negative]
# tempest.api.compute.images.test_image_metadata_negative.ImagesMetadataTestJSON.test_set_nonexistent_image_metadata[id-dc64f2ce-77e8-45b0-88c8-e15041d08eaf,negative]
# tempest.api.compute.images.test_image_metadata_negative.ImagesMetadataTestJSON.test_set_nonexistent_image_metadata_item[id-2154fd03-ab54-457c-8874-e6e3eb56e9cf,negative]
# tempest.api.compute.images.test_image_metadata_negative.ImagesMetadataTestJSON.test_update_nonexistent_image_metadata[id-a403ef9e-9f95-427c-b70a-3ce3388796f1,negative]
# tempest.api.compute.images.test_images.ImagesTestJSON.test_create_image_from_stopped_server[id-aaacd1d0-55a2-4ce8-818a-b5439df8adc9]
# tempest.api.compute.images.test_images.ImagesTestJSON.test_delete_saving_image[id-aa06b52b-2db5-4807-b218-9441f75d74e3]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_create_image_from_deleted_server[id-6cd5a89d-5b47-46a7-93bc-3916f0d84973,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_create_image_from_invalid_server[id-82c5b0c4-9dbd-463c-872b-20c4755aae7f,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_create_image_specify_uuid_35_characters_or_less[id-ec176029-73dc-4037-8d72-2e4ff60cf538,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_create_image_specify_uuid_37_characters_or_more[id-36741560-510e-4cc2-8641-55fe4dfb2437,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_delete_image_blank_id[id-e6e41425-af5c-4fe6-a4b5-7b7b963ffda5,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_delete_image_id_is_over_35_character_limit[id-b340030d-82cd-4066-a314-c72fb7c59277,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_delete_image_negative_image_id[id-68e2c175-bd26-4407-ac0f-4ea9ce2139ea,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_delete_image_non_hex_string_id[id-924540c3-f1f1-444c-8f58-718958b6724e,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_delete_image_with_invalid_image_id[id-381acb65-785a-4942-94ce-d8f8c84f1f0f,negative]
# tempest.api.compute.images.test_images_negative.ImagesNegativeTestJSON.test_delete_non_existent_image[id-137aef61-39f7-44a1-8ddf-0adf82511701,negative]
# tempest.api.compute.images.test_images_oneserver.ImagesOneServerTestJSON.test_create_delete_image[id-3731d080-d4c5-4872-b41a-64d0d0021314]
# tempest.api.compute.images.test_images_oneserver.ImagesOneServerTestJSON.test_create_image_specify_multibyte_character_image_name[id-3b7c6fe4-dfe7-477c-9243-b06359db51e6]
# tempest.api.compute.images.test_images_oneserver_negative.ImagesOneServerNegativeTestJSON.test_create_image_specify_invalid_metadata[id-55d1d38c-dd66-4933-9c8e-7d92aeb60ddc,negative]
# tempest.api.compute.images.test_images_oneserver_negative.ImagesOneServerNegativeTestJSON.test_create_image_specify_metadata_over_limits[id-3d24d11f-5366-4536-bd28-cff32b748eca,negative]
# tempest.api.compute.images.test_images_oneserver_negative.ImagesOneServerNegativeTestJSON.test_create_image_specify_name_over_256_chars[id-084f0cbc-500a-4963-8a4e-312905862581,negative]
# tempest.api.compute.images.test_images_oneserver_negative.ImagesOneServerNegativeTestJSON.test_create_second_image_when_first_image_is_being_saved[id-0460efcf-ee88-4f94-acef-1bf658695456,negative]
# tempest.api.compute.images.test_images_oneserver_negative.ImagesOneServerNegativeTestJSON.test_delete_image_that_is_not_yet_active[id-0894954d-2db2-4195-a45b-ffec0bc0187e,negative]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_filter_by_changes_since[id-18bac3ae-da27-436c-92a9-b22474d13aab]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_filter_by_name[id-33163b73-79f5-4d07-a7ea-9213bcc468ff]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_filter_by_server_id[id-9f238683-c763-45aa-b848-232ec3ce3105]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_filter_by_server_ref[id-05a377b8-28cf-4734-a1e6-2ab5c38bf606]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_filter_by_status[id-a3f5b513-aeb3-42a9-b18e-f091ef73254d]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_filter_by_type[id-e3356918-4d3e-4756-81d5-abc4524ba29f]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_limit_results[id-3a484ca9-67ba-451e-b494-7fcf28d32d62]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_with_detail_filter_by_changes_since[id-7d439e18-ac2e-4827-b049-7e18004712c4]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_with_detail_filter_by_name[id-644ea267-9bd9-4f3b-af9f-dffa02396a17]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_with_detail_filter_by_server_ref[id-8c78f822-203b-4bf6-8bba-56ebd551cf84]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_with_detail_filter_by_status[id-9b0ea018-6185-4f71-948a-a123a107988e]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_with_detail_filter_by_type[id-888c0cc0-7223-43c5-9db0-b125fd0a393b]
# tempest.api.compute.images.test_list_image_filters.ListImageFiltersTestJSON.test_list_images_with_detail_limit_results[id-ba2fa9a9-b672-47cc-b354-3b4c0600e2cb]
# tempest.api.compute.images.test_list_image_filters_negative.ListImageFiltersNegativeTestJSON.test_get_nonexistent_image[id-391b0440-432c-4d4b-b5da-c5096aa247eb,negative]
# tempest.api.compute.images.test_list_images.ListImagesTestJSON.test_get_image[id-490d0898-e12a-463f-aef0-c50156b9f789]
# tempest.api.compute.images.test_list_images.ListImagesTestJSON.test_list_images[id-fd51b7f4-d4a3-4331-9885-866658112a6f]
# tempest.api.compute.images.test_list_images.ListImagesTestJSON.test_list_images_with_detail[id-9f94cb6b-7f10-48c5-b911-a0b84d7d4cd6]
# tempest.api.compute.keypairs.test_keypairs.KeyPairsV2TestJSON.test_get_keypair_detail[id-a4233d5d-52d8-47cc-9a25-e1864527e3df]
# tempest.api.compute.keypairs.test_keypairs.KeyPairsV2TestJSON.test_keypair_create_delete[id-6c1d3123-4519-4742-9194-622cb1714b7d]
# tempest.api.compute.keypairs.test_keypairs.KeyPairsV2TestJSON.test_keypair_create_with_pub_key[id-39c90c6a-304a-49dd-95ec-2366129def05]
# tempest.api.compute.keypairs.test_keypairs.KeyPairsV2TestJSON.test_keypairs_create_list_delete[id-1d1dbedb-d7a0-432a-9d09-83f543c3c19b]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_create_keypair_invalid_name[id-45fbe5e0-acb5-49aa-837a-ff8d0719db91,negative]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_create_keypair_when_public_key_bits_exceeds_maximum[id-fc100c19-2926-4b9c-8fdc-d0589ee2f9ff,negative]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_create_keypair_with_duplicate_name[id-0359a7f1-f002-4682-8073-0c91e4011b7c,negative]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_create_keypair_with_empty_name_string[id-1398abe1-4a84-45fb-9294-89f514daff00,negative]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_create_keypair_with_empty_public_key[id-dade320e-69ca-42a9-ba4a-345300f127e0,negative]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_create_keypair_with_long_keynames[id-3faa916f-779f-4103-aca7-dc3538eee1b7,negative]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_keypair_create_with_invalid_pub_key[id-29cca892-46ae-4d48-bc32-8fe7e731eb81,negative]
# tempest.api.compute.keypairs.test_keypairs_negative.KeyPairsNegativeTestJSON.test_keypair_delete_nonexistent_key[id-7cc32e47-4c42-489d-9623-c5e2cb5a2fa5,negative]
# tempest.api.compute.keypairs.test_keypairs_v22.KeyPairsV22TestJSON.test_get_keypair_detail[id-a4233d5d-52d8-47cc-9a25-e1864527e3df]
# tempest.api.compute.keypairs.test_keypairs_v22.KeyPairsV22TestJSON.test_keypair_create_delete[id-6c1d3123-4519-4742-9194-622cb1714b7d]
# tempest.api.compute.keypairs.test_keypairs_v22.KeyPairsV22TestJSON.test_keypair_create_with_pub_key[id-39c90c6a-304a-49dd-95ec-2366129def05]
# tempest.api.compute.keypairs.test_keypairs_v22.KeyPairsV22TestJSON.test_keypairs_create_list_delete[id-1d1dbedb-d7a0-432a-9d09-83f543c3c19b]
# tempest.api.compute.keypairs.test_keypairs_v22.KeyPairsV22TestJSON.test_keypairsv22_create_list_show[id-8726fa85-7f98-4b20-af9e-f710a4f3391c]
# tempest.api.compute.keypairs.test_keypairs_v22.KeyPairsV22TestJSON.test_keypairsv22_create_list_show_with_type[id-89d59d43-f735-441a-abcf-0601727f47b6]
# tempest.api.compute.limits.test_absolute_limits.AbsoluteLimitsTestJSON.test_absLimits_get[id-b54c66af-6ab6-4cf0-a9e5-a0cb58d75e0b]
# tempest.api.compute.limits.test_absolute_limits_negative.AbsoluteLimitsNegativeTestJSON.test_max_image_meta_exceed_limit[id-215cd465-d8ae-49c9-bf33-9c911913a5c8,negative]
# tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create[id-850795d7-d4d3-4e55-b527-a774c0123d3a,network,smoke]
# tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create_with_optional_cidr[id-7a01873e-3c38-4f30-80be-31a043cfe2fd,network]
# tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_create_with_optional_group_id[id-7f5d2899-7705-4d4b-8458-4505188ffab6,network]
# tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_delete_when_peer_group_deleted[id-fc5c5acf-2091-43a6-a6ae-e42760e9ffaf,network]
# tempest.api.compute.security_groups.test_security_group_rules.SecurityGroupRulesTestJSON.test_security_group_rules_list[id-a6154130-5a55-4850-8be4-5e9e796dbf17,network,smoke]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_create_security_group_rule_duplicate[id-8bd56d02-3ffa-4d67-9933-b6b9a01d6089,negative,network]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_create_security_group_rule_with_invalid_from_port[id-12bbc875-1045-4f7a-be46-751277baedb9,negative,network]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_create_security_group_rule_with_invalid_id[id-2244d7e4-adb7-4ecb-9930-2d77e123ce4f,negative,network]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_create_security_group_rule_with_invalid_ip_protocol[id-84c81249-9f6e-439c-9bbf-cbb0d2cddbdf,negative,network]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_create_security_group_rule_with_invalid_port_range[id-00296fa9-0576-496a-ae15-fbab843189e0,negative,network]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_create_security_group_rule_with_invalid_to_port[id-ff88804d-144f-45d1-bf59-dd155838a43a,negative,network]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_create_security_group_rule_with_non_existent_id[id-1d507e98-7951-469b-82c3-23f1e6b8c254,negative,network]
# tempest.api.compute.security_groups.test_security_group_rules_negative.SecurityGroupRulesNegativeTestJSON.test_delete_security_group_rule_with_non_existent_id[id-56fddcca-dbb8-4494-a0db-96e9f869527c,negative,network]
# tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_security_group_create_get_delete[id-ecc0da4a-2117-48af-91af-993cca39a615,network]
# tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_security_groups_create_list_delete[id-eb2b087d-633d-4d0d-a7bd-9e6ba35b32de,network,smoke]
# tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_server_security_groups[id-fe4abc0d-83f5-4c50-ad11-57a1127297a2,network]
# tempest.api.compute.security_groups.test_security_groups.SecurityGroupsTestJSON.test_update_security_groups[id-7d4e1d3c-3209-4d6d-b020-986304ebad1f,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_delete_nonexistent_security_group[id-6727c00b-214c-4f9e-9a52-017ac3e98411,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_delete_security_group_without_passing_id[id-1438f330-8fa4-4aeb-8a94-37c250106d7f,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_delete_the_default_security_group[id-36a1629f-c6da-4a26-b8b8-55e7e5d5cd58,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_security_group_create_with_duplicate_name[id-9fdb4abc-6b66-4b27-b89c-eb215a956168,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_security_group_create_with_invalid_group_description[id-777b6f14-aca9-4758-9e84-38783cfa58bc,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_security_group_create_with_invalid_group_name[id-1759c3cb-b0fc-44b7-86ce-c99236be911d,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_security_group_get_nonexistent_group[id-673eaec1-9b3e-48ed-bdf1-2786c1b9661c,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_update_non_existent_security_group[id-27edee9c-873d-4da6-a68a-3c256efebe8f,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_update_security_group_with_invalid_sg_des[id-97d12b1c-a610-4194-93f1-ba859e718b45,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_update_security_group_with_invalid_sg_id[id-00579617-fe04-4e1c-9d08-ca7467d2e34b,negative,network]
# tempest.api.compute.security_groups.test_security_groups_negative.SecurityGroupsNegativeTestJSON.test_update_security_group_with_invalid_sg_name[id-cda8d8b4-59f8-4087-821d-20cf5a03b3b1,negative,network]
# tempest.api.compute.servers.test_attach_interfaces.AttachInterfacesTestJSON.test_add_remove_fixed_ip[id-c7e0e60b-ee45-43d0-abeb-8596fd42a2f9,network,smoke]
# tempest.api.compute.servers.test_attach_interfaces.AttachInterfacesTestJSON.test_create_list_show_delete_interfaces[id-73fe8f02-590d-4bf1-b184-e9ca81065051,network]
# tempest.api.compute.servers.test_availability_zone.AZV2TestJSON.test_get_availability_zone_list_with_non_admin_user[id-a8333aa2-205c-449f-a828-d38c2489bf25]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_create_server_with_scheduler_hint_group[id-ed20d3fb-9d1f-4329-b160-543fbd5d9811]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_host_name_is_same_as_server_name[id-ac1ad47f-984b-4441-9274-c9079b7a0666]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_list_servers[id-9a438d88-10c6-4bcd-8b5b-5b6e25e1346f,smoke]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_list_servers_with_detail[id-585e934c-448e-43c4-acbf-d06a9b899997]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_verify_created_server_vcpus[id-cbc0f52f-05aa-492b-bdc1-84b575ca294b]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_verify_duplicate_network_nics[id-1678d144-ed74-43f8-8e57-ab10dbf9b3c2]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_verify_multiple_nics_order[id-0578d144-ed74-43f8-8e57-ab10dbf9b3c2]
# tempest.api.compute.servers.test_create_server.ServersTestJSON.test_verify_server_details[id-5de47127-9977-400a-936f-abcfbec1218f,smoke]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_create_server_with_scheduler_hint_group[id-ed20d3fb-9d1f-4329-b160-543fbd5d9811]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_host_name_is_same_as_server_name[id-ac1ad47f-984b-4441-9274-c9079b7a0666]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_list_servers[id-9a438d88-10c6-4bcd-8b5b-5b6e25e1346f,smoke]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_list_servers_with_detail[id-585e934c-448e-43c4-acbf-d06a9b899997]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_verify_created_server_vcpus[id-cbc0f52f-05aa-492b-bdc1-84b575ca294b]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_verify_duplicate_network_nics[id-1678d144-ed74-43f8-8e57-ab10dbf9b3c2]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_verify_multiple_nics_order[id-0578d144-ed74-43f8-8e57-ab10dbf9b3c2]
# tempest.api.compute.servers.test_create_server.ServersTestManualDisk.test_verify_server_details[id-5de47127-9977-400a-936f-abcfbec1218f,smoke]
# tempest.api.compute.servers.test_create_server.ServersWithSpecificFlavorTestJSON.test_verify_created_server_ephemeral_disk[id-b3c7bcfc-bb5b-4e22-b517-c7f686b802ca]
# tempest.api.compute.servers.test_delete_server.DeleteServersAdminTestJSON.test_admin_delete_servers_of_others[id-73177903-6737-4f27-a60c-379e8ae8cf48]
# tempest.api.compute.servers.test_delete_server.DeleteServersAdminTestJSON.test_delete_server_while_in_error_state[id-99774678-e072-49d1-9d2a-49a59bc56063]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_active_server[id-925fdfb4-5b13-47ea-ac8a-c36ae6fddb05]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_server_while_in_attached_volume[id-d0f3f0d6-d9b6-4a32-8da4-23015dcab23c,volume]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_server_while_in_building_state[id-9e6e0c87-3352-42f7-9faf-5d6210dbd159]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_server_while_in_pause_state[id-943bd6e8-4d7a-4904-be83-7a6cc2d4213b]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_server_while_in_shelved_state[id-bb0cb402-09dd-4947-b6e5-5e7e1cfa61ad]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_server_while_in_shutoff_state[id-546d368c-bb6c-4645-979a-83ed16f3a6be]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_server_while_in_suspended_state[id-1f82ebd3-8253-4f4e-b93f-de9b7df56d8b]
# tempest.api.compute.servers.test_delete_server.DeleteServersTestJSON.test_delete_server_while_in_verify_resize_state[id-ab0c38b4-cdd8-49d3-9b92-0cb898723c01]
# tempest.api.compute.servers.test_disk_config.ServerDiskConfigTestJSON.test_rebuild_server_with_auto_disk_config[id-9c9fae77-4feb-402f-8450-bf1c8b609713]
# tempest.api.compute.servers.test_disk_config.ServerDiskConfigTestJSON.test_rebuild_server_with_manual_disk_config[id-bef56b09-2e8c-4883-a370-4950812f430e]
# tempest.api.compute.servers.test_disk_config.ServerDiskConfigTestJSON.test_resize_server_from_auto_to_manual[id-693d16f3-556c-489a-8bac-3d0ca2490bad]
# tempest.api.compute.servers.test_disk_config.ServerDiskConfigTestJSON.test_resize_server_from_manual_to_auto[id-414e7e93-45b5-44bc-8e03-55159c6bfc97]
# tempest.api.compute.servers.test_disk_config.ServerDiskConfigTestJSON.test_update_server_from_auto_to_manual[id-5ef18867-358d-4de9-b3c9-94d4ba35742f]
# tempest.api.compute.servers.test_instance_actions.InstanceActionsTestJSON.test_get_instance_action[id-aacc71ca-1d70-4aa5-bbf6-0ff71470e43c]
# tempest.api.compute.servers.test_instance_actions.InstanceActionsTestJSON.test_list_instance_actions[id-77ca5cc5-9990-45e0-ab98-1de8fead201a]
# tempest.api.compute.servers.test_instance_actions_negative.InstanceActionsNegativeTestJSON.test_get_instance_action_invalid_request[id-0269f40a-6f18-456c-b336-c03623c897f1,negative]
# tempest.api.compute.servers.test_instance_actions_negative.InstanceActionsNegativeTestJSON.test_list_instance_actions_non_existent_server[id-67e1fce6-7ec2-45c6-92d4-0a8f1a632910,negative]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_detailed_filter_by_flavor[id-80c574cc-0925-44ba-8602-299028357dd9]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_detailed_filter_by_image[id-b3304c3b-97df-46d2-8cd3-e2b6659724e7]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_detailed_filter_by_server_name[id-f9eb2b70-735f-416c-b260-9914ac6181e4]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_detailed_filter_by_server_status[id-de2612ab-b7dd-4044-b0b1-d2539601911f]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_detailed_limit_results[id-67aec2d0-35fe-4503-9f92-f13272b867ed]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_exceed_limit[id-37791bbd-90c0-4de0-831e-5f38cba9c6b3]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_flavor[id-573637f5-7325-47bb-9144-3476d0416908]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_image[id-05e8a8e7-9659-459a-989d-92c2f501f4ba]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_limit[id-614cdfc1-d557-4bac-915b-3e67b48eee76]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_server_name[id-9b067a7b-7fee-4f6a-b29c-be43fe18fc5a]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_server_status[id-ca78e20e-fddb-4ce6-b7f7-bcbf8605e66e]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_shutoff_status[id-451dbbb2-f330-4a9f-b0e1-5f5d2cb0f34c]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filter_by_zero_limit[id-b1495414-2d93-414c-8019-849afe8d319e]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filtered_by_ip[id-43a1242e-7b31-48d1-88f2-3f72aa9f2077]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filtered_by_ip_regex[id-a905e287-c35e-42f2-b132-d02b09f3654a]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filtered_by_name_regex[id-24a89b0c-0d55-4a28-847f-45075f19b27b]
# tempest.api.compute.servers.test_list_server_filters.ListServerFiltersTestJSON.test_list_servers_filtered_by_name_wildcard[id-e9f624ee-92af-4562-8bec-437945a18dcb]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_changes_since_future_date[id-74745ad8-b346-45b5-b9b8-509d7447fc1f,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_changes_since_invalid_date[id-87d12517-e20a-4c9c-97b6-dd1628d6d6c9,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_limits[id-12c80a9f-2dec-480e-882b-98ba15757659]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_limits_greater_than_actual_count[id-d47c17fb-eebd-4287-8e95-f20a7e627b18,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_limits_pass_negative_value[id-62610dd9-4713-4ee0-8beb-fd2c1aa7f950,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_limits_pass_string[id-679bc053-5e70-4514-9800-3dfab1a380a6,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_non_existing_flavor[id-5913660b-223b-44d4-a651-a0fbfd44ca75,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_non_existing_image[id-ff01387d-c7ad-47b4-ae9e-64fa214638fe,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_by_non_existing_server_name[id-e2c77c4a-000a-4af3-a0bd-629a328bde7c,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_detail_server_is_deleted[id-93055106-2d34-46fe-af68-d9ddbf7ee570,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_status_non_existing[id-fcdf192d-0f74-4d89-911f-1ec002b822c4,negative]
# tempest.api.compute.servers.test_list_servers_negative.ListServersNegativeTestJSON.test_list_servers_with_a_deleted_server[id-24a26f1a-1ddc-4eea-b0d7-a90cc874ad8f,negative]
# tempest.api.compute.servers.test_multiple_create.MultipleCreateTestJSON.test_multiple_create[id-61e03386-89c3-449c-9bb1-a06f423fd9d1]
# tempest.api.compute.servers.test_multiple_create.MultipleCreateTestJSON.test_multiple_create_with_reservation_return[id-864777fb-2f1e-44e3-b5b9-3eb6fa84f2f7]
# tempest.api.compute.servers.test_multiple_create_negative.MultipleCreateNegativeTestJSON.test_max_count_less_than_min_count[id-476da616-f1ef-4271-a9b1-b9fc87727cdf,negative]
# tempest.api.compute.servers.test_multiple_create_negative.MultipleCreateNegativeTestJSON.test_max_count_less_than_one[id-a6f9c2ab-e060-4b82-b23c-4532cb9390ff,negative]
# tempest.api.compute.servers.test_multiple_create_negative.MultipleCreateNegativeTestJSON.test_max_count_non_integer[id-9c5698d1-d7af-4c80-b971-9d403135eea2,negative]
# tempest.api.compute.servers.test_multiple_create_negative.MultipleCreateNegativeTestJSON.test_min_count_less_than_one[id-daf29d8d-e928-4a01-9a8c-b129603f3fc0,negative]
# tempest.api.compute.servers.test_multiple_create_negative.MultipleCreateNegativeTestJSON.test_min_count_non_integer[id-999aa722-d624-4423-b813-0d1ac9884d7a,negative]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_change_server_password[id-6158df09-4b82-4ab3-af6d-29cf36af858d]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_create_backup[id-b963d4f1-94b3-4c40-9e97-7b583f46e470,image]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_get_console_output[id-4b8867e6-fffa-4d54-b1d1-6fdda57be2f3]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_get_console_output_server_id_in_shutoff_status[id-5b65d4e7-4ecd-437c-83c0-d6b79d927568]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_get_console_output_with_unlimited_size[id-89104062-69d8-4b19-a71b-f47b7af093d7]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_get_vnc_console[id-c6bc11bf-592e-4015-9319-1c98dc64daf5]
# **DONE** tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_lock_unlock_server[id-80a8094c-211e-440a-ab88-9e59d556c7ee]
# **DONE** tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_pause_unpause_server[id-bd61a9fd-062f-4670-972b-2d6c3e3b9e73]
# **DONE** tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_reboot_server_hard[id-2cb1baf6-ac8d-4429-bf0d-ba8a0ba53e32,smoke]
# **DONE** tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_reboot_server_soft[id-4640e3ef-a5df-482e-95a1-ceeeb0faa84d]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_rebuild_server[id-aaa6cdf3-55a7-461a-add9-1c8596b9a07c]
# tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_rebuild_server_in_stop_state[id-30449a88-5aff-4f9b-9866-6ee9b17f906d]
# **DONE**tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_resize_server_confirm[id-1499262a-9328-4eda-9068-db1ac57498d2]
# **DONE**tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_resize_server_confirm_from_stopped[id-138b131d-66df-48c9-a171-64f45eb92962]
# **DONE**tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_resize_server_revert[id-c03aab19-adb1-44f5-917d-c419577e9e68]
# **DONE** tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_shelve_unshelve_server[id-77eba8e0-036e-4635-944b-f7a8f3b78dc9]
# **DONE** tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_stop_start_server[id-af8eafd4-38a7-4a4b-bdbc-75145a580560]
# **DONE** tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON.test_suspend_resume_server[id-0d8ee21e-b749-462d-83da-b85b41c86c7f]
# tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON.test_list_server_addresses[id-6eb718c0-02d9-4d5e-acd1-4e0c269cef39,network,smoke]
# tempest.api.compute.servers.test_server_addresses.ServerAddressesTestJSON.test_list_server_addresses_by_network[id-87bbc374-5538-4f64-b673-2b0e4443cc30,network,smoke]
# tempest.api.compute.servers.test_server_addresses_negative.ServerAddressesNegativeTestJSON.test_list_server_addresses_by_network_neg[id-a2ab5144-78c0-4942-a0ed-cc8edccfd9ba,negative,network]
# tempest.api.compute.servers.test_server_addresses_negative.ServerAddressesNegativeTestJSON.test_list_server_addresses_invalid_server_id[id-02c3f645-2d2e-4417-8525-68c0407d001b,negative,network]
# tempest.api.compute.servers.test_server_group.ServerGroupTestJSON.test_create_delete_multiple_server_groups_with_same_name_policy[id-154dc5a4-a2fe-44b5-b99e-f15806a4a113]
# tempest.api.compute.servers.test_server_group.ServerGroupTestJSON.test_create_delete_server_group_with_affinity_policy[id-5dc57eda-35b7-4af7-9e5f-3c2be3d2d68b]
# tempest.api.compute.servers.test_server_group.ServerGroupTestJSON.test_create_delete_server_group_with_anti_affinity_policy[id-3645a102-372f-4140-afad-13698d850d23]
# tempest.api.compute.servers.test_server_group.ServerGroupTestJSON.test_list_server_groups[id-d4874179-27b4-4d7d-80e4-6c560cdfe321]
# tempest.api.compute.servers.test_server_group.ServerGroupTestJSON.test_show_server_group[id-b3545034-dd78-48f0-bdc2-a4adfa6d0ead]
# tempest.api.compute.servers.test_server_metadata.ServerMetadataTestJSON.test_delete_server_metadata_item[id-127642d6-4c7b-4486-b7cd-07265a378658]
# tempest.api.compute.servers.test_server_metadata.ServerMetadataTestJSON.test_get_server_metadata_item[id-3043c57d-7e0e-49a6-9a96-ad569c265e6a]
# tempest.api.compute.servers.test_server_metadata.ServerMetadataTestJSON.test_list_server_metadata[id-479da087-92b3-4dcf-aeb3-fd293b2d14ce]
# tempest.api.compute.servers.test_server_metadata.ServerMetadataTestJSON.test_set_server_metadata[id-211021f6-21de-4657-a68f-908878cfe251]
# tempest.api.compute.servers.test_server_metadata.ServerMetadataTestJSON.test_set_server_metadata_item[id-58c02d4f-5c67-40be-8744-d3fa5982eb1c]
# tempest.api.compute.servers.test_server_metadata.ServerMetadataTestJSON.test_update_metadata_empty_body[id-0f58d402-e34a-481d-8af8-b392b17426d9]
# tempest.api.compute.servers.test_server_metadata.ServerMetadataTestJSON.test_update_server_metadata[id-344d981e-0c33-4997-8a5d-6c1d803e4134]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_create_server_metadata_blank_key[id-92431555-4d8b-467c-b95b-b17daa5e57ff,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_delete_metadata_non_existent_server[id-6bbd88e1-f8b3-424d-ba10-ae21c45ada8d,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_list_server_metadata_non_existent_server[id-f408e78e-3066-4097-9299-3b0182da812e,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_metadata_items_limit[id-d8c0a210-a5c3-4664-be04-69d96746b547,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_server_create_metadata_key_too_long[id-fe114a8f-3a57-4eff-9ee2-4e14628df049,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_server_metadata_non_existent_server[id-4d9cd7a3-2010-4b41-b8fe-3bbf0b169466,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_set_metadata_non_existent_server[id-0df38c2a-3d4e-4db5-98d8-d4d9fa843a12,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_set_server_metadata_blank_key[id-96100343-7fa9-40d8-80fa-d29ef588ce1c,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_set_server_metadata_missing_metadata[id-64a91aee-9723-4863-be44-4c9d9f1e7d0e,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_update_metadata_non_existent_server[id-904b13dc-0ef2-4e4c-91cd-3b4a0f2f49d8,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_update_metadata_with_blank_key[id-a452f38c-05c2-4b47-bd44-a4f0bf5a5e48,negative]
# tempest.api.compute.servers.test_server_metadata_negative.ServerMetadataNegativeTestJSON.test_wrong_key_passed_in_body[id-0025fbd6-a4ba-4cde-b8c2-96805dcfdabc,negative]
# tempest.api.compute.servers.test_server_password.ServerPasswordTestJSON.test_delete_server_password[id-f8229e8b-b625-4493-800a-bde86ac611ea]
# tempest.api.compute.servers.test_server_password.ServerPasswordTestJSON.test_get_server_password[id-f83b582f-62a8-4f22-85b0-0dee50ff783a]
# tempest.api.compute.servers.test_server_personality.ServerPersonalityTestJSON.test_can_create_server_with_max_number_personality_files[id-52f12ee8-5180-40cc-b417-31572ea3d555]
# tempest.api.compute.servers.test_server_personality.ServerPersonalityTestJSON.test_create_server_with_personality[id-3cfe87fd-115b-4a02-b942-7dc36a337fdf]
# tempest.api.compute.servers.test_server_personality.ServerPersonalityTestJSON.test_personality_files_exceed_limit[id-176cd8c9-b9e8-48ee-a480-180beab292bf]
# tempest.api.compute.servers.test_server_personality.ServerPersonalityTestJSON.test_rebuild_server_with_personality[id-128966d8-71fc-443c-8cab-08e24114ecc9]
# tempest.api.compute.servers.test_server_rescue.ServerRescueTestJSON.test_rescue_unrescue_instance[id-fd032140-714c-42e4-a8fd-adcd8df06be6]
# tempest.api.compute.servers.test_server_rescue.ServerRescueTestJSON.test_rescued_vm_add_remove_security_group[id-affca41f-7195-492d-8065-e09eee245404]
# tempest.api.compute.servers.test_server_rescue.ServerRescueTestJSON.test_rescued_vm_associate_dissociate_floating_ip[id-4842e0cf-e87d-4d9d-b61f-f4791da3cacc]
# tempest.api.compute.servers.test_server_rescue_negative.ServerRescueNegativeTestJSON.test_rescue_non_existent_server[id-6dfc0a55-3a77-4564-a144-1587b7971dde,negative]
# tempest.api.compute.servers.test_server_rescue_negative.ServerRescueNegativeTestJSON.test_rescue_paused_instance[id-cc3a883f-43c0-4fb6-a9bb-5579d64984ed,negative]
# tempest.api.compute.servers.test_server_rescue_negative.ServerRescueNegativeTestJSON.test_rescued_vm_attach_volume[id-d0ccac79-0091-4cf4-a1ce-26162d0cc55f,negative,volume]
# tempest.api.compute.servers.test_server_rescue_negative.ServerRescueNegativeTestJSON.test_rescued_vm_detach_volume[id-f56e465b-fe10-48bf-b75d-646cda3a8bc9,negative,volume]
# tempest.api.compute.servers.test_server_rescue_negative.ServerRescueNegativeTestJSON.test_rescued_vm_reboot[id-db22b618-f157-4566-a317-1b6d467a8094,negative]
# tempest.api.compute.servers.test_server_rescue_negative.ServerRescueNegativeTestJSON.test_rescued_vm_rebuild[id-70cdb8a1-89f8-437d-9448-8844fd82bf46,negative]
# tempest.api.compute.servers.test_servers.ServersTestJSON.test_create_server_with_admin_password[id-b92d5ec7-b1dd-44a2-87e4-45e888c46ef0]
# tempest.api.compute.servers.test_servers.ServersTestJSON.test_create_server_with_ipv6_addr_only[id-38fb1d02-c3c5-41de-91d3-9bc2025a75eb]
# tempest.api.compute.servers.test_servers.ServersTestJSON.test_create_specify_keypair[id-f9e15296-d7f9-4e62-b53f-a04e89160833]
# tempest.api.compute.servers.test_servers.ServersTestJSON.test_create_with_existing_server_name[id-8fea6be7-065e-47cf-89b8-496e6f96c699]
# tempest.api.compute.servers.test_servers.ServersTestJSON.test_update_access_server_address[id-89b90870-bc13-4b73-96af-f9d4f2b70077]
# tempest.api.compute.servers.test_servers.ServersTestJSON.test_update_server_name[id-5e6ccff8-349d-4852-a8b3-055df7988dd2]
# tempest.api.compute.servers.test_servers.ServersTestJSON.test_update_server_name_in_stop_state[id-6ac19cb1-27a3-40ec-b350-810bdc04c08e]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_numeric_server_name[id-fd57f159-68d6-4c2a-902b-03070828a87e,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_server_metadata_exceeds_length_limit[id-7fc74810-0bd2-4cd7-8244-4f33a9db865a,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_server_name_length_exceeds_256[id-c3e0fb12-07fc-4d76-a22e-37409887afe8,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_with_invalid_flavor[id-18f5227f-d155-4429-807c-ccb103887537,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_with_invalid_image[id-fcba1052-0a50-4cf3-b1ac-fae241edf02f,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_with_invalid_network_uuid[id-4e72dc2d-44c5-4336-9667-f7972e95c402,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_with_non_existent_keypair[id-7a2efc39-530c-47de-b875-2dd01c8d39bd,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_create_with_nonexistent_security_group[id-c5fa6041-80cd-483b-aa6d-4e45f19d093c,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_delete_non_existent_server[id-1041b4e6-514b-4855-96a5-e974b60870a3,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_delete_server_pass_id_exceeding_length_limit[id-f4d7279b-5fd2-4bf2-9ba4-ae35df0d18c5,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_delete_server_pass_negative_id[id-75f79124-277c-45e6-a373-a1d6803f4cc4,negative]
# **DONE** tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_force_delete_nonexistent_server_id[id-6f47992b-5144-4250-9f8b-f00aa33950f3,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_get_console_output_of_non_existent_server[id-7dd919e7-413f-4198-bebb-35e2a01b13e9,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_get_non_existent_server[id-3436b02f-1b1e-4f03-881e-c6a602327439,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_invalid_access_ip_v4_address[id-7f70a4d1-608f-4794-9e56-cb182765972c,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_invalid_ip_v6_address[id-5226dd80-1e9c-4d8a-b5f9-b26ca4763fd0,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_pause_non_existent_server[id-6a8dc0c6-6cd4-4c0a-9f32-413881828091,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_pause_paused_server[id-d1417e7f-a509-41b5-a102-d5eed8613369,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_personality_file_contents_not_encoded[id-b8a7235e-5246-4a8f-a08e-b34877c6586f,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_reboot_deleted_server[id-581a397d-5eab-486f-9cf9-1014bbd4c984,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_reboot_non_existent_server[id-d4c023a0-9c55-4747-9dd5-413b820143c7,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_rebuild_deleted_server[id-98fa0458-1485-440f-873b-fe7f0d714930,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_rebuild_non_existent_server[id-d86141a7-906e-4731-b187-d64a2ea61422,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_resize_nonexistent_server[id-7ea45b3e-e770-46fa-bfcc-9daaf6d987c0,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_resize_server_with_non_existent_flavor[id-ced1a1d7-2ab6-45c9-b90f-b27d87b30efd,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_resize_server_with_null_flavor[id-45436a7d-a388-4a35-a9d8-3adc5d0d940b,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_restore_nonexistent_server_id[id-9c6d38cc-fcfb-437a-85b9-7b788af8bf01,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_restore_server_invalid_state[id-7fcadfab-bd6a-4753-8db7-4a51e51aade9,negative]
# **DONE** tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_resume_non_existent_server[id-221cd282-bddb-4837-a683-89c2487389b6,negative]
# **DONE** tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_resume_server_invalid_state[id-ccb6294d-c4c9-498f-8a43-554c098bfadb,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_server_name_blank[id-dbbfd247-c40c-449e-8f6c-d2aa7c7da7cf,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_shelve_non_existent_server[id-abca56e2-a892-48ea-b5e5-e07e69774816,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_shelve_shelved_server[id-443e4f9b-e6bf-4389-b601-3a710f15fddd,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_stop_non_existent_server[id-a31460a9-49e1-42aa-82ee-06e0bb7c2d03,negative]
# **DONE** tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_suspend_non_existent_server[id-d1f032d5-7b6e-48aa-b252-d5f16dd994ca,negative]
# **DONE** tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_suspend_server_invalid_state[id-7f323206-05a9-4bf8-996b-dd5b2036501b,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_unpause_non_existent_server[id-705b8e3a-e8a7-477c-a19b-6868fc24ac75,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_unpause_server_invalid_state[id-c8e639a7-ece8-42dd-a2e0-49615917ba4f,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_unshelve_non_existent_server[id-23d23b37-afaf-40d7-aa5d-5726f82d8821,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_unshelve_server_invalid_state[id-8f198ded-1cca-4228-9e65-c6b449c54880,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_update_name_of_non_existent_server[id-aa8eed43-e2cb-4ebf-930b-da14f6a21d81,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_update_server_name_length_exceeds_256[id-5c8e244c-dada-4590-9944-749c455b431f,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestJSON.test_update_server_set_empty_name[id-38204696-17c6-44da-9590-40f87fb5a899,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestMultiTenantJSON.test_delete_a_server_of_another_tenant[id-5c75009d-3eea-423e-bea3-61b09fd25f9c,negative]
# tempest.api.compute.servers.test_servers_negative.ServersNegativeTestMultiTenantJSON.test_update_server_of_another_tenant[id-543d84c1-dd2e-4c6d-8cb2-b9da0efaa384,negative]
# tempest.api.compute.servers.test_virtual_interfaces.VirtualInterfacesTestJSON.test_list_virtual_interfaces[id-96c4e2ef-5e4d-4d7f-87f5-fed6dca18016,network]
# tempest.api.compute.servers.test_virtual_interfaces_negative.VirtualInterfacesNegativeTestJSON.test_list_virtual_interfaces_invalid_server_id[id-64ebd03c-1089-4306-93fa-60f5eb5c803c,negative,network]
# tempest.api.compute.test_extensions.ExtensionsTestJSON.test_get_extension[id-05762f39-bdfa-4cdb-9b46-b78f8e78e2fd]
# tempest.api.compute.test_extensions.ExtensionsTestJSON.test_list_extensions[id-3bb27738-b759-4e0d-a5fa-37d7a6df07d1]
# tempest.api.compute.test_live_block_migration_negative.LiveBlockMigrationNegativeTestJSON.test_invalid_host_for_migration[id-7fb7856e-ae92-44c9-861a-af62d7830bcb,negative]
# tempest.api.compute.test_networks.ComputeNetworksTest.test_list_networks[id-3fe07175-312e-49a5-a623-5f52eeada4c2]
# tempest.api.compute.test_quotas.QuotasTestJSON.test_compare_tenant_quotas_with_default_quotas[id-cd65d997-f7e4-4966-a7e9-d5001b674fdc]
# tempest.api.compute.test_quotas.QuotasTestJSON.test_get_default_quotas[id-9bfecac7-b966-4f47-913f-1a9e2c12134a]
# tempest.api.compute.test_quotas.QuotasTestJSON.test_get_quotas[id-f1ef0a97-dbbb-4cca-adc5-c9fbc4f76107]
# tempest.api.compute.test_tenant_networks.ComputeTenantNetworksTest.test_list_show_tenant_networks[id-edfea98e-bbe3-4c7a-9739-87b986baff26,network]
# **DONE** tempest.api.compute.test_versions.TestVersions.test_get_version_details[id-b953a29e-929c-4a8e-81be-ec3a7e03cb76]
# **DONE** tempest.api.compute.test_versions.TestVersions.test_list_api_versions[id-6c0a0990-43b6-4529-9b61-5fd8daf7c55c]
# tempest.api.compute.volumes.test_attach_volume.AttachVolumeShelveTestJSON.test_attach_detach_volume[id-52e9045a-e90d-4c0d-9087-79d657faffff]
# tempest.api.compute.volumes.test_attach_volume.AttachVolumeShelveTestJSON.test_attach_volume_shelved_or_offload_server[id-13a940b6-3474-4c3c-b03f-29b89112bfee]
# **DONE** tempest.api.compute.volumes.test_attach_volume.AttachVolumeShelveTestJSON.test_detach_volume_shelved_or_offload_server[id-b54e86dd-a070-49c4-9c07-59ae6dae15aa]
# tempest.api.compute.volumes.test_attach_volume.AttachVolumeShelveTestJSON.test_list_get_volume_attachments[id-7fa563fe-f0f7-43eb-9e22-a1ece036b513]
# **DONE** tempest.api.compute.volumes.test_attach_volume.AttachVolumeTestJSON.test_attach_detach_volume[id-52e9045a-e90d-4c0d-9087-79d657faffff]
# **DONE** tempest.api.compute.volumes.test_attach_volume.AttachVolumeTestJSON.test_list_get_volume_attachments[id-7fa563fe-f0f7-43eb-9e22-a1ece036b513]
# tempest.api.compute.volumes.test_volume_snapshots.VolumesSnapshotsTestJSON.test_volume_snapshot_create_get_list_delete[id-cd4ec87d-7825-450d-8040-6e2068f2da8f]
# tempest.api.compute.volumes.test_volumes_get.VolumesGetTestJSON.test_volume_create_get_delete[id-f10f25eb-9775-4d9d-9cbe-1cf54dae9d5f]
# tempest.api.compute.volumes.test_volumes_list.VolumesTestJSON.test_volume_list[id-bc2dd1a0-15af-48e5-9990-f2e75a48325d]
# tempest.api.compute.volumes.test_volumes_list.VolumesTestJSON.test_volume_list_param_limit[id-1048ed81-2baf-487a-b284-c0622b86e7b8]
# tempest.api.compute.volumes.test_volumes_list.VolumesTestJSON.test_volume_list_param_offset_and_limit[id-51c22651-a074-4ea7-af0b-094f9331303e]
# tempest.api.compute.volumes.test_volumes_list.VolumesTestJSON.test_volume_list_with_detail_param_limit[id-33985568-4965-49d5-9bcc-0aa007ca5b7a]
# tempest.api.compute.volumes.test_volumes_list.VolumesTestJSON.test_volume_list_with_detail_param_offset_and_limit[id-06b6abc4-3f10-48e9-a7a1-3facc98f03e5]
# tempest.api.compute.volumes.test_volumes_list.VolumesTestJSON.test_volume_list_with_details[id-bad0567a-5a4f-420b-851e-780b55bb867c]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_create_volume_with_invalid_size[id-5125ae14-152b-40a7-b3c5-eae15e9022ef,negative]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_create_volume_with_out_passing_size[id-131cb3a1-75cc-4d40-b4c3-1317f64719b0,negative]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_create_volume_with_size_zero[id-8cce995e-0a83-479a-b94d-e1e40b8a09d1,negative]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_delete_invalid_volume_id[id-62972737-124b-4513-b6cf-2f019f178494,negative]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_delete_volume_without_passing_volume_id[id-0d1417c5-4ae8-4c2c-adc5-5f0b864253e5,negative]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_get_invalid_volume_id[id-f01904f2-e975-4915-98ce-cb5fa27bde4f,negative]
# **DONE** tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_get_volume_without_passing_volume_id[id-62bab09a-4c03-4617-8cca-8572bc94af9b,negative]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_volume_delete_nonexistent_volume_id[id-54a34226-d910-4b00-9ef8-8683e6c55846,negative]
# tempest.api.compute.volumes.test_volumes_negative.VolumesNegativeTest.test_volume_get_nonexistent_volume_id[id-c03ea686-905b-41a2-8748-9635154b7c57,negative]

View File

@ -1,326 +0,0 @@
#!/bin/bash -e
#
# 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.
export DEST=$BASE/new
export TEMPEST_DIR=$DEST/tempest
export TEMPEST_CONF=$TEMPEST_DIR/etc/tempest.conf
# preparation for the tests
cd $TEMPEST_DIR
# Run functional test
echo "Running Tricircle functional test suite..."
# all test cases with following prefix
TESTCASES="(tempest.api.volume.test_volumes_list"
TESTCASES="$TESTCASES|tempest.api.volume.test_volumes_get"
# add new test cases like following line for volume_type test
# TESTCASES="$TESTCASES|tempest.api.volume.admin.test_volumes_type"
TESTCASES="$TESTCASES)"
ostestr --regex $TESTCASES
# --------------------- IMPORTANT begin -------------------- #
# all following test cases are from Cinder tempest test cases,
# the purpose to list them here is to check which test cases
# are still not covered and tested in Cinder-APIGW.
#
# Those test cases which have been covered by ostestr running
# above should be marked with **DONE** after the "#".
# please leave the length of each line > 80 characters in order
# to keep one test case one line.
#
# When you add new feature to Cinder-APIGW, please select
# proper test cases to test against the feature, and marked
# these test cases with **DONE** after the "#". For those test
# cases which are not needed to be tested in Cinder-APIGW, for
# example V1(which has been deprecated) should be marked with
# **SKIP** after "#"
#
# The test cases running through ostestr could be filtered
# by regex expression, for example, for Cinder volume type
# releated test cases could be executed by a single clause:
# ostestr --regex tempest.api.volume.admin.test_volume_types
# --------------------- IMPORTANT end -----------------------#
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV1Test.test_backend_name_distinction[id-46435ab1-a0af-4401-8373-f14e66b0dd58]
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV1Test.test_backend_name_distinction_with_prefix[id-4236305b-b65a-4bfc-a9d2-69cb5b2bf2ed]
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV1Test.test_backend_name_reporting[id-c1a41f3f-9dad-493e-9f09-3ff197d477cc]
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV1Test.test_backend_name_reporting_with_prefix[id-f38e647f-ab42-4a31-a2e7-ca86a6485215]
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV2Test.test_backend_name_distinction[id-46435ab1-a0af-4401-8373-f14e66b0dd58]
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV2Test.test_backend_name_distinction_with_prefix[id-4236305b-b65a-4bfc-a9d2-69cb5b2bf2ed]
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV2Test.test_backend_name_reporting[id-c1a41f3f-9dad-493e-9f09-3ff197d477cc]
# tempest.api.volume.admin.test_multi_backend.VolumeMultiBackendV2Test.test_backend_name_reporting_with_prefix[id-f38e647f-ab42-4a31-a2e7-ca86a6485215]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV1Test.test_reset_snapshot_status[id-3e13ca2f-48ea-49f3-ae1a-488e9180d535]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV1Test.test_snapshot_force_delete_when_snapshot_is_creating[id-05f711b6-e629-4895-8103-7ca069f2073a]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV1Test.test_snapshot_force_delete_when_snapshot_is_deleting[id-92ce8597-b992-43a1-8868-6316b22a969e]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV1Test.test_snapshot_force_delete_when_snapshot_is_error[id-645a4a67-a1eb-4e8e-a547-600abac1525d]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV1Test.test_snapshot_force_delete_when_snapshot_is_error_deleting[id-bf89080f-8129-465e-9327-b2f922666ba5]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV1Test.test_update_snapshot_status[id-41288afd-d463-485e-8f6e-4eea159413eb]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV2Test.test_reset_snapshot_status[id-3e13ca2f-48ea-49f3-ae1a-488e9180d535]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV2Test.test_snapshot_force_delete_when_snapshot_is_creating[id-05f711b6-e629-4895-8103-7ca069f2073a]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV2Test.test_snapshot_force_delete_when_snapshot_is_deleting[id-92ce8597-b992-43a1-8868-6316b22a969e]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV2Test.test_snapshot_force_delete_when_snapshot_is_error[id-645a4a67-a1eb-4e8e-a547-600abac1525d]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV2Test.test_snapshot_force_delete_when_snapshot_is_error_deleting[id-bf89080f-8129-465e-9327-b2f922666ba5]
# tempest.api.volume.admin.test_snapshots_actions.SnapshotsActionsV2Test.test_update_snapshot_status[id-41288afd-d463-485e-8f6e-4eea159413eb]
# tempest.api.volume.admin.test_volume_hosts.VolumeHostsAdminV1TestsJSON.test_list_hosts[id-d5f3efa2-6684-4190-9ced-1c2f526352ad]
# tempest.api.volume.admin.test_volume_hosts.VolumeHostsAdminV2TestsJSON.test_list_hosts[id-d5f3efa2-6684-4190-9ced-1c2f526352ad]
# tempest.api.volume.admin.test_volume_quotas.BaseVolumeQuotasAdminV2TestJSON.test_delete_quota[id-874b35a9-51f1-4258-bec5-cd561b6690d3]
# tempest.api.volume.admin.test_volume_quotas.BaseVolumeQuotasAdminV2TestJSON.test_list_default_quotas[id-2be020a2-5fdd-423d-8d35-a7ffbc36e9f7]
# tempest.api.volume.admin.test_volume_quotas.BaseVolumeQuotasAdminV2TestJSON.test_list_quotas[id-59eada70-403c-4cef-a2a3-a8ce2f1b07a0]
# tempest.api.volume.admin.test_volume_quotas.BaseVolumeQuotasAdminV2TestJSON.test_quota_usage[id-ae8b6091-48ad-4bfa-a188-bbf5cc02115f]
# tempest.api.volume.admin.test_volume_quotas.BaseVolumeQuotasAdminV2TestJSON.test_show_quota_usage[id-18c51ae9-cb03-48fc-b234-14a19374dbed]
# tempest.api.volume.admin.test_volume_quotas.BaseVolumeQuotasAdminV2TestJSON.test_update_all_quota_resources_for_tenant[id-3d45c99e-cc42-4424-a56e-5cbd212b63a6]
# tempest.api.volume.admin.test_volume_quotas.VolumeQuotasAdminV1TestJSON.test_delete_quota[id-874b35a9-51f1-4258-bec5-cd561b6690d3]
# tempest.api.volume.admin.test_volume_quotas.VolumeQuotasAdminV1TestJSON.test_list_default_quotas[id-2be020a2-5fdd-423d-8d35-a7ffbc36e9f7]
# tempest.api.volume.admin.test_volume_quotas.VolumeQuotasAdminV1TestJSON.test_list_quotas[id-59eada70-403c-4cef-a2a3-a8ce2f1b07a0]
# tempest.api.volume.admin.test_volume_quotas.VolumeQuotasAdminV1TestJSON.test_quota_usage[id-ae8b6091-48ad-4bfa-a188-bbf5cc02115f]
# tempest.api.volume.admin.test_volume_quotas.VolumeQuotasAdminV1TestJSON.test_show_quota_usage[id-18c51ae9-cb03-48fc-b234-14a19374dbed]
# tempest.api.volume.admin.test_volume_quotas.VolumeQuotasAdminV1TestJSON.test_update_all_quota_resources_for_tenant[id-3d45c99e-cc42-4424-a56e-5cbd212b63a6]
# tempest.api.volume.admin.test_volume_quotas_negative.BaseVolumeQuotasNegativeV2TestJSON.test_quota_volume_gigabytes[id-2dc27eee-8659-4298-b900-169d71a91374,negative]
# tempest.api.volume.admin.test_volume_quotas_negative.BaseVolumeQuotasNegativeV2TestJSON.test_quota_volumes[id-bf544854-d62a-47f2-a681-90f7a47d86b6,negative]
# tempest.api.volume.admin.test_volume_quotas_negative.VolumeQuotasNegativeV1TestJSON.test_quota_volume_gigabytes[id-2dc27eee-8659-4298-b900-169d71a91374,negative]
# tempest.api.volume.admin.test_volume_quotas_negative.VolumeQuotasNegativeV1TestJSON.test_quota_volumes[id-bf544854-d62a-47f2-a681-90f7a47d86b6,negative]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV1TestJSON.test_get_service_by_host_name[id-178710e4-7596-4e08-9333-745cb8bc4f8d]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV1TestJSON.test_get_service_by_service_and_host_name[id-ffa6167c-4497-4944-a464-226bbdb53908]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV1TestJSON.test_get_service_by_service_binary_name[id-63a3e1ca-37ee-4983-826d-83276a370d25]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV1TestJSON.test_list_services[id-e0218299-0a59-4f43-8b2b-f1c035b3d26d]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV2TestJSON.test_get_service_by_host_name[id-178710e4-7596-4e08-9333-745cb8bc4f8d]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV2TestJSON.test_get_service_by_service_and_host_name[id-ffa6167c-4497-4944-a464-226bbdb53908]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV2TestJSON.test_get_service_by_service_binary_name[id-63a3e1ca-37ee-4983-826d-83276a370d25]
# tempest.api.volume.admin.test_volume_services.VolumesServicesV2TestJSON.test_list_services[id-e0218299-0a59-4f43-8b2b-f1c035b3d26d]
# tempest.api.volume.admin.test_volume_snapshot_quotas_negative.VolumeSnapshotNegativeV1TestJSON.test_quota_volume_gigabytes_snapshots[id-c99a1ca9-6cdf-498d-9fdf-25832babef27,negative]
# tempest.api.volume.admin.test_volume_snapshot_quotas_negative.VolumeSnapshotNegativeV1TestJSON.test_quota_volume_snapshots[id-02bbf63f-6c05-4357-9d98-2926a94064ff,negative]
# tempest.api.volume.admin.test_volume_snapshot_quotas_negative.VolumeSnapshotQuotasNegativeV2TestJSON.test_quota_volume_gigabytes_snapshots[id-c99a1ca9-6cdf-498d-9fdf-25832babef27,negative]
# tempest.api.volume.admin.test_volume_snapshot_quotas_negative.VolumeSnapshotQuotasNegativeV2TestJSON.test_quota_volume_snapshots[id-02bbf63f-6c05-4357-9d98-2926a94064ff,negative]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV1Test.test_volume_crud_with_volume_type_and_extra_specs[id-c03cc62c-f4e9-4623-91ec-64ce2f9c1260]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV1Test.test_volume_type_create_get_delete[id-4e955c3b-49db-4515-9590-0c99f8e471ad]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV1Test.test_volume_type_encryption_create_get_delete[id-7830abd0-ff99-4793-a265-405684a54d46]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV1Test.test_volume_type_list[id-9d9b28e3-1b2e-4483-a2cc-24aa0ea1de54]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV2Test.test_volume_crud_with_volume_type_and_extra_specs[id-c03cc62c-f4e9-4623-91ec-64ce2f9c1260]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV2Test.test_volume_type_create_get_delete[id-4e955c3b-49db-4515-9590-0c99f8e471ad]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV2Test.test_volume_type_encryption_create_get_delete[id-7830abd0-ff99-4793-a265-405684a54d46]
# tempest.api.volume.admin.test_volume_types.VolumeTypesV2Test.test_volume_type_list[id-9d9b28e3-1b2e-4483-a2cc-24aa0ea1de54]
# tempest.api.volume.admin.test_volume_types_extra_specs.VolumeTypesExtraSpecsV1Test.test_volume_type_extra_spec_create_get_delete[id-d4772798-601f-408a-b2a5-29e8a59d1220]
# tempest.api.volume.admin.test_volume_types_extra_specs.VolumeTypesExtraSpecsV1Test.test_volume_type_extra_specs_list[id-b42923e9-0452-4945-be5b-d362ae533e60]
# tempest.api.volume.admin.test_volume_types_extra_specs.VolumeTypesExtraSpecsV1Test.test_volume_type_extra_specs_update[id-0806db36-b4a0-47a1-b6f3-c2e7f194d017]
# tempest.api.volume.admin.test_volume_types_extra_specs.VolumeTypesExtraSpecsV2Test.test_volume_type_extra_spec_create_get_delete[id-d4772798-601f-408a-b2a5-29e8a59d1220]
# tempest.api.volume.admin.test_volume_types_extra_specs.VolumeTypesExtraSpecsV2Test.test_volume_type_extra_specs_list[id-b42923e9-0452-4945-be5b-d362ae533e60]
# tempest.api.volume.admin.test_volume_types_extra_specs.VolumeTypesExtraSpecsV2Test.test_volume_type_extra_specs_update[id-0806db36-b4a0-47a1-b6f3-c2e7f194d017]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_create_invalid_body[id-bc772c71-1ed4-4716-b945-8b5ed0f15e87]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_create_none_body[id-c821bdc8-43a4-4bf4-86c8-82f3858d5f7d]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_create_nonexistent_type_id[id-49d5472c-a53d-4eab-a4d3-450c4db1c545]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_delete_nonexistent_volume_type_id[id-031cda8b-7d23-4246-8bf6-bbe73fd67074]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_get_nonexistent_extra_spec_id[id-c881797d-12ff-4f1a-b09d-9f6212159753]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_get_nonexistent_volume_type_id[id-9f402cbd-1838-4eb4-9554-126a6b1908c9]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_list_nonexistent_volume_type_id[id-dee5cf0c-cdd6-4353-b70c-e847050d71fb]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_update_multiple_extra_spec[id-a77dfda2-9100-448e-9076-ed1711f4bdfc]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_update_no_body[id-08961d20-5cbb-4910-ac0f-89ad6dbb2da1]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_update_none_extra_spec_id[id-9bf7a657-b011-4aec-866d-81c496fbe5c8]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV1Test.test_update_nonexistent_extra_spec_id[id-25e5a0ee-89b3-4c53-8310-236f76c75365]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_create_invalid_body[id-bc772c71-1ed4-4716-b945-8b5ed0f15e87]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_create_none_body[id-c821bdc8-43a4-4bf4-86c8-82f3858d5f7d]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_create_nonexistent_type_id[id-49d5472c-a53d-4eab-a4d3-450c4db1c545]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_delete_nonexistent_volume_type_id[id-031cda8b-7d23-4246-8bf6-bbe73fd67074]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_get_nonexistent_extra_spec_id[id-c881797d-12ff-4f1a-b09d-9f6212159753]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_get_nonexistent_volume_type_id[id-9f402cbd-1838-4eb4-9554-126a6b1908c9]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_list_nonexistent_volume_type_id[id-dee5cf0c-cdd6-4353-b70c-e847050d71fb]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_update_multiple_extra_spec[id-a77dfda2-9100-448e-9076-ed1711f4bdfc]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_update_no_body[id-08961d20-5cbb-4910-ac0f-89ad6dbb2da1]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_update_none_extra_spec_id[id-9bf7a657-b011-4aec-866d-81c496fbe5c8]
# tempest.api.volume.admin.test_volume_types_extra_specs_negative.ExtraSpecsNegativeV2Test.test_update_nonexistent_extra_spec_id[id-25e5a0ee-89b3-4c53-8310-236f76c75365]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV1Test.test_create_with_empty_name[id-878b4e57-faa2-4659-b0d1-ce740a06ae81]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV1Test.test_create_with_nonexistent_volume_type[id-b48c98f2-e662-4885-9b71-032256906314]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV1Test.test_delete_nonexistent_type_id[id-6b3926d2-7d73-4896-bc3d-e42dfd11a9f6]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV1Test.test_get_nonexistent_type_id[id-994610d6-0476-4018-a644-a2602ef5d4aa]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV2Test.test_create_with_empty_name[id-878b4e57-faa2-4659-b0d1-ce740a06ae81]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV2Test.test_create_with_nonexistent_volume_type[id-b48c98f2-e662-4885-9b71-032256906314]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV2Test.test_delete_nonexistent_type_id[id-6b3926d2-7d73-4896-bc3d-e42dfd11a9f6]
# tempest.api.volume.admin.test_volume_types_negative.VolumeTypesNegativeV2Test.test_get_nonexistent_type_id[id-994610d6-0476-4018-a644-a2602ef5d4aa]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV1Test.test_volume_force_delete_when_volume_is_attaching[id-db8d607a-aa2e-4beb-b51d-d4005c232011]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV1Test.test_volume_force_delete_when_volume_is_creating[id-21737d5a-92f2-46d7-b009-a0cc0ee7a570]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV1Test.test_volume_force_delete_when_volume_is_error[id-3e33a8a8-afd4-4d64-a86b-c27a185c5a4a]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV1Test.test_volume_reset_status[id-d063f96e-a2e0-4f34-8b8a-395c42de1845]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV2Test.test_volume_force_delete_when_volume_is_attaching[id-db8d607a-aa2e-4beb-b51d-d4005c232011]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV2Test.test_volume_force_delete_when_volume_is_creating[id-21737d5a-92f2-46d7-b009-a0cc0ee7a570]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV2Test.test_volume_force_delete_when_volume_is_error[id-3e33a8a8-afd4-4d64-a86b-c27a185c5a4a]
# tempest.api.volume.admin.test_volumes_actions.VolumesActionsV2Test.test_volume_reset_status[id-d063f96e-a2e0-4f34-8b8a-395c42de1845]
# tempest.api.volume.admin.test_volumes_backup.VolumesBackupsV1Test.test_volume_backup_create_get_detailed_list_restore_delete[id-a66eb488-8ee1-47d4-8e9f-575a095728c6]
# tempest.api.volume.admin.test_volumes_backup.VolumesBackupsV1Test.test_volume_backup_export_import[id-a99c54a1-dd80-4724-8a13-13bf58d4068d]
# tempest.api.volume.admin.test_volumes_backup.VolumesBackupsV2Test.test_volume_backup_create_get_detailed_list_restore_delete[id-a66eb488-8ee1-47d4-8e9f-575a095728c6]
# tempest.api.volume.admin.test_volumes_backup.VolumesBackupsV2Test.test_volume_backup_export_import[id-a99c54a1-dd80-4724-8a13-13bf58d4068d]
# tempest.api.volume.test_availability_zone.AvailabilityZoneV1TestJSON.test_get_availability_zone_list[id-01f1ae88-eba9-4c6b-a011-6f7ace06b725]
# tempest.api.volume.test_availability_zone.AvailabilityZoneV2TestJSON.test_get_availability_zone_list[id-01f1ae88-eba9-4c6b-a011-6f7ace06b725]
# tempest.api.volume.test_extensions.ExtensionsV1TestJSON.test_list_extensions[id-94607eb0-43a5-47ca-82aa-736b41bd2e2c]
# tempest.api.volume.test_extensions.ExtensionsV2TestJSON.test_list_extensions[id-94607eb0-43a5-47ca-82aa-736b41bd2e2c]
# tempest.api.volume.test_qos.QosSpecsV1TestJSON.test_associate_disassociate_qos[id-1dd93c76-6420-485d-a771-874044c416ac]
# tempest.api.volume.test_qos.QosSpecsV1TestJSON.test_create_delete_qos_with_back_end_consumer[id-b115cded-8f58-4ee4-aab5-9192cfada08f]
# tempest.api.volume.test_qos.QosSpecsV1TestJSON.test_create_delete_qos_with_both_consumer[id-f88d65eb-ea0d-487d-af8d-71f4011575a4]
# tempest.api.volume.test_qos.QosSpecsV1TestJSON.test_create_delete_qos_with_front_end_consumer[id-7e15f883-4bef-49a9-95eb-f94209a1ced1]
# tempest.api.volume.test_qos.QosSpecsV1TestJSON.test_get_qos[id-7aa214cc-ac1a-4397-931f-3bb2e83bb0fd]
# tempest.api.volume.test_qos.QosSpecsV1TestJSON.test_list_qos[id-75e04226-bcf7-4595-a34b-fdf0736f38fc]
# tempest.api.volume.test_qos.QosSpecsV1TestJSON.test_set_unset_qos_key[id-ed00fd85-4494-45f2-8ceb-9e2048919aed]
# tempest.api.volume.test_qos.QosSpecsV2TestJSON.test_associate_disassociate_qos[id-1dd93c76-6420-485d-a771-874044c416ac]
# tempest.api.volume.test_qos.QosSpecsV2TestJSON.test_create_delete_qos_with_back_end_consumer[id-b115cded-8f58-4ee4-aab5-9192cfada08f]
# tempest.api.volume.test_qos.QosSpecsV2TestJSON.test_create_delete_qos_with_both_consumer[id-f88d65eb-ea0d-487d-af8d-71f4011575a4]
# tempest.api.volume.test_qos.QosSpecsV2TestJSON.test_create_delete_qos_with_front_end_consumer[id-7e15f883-4bef-49a9-95eb-f94209a1ced1]
# tempest.api.volume.test_qos.QosSpecsV2TestJSON.test_get_qos[id-7aa214cc-ac1a-4397-931f-3bb2e83bb0fd]
# tempest.api.volume.test_qos.QosSpecsV2TestJSON.test_list_qos[id-75e04226-bcf7-4595-a34b-fdf0736f38fc]
# tempest.api.volume.test_qos.QosSpecsV2TestJSON.test_set_unset_qos_key[id-ed00fd85-4494-45f2-8ceb-9e2048919aed]
# tempest.api.volume.test_snapshot_metadata.SnapshotV1MetadataTestJSON.test_create_get_delete_snapshot_metadata[id-a2f20f99-e363-4584-be97-bc33afb1a56c]
# tempest.api.volume.test_snapshot_metadata.SnapshotV1MetadataTestJSON.test_update_snapshot_metadata[id-bd2363bc-de92-48a4-bc98-28943c6e4be1]
# tempest.api.volume.test_snapshot_metadata.SnapshotV1MetadataTestJSON.test_update_snapshot_metadata_item[id-e8ff85c5-8f97-477f-806a-3ac364a949ed]
# tempest.api.volume.test_snapshot_metadata.SnapshotV2MetadataTestJSON.test_create_get_delete_snapshot_metadata[id-a2f20f99-e363-4584-be97-bc33afb1a56c]
# tempest.api.volume.test_snapshot_metadata.SnapshotV2MetadataTestJSON.test_update_snapshot_metadata[id-bd2363bc-de92-48a4-bc98-28943c6e4be1]
# tempest.api.volume.test_snapshot_metadata.SnapshotV2MetadataTestJSON.test_update_snapshot_metadata_item[id-e8ff85c5-8f97-477f-806a-3ac364a949ed]
# tempest.api.volume.test_volume_metadata.VolumesV1MetadataTest.test_create_get_delete_volume_metadata[id-6f5b125b-f664-44bf-910f-751591fe5769]
# tempest.api.volume.test_volume_metadata.VolumesV1MetadataTest.test_update_volume_metadata[id-774d2918-9beb-4f30-b3d1-2a4e8179ec0a]
# tempest.api.volume.test_volume_metadata.VolumesV1MetadataTest.test_update_volume_metadata_item[id-862261c5-8df4-475a-8c21-946e50e36a20]
# tempest.api.volume.test_volume_metadata.VolumesV2MetadataTest.test_create_get_delete_volume_metadata[id-6f5b125b-f664-44bf-910f-751591fe5769]
# tempest.api.volume.test_volume_metadata.VolumesV2MetadataTest.test_update_volume_metadata[id-774d2918-9beb-4f30-b3d1-2a4e8179ec0a]
# tempest.api.volume.test_volume_metadata.VolumesV2MetadataTest.test_update_volume_metadata_item[id-862261c5-8df4-475a-8c21-946e50e36a20]
# tempest.api.volume.test_volume_transfers.VolumesV1TransfersTest.test_create_get_list_accept_volume_transfer[id-4d75b645-a478-48b1-97c8-503f64242f1a]
# tempest.api.volume.test_volume_transfers.VolumesV1TransfersTest.test_create_list_delete_volume_transfer[id-ab526943-b725-4c07-b875-8e8ef87a2c30]
# tempest.api.volume.test_volume_transfers.VolumesV2TransfersTest.test_create_get_list_accept_volume_transfer[id-4d75b645-a478-48b1-97c8-503f64242f1a]
# tempest.api.volume.test_volume_transfers.VolumesV2TransfersTest.test_create_list_delete_volume_transfer[id-ab526943-b725-4c07-b875-8e8ef87a2c30]
# tempest.api.volume.test_volumes_actions.VolumesV1ActionsTest.test_attach_detach_volume_to_instance[compute,id-fff42874-7db5-4487-a8e1-ddda5fb5288d,smoke,stress]
# tempest.api.volume.test_volumes_actions.VolumesV1ActionsTest.test_get_volume_attachment[compute,id-9516a2c8-9135-488c-8dd6-5677a7e5f371,stress]
# tempest.api.volume.test_volumes_actions.VolumesV1ActionsTest.test_reserve_unreserve_volume[id-92c4ef64-51b2-40c0-9f7e-4749fbaaba33]
# tempest.api.volume.test_volumes_actions.VolumesV1ActionsTest.test_volume_bootable[id-63e21b4c-0a0c-41f6-bfc3-7c2816815599]
# tempest.api.volume.test_volumes_actions.VolumesV1ActionsTest.test_volume_readonly_update[id-fff74e1e-5bd3-4b33-9ea9-24c103bc3f59]
# tempest.api.volume.test_volumes_actions.VolumesV1ActionsTest.test_volume_upload[id-d8f1ca95-3d5b-44a3-b8ca-909691c9532d,image]
# tempest.api.volume.test_volumes_actions.VolumesV2ActionsTest.test_attach_detach_volume_to_instance[compute,id-fff42874-7db5-4487-a8e1-ddda5fb5288d,smoke,stress]
# tempest.api.volume.test_volumes_actions.VolumesV2ActionsTest.test_get_volume_attachment[compute,id-9516a2c8-9135-488c-8dd6-5677a7e5f371,stress]
# tempest.api.volume.test_volumes_actions.VolumesV2ActionsTest.test_reserve_unreserve_volume[id-92c4ef64-51b2-40c0-9f7e-4749fbaaba33]
# tempest.api.volume.test_volumes_actions.VolumesV2ActionsTest.test_volume_bootable[id-63e21b4c-0a0c-41f6-bfc3-7c2816815599]
# tempest.api.volume.test_volumes_actions.VolumesV2ActionsTest.test_volume_readonly_update[id-fff74e1e-5bd3-4b33-9ea9-24c103bc3f59]
# tempest.api.volume.test_volumes_actions.VolumesV2ActionsTest.test_volume_upload[id-d8f1ca95-3d5b-44a3-b8ca-909691c9532d,image]
# tempest.api.volume.test_volumes_extend.VolumesV1ExtendTest.test_volume_extend[id-9a36df71-a257-43a5-9555-dc7c88e66e0e]
# tempest.api.volume.test_volumes_extend.VolumesV2ExtendTest.test_volume_extend[id-9a36df71-a257-43a5-9555-dc7c88e66e0e]
# **SKIP** tempest.api.volume.test_volumes_get.VolumesV1GetTest.test_volume_create_get_update_delete[id-27fb0e9f-fb64-41dd-8bdb-1ffa762f0d51,smoke]
# **SKIP** tempest.api.volume.test_volumes_get.VolumesV1GetTest.test_volume_create_get_update_delete_as_clone[id-3f591b4a-7dc6-444c-bd51-77469506b3a1]
# **SKIP** tempest.api.volume.test_volumes_get.VolumesV1GetTest.test_volume_create_get_update_delete_from_image[id-54a01030-c7fc-447c-86ee-c1182beae638,image,smoke]
# **DONE** tempest.api.volume.test_volumes_get.VolumesV2GetTest.test_volume_create_get_update_delete[id-27fb0e9f-fb64-41dd-8bdb-1ffa762f0d51,smoke]
# **DONE** tempest.api.volume.test_volumes_get.VolumesV2GetTest.test_volume_create_get_update_delete_as_clone[id-3f591b4a-7dc6-444c-bd51-77469506b3a1]
# **DONE** tempest.api.volume.test_volumes_get.VolumesV2GetTest.test_volume_create_get_update_delete_from_image[id-54a01030-c7fc-447c-86ee-c1182beae638,image,smoke]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list[id-0b6ddd39-b948-471f-8038-4787978747c4,smoke]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list_by_name[id-a28e8da4-0b56-472f-87a8-0f4d3f819c02]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list_details_by_name[id-2de3a6d4-12aa-403b-a8f2-fdeb42a89623]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list_param_display_name_and_status[id-777c87c1-2fc4-4883-8b8e-5c0b951d1ec8]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list_with_detail_param_display_name_and_status[id-856ab8ca-6009-4c37-b691-be1065528ad4]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list_with_detail_param_metadata[id-1ca92d3c-4a8e-4b43-93f5-e4c7fb3b291d]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list_with_details[id-adcbb5a7-5ad8-4b61-bd10-5380e111a877]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volume_list_with_param_metadata[id-b5ebea1b-0603-40a0-bb41-15fcd0a53214]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volumes_list_by_availability_zone[id-c0cfa863-3020-40d7-b587-e35f597d5d87]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volumes_list_by_status[id-39654e13-734c-4dab-95ce-7613bf8407ce]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volumes_list_details_by_availability_zone[id-e1b80d13-94f0-4ba2-a40e-386af29f8db1]
# **SKIP** tempest.api.volume.test_volumes_list.VolumesV1ListTestJSON.test_volumes_list_details_by_status[id-2943f712-71ec-482a-bf49-d5ca06216b9f]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list[id-0b6ddd39-b948-471f-8038-4787978747c4,smoke]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_by_name[id-a28e8da4-0b56-472f-87a8-0f4d3f819c02]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_details_by_name[id-2de3a6d4-12aa-403b-a8f2-fdeb42a89623]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_param_display_name_and_status[id-777c87c1-2fc4-4883-8b8e-5c0b951d1ec8]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_with_detail_param_display_name_and_status[id-856ab8ca-6009-4c37-b691-be1065528ad4]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_with_detail_param_metadata[id-1ca92d3c-4a8e-4b43-93f5-e4c7fb3b291d]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_with_details[id-adcbb5a7-5ad8-4b61-bd10-5380e111a877]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_with_param_metadata[id-b5ebea1b-0603-40a0-bb41-15fcd0a53214]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volumes_list_by_availability_zone[id-c0cfa863-3020-40d7-b587-e35f597d5d87]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volumes_list_by_status[id-39654e13-734c-4dab-95ce-7613bf8407ce]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volumes_list_details_by_availability_zone[id-e1b80d13-94f0-4ba2-a40e-386af29f8db1]
# **DONE** tempest.api.volume.test_volumes_list.VolumesV2ListTestJSON.test_volumes_list_details_by_status[id-2943f712-71ec-482a-bf49-d5ca06216b9f]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_attach_volumes_with_nonexistent_volume_id[compute,id-f5e56b0a-5d02-43c1-a2a7-c9b792c2e3f6,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_create_volume_with_invalid_size[id-1ed83a8a-682d-4dfb-a30e-ee63ffd6c049,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_create_volume_with_nonexistent_snapshot_id[id-0c36f6ae-4604-4017-b0a9-34fdc63096f9,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_create_volume_with_nonexistent_source_volid[id-47c73e08-4be8-45bb-bfdf-0c4e79b88344,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_create_volume_with_nonexistent_volume_type[id-10254ed8-3849-454e-862e-3ab8e6aa01d2,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_create_volume_with_out_passing_size[id-9387686f-334f-4d31-a439-33494b9e2683,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_create_volume_with_size_negative[id-8b472729-9eba-446e-a83b-916bdb34bef7,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_create_volume_with_size_zero[id-41331caa-eaf4-4001-869d-bc18c1869360,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_delete_invalid_volume_id[id-1f035827-7c32-4019-9240-b4ec2dbd9dfd,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_delete_volume_without_passing_volume_id[id-441a1550-5d44-4b30-af0f-a6d402f52026,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_detach_volumes_with_invalid_volume_id[id-9f9c24e4-011d-46b5-b992-952140ce237a,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_get_invalid_volume_id[id-30799cfd-7ee4-446c-b66c-45b383ed211b,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_get_volume_without_passing_volume_id[id-c6c3db06-29ad-4e91-beb0-2ab195fe49e3,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_list_volumes_detail_with_invalid_status[id-ba94b27b-be3f-496c-a00e-0283b373fa75,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_list_volumes_detail_with_nonexistent_name[id-9ca17820-a0e7-4cbd-a7fa-f4468735e359,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_list_volumes_with_invalid_status[id-143b279b-7522-466b-81be-34a87d564a7c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_list_volumes_with_nonexistent_name[id-0f4aa809-8c7b-418f-8fb3-84c7a5dfc52f,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_reserve_volume_with_negative_volume_status[id-449c4ed2-ecdd-47bb-98dc-072aeccf158c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_reserve_volume_with_nonexistent_volume_id[id-ac6084c0-0546-45f9-b284-38a367e0e0e2,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_unreserve_volume_with_nonexistent_volume_id[id-eb467654-3dc1-4a72-9b46-47c29d22654c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_update_volume_with_empty_volume_id[id-72aeca85-57a5-4c1f-9057-f320f9ea575b,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_update_volume_with_invalid_volume_id[id-e66e40d6-65e6-4e75-bdc7-636792fa152d,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_update_volume_with_nonexistent_volume_id[id-0186422c-999a-480e-a026-6a665744c30c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_volume_delete_nonexistent_volume_id[id-555efa6e-efcd-44ef-8a3b-4a7ca4837a29,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_volume_extend_with_None_size[id-355218f1-8991-400a-a6bb-971239287d92,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_volume_extend_with_non_number_size[id-5d0b480d-e833-439f-8a5a-96ad2ed6f22f,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_volume_extend_with_nonexistent_volume_id[id-8f05a943-013c-4063-ac71-7baf561e82eb,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_volume_extend_with_size_smaller_than_original_size[id-e0c75c74-ee34-41a9-9288-2a2051452854,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_volume_extend_without_passing_volume_id[id-aff8ba64-6d6f-4f2e-bc33-41a08ee9f115,negative]
# tempest.api.volume.test_volumes_negative.VolumesV1NegativeTest.test_volume_get_nonexistent_volume_id[id-f131c586-9448-44a4-a8b0-54ca838aa43e,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_attach_volumes_with_nonexistent_volume_id[compute,id-f5e56b0a-5d02-43c1-a2a7-c9b792c2e3f6,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_create_volume_with_invalid_size[id-1ed83a8a-682d-4dfb-a30e-ee63ffd6c049,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_create_volume_with_nonexistent_snapshot_id[id-0c36f6ae-4604-4017-b0a9-34fdc63096f9,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_create_volume_with_nonexistent_source_volid[id-47c73e08-4be8-45bb-bfdf-0c4e79b88344,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_create_volume_with_nonexistent_volume_type[id-10254ed8-3849-454e-862e-3ab8e6aa01d2,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_create_volume_with_out_passing_size[id-9387686f-334f-4d31-a439-33494b9e2683,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_create_volume_with_size_negative[id-8b472729-9eba-446e-a83b-916bdb34bef7,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_create_volume_with_size_zero[id-41331caa-eaf4-4001-869d-bc18c1869360,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_delete_invalid_volume_id[id-1f035827-7c32-4019-9240-b4ec2dbd9dfd,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_delete_volume_without_passing_volume_id[id-441a1550-5d44-4b30-af0f-a6d402f52026,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_detach_volumes_with_invalid_volume_id[id-9f9c24e4-011d-46b5-b992-952140ce237a,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_get_invalid_volume_id[id-30799cfd-7ee4-446c-b66c-45b383ed211b,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_get_volume_without_passing_volume_id[id-c6c3db06-29ad-4e91-beb0-2ab195fe49e3,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_list_volumes_detail_with_invalid_status[id-ba94b27b-be3f-496c-a00e-0283b373fa75,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_list_volumes_detail_with_nonexistent_name[id-9ca17820-a0e7-4cbd-a7fa-f4468735e359,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_list_volumes_with_invalid_status[id-143b279b-7522-466b-81be-34a87d564a7c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_list_volumes_with_nonexistent_name[id-0f4aa809-8c7b-418f-8fb3-84c7a5dfc52f,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_reserve_volume_with_negative_volume_status[id-449c4ed2-ecdd-47bb-98dc-072aeccf158c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_reserve_volume_with_nonexistent_volume_id[id-ac6084c0-0546-45f9-b284-38a367e0e0e2,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_unreserve_volume_with_nonexistent_volume_id[id-eb467654-3dc1-4a72-9b46-47c29d22654c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_update_volume_with_empty_volume_id[id-72aeca85-57a5-4c1f-9057-f320f9ea575b,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_update_volume_with_invalid_volume_id[id-e66e40d6-65e6-4e75-bdc7-636792fa152d,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_update_volume_with_nonexistent_volume_id[id-0186422c-999a-480e-a026-6a665744c30c,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_volume_delete_nonexistent_volume_id[id-555efa6e-efcd-44ef-8a3b-4a7ca4837a29,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_volume_extend_with_None_size[id-355218f1-8991-400a-a6bb-971239287d92,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_volume_extend_with_non_number_size[id-5d0b480d-e833-439f-8a5a-96ad2ed6f22f,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_volume_extend_with_nonexistent_volume_id[id-8f05a943-013c-4063-ac71-7baf561e82eb,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_volume_extend_with_size_smaller_than_original_size[id-e0c75c74-ee34-41a9-9288-2a2051452854,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_volume_extend_without_passing_volume_id[id-aff8ba64-6d6f-4f2e-bc33-41a08ee9f115,negative]
# tempest.api.volume.test_volumes_negative.VolumesV2NegativeTest.test_volume_get_nonexistent_volume_id[id-f131c586-9448-44a4-a8b0-54ca838aa43e,negative]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_snapshot_create_get_list_update_delete[id-2a8abbe4-d871-46db-b049-c41f5af8216e]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_snapshot_create_with_volume_in_use[compute,id-b467b54c-07a4-446d-a1cf-651dedcc3ff1]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_snapshot_list_param_limit[id-db4d8e0a-7a2e-41cc-a712-961f6844e896]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_snapshot_list_param_limit_equals_infinite[id-a1427f61-420e-48a5-b6e3-0b394fa95400]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_snapshot_list_param_limit_equals_zero[id-e3b44b7f-ae87-45b5-8a8c-66110eb24d0a]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_snapshots_list_details_with_params[id-220a1022-1fcd-4a74-a7bd-6b859156cda2]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_snapshots_list_with_params[id-59f41f43-aebf-48a9-ab5d-d76340fab32b]
# tempest.api.volume.test_volumes_snapshots.VolumesV1SnapshotTestJSON.test_volume_from_snapshot[id-677863d1-3142-456d-b6ac-9924f667a7f4]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_snapshot_create_get_list_update_delete[id-2a8abbe4-d871-46db-b049-c41f5af8216e]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_snapshot_create_with_volume_in_use[compute,id-b467b54c-07a4-446d-a1cf-651dedcc3ff1]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_snapshot_list_param_limit[id-db4d8e0a-7a2e-41cc-a712-961f6844e896]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_snapshot_list_param_limit_equals_infinite[id-a1427f61-420e-48a5-b6e3-0b394fa95400]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_snapshot_list_param_limit_equals_zero[id-e3b44b7f-ae87-45b5-8a8c-66110eb24d0a]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_snapshots_list_details_with_params[id-220a1022-1fcd-4a74-a7bd-6b859156cda2]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_snapshots_list_with_params[id-59f41f43-aebf-48a9-ab5d-d76340fab32b]
# tempest.api.volume.test_volumes_snapshots.VolumesV2SnapshotTestJSON.test_volume_from_snapshot[id-677863d1-3142-456d-b6ac-9924f667a7f4]
# tempest.api.volume.test_volumes_snapshots_negative.VolumesV1SnapshotNegativeTestJSON.test_create_snapshot_with_nonexistent_volume_id[id-e3e466af-70ab-4f4b-a967-ab04e3532ea7,negative]
# tempest.api.volume.test_volumes_snapshots_negative.VolumesV1SnapshotNegativeTestJSON.test_create_snapshot_without_passing_volume_id[id-bb9da53e-d335-4309-9c15-7e76fd5e4d6d,negative]
# tempest.api.volume.test_volumes_snapshots_negative.VolumesV2SnapshotNegativeTestJSON.test_create_snapshot_with_nonexistent_volume_id[id-e3e466af-70ab-4f4b-a967-ab04e3532ea7,negative]
# tempest.api.volume.test_volumes_snapshots_negative.VolumesV2SnapshotNegativeTestJSON.test_create_snapshot_without_passing_volume_id[id-bb9da53e-d335-4309-9c15-7e76fd5e4d6d,negative]
# tempest.api.volume.v2.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_details_pagination[id-e9138a2c-f67b-4796-8efa-635c196d01de]
# tempest.api.volume.v2.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_details_with_multiple_params[id-2a7064eb-b9c3-429b-b888-33928fc5edd3]
# tempest.api.volume.v2.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_pagination[id-af55e775-8e4b-4feb-8719-215c43b0238c]
# tempest.api.volume.v2.test_volumes_list.VolumesV2ListTestJSON.test_volume_list_with_detail_param_marker[id-46eff077-100b-427f-914e-3db2abcdb7e2]

View File

@ -1,172 +0,0 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from oslo_config import cfg
from oslo_config import fixture as fixture_config
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from tricircle.cinder_apigw import app
from tricircle.tests import base
OPT_GROUP_NAME = 'keystone_authtoken'
cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token")
class Cinder_API_GW_FunctionalTest(base.TestCase):
def setUp(self):
super(Cinder_API_GW_FunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
self.CONF.set_override('auth_strategy', 'noauth')
self.app = self._make_app()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root':
'tricircle.cinder_apigw.controllers.root.RootController',
'modules': ['tricircle.cinder_apigw'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def tearDown(self):
super(Cinder_API_GW_FunctionalTest, self).tearDown()
cfg.CONF.unregister_opts(app.common_opts)
pecan.set_config({}, overwrite=True)
class TestRootController(Cinder_API_GW_FunctionalTest):
"""Test version listing on root URI."""
def test_get(self):
response = self.app.get('/')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
versions = json_body.get('versions')
self.assertEqual(1, len(versions))
self.assertEqual(versions[0]["id"], "v2.0")
def _test_method_returns_405(self, method):
api_method = getattr(self.app, method)
response = api_method('/', expect_errors=True)
self.assertEqual(response.status_int, 405)
def test_post(self):
self._test_method_returns_405('post')
def test_put(self):
self._test_method_returns_405('put')
def test_patch(self):
self._test_method_returns_405('patch')
def test_delete(self):
self._test_method_returns_405('delete')
def test_head(self):
self._test_method_returns_405('head')
class TestV2Controller(Cinder_API_GW_FunctionalTest):
def test_get(self):
response = self.app.get('/v2/')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
version = json_body.get('version')
self.assertEqual(version["id"], "v2.0")
def _test_method_returns_405(self, method):
api_method = getattr(self.app, method)
response = api_method('/v2/', expect_errors=True)
self.assertEqual(response.status_int, 405)
def test_post(self):
self._test_method_returns_405('post')
def test_put(self):
self._test_method_returns_405('put')
def test_patch(self):
self._test_method_returns_405('patch')
def test_delete(self):
self._test_method_returns_405('delete')
def test_head(self):
self._test_method_returns_405('head')
class TestErrors(Cinder_API_GW_FunctionalTest):
def test_404(self):
response = self.app.get('/assert_called_once', expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_bad_method(self):
response = self.app.patch('/v2/123',
expect_errors=True)
self.assertEqual(response.status_int, 404)
class TestRequestID(Cinder_API_GW_FunctionalTest):
def test_request_id(self):
response = self.app.get('/')
self.assertIn('x-openstack-request-id', response.headers)
self.assertTrue(
response.headers['x-openstack-request-id'].startswith('req-'))
id_part = response.headers['x-openstack-request-id'].split('req-')[1]
self.assertTrue(uuidutils.is_uuid_like(id_part))
class TestKeystoneAuth(Cinder_API_GW_FunctionalTest):
def setUp(self):
super(Cinder_API_GW_FunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
cfg.CONF.set_override('auth_strategy', 'keystone')
self.app = self._make_app()
def test_auth_enforced(self):
response = self.app.get('/', expect_errors=True)
self.assertEqual(response.status_int, 401)

View File

@ -1,629 +0,0 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
import urlparse
import pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from requests import Response
from oslo_config import cfg
from oslo_config import fixture as fixture_config
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from tricircle.cinder_apigw import app
from tricircle.common import constants as cons
from tricircle.common import context
from tricircle.common import httpclient as hclient
from tricircle.db import api as db_api
from tricircle.db import core
from tricircle.tests import base
OPT_GROUP_NAME = 'keystone_authtoken'
cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token")
FAKE_AZ = 'fake_az'
fake_volumes = []
def fake_volumes_forward_req(ctx, action, b_header, b_url, b_req_body):
resp = Response()
resp.status_code = 404
parse = urlparse.urlsplit(b_url)
if action == 'POST':
b_body = jsonutils.loads(b_req_body)
if b_body.get('volume'):
vol = b_body['volume']
vol['id'] = uuidutils.generate_uuid()
stored_vol = {
'volume': vol,
'host': parse.hostname
}
fake_volumes.append(stored_vol)
resp.status_code = 202
vol_dict = {'volume': vol}
resp._content = jsonutils.dumps(vol_dict)
# resp.json = vol_dict
return resp
b_path = parse.path
pos = b_path.rfind('/volumes')
op = ''
if pos > 0:
op = b_path[pos:]
op = op[len('/volumes'):]
if action == 'GET':
if op == '' or op == '/detail':
tenant_id = b_path[:pos]
pos2 = tenant_id.rfind('/')
if pos2 > 0:
tenant_id = tenant_id[(pos2 + 1):]
else:
resp.status_code = 404
return resp
ret_vols = []
cmp_host = parse.hostname
for temp_vol in fake_volumes:
if temp_vol['host'] != cmp_host:
continue
if temp_vol['volume']['project_id'] == tenant_id:
ret_vols.append(temp_vol['volume'])
vol_dicts = {'volumes': ret_vols}
resp._content = jsonutils.dumps(vol_dicts)
resp.status_code = 200
return resp
elif op != '':
if op[0] == '/':
_id = op[1:]
for vol in fake_volumes:
if vol['volume']['id'] == _id:
vol_dict = {'volume': vol['volume']}
resp._content = jsonutils.dumps(vol_dict)
resp.status_code = 200
return resp
if action == 'DELETE':
if op != '':
if op[0] == '/':
_id = op[1:]
for vol in fake_volumes:
if vol['volume']['id'] == _id:
fake_volumes.remove(vol)
resp.status_code = 202
return resp
if action == 'PUT':
b_body = jsonutils.loads(b_req_body)
update_vol = b_body.get('volume', {})
if op != '':
if op[0] == '/':
_id = op[1:]
for vol in fake_volumes:
if vol['volume']['id'] == _id:
vol['volume'].update(update_vol)
vol_dict = {'volume': vol['volume']}
resp._content = jsonutils.dumps(vol_dict)
resp.status_code = 200
return resp
else:
resp.status_code = 404
return resp
class CinderVolumeFunctionalTest(base.TestCase):
def setUp(self):
super(CinderVolumeFunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
self.CONF.set_override('auth_strategy', 'noauth')
self.app = self._make_app()
self._init_db()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root':
'tricircle.cinder_apigw.controllers.root.RootController',
'modules': ['tricircle.cinder_apigw'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def _init_db(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
# enforce foreign key constraint for sqlite
core.get_engine().execute('pragma foreign_keys=on')
self.context = context.Context()
pod_dict = {
'pod_id': 'fake_pod_id',
'pod_name': 'fake_pod_name',
'az_name': FAKE_AZ
}
config_dict = {
'service_id': 'fake_service_id',
'pod_id': 'fake_pod_id',
'service_type': cons.ST_CINDER,
'service_url': 'http://127.0.0.1:8774/v2/$(tenant_id)s'
}
pod_dict2 = {
'pod_id': 'fake_pod_id' + '2',
'pod_name': 'fake_pod_name' + '2',
'az_name': FAKE_AZ + '2'
}
config_dict2 = {
'service_id': 'fake_service_id' + '2',
'pod_id': 'fake_pod_id' + '2',
'service_type': cons.ST_CINDER,
'service_url': 'http://10.0.0.2:8774/v2/$(tenant_id)s'
}
top_pod = {
'pod_id': 'fake_top_pod_id',
'pod_name': 'RegionOne',
'az_name': ''
}
top_config = {
'service_id': 'fake_top_service_id',
'pod_id': 'fake_top_pod_id',
'service_type': cons.ST_CINDER,
'service_url': 'http://127.0.0.1:19998/v2/$(tenant_id)s'
}
db_api.create_pod(self.context, pod_dict)
db_api.create_pod(self.context, pod_dict2)
db_api.create_pod(self.context, top_pod)
db_api.create_pod_service_configuration(self.context, config_dict)
db_api.create_pod_service_configuration(self.context, config_dict2)
db_api.create_pod_service_configuration(self.context, top_config)
def tearDown(self):
super(CinderVolumeFunctionalTest, self).tearDown()
cfg.CONF.unregister_opts(app.common_opts)
pecan.set_config({}, overwrite=True)
core.ModelBase.metadata.drop_all(core.get_engine())
del fake_volumes[:]
class TestVolumeController(CinderVolumeFunctionalTest):
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_post_error_case(self):
volumes = [
{
"volume_xxx":
{
"name": 'vol_1',
"size": 10,
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 400
},
# no AZ parameter
{
"volume":
{
"name": 'vol_1',
"size": 10,
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
# incorrect AZ parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ + FAKE_AZ,
"size": 10,
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 500
},
]
self._test_and_check(volumes, 'my_tenant_id')
def fake_create_resource(context, ag_name, az_name):
raise Exception
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
@patch.object(core, 'create_resource',
new=fake_create_resource)
def test_post_exception(self):
volumes = [
# no 'volume' parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ,
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 500
}
]
self._test_and_check(volumes, 'my_tenant_id')
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_post_one_and_get_one(self):
tenant1_volumes = [
# normal volume with correct parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 10,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
# same tenant, multiple volumes
{
"volume":
{
"name": 'vol_2',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
# same tenant, different az
{
"volume":
{
"name": 'vol_3',
"availability_zone": FAKE_AZ + '2',
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
]
tenant2_volumes = [
# different tenant, same az
{
"volume":
{
"name": 'vol_4',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id_2',
"metadata": {}
},
"expected_error": 202
},
]
self._test_and_check(tenant1_volumes, 'my_tenant_id')
self._test_and_check(tenant2_volumes, 'my_tenant_id_2')
self._test_detail_check('my_tenant_id', 3)
self._test_detail_check('my_tenant_id_2', 1)
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_post_one_and_delete_one(self):
volumes = [
# normal volume with correct parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 10,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
]
self._test_and_check_delete(volumes, 'my_tenant_id')
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_get(self):
response = self.app.get('/v2/my_tenant_id/volumes')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
vols = json_body.get('volumes')
self.assertEqual(0, len(vols))
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_get_all(self):
update_dict = {'pod_az_name': 'fake_pod_az2'}
# update pod2 to set pod_az_name
db_api.update_pod(self.context, 'fake_pod_id2', update_dict)
volumes = [
# normal volume with correct parameter
{
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 10,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
# same tenant, multiple volumes
{
"volume":
{
"name": 'vol_2',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
# same tenant, different az
{
"volume":
{
"name": 'vol_3',
"availability_zone": FAKE_AZ + '2',
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 20,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
},
]
tenant_id = 'my_tenant_id'
for volume in volumes:
self.app.post_json('/v2/' + tenant_id + '/volumes',
dict(volume=volume['volume']),
expect_errors=True)
query_string = '?availability_zone=' + FAKE_AZ
resp = self.app.get('/v2/' + tenant_id + '/volumes' + query_string)
self.assertEqual(resp.status_int, 200)
json_body = jsonutils.loads(resp.body)
ret_vols = json_body.get('volumes')
self.assertEqual(len(ret_vols), 2)
query_string = '?availability_zone=' + FAKE_AZ + '2'
resp = self.app.get('/v2/' + tenant_id + '/volumes' + query_string)
self.assertEqual(resp.status_int, 200)
json_body = jsonutils.loads(resp.body)
ret_vols = json_body.get('volumes')
self.assertEqual(len(ret_vols), 1)
@patch.object(hclient, 'forward_req',
new=fake_volumes_forward_req)
def test_put(self):
volume = {
"volume":
{
"name": 'vol_1',
"availability_zone": FAKE_AZ,
"source_volid": '',
"consistencygroup_id": '',
"snapshot_id": '',
"source_replica": '',
"size": 10,
"user_id": '',
"imageRef": '',
"attach_status": "detached",
"volume_type": '',
"project_id": 'my_tenant_id',
"metadata": {}
},
"expected_error": 202
}
tenant_id = 'my_tenant_id'
resp = self.app.post_json('/v2/' + tenant_id + '/volumes',
dict(volume=volume['volume']),
expect_errors=True)
volume_dict = jsonutils.loads(resp.body)
volume_id = volume_dict['volume']['id']
update_dict = {"volume": {"name": 'vol_2'}}
resp = self.app.put_json('/v2/' + tenant_id + '/volumes/' + volume_id,
dict(volume=update_dict['volume']),
expect_errors=True)
volume_dict = jsonutils.loads(resp.body)
self.assertEqual(resp.status_int, 200)
self.assertEqual(volume_dict['volume']['name'], 'vol_2')
def _test_and_check(self, volumes, tenant_id):
for test_vol in volumes:
if test_vol.get('volume'):
response = self.app.post_json(
'/v2/' + tenant_id + '/volumes',
dict(volume=test_vol['volume']),
expect_errors=True)
elif test_vol.get('volume_xxx'):
response = self.app.post_json(
'/v2/' + tenant_id + '/volumes',
dict(volume_xxx=test_vol['volume_xxx']),
expect_errors=True)
else:
return
self.assertEqual(response.status_int,
test_vol['expected_error'])
if response.status_int == 202:
json_body = jsonutils.loads(response.body)
res_vol = json_body.get('volume')
query_resp = self.app.get(
'/v2/' + tenant_id + '/volumes/' + res_vol['id'])
self.assertEqual(query_resp.status_int, 200)
json_body = jsonutils.loads(query_resp.body)
query_vol = json_body.get('volume')
self.assertEqual(res_vol['id'], query_vol['id'])
self.assertEqual(res_vol['name'], query_vol['name'])
self.assertEqual(res_vol['availability_zone'],
query_vol['availability_zone'])
self.assertIn(res_vol['availability_zone'],
[FAKE_AZ, FAKE_AZ + '2'])
def _test_and_check_delete(self, volumes, tenant_id):
for test_vol in volumes:
if test_vol.get('volume'):
response = self.app.post_json(
'/v2/' + tenant_id + '/volumes',
dict(volume=test_vol['volume']),
expect_errors=True)
self.assertEqual(response.status_int,
test_vol['expected_error'])
if response.status_int == 202:
json_body = jsonutils.loads(response.body)
_id = json_body.get('volume')['id']
query_resp = self.app.get(
'/v2/' + tenant_id + '/volumes/' + _id)
self.assertEqual(query_resp.status_int, 200)
delete_resp = self.app.delete(
'/v2/' + tenant_id + '/volumes/' + _id)
self.assertEqual(delete_resp.status_int, 202)
def _test_detail_check(self, tenant_id, vol_size):
resp = self.app.get(
'/v2/' + tenant_id + '/volumes' + '/detail',
expect_errors=True)
self.assertEqual(resp.status_int, 200)
json_body = jsonutils.loads(resp.body)
ret_vols = json_body.get('volumes')
self.assertEqual(len(ret_vols), vol_size)

View File

@ -1,447 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from novaclient import api_versions
from novaclient.v2 import client as n_client
import pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from tricircle.common import constants
from tricircle.common import constants as cons
from tricircle.common import context
from tricircle.common import resource_handle
from tricircle.db import api as db_api
from tricircle.db import core
from tricircle.nova_apigw import app
from tricircle.nova_apigw.controllers import server
from tricircle.tests import base
from oslo_config import cfg
from oslo_config import fixture as fixture_config
FAKE_AZ = 'fake_az'
def get_tricircle_client(self, pod):
return FakeTricircleClient()
class FakeTricircleClient(object):
def __init__(self):
pass
def list_servers(self, cxt, filters=None):
handle = FakeNovaAPIGWResourceHandle()
return handle.handle_list(cxt, 'server', filters)
class FakeNovaAPIGWResourceHandle(resource_handle.NovaResourceHandle):
def __init__(self):
self.auth_url = 'auth_url'
self.endpoint_url = 'endpoint_url'
def handle_list(self, cxt, resource, filters):
super(FakeNovaAPIGWResourceHandle, self).handle_list(
cxt, resource, filters)
return []
class FakeNovaClient(object):
def __init__(self):
self.servers = FakeNovaServer()
def set_management_url(self, url):
pass
class FakeNovaServer(object):
def __init__(self):
pass
def list(self, detailed=True, search_opts=None, marker=None, limit=None,
sort_keys=None, sort_dirs=None):
return []
class MicroVersionFunctionTest(base.TestCase):
def setUp(self):
super(MicroVersionFunctionTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
self.CONF.set_override('auth_strategy', 'noauth')
self.CONF.set_override('tricircle_db_connection', 'sqlite:///:memory:')
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.app = self._make_app()
self._init_db()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root': 'tricircle.nova_apigw.controllers.root.RootController',
'modules': ['tricircle.nova_apigw'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def _init_db(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
# enforce foreign key constraint for sqlite
core.get_engine().execute('pragma foreign_keys=on')
self.context = context.Context()
pod_dict = {
'pod_id': 'fake_pod_id',
'pod_name': 'fake_pod_name',
'az_name': FAKE_AZ
}
config_dict = {
'service_id': 'fake_service_id',
'pod_id': 'fake_pod_id',
'service_type': cons.ST_NOVA,
'service_url': 'http://127.0.0.1:8774/v2/$(tenant_id)s'
}
pod_dict2 = {
'pod_id': 'fake_pod_id' + '2',
'pod_name': 'fake_pod_name' + '2',
'az_name': FAKE_AZ + '2'
}
config_dict2 = {
'service_id': 'fake_service_id' + '2',
'pod_id': 'fake_pod_id' + '2',
'service_type': cons.ST_CINDER,
'service_url': 'http://10.0.0.2:8774/v2/$(tenant_id)s'
}
top_pod = {
'pod_id': 'fake_top_pod_id',
'pod_name': 'RegionOne',
'az_name': ''
}
top_config = {
'service_id': 'fake_top_service_id',
'pod_id': 'fake_top_pod_id',
'service_type': cons.ST_CINDER,
'service_url': 'http://127.0.0.1:19998/v2/$(tenant_id)s'
}
db_api.create_pod(self.context, pod_dict)
db_api.create_pod(self.context, pod_dict2)
db_api.create_pod(self.context, top_pod)
db_api.create_pod_service_configuration(self.context, config_dict)
db_api.create_pod_service_configuration(self.context, config_dict2)
db_api.create_pod_service_configuration(self.context, top_config)
def tearDown(self):
super(MicroVersionFunctionTest, self).tearDown()
cfg.CONF.unregister_opts(app.common_opts)
pecan.set_config({}, overwrite=True)
core.ModelBase.metadata.drop_all(core.get_engine())
class MicroversionsTest(MicroVersionFunctionTest):
min_version = constants.NOVA_APIGW_MIN_VERSION
max_version = 'compute %s' % constants.NOVA_APIGW_MAX_VERSION
lower_boundary = str(float(constants.NOVA_APIGW_MIN_VERSION) - 0.1)
upper_boundary = 'compute %s' % str(
float(constants.NOVA_APIGW_MAX_VERSION) + 0.1)
vaild_version = 'compute 2.30'
vaild_leagcy_version = '2.5'
invaild_major = 'compute a.2'
invaild_minor = 'compute 2.a'
latest_version = 'compute 2.latest'
invaild_compute_format = 'compute2.30'
only_major = '2'
invaild_major2 = '1.5'
invaild_major3 = 'compute 3.2'
invaild_version = '2.30'
invaild_leagecy_version = 'compute 2.5'
invaild_version2 = 'aa 2.30'
invaild_version3 = 'compute 2.30 2.31'
invaild_version4 = 'acompute 2.30'
tenant_id = 'tenant_id'
def _make_headers(self, version, type='current'):
headers = {}
headers['X_TENANT_ID'] = self.tenant_id
if version is None:
type = 'leagecy'
version = constants.NOVA_APIGW_MIN_VERSION
if type == 'both':
headers[constants.NOVA_API_VERSION_REQUEST_HEADER] = version
headers[constants.LEGACY_NOVA_API_VERSION_REQUEST_HEADER] = '2.5'
elif type == 'current':
headers[constants.NOVA_API_VERSION_REQUEST_HEADER] = version
else:
headers[constants.LEGACY_NOVA_API_VERSION_REQUEST_HEADER] = version
return headers
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_no_header(self, mock_client):
headers = self._make_headers(None)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
self.app.get(url, headers=headers)
mock_client.assert_called_with(
api_version=api_versions.APIVersion(
constants.NOVA_APIGW_MIN_VERSION),
auth_token=None, auth_url='auth_url',
direct_use=False, project_id=None,
timeout=60, username=None, api_key=None)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_vaild_version(self, mock_client):
headers = self._make_headers(self.vaild_version)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
self.app.get(url, headers=headers)
mock_client.assert_called_with(
api_version=api_versions.APIVersion(self.vaild_version.split()[1]),
auth_token=None, auth_url='auth_url',
direct_use=False, project_id=None,
timeout=60, username=None, api_key=None)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_vaild_leagcy_version(self, mock_client):
headers = self._make_headers(self.vaild_leagcy_version, 'leagcy')
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
self.app.get(url, headers=headers)
mock_client.assert_called_with(
api_version=api_versions.APIVersion(self.vaild_leagcy_version),
auth_token=None, auth_url='auth_url',
direct_use=False, project_id=None,
timeout=60, username=None, api_key=None)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_latest_version(self, mock_client):
headers = self._make_headers(self.latest_version)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
self.app.get(url, headers=headers)
mock_client.assert_called_with(
api_version=api_versions.APIVersion(
constants.NOVA_APIGW_MAX_VERSION),
auth_token=None, auth_url='auth_url',
direct_use=False, project_id=None,
timeout=60, username=None, api_key=None)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_min_version(self, mock_client):
headers = self._make_headers(self.min_version, 'leagecy')
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
self.app.get(url, headers=headers)
mock_client.assert_called_with(
api_version=api_versions.APIVersion(self.min_version),
auth_token=None, auth_url='auth_url',
direct_use=False, project_id=None,
timeout=60, username=None, api_key=None)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_max_version(self, mock_client):
headers = self._make_headers(self.max_version)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
self.app.get(url, headers=headers)
mock_client.assert_called_with(
api_version=api_versions.APIVersion(self.max_version.split()[1]),
auth_token=None, auth_url='auth_url',
direct_use=False, project_id=None,
timeout=60, username=None, api_key=None)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_major(self, mock_client):
headers = self._make_headers(self.invaild_major)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_major2(self, mock_client):
headers = self._make_headers(self.invaild_major2, 'leagecy')
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_major3(self, mock_client):
headers = self._make_headers(self.invaild_major3)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_minor(self, mock_client):
headers = self._make_headers(self.invaild_minor)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_lower_boundary(self, mock_client):
headers = self._make_headers(self.lower_boundary)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_upper_boundary(self, mock_client):
headers = self._make_headers(self.upper_boundary)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_compute_format(self, mock_client):
headers = self._make_headers(self.invaild_compute_format)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_only_major(self, mock_client):
headers = self._make_headers(self.only_major, 'leagecy')
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_version(self, mock_client):
headers = self._make_headers(self.invaild_version)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_leagecy_version(self, mock_client):
headers = self._make_headers(self.invaild_leagecy_version, 'leagecy')
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_both_version(self, mock_client):
headers = self._make_headers(self.vaild_version, 'both')
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
self.app.get(url, headers=headers, expect_errors=True)
# The new format microversion priority to leagecy
mock_client.assert_called_with(
api_version=api_versions.APIVersion(self.vaild_version.split()[1]),
auth_token=None, auth_url='auth_url',
direct_use=False, project_id=None,
timeout=60, username=None, api_key=None)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_version2(self, mock_client):
headers = self._make_headers(self.invaild_version2)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_version3(self, mock_client):
headers = self._make_headers(self.invaild_version3)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)
@mock.patch.object(server.ServerController, '_get_client',
new=get_tricircle_client)
@mock.patch.object(n_client, 'Client')
def test_microversions_invaild_version4(self, mock_client):
headers = self._make_headers(self.invaild_version4)
url = '/v2.1/' + self.tenant_id + '/servers/detail'
mock_client.return_value = FakeNovaClient()
res = self.app.get(url, headers=headers, expect_errors=True)
self.assertEqual(406, res.status_int)

View File

@ -1,479 +0,0 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved.
#
# 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 copy
import mock
import pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from oslo_config import cfg
from oslo_config import fixture as fixture_config
from oslo_serialization import jsonutils
from tricircle.nova_apigw import app
from tricircle.nova_apigw.controllers import quota_sets
from tricircle.common import context
from tricircle.common import exceptions as t_exceptions
from tricircle.common import quota
from tricircle.db import core
from tricircle.tests.unit.common import test_quota
QUOTAS = quota.QUOTAS
def _make_body(tenant_id='foo', root=True, **kw):
resources = copy.copy(kw)
if tenant_id:
resources['id'] = tenant_id
if root:
result = {'quota_set': resources}
else:
result = resources
return result
def _update_body(src_body, root=True, **kw):
for k, v in kw.iteritems():
if root:
src_body['quota_set'][k] = v
else:
src_body[k] = v
return src_body
def _update_subproject_body(src_body, root=True, **kw):
for k, v in kw.iteritems():
if root:
src_body['quota_set'][k] = v
else:
src_body[k] = v
if root:
for k, v in src_body['quota_set'].iteritems():
if not kw.get(k):
src_body['quota_set'][k] = 0
else:
for k, v in src_body.iteritems():
if not kw.get(k) and k != 'id':
src_body[k] = 0
return src_body
def _make_subproject_body(tenant_id='foo', root=True, **kw):
return _make_body(tenant_id=tenant_id, root=root, **kw)
class QuotaControllerTest(test_quota.QuotaSetsOperationTest):
def setUp(self):
super(QuotaControllerTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
self.CONF.set_override('auth_strategy', 'noauth')
self.exception_string = 'NotFound'
self.test_exception = [
{'exception_raise': 'NotFound',
'expected_error': 404},
{'exception_raise': 'AdminRequired',
'expected_error': 403},
{'exception_raise': 'NotAuthorized',
'expected_error': 403},
{'exception_raise': 'HTTPForbiddenError',
'expected_error': 403},
{'exception_raise': 'Conflict',
'expected_error': 400}, ]
self._flags_rest(use_default_quota_class=True)
self.app = self._make_app()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root':
'tricircle.nova_apigw.controllers.root.RootController',
'modules': ['tricircle.nova_apigw'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def _override_config_rest(self, name, override, group=None):
"""Cleanly override CONF variables."""
self.CONF.set_override(name, override, group)
self.addCleanup(self.CONF.clear_override, name, group)
def _flags_rest(self, **kw):
"""Override CONF variables for a test."""
for k, v in kw.items():
self._override_config_rest(k, v, group='quota')
def tearDown(self):
super(QuotaControllerTest, self).tearDown()
pecan.set_config({}, overwrite=True)
cfg.CONF.unregister_opts(app.common_opts)
core.ModelBase.metadata.drop_all(core.get_engine())
def _get_mock_ctx(self):
return self.ctx
def _mock_func_and_obj(self):
quota.QuotaSetOperation._get_project = mock.Mock()
quota.QuotaSetOperation._get_project.side_effect = self._get_project
context.extract_context_from_environ = mock.Mock()
context.extract_context_from_environ.side_effect = self._get_mock_ctx
def test_quota_set_update_show_defaults(self):
self._mock_func_and_obj()
# show quota before update, should be equal to defaults
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
default_body = _make_body(tenant_id=self.A.id, root=True,
**self.default_quota)
result = self._DictIn(json_body['quota_set'],
default_body['quota_set'])
self.assertEqual(result, True)
# quota update with wrong parameter
quota_a = dict(instances=5, cores=10, ram=25600)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.put_json(url,
{'quota_s': quota_a},
expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
# quota update with non-admin
self.ctx.is_admin = False
quota_a = dict(instances=5, cores=10, ram=25600)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.put_json(url,
{'quota_set': quota_a},
expect_errors=True)
self.assertIn(res.status_int, [409])
self.ctx.is_admin = True
# show quota after update
quota_a = dict(instances=5, cores=10, ram=25600)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.put_json(url,
{'quota_set': quota_a},
expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_a = _make_body(tenant_id=self.A.id, root=True,
**self.default_quota)
updated_a = _update_body(updated_a, root=True, **quota_a)
result = self._DictIn(json_body['quota_set'], updated_a['quota_set'])
self.assertEqual(result, True)
self.ctx.is_admin = False
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
self.ctx.is_admin = True
# show quota after update for child
quota_b = dict(instances=3, cores=5, ram=12800)
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.put_json(url,
{'quota_set': quota_b},
expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_b = _make_body(tenant_id=self.B.id, root=False,
**self.default_quota)
updated_b = _update_subproject_body(updated_b, root=False, **quota_b)
result = self._DictIn(json_body['quota_set'], updated_b)
self.assertEqual(result, True)
# show default quota after update
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url + '/defaults', expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'],
default_body['quota_set'])
self.assertEqual(result, True)
# show default quota for child, should be all 0
quota_c = {}
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url + '/defaults', expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_c = _make_body(tenant_id=self.B.id, root=False,
**self.default_quota)
updated_c = _update_subproject_body(updated_c, root=False, **quota_c)
result = self._DictIn(json_body['quota_set'], updated_c)
self.assertEqual(result, True)
# show quota after update
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'], updated_a['quota_set'])
self.assertEqual(result, True)
# show quota for child, should be equal to update_b
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'], updated_b)
self.assertEqual(result, True)
# delete with non-admin
self.ctx.is_admin = False
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.delete(url, expect_errors=True)
self.assertIn(res.status_int, [409])
self.ctx.is_admin = True
# delete parent quota when child quota is not zero
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.delete(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
# delete child quota
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.delete(url, expect_errors=True)
self.assertEqual(res.status_int, 202)
# delete parent quota when child quota is deleted
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.delete(url, expect_errors=True)
self.assertEqual(res.status_int, 202)
# show quota for parent after delete, equal to defaults
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'],
default_body['quota_set'])
self.assertEqual(result, True)
# show quota for child after delete, should be all 0
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url, expect_errors=True)
json_body = jsonutils.loads(res.body)
result = self._DictIn(json_body['quota_set'], updated_c)
self.assertEqual(result, True)
def test_quota_detail_limits(self):
self._mock_func_and_obj()
def _make_default_detail_body(tenant_id='foo'):
resources = copy.copy(self.default_quota)
for k, v in self.default_quota.iteritems():
resources[k] = {}
resources[k]['limit'] = v
resources[k]['reserved'] = 0
resources[k]['in_use'] = 0
if tenant_id:
resources['id'] = tenant_id
return resources
def _update_usage_in_default_detail(quota_item,
reserved, in_use, **kw):
kw[quota_item]['reserved'] = reserved
kw[quota_item]['in_use'] = in_use
return kw
# show quota usage before update, should be equal to defaults
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
default_detail = _make_default_detail_body(self.A.id)
result = self._DictIn(json_body['quota_set'], default_detail)
self.assertEqual(result, True)
# show quota usage after reserve and in_use update
inuse_opts = {'instances': 2, 'cores': 5}
reserve_opts = {'instances': 3, 'cores': 3}
self.ctx.project_id = self.A.id
reservations = QUOTAS.reserve(self.ctx,
project_id=self.A.id,
**inuse_opts)
QUOTAS.commit(self.ctx, reservations, self.A.id)
QUOTAS.reserve(self.ctx, project_id=self.A.id, **reserve_opts)
url = self._url_for_quota_set(self.A.id, self.A.id)
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
default_detail = _make_default_detail_body(self.A.id)
update_detail = _update_usage_in_default_detail(
'instances',
reserve_opts['instances'],
inuse_opts['instances'],
**default_detail)
update_detail = _update_usage_in_default_detail(
'cores',
reserve_opts['cores'],
inuse_opts['cores'],
**update_detail)
result = self._DictIn(json_body['quota_set'], update_detail)
self.assertEqual(result, True)
# Wrong parameter
url = '/v2.1/' + self.A.id + '/limits?_id=' + self.A.id
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
url = '/v2.1/' + self.A.id + '/limits'
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
self.ctx.is_admin = False
url = '/v2.1/' + self.B.id + '/limits?tenant_id=' + self.C.id
res = self.app.get(url, expect_errors=True)
self.assertIn(res.status_int, [400, 403, 404])
self.ctx.is_admin = True
# test absolute limits and usage
url = '/v2.1/' + self.A.id + '/limits?tenant_id=' + self.A.id
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
ret_limits = json_body['limits']['absolute']
absolute = {}
absolute.update(quota_sets.build_absolute_limits(update_detail))
absolute.update(quota_sets.build_used_limits(update_detail))
result = self._DictIn(absolute, ret_limits)
self.assertEqual(result, True)
# test child limits, set child quota
quota_b = dict(instances=3, cores=5)
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.put_json(url,
{'quota_set': quota_b},
expect_errors=True)
json_body = jsonutils.loads(res.body)
updated_b = _make_body(tenant_id=self.B.id, root=False,
**self.default_quota)
updated_b = _update_subproject_body(updated_b, root=False, **quota_b)
result = self._DictIn(json_body['quota_set'], updated_b)
self.assertEqual(result, True)
# test child limits, use and reserve child quota
inuse_opts = {'instances': 1, 'cores': 1}
reserve_opts = {'instances': 1, 'cores': 2}
self.ctx.project_id = self.A.id
reservations = QUOTAS.reserve(self.ctx,
project_id=self.B.id,
**inuse_opts)
QUOTAS.commit(self.ctx, reservations, self.B.id)
QUOTAS.reserve(self.ctx, project_id=self.B.id, **reserve_opts)
url = self._url_for_quota_set(self.A.id, self.B.id)
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, 200)
child_json_body = jsonutils.loads(res.body)
self.assertEqual(
child_json_body['quota_set']['instances']['limit'],
quota_b['instances'])
self.assertEqual(
child_json_body['quota_set']['instances']['in_use'],
inuse_opts['instances'])
self.assertEqual(
child_json_body['quota_set']['instances']['reserved'],
reserve_opts['instances'])
self.assertEqual(
child_json_body['quota_set']['cores']['limit'],
quota_b['cores'])
self.assertEqual(
child_json_body['quota_set']['cores']['in_use'],
inuse_opts['cores'])
self.assertEqual(
child_json_body['quota_set']['cores']['reserved'],
reserve_opts['cores'])
# test child limits, get child quota limits and compare
url = '/v2.1/' + self.A.id + '/limits?tenant_id=' + self.B.id
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, 200)
json_body = jsonutils.loads(res.body)
ret_limits = json_body['limits']['absolute']
self.assertEqual(
ret_limits['maxTotalInstances'],
quota_b['instances'])
self.assertEqual(
ret_limits['maxTotalCores'],
quota_b['cores'])
self.assertEqual(
ret_limits['totalInstancesUsed'],
inuse_opts['instances'] + reserve_opts['instances'])
self.assertEqual(
ret_limits['totalCoresUsed'],
inuse_opts['cores'] + reserve_opts['cores'])
def _show_detail_exception(self, context, show_usage=False):
for todo_exception in self.test_exception:
if todo_exception['exception_raise'] == self.exception_string:
e = getattr(t_exceptions, self.exception_string)
raise e()
def test_quota_sets_exception_catch(self):
orig_show = quota.QuotaSetOperation.show_detail_quota
quota.QuotaSetOperation.show_detail_quota = mock.Mock()
quota.QuotaSetOperation.show_detail_quota.side_effect = \
self._show_detail_exception
# show quota usage before update, should be equal to defaults
for todo_exception in self.test_exception:
self.exception_string = todo_exception['exception_raise']
url = self._url_for_quota_set(self.A.id, self.A.id)
# exception raised in LimitsController
res = self.app.get(url + '/detail', expect_errors=True)
self.assertEqual(res.status_int, todo_exception['expected_error'])
# exception raised in QuotaSetController
res = self.app.get(url, expect_errors=True)
self.assertEqual(res.status_int, todo_exception['expected_error'])
quota.QuotaSetOperation.show_detail_quota = orig_show
def _url_for_quota_set(self, owner_tenant_id, target_tenant_id):
return '/v2.1/' + owner_tenant_id + \
'/os-quota-sets/' + target_tenant_id
def _DictIn(self, dict_small, dict_full):
for k, v in dict_small.iteritems():
if dict_full[k] != v:
return False
return True

View File

@ -1,174 +0,0 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved.
#
# 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 pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from oslo_config import cfg
from oslo_config import fixture as fixture_config
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from tricircle.nova_apigw import app
from tricircle.tests import base
OPT_GROUP_NAME = 'keystone_authtoken'
cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token")
class Nova_API_GW_FunctionalTest(base.TestCase):
def setUp(self):
super(Nova_API_GW_FunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
self.CONF.set_override('auth_strategy', 'noauth')
self.app = self._make_app()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root': 'tricircle.nova_apigw.controllers.root.RootController',
'modules': ['tricircle.nova_apigw'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def tearDown(self):
super(Nova_API_GW_FunctionalTest, self).tearDown()
cfg.CONF.unregister_opts(app.common_opts)
pecan.set_config({}, overwrite=True)
class TestRootController(Nova_API_GW_FunctionalTest):
"""Test version listing on root URI."""
def test_get(self):
response = self.app.get('/')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
versions = json_body.get('versions')
self.assertEqual(2, len(versions))
self.assertEqual(versions[0]["id"], "v2.0")
self.assertEqual(versions[1]["min_version"], "2.1")
self.assertEqual(versions[1]["id"], "v2.1")
def _test_method_returns_404(self, method):
api_method = getattr(self.app, method)
response = api_method('/', expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_post(self):
self._test_method_returns_404('post')
def test_put(self):
self._test_method_returns_404('put')
def test_patch(self):
self._test_method_returns_404('patch')
def test_delete(self):
self._test_method_returns_404('delete')
def test_head(self):
self._test_method_returns_404('head')
class TestV21Controller(Nova_API_GW_FunctionalTest):
def test_get(self):
response = self.app.get('/v2.1/')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
version = json_body.get('version')
self.assertEqual(version["min_version"], "2.1")
self.assertEqual(version["id"], "v2.1")
def _test_method_returns_404(self, method):
api_method = getattr(self.app, method)
response = api_method('/', expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_post(self):
self._test_method_returns_404('post')
def test_put(self):
self._test_method_returns_404('put')
def test_patch(self):
self._test_method_returns_404('patch')
def test_delete(self):
self._test_method_returns_404('delete')
def test_head(self):
self._test_method_returns_404('head')
class TestErrors(Nova_API_GW_FunctionalTest):
def test_404(self):
response = self.app.get('/assert_called_once', expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_bad_method(self):
response = self.app.patch('/v2.1/123',
expect_errors=True)
self.assertEqual(response.status_int, 404)
class TestRequestID(Nova_API_GW_FunctionalTest):
def test_request_id(self):
response = self.app.get('/v2.1/')
self.assertIn('x-openstack-request-id', response.headers)
self.assertTrue(
response.headers['x-openstack-request-id'].startswith('req-'))
id_part = response.headers['x-openstack-request-id'].split('req-')[1]
self.assertTrue(uuidutils.is_uuid_like(id_part))
class TestKeystoneAuth(Nova_API_GW_FunctionalTest):
def setUp(self):
super(Nova_API_GW_FunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
cfg.CONF.register_opts(app.common_opts)
self.CONF = self.useFixture(fixture_config.Config()).conf
cfg.CONF.set_override('auth_strategy', 'keystone')
self.app = self._make_app()
def test_auth_enforced(self):
response = self.app.get('/v2.1/', expect_errors=True)
self.assertEqual(response.status_int, 401)

View File

@ -1,271 +0,0 @@
# Copyright 2016 OpenStack Foundation.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
import pecan
import unittest
from tricircle.cinder_apigw.controllers import volume_type
from tricircle.common import context
from tricircle.db import api as db_api
from tricircle.db import core
class FakeResponse(object):
def __new__(cls, code=500):
cls.status = code
cls.status_code = code
return super(FakeResponse, cls).__new__(cls)
class VolumeTypeTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.get_admin_context()
self.project_id = 'test_project'
self.controller = volume_type.VolumeTypeController(self.project_id)
def _validate_error_code(self, res, code):
self.assertEqual(code, res[res.keys()[0]]['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_post(self, mock_context):
mock_context.return_value = self.context
body = {'volume_type': {'name': 'vol-type-001',
'description': 'volume type 001',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**body)
res = db_api.volume_type_get_by_name(self.context, 'vol-type-001')
self.assertEqual('vol-type-001', res['name'])
self.assertEqual('volume type 001', res['description'])
capabilities = res['extra_specs']['capabilities']
self.assertEqual('gpu', capabilities)
# failure case, only admin can create volume type
self.context.is_admin = False
res = self.controller.post(**body)
self._validate_error_code(res, 403)
self.context.is_admin = True
# failure case, volume_type body is required
body = {'name': 'vol-type-002'}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
# failure case, volume type name is empty
body = {'volume_type': {'name': '',
'description': 'volume type 001',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
# failure case, volume type name has more than 255 characters
body = {'volume_type': {'name': ('a' * 500),
'description': 'volume type 001',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu', }
}
}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
# failure case, volume type description has more than 255 characters
body = {'volume_type': {'name': 'vol-type-001',
'description': ('a' * 500),
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**body)
self._validate_error_code(res, 400)
# failure case, is_public is invalid input
body = {'volume_type': {'name': 'vol-type-001',
'description': 'volume type 001',
'os-volume-type-access:is_public': 'a',
'extra_specs': {
'capabilities': 'gpu',
}}}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
# failure case, volume type name is unique
body = {'volume_type': {'name': 'vol-type-001',
'description': 'volume type 001',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
res = self.controller.post(**body)
self._validate_error_code(res, 409)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_get_one(self, mock_context):
mock_context.return_value = self.context
body = {'volume_type': {'name': 'vol-type-001',
'description': 'volume type 001',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**body)
vtype = db_api.volume_type_get_by_name(self.context, 'vol-type-001')
res = self.controller.get_one(vtype['id'])['volume_type']
self.assertEqual('vol-type-001', res['name'])
self.assertEqual('volume type 001', res['description'])
capabilities = res['extra_specs']['capabilities']
self.assertEqual('gpu', capabilities)
# failure case, volume type is not exist.
fake_id = "Fake_ID"
res = self.controller.get_one(fake_id)
self._validate_error_code(res, 404)
# failure case, the volume type is private.
body = {'volume_type': {'name': 'vol-type-002',
'description': 'volume type 002',
'os-volume-type-access:is_public': False,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**body)
vtype = db_api.volume_type_get_by_name(self.context, 'vol-type-002')
self.context.is_admin = False
res = self.controller.get_one(vtype['id'])
self._validate_error_code(res, 404)
@patch.object(context, 'extract_context_from_environ')
def test_get_all(self, mock_context):
mock_context.return_value = self.context
volume_type_001 = {'volume_type': {'name': 'vol-type-001',
'description': 'volume type 001',
'os-volume-'
'type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
volume_type_002 = {'volume_type': {'name': 'vol-type-002',
'description': 'volume type 002',
'os-volume-'
'type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**volume_type_001)
self.controller.post(**volume_type_002)
volume_types = self.controller.get_all()['volume_types']
self.assertEqual('vol-type-001', volume_types[0]['name'])
self.assertEqual('volume type 001', volume_types[0]['description'])
capabilities_001 = volume_types[0]['extra_specs']['capabilities']
self.assertEqual('gpu', capabilities_001)
self.assertEqual('vol-type-002', volume_types[1]['name'])
self.assertEqual('volume type 002', volume_types[1]['description'])
capabilities_002 = volume_types[1]['extra_specs']['capabilities']
self.assertEqual('gpu', capabilities_002)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_put(self, mock_context):
mock_context.return_value = self.context
body = {'volume_type': {'name': 'vol-type-001',
'description': 'volume type 001',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
body_update = {'volume_type': {'name': 'vol-type-002',
'description': 'volume type 002',
'os-volume-'
'type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**body)
vtype = db_api.volume_type_get_by_name(self.context, 'vol-type-001')
res = self.controller.put(vtype['id'], **body_update)['volume_type']
self.assertEqual('vol-type-002', res['name'])
self.assertEqual('volume type 002', res['description'])
capabilities = res['extra_specs']['capabilities']
self.assertEqual('gpu', capabilities)
# failure case, volume type name, description, is_public
# not None at the same time
body = {'volume_type': {'extra_specs': {
'capabilities': 'gpu',
}}}
res = self.controller.put(vtype['id'], **body)
self._validate_error_code(res, 400)
# failure case, name exists in db
body = {'volume_type': {'name': 'vol-type-003',
'description': 'volume type 003',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**body)
res = self.controller.put(vtype['id'], **body)
self._validate_error_code(res, 500)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(db_api, 'volume_type_delete')
@patch.object(context, 'extract_context_from_environ')
def test_delete(self, mock_context, mock_delete):
mock_context.return_value = self.context
mock_delete.return_value = Exception()
body = {'volume_type': {'name': 'vol-type-001',
'description': 'volume type 001',
'os-volume-type-access:is_public': True,
'extra_specs': {
'capabilities': 'gpu',
}}}
self.controller.post(**body)
vtype = db_api.volume_type_get_by_name(self.context, 'vol-type-001')
# failure case, only admin delete create volume type
self.context.is_admin = False
res = self.controller.delete(vtype['id'])
self._validate_error_code(res, 403)
# failure case, bad request
self.context.is_admin = True
res = self.controller.delete(_id=None)
self._validate_error_code(res, 404)
res = self.controller.delete(vtype['id'])
self.assertEqual(res.status, 202)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

View File

@ -1,306 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
import pecan
import unittest
from cinderclient.client import HTTPClient
from oslo_utils import uuidutils
from tricircle.cinder_apigw.controllers import volume_actions as action
from tricircle.common import constants
from tricircle.common import context
from tricircle.common import exceptions
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
class FakeResponse(object):
def __new__(cls, code=500):
cls.status = code
cls.status_code = code
return super(FakeResponse, cls).__new__(cls)
class VolumeActionTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
self.project_id = 'test_project'
self.context.tenant = self.project_id
self.controller = action.VolumeActionController(self.project_id, '')
def _prepare_pod(self, bottom_pod_num=1):
t_pod = {'pod_id': 't_pod_uuid', 'pod_name': 't_region',
'az_name': ''}
api.create_pod(self.context, t_pod)
b_pods = []
if bottom_pod_num == 1:
b_pod = {'pod_id': 'b_pod_uuid', 'pod_name': 'b_region',
'az_name': 'b_az'}
api.create_pod(self.context, b_pod)
b_pods.append(b_pod)
else:
for i in xrange(1, bottom_pod_num + 1):
b_pod = {'pod_id': 'b_pod_%d_uuid' % i,
'pod_name': 'b_region_%d' % i,
'az_name': 'b_az_%d' % i}
api.create_pod(self.context, b_pod)
b_pods.append(b_pod)
return t_pod, b_pods
def _prepare_pod_service(self, pod_id, service):
config_dict = {'service_id': uuidutils.generate_uuid(),
'pod_id': pod_id,
'service_type': service,
'service_url': 'fake_pod_service'}
api.create_pod_service_configuration(self.context, config_dict)
pass
def _prepare_volume(self, pod):
t_volume_id = uuidutils.generate_uuid()
b_volume_id = t_volume_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_volume_id, 'bottom_id': b_volume_id,
'pod_id': pod['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_VOLUME})
return t_volume_id
def _prepare_server(self, pod):
t_server_id = uuidutils.generate_uuid()
b_server_id = t_server_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_server_id, 'bottom_id': b_server_id,
'pod_id': pod['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_SERVER})
return t_server_id
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_action_not_supported(self, mock_context):
mock_context.return_value = self.context
body = {'unsupported_action': ''}
res = self.controller.post(**body)
self.assertEqual('Volume action not supported',
res['badRequest']['message'])
self.assertEqual(400, res['badRequest']['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_action_volume_not_found(self, mock_context):
mock_context.return_value = self.context
body = {'os-extend': ''}
self.controller.volume_id = 'Fake_volume_id'
res = self.controller.post(**body)
self.assertEqual(
'Volume %(volume_id)s could not be found.' % {
'volume_id': self.controller.volume_id},
res['itemNotFound']['message'])
self.assertEqual(404, res['itemNotFound']['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_action_exception(self, mock_context, mock_action):
mock_context.return_value = self.context
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
mock_action.side_effect = exceptions.HTTPForbiddenError(
msg='Volume operation forbidden')
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
# this is the message of HTTPForbiddenError exception
self.assertEqual('Volume operation forbidden',
res['forbidden']['message'])
# this is the code of HTTPForbiddenError exception
self.assertEqual(403, res['forbidden']['code'])
mock_action.side_effect = exceptions.ServiceUnavailable
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
# this is the message of ServiceUnavailable exception
self.assertEqual('The service is unavailable',
res['internalServerError']['message'])
# code is 500 by default
self.assertEqual(500, res['internalServerError']['code'])
mock_action.side_effect = Exception
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
# use default message if exception's message is empty
self.assertEqual('Action os-extend on volume %s fails' % t_volume_id,
res['internalServerError']['message'])
# code is 500 by default
self.assertEqual(500, res['internalServerError']['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_extend_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {'os-extend': {'new_size': 2}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_attach_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
t_server_id = self._prepare_server(b_pods[0])
self.controller.volume_id = t_volume_id
body = {'os-attach': {
'instance_uuid': t_server_id,
'mountpoint': '/dev/vdc'
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_force_detach_volume_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
t_server_id = self._prepare_server(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-force_detach": {
"attachment_id": t_server_id,
"connector": {
"initiator": "iqn.2012-07.org.fake:01"}}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_reset_status_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-reset_status": {
"status": "available",
"attach_status": "detached",
"migration_status": "migrating"
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_set_image_metadata_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-set_image_metadata": {
"metadata": {
"image_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c",
"image_name": "image",
"kernel_id": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"ramdisk_id": "somedisk"
}
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_unset_image_metadata_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-unset_image_metadata": {
'key': 'image_name'
}}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_show_image_metadata_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_CINDER)
t_volume_id = self._prepare_volume(b_pods[0])
self.controller.volume_id = t_volume_id
body = {"os-show_image_metadata": None}
res = self.controller.post(**body)
url = '/volumes/%s/action' % t_volume_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

View File

@ -1,141 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from tricircle.common import context
from tricircle.common.scheduler import filter_scheduler
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
import unittest
class FilterSchedulerTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
self.project_id = 'test_fs_project'
self.az_name_1 = 'b_az_fs_1'
self.az_name_2 = 'b_az_fs_2'
self.filter_scheduler = filter_scheduler.FilterScheduler()
def _prepare_binding(self, pod_id):
binding = {'tenant_id': self.project_id,
'pod_id': pod_id,
'is_binding': True}
api.create_pod_binding(self.context, self.project_id, pod_id)
return binding
def test_select_destination(self):
b_pod_1 = {'pod_id': 'b_pod_fs_uuid_1', 'pod_name': 'b_region_fs_1',
'az_name': self.az_name_1}
api.create_pod(self.context, b_pod_1)
b_pod_2 = {'pod_id': 'b_pod_fs_uuid_2', 'pod_name': 'b_region_fs_2',
'az_name': self.az_name_2}
api.create_pod(self.context, b_pod_2)
b_pod_3 = {'pod_id': 'b_pod_fs_uuid_3', 'pod_name': 'b_region_fs_3',
'az_name': self.az_name_2}
api.create_pod(self.context, b_pod_3)
t_pod = {'pod_id': 'b_pod_fs_uuid_t_pod',
'pod_name': 'b_region_fs_t_pod',
'az_name': ''}
api.create_pod(self.context, t_pod)
self._prepare_binding(b_pod_1['pod_id'])
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(binding_q[0]['pod_id'], b_pod_1['pod_id'])
self.assertEqual(binding_q[0]['tenant_id'], self.project_id)
self.assertEqual(binding_q[0]['is_binding'], True)
pod_1, _ = self.filter_scheduler.select_destination(
self.context, '', self.project_id, pod_group='')
self.assertEqual(pod_1['pod_id'], b_pod_1['pod_id'])
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(len(binding_q), 1)
self.assertEqual(binding_q[0]['pod_id'], pod_1['pod_id'])
self.assertEqual(binding_q[0]['tenant_id'], self.project_id)
self.assertEqual(binding_q[0]['is_binding'], True)
pod_2, _ = self.filter_scheduler.select_destination(
self.context, '', 'new_project', pod_group='')
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': 'new_project'}], [])
self.assertEqual(len(binding_q), 1)
self.assertEqual(binding_q[0]['pod_id'], pod_2['pod_id'])
self.assertEqual(binding_q[0]['tenant_id'], 'new_project')
self.assertEqual(binding_q[0]['is_binding'], True)
pod_3, _ = self.filter_scheduler.select_destination(
self.context, self.az_name_1, 'new_project', pod_group='')
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': 'new_project'}], [])
self.assertEqual(len(binding_q), 1)
self.assertEqual(binding_q[0]['pod_id'], pod_3['pod_id'])
self.assertEqual(binding_q[0]['tenant_id'], 'new_project')
self.assertEqual(binding_q[0]['is_binding'], True)
pod_4, _ = self.filter_scheduler.select_destination(
self.context, self.az_name_2, 'new_project', pod_group='')
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': 'new_project'}], [])
self.assertEqual(len(binding_q), 2)
self.assertEqual(binding_q[1]['pod_id'], pod_4['pod_id'])
self.assertEqual(binding_q[1]['tenant_id'], 'new_project')
self.assertEqual(binding_q[1]['is_binding'], True)
pod_5, _ = self.filter_scheduler.select_destination(
self.context, self.az_name_2, self.project_id, pod_group='')
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(len(binding_q), 2)
self.assertEqual(pod_5['az_name'], self.az_name_2)
self.assertEqual(binding_q[1]['pod_id'], pod_5['pod_id'])
self.assertEqual(binding_q[1]['tenant_id'], self.project_id)
self.assertEqual(binding_q[1]['is_binding'], True)
pod_6, _ = self.filter_scheduler.select_destination(
self.context, self.az_name_1, self.project_id, pod_group='test')
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(len(binding_q), 2)
self.assertEqual(pod_6, None)
pod_7, _ = self.filter_scheduler.select_destination(
self.context, self.az_name_2, self.project_id, pod_group='test')
binding_q = core.query_resource(
self.context, models.PodBinding, [{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(len(binding_q), 3)
self.assertEqual(pod_7['az_name'], self.az_name_2)
self.assertEqual(binding_q[1]['tenant_id'], self.project_id)
self.assertEqual(binding_q[1]['is_binding'], False)
self.assertEqual(binding_q[2]['pod_id'], pod_7['pod_id'])
self.assertEqual(binding_q[2]['tenant_id'], self.project_id)
self.assertEqual(binding_q[2]['is_binding'], True)

View File

@ -1,141 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from stevedore import driver
from tricircle.common import context
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
import unittest
class PodManagerTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
self.project_id = 'test_pm_project'
self.az_name_2 = 'b_az_pm_2'
self.az_name_1 = 'b_az_pm_1'
self.pod_manager = driver.DriverManager(
namespace='tricircle.common.schedulers',
name='pod_manager',
invoke_on_load=True
).driver
self.b_pod_1 = {'pod_id': 'b_pod_pm_uuid_1',
'pod_name': 'b_region_pm_1',
'az_name': self.az_name_1}
self.b_pod_2 = {'pod_id': 'b_pod_pm_uuid_2',
'pod_name': 'b_region_pm_2',
'az_name': self.az_name_2}
self.b_pod_3 = {'pod_id': 'b_pod_pm_uuid_3',
'pod_name': 'b_region_pm_3',
'az_name': self.az_name_2}
self.b_pod_4 = {'pod_id': 'b_pod_pm_uuid_4',
'pod_name': 'b_region_pm_4',
'az_name': self.az_name_2}
def test_get_current_binding_and_pod(self):
api.create_pod(self.context, self.b_pod_1)
api.create_pod_binding(
self.context, self.project_id, self.b_pod_1['pod_id'])
pod_b_1, pod_1 = self.pod_manager.get_current_binding_and_pod(
self.context, self.az_name_1, self.project_id, pod_group='')
binding_q = core.query_resource(
self.context, models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(len(binding_q), 1)
self.assertEqual(binding_q[0]['id'], pod_b_1['id'])
pod_b_2, pod_2 = self.pod_manager.get_current_binding_and_pod(
self.context, self.az_name_1, 'new_project_pm_1', pod_group='')
binding_q = core.query_resource(
self.context, models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': 'new_project_pm_1'}], [])
self.assertEqual(len(binding_q), 0)
self.assertEqual(pod_b_2, None)
self.assertEqual(pod_2, None)
pod_b_3, pod_3 = self.pod_manager.get_current_binding_and_pod(
self.context, 'unknown_az', self.project_id, pod_group='')
binding_q = core.query_resource(
self.context, models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(len(binding_q), 1)
self.assertEqual(pod_b_3, None)
self.assertEqual(pod_3, None)
pod_b_4, pod_4 = self.pod_manager.get_current_binding_and_pod(
self.context, self.az_name_1, self.project_id, pod_group='test')
binding_q = core.query_resource(
self.context, models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': self.project_id}], [])
self.assertEqual(len(binding_q), 1)
self.assertEqual(pod_b_4['id'], binding_q[0]['id'])
self.assertEqual(pod_4, None)
def test_create_binding(self):
api.create_pod(self.context, self.b_pod_2)
flag = self.pod_manager.create_binding(
self.context, 'new_project_pm_2', self.b_pod_2['pod_id'])
self.assertEqual(flag, True)
binding_q = core.query_resource(
self.context, models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': 'new_project_pm_2'}], [])
self.assertEqual(len(binding_q), 1)
self.assertEqual(binding_q[0]['pod_id'], self.b_pod_2['pod_id'])
self.assertEqual(binding_q[0]['tenant_id'], 'new_project_pm_2')
self.assertEqual(binding_q[0]['is_binding'], True)
def test_update_binding(self):
api.create_pod(self.context, self.b_pod_4)
api.create_pod(self.context, self.b_pod_3)
flag = self.pod_manager.create_binding(
self.context, 'new_project_pm_3', self.b_pod_3['pod_id'])
self.assertEqual(flag, True)
current_binding = core.query_resource(
self.context, models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': 'new_project_pm_3'}], [])
flag = self.pod_manager.update_binding(
self.context, current_binding[0], self.b_pod_4['pod_id'])
self.assertEqual(flag, True)
binding_q = core.query_resource(
self.context, models.PodBinding,
[{'key': 'tenant_id',
'comparator': 'eq',
'value': 'new_project_pm_3'}], [])
self.assertEqual(len(binding_q), 2)
self.assertEqual(binding_q[0]['pod_id'], self.b_pod_3['pod_id'])
self.assertEqual(binding_q[0]['tenant_id'], 'new_project_pm_3')
self.assertEqual(binding_q[0]['is_binding'], False)
self.assertEqual(binding_q[1]['pod_id'], self.b_pod_4['pod_id'])
self.assertEqual(binding_q[1]['tenant_id'], 'new_project_pm_3')
self.assertEqual(binding_q[1]['is_binding'], True)

View File

@ -1,473 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
from novaclient.client import HTTPClient
import pecan
import unittest
from oslo_utils import uuidutils
from tricircle.common import client
from tricircle.common import constants
from tricircle.common import context
from tricircle.common import exceptions
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
from tricircle.nova_apigw.controllers import action
class FakeResponse(object):
def __new__(cls, code=500):
cls.status = code
cls.status_code = code
return super(FakeResponse, cls).__new__(cls)
class ActionTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
self.project_id = 'test_project'
self.context.tenant = self.project_id
self.controller = action.ActionController(self.project_id, '')
def _prepare_pod(self, bottom_pod_num=1):
t_pod = {'pod_id': 't_pod_uuid', 'pod_name': 't_region',
'az_name': ''}
api.create_pod(self.context, t_pod)
b_pods = []
if bottom_pod_num == 1:
b_pod = {'pod_id': 'b_pod_uuid', 'pod_name': 'b_region',
'az_name': 'b_az'}
api.create_pod(self.context, b_pod)
b_pods.append(b_pod)
else:
for i in xrange(1, bottom_pod_num + 1):
b_pod = {'pod_id': 'b_pod_%d_uuid' % i,
'pod_name': 'b_region_%d' % i,
'az_name': 'b_az_%d' % i}
api.create_pod(self.context, b_pod)
b_pods.append(b_pod)
return t_pod, b_pods
def _prepare_pod_service(self, pod_id, service):
config_dict = {'service_id': uuidutils.generate_uuid(),
'pod_id': pod_id,
'service_type': service,
'service_url': 'fake_pod_service'}
api.create_pod_service_configuration(self.context, config_dict)
def _prepare_server(self, pod):
t_server_id = uuidutils.generate_uuid()
b_server_id = t_server_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_server_id, 'bottom_id': b_server_id,
'pod_id': pod['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_SERVER})
return t_server_id
def _validate_error_code(self, res, code):
self.assertEqual(code, res[res.keys()[0]]['code'])
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_action_not_supported(self, mock_context):
mock_context.return_value = self.context
body = {'unsupported_action': ''}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(context, 'extract_context_from_environ')
def test_action_server_not_found(self, mock_context):
mock_context.return_value = self.context
body = {'os-start': ''}
res = self.controller.post(**body)
self._validate_error_code(res, 404)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_action_exception(self, mock_context, mock_action):
mock_context.return_value = self.context
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
mock_action.side_effect = exceptions.HTTPForbiddenError(
msg='Server operation forbidden')
body = {'os-start': ''}
res = self.controller.post(**body)
self._validate_error_code(res, 403)
mock_action.side_effect = exceptions.ServiceUnavailable
body = {'os-start': ''}
res = self.controller.post(**body)
self._validate_error_code(res, 500)
mock_action.side_effect = Exception
body = {'os-start': ''}
res = self.controller.post(**body)
self._validate_error_code(res, 500)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_start_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'os-start': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'start', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_stop_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'os-stop': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'stop', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_force_delete_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'forceDelete': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'force_delete', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_lock_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'lock': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'lock', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_unlock_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'unlock': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'unlock', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_pause_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'pause': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'pause', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_unpause_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'unpause': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'unpause', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_suspend_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'suspend': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'suspend', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_resume_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'resume': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'resume', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_shelveOffload_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'shelveOffload': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'shelve_offload', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_shelve_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'shelve': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'shelve', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_unshelve_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'unshelve': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'unshelve', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_trigger_crash_dump_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'trigger_crash_dump': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'trigger_crash_dump', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.Client, 'action_resources')
@patch.object(context, 'extract_context_from_environ')
def test_migrate_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {'migrate': ''}
res = self.controller.post(**body)
mock_action.assert_called_once_with(
'server', self.context, 'migrate', t_server_id)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_confirm_resize_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_NOVA)
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {"confirmResize": ''}
res = self.controller.post(**body)
url = '/servers/%s/action' % t_server_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_revert_resize_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_NOVA)
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {"revertResize": ''}
res = self.controller.post(**body)
url = '/servers/%s/action' % t_server_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_resize_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_NOVA)
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {"resize": {"flavorRef": "2"}}
res = self.controller.post(**body)
url = '/servers/%s/action' % t_server_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_reset_state_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_NOVA)
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {"os-resetState": {"state": "active"}}
res = self.controller.post(**body)
url = '/servers/%s/action' % t_server_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_soft_reboot_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_NOVA)
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {"reboot": {"type": "SOFT"}}
res = self.controller.post(**body)
url = '/servers/%s/action' % t_server_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(HTTPClient, 'post')
@patch.object(context, 'extract_context_from_environ')
def test_hard_reboot_action(self, mock_context, mock_action):
mock_context.return_value = self.context
mock_action.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_NOVA)
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
body = {"reboot": {"type": "HARD"}}
res = self.controller.post(**body)
url = '/servers/%s/action' % t_server_id
mock_action.assert_called_once_with(url, body=body)
self.assertEqual(202, res.status)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

View File

@ -1,62 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
import unittest
from tricircle.common import context
from tricircle.db import core
from tricircle.nova_apigw.controllers import aggregate
class AggregateTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.get_admin_context()
self.project_id = 'test_project'
self.controller = aggregate.AggregateController(self.project_id)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())
@patch.object(context, 'extract_context_from_environ')
def test_post(self, mock_context):
mock_context.return_value = self.context
body = {'aggregate': {'name': 'ag1',
'availability_zone': 'az1'}}
aggregate_id = self.controller.post(**body)['aggregate']['id']
aggregate_dict = self.controller.get_one(aggregate_id)['aggregate']
self.assertEqual('ag1', aggregate_dict['name'])
self.assertEqual('az1', aggregate_dict['availability_zone'])
self.assertEqual('az1',
aggregate_dict['metadata']['availability_zone'])
@patch.object(context, 'extract_context_from_environ')
def test_post_action(self, mock_context):
mock_context.return_value = self.context
body = {'aggregate': {'name': 'ag1',
'availability_zone': 'az1'}}
return_ag1 = self.controller.post(**body)['aggregate']
action_controller = aggregate.AggregateActionController(
self.project_id, return_ag1['id'])
return_ag2 = action_controller.post(**body)['aggregate']
self.assertEqual('ag1', return_ag2['name'])
self.assertEqual('az1', return_ag2['availability_zone'])

View File

@ -1,47 +0,0 @@
# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
import unittest
from tricircle.common import context
from tricircle.db import core
from tricircle.nova_apigw.controllers import flavor
class FlavorTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.get_admin_context()
self.project_id = 'test_project'
self.controller = flavor.FlavorController(self.project_id)
@patch.object(context, 'extract_context_from_environ')
def test_post(self, mock_context):
mock_context.return_value = self.context
body = {'flavor': {'id': '1', 'name': 'test_flavor',
'ram': 1024, 'vcpus': 1, 'disk': 10}}
self.controller.post(**body)
flavor_dict = self.controller.get_one('1')['flavor']
self.assertEqual('1', flavor_dict['id'])
self.assertEqual('test_flavor', flavor_dict['name'])
self.assertEqual(1024, flavor_dict['memory_mb'])
self.assertEqual(1, flavor_dict['vcpus'])
self.assertEqual(10, flavor_dict['root_gb'])
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

File diff suppressed because it is too large Load Diff

View File

@ -1,103 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from mock import patch
import pecan
import unittest
from oslo_utils import uuidutils
from novaclient import client
from tricircle.common import constants
from tricircle.common import context
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
from tricircle.nova_apigw.controllers import server_ips
class FakeResponse(object):
def __new__(cls, code=500):
cls.status = code
cls.status_code = code
return super(FakeResponse, cls).__new__(cls)
class ServerIpsTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.Context()
self.project_id = 'test_project'
self.context.tenant = self.project_id
self.context.user = 'test_user'
self.controller = server_ips.ServerIpsController(self.project_id, '')
def _prepare_pod(self, bottom_pod_num=1):
t_pod = {'pod_id': 't_pod_uuid', 'pod_name': 't_region',
'az_name': ''}
api.create_pod(self.context, t_pod)
b_pods = []
if bottom_pod_num == 1:
b_pod = {'pod_id': 'b_pod_uuid', 'pod_name': 'b_region',
'az_name': 'b_az'}
api.create_pod(self.context, b_pod)
b_pods.append(b_pod)
else:
for i in xrange(1, bottom_pod_num + 1):
b_pod = {'pod_id': 'b_pod_%d_uuid' % i,
'pod_name': 'b_region_%d' % i,
'az_name': 'b_az_%d' % i}
api.create_pod(self.context, b_pod)
b_pods.append(b_pod)
return t_pod, b_pods
def _prepare_server(self, pod):
t_server_id = uuidutils.generate_uuid()
b_server_id = t_server_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_server_id, 'bottom_id': b_server_id,
'pod_id': pod['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_SERVER})
return t_server_id
def _prepare_pod_service(self, pod_id, service):
config_dict = {'service_id': uuidutils.generate_uuid(),
'pod_id': pod_id,
'service_type': service,
'service_url': 'fake_pod_service'}
api.create_pod_service_configuration(self.context, config_dict)
@patch.object(pecan, 'response', new=FakeResponse)
@patch.object(client.HTTPClient, 'get')
@patch.object(context, 'extract_context_from_environ')
def test_list_ips(self, mock_context, mock_get):
mock_context.return_value = self.context
mock_get.return_value = (FakeResponse(202), None)
t_pod, b_pods = self._prepare_pod()
self._prepare_pod_service(b_pods[0]['pod_id'], constants.ST_NOVA)
t_server_id = self._prepare_server(b_pods[0])
self.controller.server_id = t_server_id
res = self.controller.get_all()
url = '/servers/%s/ips' % t_server_id
mock_get.assert_called_once_with(url)
self.assertEqual(202, res.status)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())

View File

@ -1,304 +0,0 @@
# Copyright (c) 2015 Huawei Tech. Co., Ltd.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from mock import patch
import pecan
import unittest
from oslo_utils import uuidutils
from tricircle.common import client
from tricircle.common import constants
from tricircle.common import context
from tricircle.db import api
from tricircle.db import core
from tricircle.db import models
from tricircle.nova_apigw.controllers import volume
class FakeResponse(object):
def __new__(cls, code=500):
cls.status = code
cls.status_code = code
return super(FakeResponse, cls).__new__(cls)
class FakeVolume(object):
def to_dict(self):
pass
class FakeClient(object):
def post(self, url, body):
return FakeResponse(), FakeVolume()
def get(self, url):
return FakeResponse(), FakeVolume()
def put(self, url, body):
return FakeResponse(), FakeVolume()
def delete(self, url):
return FakeResponse(), None
class FakeApi(object):
def __init__(self):
self.client = FakeClient()
class VolumeTest(unittest.TestCase):
def setUp(self):
core.initialize()
core.ModelBase.metadata.create_all(core.get_engine())
self.context = context.get_admin_context()
self.project_id = 'test_project'
self.controller = volume.VolumeController(self.project_id, '')
def _prepare_pod(self, bottom_pod_num=1):
t_pod = {'pod_id': 't_pod_uuid', 'pod_name': 't_region',
'az_name': ''}
api.create_pod(self.context, t_pod)
if bottom_pod_num == 1:
b_pod = {'pod_id': 'b_pod_uuid', 'pod_name': 'b_region',
'az_name': 'b_az'}
api.create_pod(self.context, b_pod)
return t_pod, b_pod
b_pods = []
for i in xrange(1, bottom_pod_num + 1):
b_pod = {'pod_id': 'b_pod_%d_uuid' % i,
'pod_name': 'b_region_%d' % i,
'az_name': 'b_az_%d' % i}
api.create_pod(self.context, b_pod)
b_pods.append(b_pod)
return t_pod, b_pods
def _validate_error_code(self, res, code):
self.assertEqual(code, res[res.keys()[0]]['code'])
@patch.object(pecan, 'response')
@patch.object(FakeClient, 'post')
@patch.object(client.Client, 'get_native_client')
@patch.object(context, 'extract_context_from_environ')
def test_attach_volume(self, mock_context, mock_api, mock_post,
mock_response):
mock_context.return_value = self.context
mock_api.return_value = FakeApi()
mock_response = FakeResponse()
mock_response.status = 202
t_pod, b_pods = self._prepare_pod(bottom_pod_num=2)
b_pod1 = b_pods[0]
b_pod2 = b_pods[1]
t_server_id = uuidutils.generate_uuid()
b_server_id = t_server_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_server_id, 'bottom_id': b_server_id,
'pod_id': b_pod1['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_SERVER})
t_volume1_id = uuidutils.generate_uuid()
b_volume1_id = t_volume1_id
t_volume2_id = uuidutils.generate_uuid()
b_volume2_id = t_volume1_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_volume1_id, 'bottom_id': b_volume1_id,
'pod_id': b_pod1['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_VOLUME})
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_volume2_id, 'bottom_id': b_volume2_id,
'pod_id': b_pod2['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_VOLUME})
# success case
self.controller.server_id = t_server_id
body = {'volumeAttachment': {'volumeId': t_volume1_id}}
self.controller.post(**body)
calls = [mock.call('server_volume', self.context)]
mock_api.assert_has_calls(calls)
url = "/servers/%s/os-volume_attachments" % t_server_id
calls = [mock.call(url, body=body)]
mock_post.assert_has_calls(calls)
body = {'volumeAttachment': {'volumeId': t_volume1_id,
'device': '/dev/vdb'}}
self.controller.post(**body)
calls = [mock.call('server_volume', self.context)]
mock_api.assert_has_calls(calls)
calls = [mock.call(url, body=body)]
mock_post.assert_has_calls(calls)
# failure case, bad request
body = {'volumeAttachment': {'volumeId': t_volume2_id}}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
body = {'fakePara': ''}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
body = {'volumeAttachment': {}}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
# each part of path should not start with digit
body = {'volumeAttachment': {'volumeId': t_volume1_id,
'device': '/dev/001disk'}}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
# the first part should be "dev", and only two parts are allowed
body = {'volumeAttachment': {'volumeId': t_volume1_id,
'device': '/dev/vdb/disk'}}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
body = {'volumeAttachment': {'volumeId': t_volume1_id,
'device': '/disk/vdb'}}
res = self.controller.post(**body)
self._validate_error_code(res, 400)
# failure case, resource not found
body = {'volumeAttachment': {'volumeId': 'fake_volume_id'}}
res = self.controller.post(**body)
self._validate_error_code(res, 404)
self.controller.server_id = 'fake_server_id'
body = {'volumeAttachment': {'volumeId': t_volume1_id}}
res = self.controller.post(**body)
self._validate_error_code(res, 404)
@patch.object(pecan, 'response')
@patch.object(FakeClient, 'delete')
@patch.object(client.Client, 'get_native_client')
@patch.object(context, 'extract_context_from_environ')
def test_detach_volume(self, mock_context, mock_api, mock_delete,
mock_response):
mock_context.return_value = self.context
mock_api.return_value = FakeApi()
mock_response = FakeResponse()
mock_response.status = 202
t_pod, b_pods = self._prepare_pod(bottom_pod_num=1)
b_pod1 = b_pods
t_server_id = uuidutils.generate_uuid()
b_server_id = t_server_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_server_id, 'bottom_id': b_server_id,
'pod_id': b_pod1['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_SERVER})
t_volume1_id = uuidutils.generate_uuid()
b_volume1_id = t_volume1_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_volume1_id, 'bottom_id': b_volume1_id,
'pod_id': b_pod1['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_VOLUME})
# success case
self.controller.server_id = t_server_id
body = {'volumeAttachment': {'volumeId': t_volume1_id}}
self.controller.post(**body)
self.controller.delete(t_volume1_id)
calls = [mock.call('server_volume', self.context),
mock.call('server_volume', self.context)]
mock_api.assert_has_calls(calls)
url = "/servers/%s/os-volume_attachments/%s" % (
t_server_id, t_volume1_id)
calls = [mock.call(url)]
mock_delete.assert_has_calls(calls)
# failure case, resource not found
body = {'volumeAttachment': {'volumeId': t_volume1_id}}
self.controller.post(**body)
self.controller.server_id = 'fake_server_id'
res = self.controller.delete(t_volume1_id)
self._validate_error_code(res, 404)
@patch.object(pecan, 'response')
@patch.object(FakeClient, 'get')
@patch.object(client.Client, 'get_native_client')
@patch.object(context, 'extract_context_from_environ')
def test_get_volume_attachments(self, mock_context, mock_api,
mock_get, mock_response):
mock_context.return_value = self.context
mock_api.return_value = FakeApi()
mock_response = FakeResponse()
mock_response.status = 202
t_pod, b_pods = self._prepare_pod(bottom_pod_num=1)
b_pod1 = b_pods
t_server_id = uuidutils.generate_uuid()
b_server_id = t_server_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_server_id, 'bottom_id': b_server_id,
'pod_id': b_pod1['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_SERVER})
t_volume1_id = uuidutils.generate_uuid()
b_volume1_id = t_volume1_id
t_volume2_id = uuidutils.generate_uuid()
b_volume2_id = t_volume2_id
with self.context.session.begin():
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_volume1_id, 'bottom_id': b_volume1_id,
'pod_id': b_pod1['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_VOLUME})
core.create_resource(
self.context, models.ResourceRouting,
{'top_id': t_volume2_id, 'bottom_id': b_volume2_id,
'pod_id': b_pod1['pod_id'], 'project_id': self.project_id,
'resource_type': constants.RT_VOLUME})
# success case
self.controller.server_id = t_server_id
body = {'volumeAttachment': {'volumeId': t_volume1_id}}
self.controller.post(**body)
body = {'volumeAttachment': {'volumeId': t_volume2_id}}
self.controller.post(**body)
self.controller.get_one(t_volume1_id)
url = "/servers/%s/os-volume_attachments/%s" % (
t_server_id, t_volume1_id)
calls = [mock.call(url)]
mock_get.asset_has_calls(calls)
self.controller.get_all()
url = "/servers/%s/os-volume_attachments" % t_server_id
calls = [mock.call(calls)]
mock_get.asset_has_calls(calls)
calls = [mock.call('server_volume', self.context),
mock.call('server_volume', self.context),
mock.call('server_volume', self.context),
mock.call('server_volume', self.context)]
mock_api.assert_has_calls(calls)
# failure case, resource not found
self.controller.server_id = 'fake_server_id'
res = self.controller.get_one(t_volume1_id)
self._validate_error_code(res, 404)
res = self.controller.get_all()
self._validate_error_code(res, 404)
def tearDown(self):
core.ModelBase.metadata.drop_all(core.get_engine())