Nova_APIGW REST API Microversion Support

1. What is the problem
The current Nova_APIGW does not support microversion function, the service
controller uses a fixed API version number to initialize novaclient

2. What is the solution to the problem
When the service controller receives an API request, it will get the
microversion number from request headers , and use this to initialize
novaclient.

For how to get the microversion number, please refer to:
https://specs.openstack.org/openstack/nova-specs/specs/kilo/implemented/api-microversions.html

The microversion supported range is 2.1 to latest version

3. What the features need to be implemented to the Tricircle
   to realize the solution
   Nova_APIGW microversion support added

Change-Id: Idf44c91100e5cb8ad0355164c9be991aa54a652b
This commit is contained in:
yinxiulin 2016-08-24 14:24:53 +08:00
parent 361f7f7a27
commit 707c914e30
10 changed files with 765 additions and 55 deletions

View File

@ -81,3 +81,19 @@ JT_PORT_DELETE = 'port_delete'
# network type
NT_LOCAL = 'local'
NT_SHARED_VLAN = 'shared_vlan'
# nova microverson headers key word
NOVA_API_VERSION_REQUEST_HEADER = 'OpenStack-API-Version'
LEGACY_NOVA_API_VERSION_REQUEST_HEADER = 'X-OpenStack-Nova-API-Version'
HTTP_NOVA_API_VERSION_REQUEST_HEADER = 'HTTP_OPENSTACK_API_VERSION'
HTTP_LEGACY_NOVA_API_VERSION_REQUEST_HEADER = \
'HTTP_X_OPENSTACK_NOVA_API_VERSION'
# nova microverson prefix
NOVA_MICRO_VERSION_PREFIX = 'compute'
# support nova version range
NOVA_APIGW_MIN_VERSION = '2.1'
NOVA_APIGW_MAX_VERSION = '2.36'

View File

@ -19,6 +19,7 @@ from pecan import request
import oslo_context.context as oslo_ctx
from tricircle.common import constants
from tricircle.common.i18n import _
from tricircle.db import core
@ -46,7 +47,9 @@ def extract_context_from_environ():
'domain': 'HTTP_X_DOMAIN_ID',
'user_domain': 'HTTP_X_USER_DOMAIN_ID',
'project_domain': 'HTTP_X_PROJECT_DOMAIN_ID',
'request_id': 'openstack.request_id'}
'request_id': 'openstack.request_id',
'nova_micro_version':
constants.NOVA_API_VERSION_REQUEST_HEADER}
environ = request.environ
@ -100,6 +103,8 @@ class ContextBase(oslo_ctx.RequestContext):
self.tenant_name = tenant_name
self.quota_class = quota_class
self.read_deleted = read_deleted
self.nova_micro_version = kwargs.get('nova_micro_version',
constants.NOVA_APIGW_MIN_VERSION)
def _get_read_deleted(self):
return self._read_deleted

View File

