diff --git a/tricircle/common/constants.py b/tricircle/common/constants.py index 51470f7..f66d00b 100644 --- a/tricircle/common/constants.py +++ b/tricircle/common/constants.py @@ -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' diff --git a/tricircle/common/context.py b/tricircle/common/context.py index f3d81ea..3201aa8 100644 --- a/tricircle/common/context.py +++ b/tricircle/common/context.py @@ -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 diff --git a/tricircle/common/resource_handle.py b/tricircle/common/resource_handle.py index d6c0323..c3fde33 100644 --- a/tricircle/common/resource_handle.py +++ b/tricircle/common/resource_handle.py @@ -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) diff --git a/tricircle/nova_apigw/app.py b/tricircle/nova_apigw/app.py index 9a16994..35f96a1 100644 --- a/tricircle/nova_apigw/app.py +++ b/tricircle/nova_apigw/app.py @@ -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 diff --git a/tricircle/nova_apigw/controllers/micro_versions.py b/tricircle/nova_apigw/controllers/micro_versions.py new file mode 100644 index 0000000..ad1442f --- /dev/null +++ b/tricircle/nova_apigw/controllers/micro_versions.py @@ -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 diff --git a/tricircle/nova_apigw/controllers/root.py b/tricircle/nova_apigw/controllers/root.py index b9a8301..d3c93e3 100644 --- a/tricircle/nova_apigw/controllers/root.py +++ b/tricircle/nova_apigw/controllers/root.py @@ -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): diff --git a/tricircle/nova_apigw/controllers/root_versions.py b/tricircle/nova_apigw/controllers/root_versions.py new file mode 100644 index 0000000..0f96240 --- /dev/null +++ b/tricircle/nova_apigw/controllers/root_versions.py @@ -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 diff --git a/tricircle/tempestplugin/tempest_compute.sh b/tricircle/tempestplugin/tempest_compute.sh index b8f1c7a..4e8d4a2 100755 --- a/tricircle/tempestplugin/tempest_compute.sh +++ b/tricircle/tempestplugin/tempest_compute.sh @@ -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] diff --git a/tricircle/tests/functional/nova_apigw/controllers/test_microversion.py b/tricircle/tests/functional/nova_apigw/controllers/test_microversion.py new file mode 100644 index 0000000..979554e --- /dev/null +++ b/tricircle/tests/functional/nova_apigw/controllers/test_microversion.py @@ -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) diff --git a/tricircle/tests/functional/nova_apigw/controllers/test_root.py b/tricircle/tests/functional/nova_apigw/controllers/test_root.py index 2d34aa2..26e557c 100644 --- a/tricircle/tests/functional/nova_apigw/controllers/test_root.py +++ b/tricircle/tests/functional/nova_apigw/controllers/test_root.py @@ -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)