diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index 6871947816c7..169d60bb51bf 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -108,6 +108,9 @@ paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware. [filter:sizelimit] paste.filter_factory = oslo_middleware:RequestBodySizeLimiter.factory +[filter:legacy_v2_compatible] +paste.filter_factory = nova.api.openstack:LegacyV2CompatibleWrapper.factory + [app:osapi_compute_app_v2] paste.app_factory = nova.api.openstack.compute:APIRouter.factory diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index a46acda39adc..643292f88c85 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -128,6 +128,46 @@ class FaultWrapper(base_wsgi.Middleware): return self._error(ex, req) +class LegacyV2CompatibleWrapper(base_wsgi.Middleware): + + def _filter_request_headers(self, req): + """For keeping same behavior with v2 API, ignores microversions + HTTP header X-OpenStack-Nova-API-Version in the request. + """ + + if wsgi.API_VERSION_REQUEST_HEADER in req.headers: + del req.headers[wsgi.API_VERSION_REQUEST_HEADER] + return req + + def _filter_response_headers(self, response): + """For keeping same behavior with v2 API, filter out microversions + HTTP header and microversions field in header 'Vary'. + """ + + if wsgi.API_VERSION_REQUEST_HEADER in response.headers: + del response.headers[wsgi.API_VERSION_REQUEST_HEADER] + + if 'Vary' in response.headers: + vary_headers = response.headers['Vary'].split(',') + filtered_vary = [] + for vary in vary_headers: + vary = vary.strip() + if vary == wsgi.API_VERSION_REQUEST_HEADER: + continue + filtered_vary.append(vary) + if filtered_vary: + response.headers['Vary'] = ','.join(filtered_vary) + else: + del response.headers['Vary'] + return response + + @webob.dec.wsgify + def __call__(self, req): + req = self._filter_request_headers(req) + response = req.get_response(self.application) + return self._filter_response_headers(response) + + class APIMapper(routes.Mapper): def routematch(self, url=None, environ=None): if url == "": diff --git a/nova/tests/functional/test_legacy_v3_compatible_wrapper.py b/nova/tests/functional/test_legacy_v3_compatible_wrapper.py new file mode 100644 index 000000000000..e3e25fe19a3b --- /dev/null +++ b/nova/tests/functional/test_legacy_v3_compatible_wrapper.py @@ -0,0 +1,34 @@ +# Copyright 2015 Intel Corporation. +# 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 nova.api.openstack import wsgi +from nova.tests.functional import integrated_helpers +from nova.tests.functional.v3 import api_paste_fixture + + +class LegacyV2CompatibleTestBase(integrated_helpers._IntegratedTestBase): + _api_version = 'v2' + + def setUp(self): + self.useFixture(api_paste_fixture.ApiPasteV2CompatibleFixture()) + super(LegacyV2CompatibleTestBase, self).setUp() + + def test_request_with_microversion_headers(self): + response = self.api.api_post('os-keypairs', + {"keypair": {"name": "test"}}, + headers={wsgi.API_VERSION_REQUEST_HEADER: '2.100'}) + self.assertNotIn(wsgi.API_VERSION_REQUEST_HEADER, response.headers) + self.assertNotIn('Vary', response.headers) + self.assertNotIn('type', response.body["keypair"]) diff --git a/nova/tests/functional/v3/api_paste_fixture.py b/nova/tests/functional/v3/api_paste_fixture.py index 4dd74ae41059..61a05365bddf 100644 --- a/nova/tests/functional/v3/api_paste_fixture.py +++ b/nova/tests/functional/v3/api_paste_fixture.py @@ -25,6 +25,11 @@ CONF = cfg.CONF class ApiPasteFixture(fixtures.Fixture): + def _replace_line(self, target_file, line): + target_file.write(line.replace( + "/v2: openstack_compute_api_v2", + "/v2: openstack_compute_api_v21")) + def setUp(self): super(ApiPasteFixture, self).setUp() CONF.set_default('api_paste_config', @@ -35,7 +40,16 @@ class ApiPasteFixture(fixtures.Fixture): with open(CONF.api_paste_config, 'r') as orig_api_paste: with open(tmp_api_paste_file_name, 'w') as tmp_file: for line in orig_api_paste: - tmp_file.write(line.replace( - "/v2: openstack_compute_api_v2", - "/v2: openstack_compute_api_v21")) + self._replace_line(tmp_file, line) CONF.set_override('api_paste_config', tmp_api_paste_file_name) + + +class ApiPasteV2CompatibleFixture(ApiPasteFixture): + + def _replace_line(self, target_file, line): + line = line.replace("noauth2 osapi_compute_app_v21", + "noauth2 legacy_v2_compatible osapi_compute_app_v21") + line = line.replace( + "/v2: openstack_compute_api_v2", + "/v2: openstack_compute_api_v21") + target_file.write(line) diff --git a/nova/tests/unit/api/openstack/test_legacy_v2_compatible_wrapper.py b/nova/tests/unit/api/openstack/test_legacy_v2_compatible_wrapper.py new file mode 100644 index 000000000000..aee320b397b3 --- /dev/null +++ b/nova/tests/unit/api/openstack/test_legacy_v2_compatible_wrapper.py @@ -0,0 +1,95 @@ +# Copyright 2015 Intel Corporation +# 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 webob +import webob.dec + +import nova.api.openstack +from nova.api.openstack import wsgi +from nova import test + + +class TestLegacyV2CompatibleWrapper(test.NoDBTestCase): + + def test_filter_out_microverions_request_header(self): + req = webob.Request.blank('/') + req.headers[wsgi.API_VERSION_REQUEST_HEADER] = '2.2' + + @webob.dec.wsgify + def fake_app(req, *args, **kwargs): + self.assertNotIn(wsgi.API_VERSION_REQUEST_HEADER, req) + resp = webob.Response() + return resp + + wrapper = nova.api.openstack.LegacyV2CompatibleWrapper(fake_app) + req.get_response(wrapper) + + def test_filter_out_microverions_response_header(self): + req = webob.Request.blank('/') + + @webob.dec.wsgify + def fake_app(req, *args, **kwargs): + resp = webob.Response() + resp.status_int = 204 + resp.headers[wsgi.API_VERSION_REQUEST_HEADER] = '2.3' + return resp + + wrapper = nova.api.openstack.LegacyV2CompatibleWrapper(fake_app) + response = req.get_response(wrapper) + self.assertNotIn(wsgi.API_VERSION_REQUEST_HEADER, response.headers) + + def test_filter_out_microverions_vary_header(self): + req = webob.Request.blank('/') + + @webob.dec.wsgify + def fake_app(req, *args, **kwargs): + resp = webob.Response() + resp.status_int = 204 + resp.headers['Vary'] = wsgi.API_VERSION_REQUEST_HEADER + return resp + + wrapper = nova.api.openstack.LegacyV2CompatibleWrapper(fake_app) + response = req.get_response(wrapper) + self.assertNotIn('Vary', response.headers) + + def test_filter_out_microverions_vary_header_with_multi_fields(self): + req = webob.Request.blank('/') + + @webob.dec.wsgify + def fake_app(req, *args, **kwargs): + resp = webob.Response() + resp.status_int = 204 + resp.headers['Vary'] = '%s, %s, %s' % ( + wsgi.API_VERSION_REQUEST_HEADER, 'FAKE_HEADER1', + 'FAKE_HEADER2') + return resp + + wrapper = nova.api.openstack.LegacyV2CompatibleWrapper(fake_app) + response = req.get_response(wrapper) + self.assertEqual('FAKE_HEADER1,FAKE_HEADER2', + response.headers['Vary']) + + def test_filter_out_microverions_no_vary_header(self): + req = webob.Request.blank('/') + + @webob.dec.wsgify + def fake_app(req, *args, **kwargs): + resp = webob.Response() + resp.status_int = 204 + return resp + + wrapper = nova.api.openstack.LegacyV2CompatibleWrapper(fake_app) + response = req.get_response(wrapper) + self.assertNotIn('Vary', response.headers)