@ -215,7 +215,7 @@ class NovaResourceHandle(ResourceHandle):
'server_volume': ACTION}
def _get_client(self, cxt):
cli = n_client.Client(api_versions.APIVersion('2.1'),
cli = n_client.Client(api_versions.APIVersion(cxt.nova_micro_version),
auth_token=cxt.auth_token,
auth_url=self.auth_url,
timeout=cfg.CONF.client.nova_timeout)

View File

@ -18,7 +18,9 @@ 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 = [
@ -73,4 +75,9 @@ def setup_app(*args, **kwargs):
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

@ -0,0 +1,120 @@
# 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

@ -23,6 +23,7 @@ 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
@ -54,34 +55,6 @@ class RootController(object):
if version == 'v2.1':
return V21Controller(), remainder
@pecan.expose(generic=True, template='json')
def index(self):
return {
"versions": [
{
"status": "CURRENT",
"updated": "2013-07-23T11:33:21Z",
"links": [
{
"href": pecan.request.application_url + "/v2.1/",
"rel": "self"
}
],
"min_version": "2.1",
"version": "2.12",
"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(405)
class V21Controller(object):
@ -154,8 +127,8 @@ class V21Controller(object):
"rel": "describedby"
}
],
"min_version": "2.1",
"version": "2.12",
"min_version": constants.NOVA_APIGW_MIN_VERSION,
"version": constants.NOVA_APIGW_MAX_VERSION,
"media-types": [
{
"base": "application/json",
@ -172,7 +145,7 @@ class V21Controller(object):
@index.when(method='HEAD')
@index.when(method='PATCH')
def not_supported(self):
pecan.abort(405)
pecan.abort(404)
class TestRPCController(rest.RestController):

View File

@ -0,0 +1,82 @@
# 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,3 +1,62 @@
#!/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.test_versions"
#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.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]
@ -494,8 +553,8 @@
# 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]
# tempest.api.compute.test_versions.TestVersions.test_get_version_details[id-b953a29e-929c-4a8e-81be-ec3a7e03cb76]
# tempest.api.compute.test_versions.TestVersions.test_list_api_versions[id-6c0a0990-43b6-4529-9b61-5fd8daf7c55c]
# **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]
# tempest.api.compute.volumes.test_attach_volume.AttachVolumeShelveTestJSON.test_detach_volume_shelved_or_offload_server[id-b54e86dd-a070-49c4-9c07-59ae6dae15aa]

View File

@ -0,0 +1,447 @@
# 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

@ -74,29 +74,30 @@ class TestRootController(Nova_API_GW_FunctionalTest):
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]["min_version"], "2.1")
self.assertEqual(versions[0]["id"], "v2.1")
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_405(self, method):
def _test_method_returns_404(self, method):
api_method = getattr(self.app, method)
response = api_method('/', expect_errors=True)
self.assertEqual(response.status_int, 405)
self.assertEqual(response.status_int, 404)
def test_post(self):
self._test_method_returns_405('post')
self._test_method_returns_404('post')
def test_put(self):
self._test_method_returns_405('put')
self._test_method_returns_404('put')
def test_patch(self):
self._test_method_returns_405('patch')
self._test_method_returns_404('patch')
def test_delete(self):
self._test_method_returns_405('delete')
self._test_method_returns_404('delete')
def test_head(self):
self._test_method_returns_405('head')
self._test_method_returns_404('head')
class TestV21Controller(Nova_API_GW_FunctionalTest):
@ -109,25 +110,25 @@ class TestV21Controller(Nova_API_GW_FunctionalTest):
self.assertEqual(version["min_version"], "2.1")
self.assertEqual(version["id"], "v2.1")
def _test_method_returns_405(self, method):
def _test_method_returns_404(self, method):
api_method = getattr(self.app, method)
response = api_method('/v2.1', expect_errors=True)
self.assertEqual(response.status_int, 405)
response = api_method('/', expect_errors=True)
self.assertEqual(response.status_int, 404)
def test_post(self):
self._test_method_returns_405('post')
self._test_method_returns_404('post')
def test_put(self):
self._test_method_returns_405('put')
self._test_method_returns_404('put')
def test_patch(self):
self._test_method_returns_405('patch')
self._test_method_returns_404('patch')
def test_delete(self):
self._test_method_returns_405('delete')
self._test_method_returns_404('delete')
def test_head(self):
self._test_method_returns_405('head')
self._test_method_returns_404('head')
class TestErrors(Nova_API_GW_FunctionalTest):
@ -145,7 +146,7 @@ class TestErrors(Nova_API_GW_FunctionalTest):
class TestRequestID(Nova_API_GW_FunctionalTest):
def test_request_id(self):
response = self.app.get('/')
response = self.app.get('/v2.1/')
self.assertIn('x-openstack-request-id', response.headers)
self.assertTrue(
response.headers['x-openstack-request-id'].startswith('req-'))
@ -169,5 +170,5 @@ class TestKeystoneAuth(Nova_API_GW_FunctionalTest):
self.app = self._make_app()
def test_auth_enforced(self):
response = self.app.get('/', expect_errors=True)
response = self.app.get('/v2.1/', expect_errors=True)
self.assertEqual(response.status_int, 401)