From 49d83e356afd6197fc95277f0d18723bdfa11652 Mon Sep 17 00:00:00 2001 From: Chris Buccella Date: Wed, 15 Jan 2014 06:21:27 +0000 Subject: [PATCH] Add x-openstack-request-id to nova v3 responses x-openstack-request-id is the common header name for request ID going forward. Nova, cinder, and neutron return this header. Using the request_id middleware is a convenient way to ensure this header is in the response. We will also be using the middleware to generate the request ID. The ID will be generated by the middleware and is inserted into the request environment. The current code generates the request ID in the constructor of RequestContext; with this change, the ID is generated beforehand and passed in to the RequestContext constructor. Not much different here: a request ID is generated and is available for use in the handling of the request. On the response end, the middleware is again used, this time to attach the x-openstack-request-id header, using the value of the generated request ID. For v3, we will be using the request_id middleware provided in oslo. For v2, nova-specific middleware (compute_request_id.py) is used. Therefore, v3 responses will have the header x-openstack-request-id, and v2 responses will have the header x-compute-request-id (no change). It is necessary to move the existing code out into middleware, so that the old header is not attached to v3 responses. UpgradeImpact: api-paste.ini is modified DocImpact: v3 responses of the API will only include x-openstack-request-id Implements: blueprint cross-service-request-id Change-Id: I5e370fd3de5ee2f8a8d13553015d88910ff5ea87 --- etc/nova/api-paste.ini | 16 ++++--- nova/api/auth.py | 6 ++- nova/api/compute_req_id.py | 45 ++++++++++++++++++++ nova/api/openstack/wsgi.py | 10 ----- nova/tests/api/openstack/compute/test_api.py | 11 ----- nova/tests/api/openstack/test_wsgi.py | 15 ++++--- nova/tests/api/test_auth.py | 9 ++++ nova/tests/api/test_compute_req_id.py | 40 +++++++++++++++++ nova/tests/utils.py | 6 +++ 9 files changed, 126 insertions(+), 32 deletions(-) create mode 100644 nova/api/compute_req_id.py create mode 100644 nova/tests/api/test_compute_req_id.py diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index 893e866b0b..5f50e8a6c0 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -65,14 +65,20 @@ use = call:nova.api.openstack.urlmap:urlmap_factory [composite:openstack_compute_api_v2] use = call:nova.api.auth:pipeline_factory -noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2 -keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2 -keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2 +noauth = compute_req_id faultwrap sizelimit noauth ratelimit osapi_compute_app_v2 +keystone = compute_req_id faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2 +keystone_nolimit = compute_req_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2 [composite:openstack_compute_api_v3] use = call:nova.api.auth:pipeline_factory_v3 -noauth = faultwrap sizelimit noauth_v3 osapi_compute_app_v3 -keystone = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v3 +noauth = request_id faultwrap sizelimit noauth_v3 osapi_compute_app_v3 +keystone = request_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v3 + +[filter:request_id] +paste.filter_factory = nova.openstack.common.middleware.request_id:RequestIdMiddleware.factory + +[filter:compute_req_id] +paste.filter_factory = nova.api.compute_req_id:ComputeReqIdMiddleware.factory [filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factory diff --git a/nova/api/auth.py b/nova/api/auth.py index 2648d94e99..b0015cce2b 100644 --- a/nova/api/auth.py +++ b/nova/api/auth.py @@ -24,6 +24,7 @@ from nova import context from nova.openstack.common.gettextutils import _ from nova.openstack.common import jsonutils from nova.openstack.common import log as logging +from nova.openstack.common.middleware import request_id from nova import wsgi @@ -113,6 +114,8 @@ class NovaKeystoneContext(wsgi.Middleware): project_name = req.headers.get('X_TENANT_NAME') user_name = req.headers.get('X_USER_NAME') + req_id = req.environ.get(request_id.ENV_REQUEST_ID) + # Get the auth token auth_token = req.headers.get('X_AUTH_TOKEN', req.headers.get('X_STORAGE_TOKEN')) @@ -138,7 +141,8 @@ class NovaKeystoneContext(wsgi.Middleware): roles=roles, auth_token=auth_token, remote_address=remote_address, - service_catalog=service_catalog) + service_catalog=service_catalog, + request_id=req_id) req.environ['nova.context'] = ctx return self.application diff --git a/nova/api/compute_req_id.py b/nova/api/compute_req_id.py new file mode 100644 index 0000000000..2118d96eb0 --- /dev/null +++ b/nova/api/compute_req_id.py @@ -0,0 +1,45 @@ +# Copyright (c) 2014 IBM Corp. +# 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. + +"""Middleware that ensures x-compute-request-id + +Using this middleware provides a convenient way to attach the +x-compute-request-id to only v2 responses. Previously, this header was set in +api/openstack/wsgi.py + +Responses for APIv3 are taken care of by the request_id middleware provided +in oslo. +""" + +import webob.dec + +from nova.openstack.common import context +from nova.openstack.common.middleware import base + + +ENV_REQUEST_ID = 'openstack.request_id' +HTTP_RESP_HEADER_REQUEST_ID = 'x-compute-request-id' + + +class ComputeReqIdMiddleware(base.Middleware): + + @webob.dec.wsgify + def __call__(self, req): + req_id = context.generate_request_id() + req.environ[ENV_REQUEST_ID] = req_id + response = req.get_response(self.application) + if HTTP_RESP_HEADER_REQUEST_ID not in response.headers: + response.headers.add(HTTP_RESP_HEADER_REQUEST_ID, req_id) + return response diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 385cf52821..287fc4e2f9 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -1013,9 +1013,6 @@ class Resource(wsgi.Application): self.default_serializers) if hasattr(response, 'headers'): - if context: - response.headers.add('x-compute-request-id', - context.request_id) for hdr, val in response.headers.items(): # Headers must be utf-8 strings @@ -1238,7 +1235,6 @@ class Fault(webob.exc.HTTPException): self.wrapped_exc.body = serializer.serialize(fault_data) self.wrapped_exc.content_type = content_type - _set_request_id_header(req, self.wrapped_exc.headers) return self.wrapped_exc @@ -1298,9 +1294,3 @@ class RateLimitFault(webob.exc.HTTPException): self.wrapped_exc.content_type = content_type return self.wrapped_exc - - -def _set_request_id_header(req, headers): - context = req.environ.get('nova.context') - if context: - headers['x-compute-request-id'] = context.request_id diff --git a/nova/tests/api/openstack/compute/test_api.py b/nova/tests/api/openstack/compute/test_api.py index 437458cac6..35f3976f21 100644 --- a/nova/tests/api/openstack/compute/test_api.py +++ b/nova/tests/api/openstack/compute/test_api.py @@ -20,7 +20,6 @@ import webob.exc from nova.api import openstack as openstack_api from nova.api.openstack import wsgi -import nova.context from nova import exception from nova.openstack.common import jsonutils from nova import test @@ -187,13 +186,3 @@ class APITest(test.NoDBTestCase): api = self._wsgi_app(fail) resp = webob.Request.blank('/').get_response(api) self.assertEqual(500, resp.status_int) - - def test_request_id_in_response(self): - req = webob.Request.blank('/') - req.method = 'GET' - context = nova.context.RequestContext('bob', 1) - context.request_id = 'test-req-id' - req.environ['nova.context'] = context - - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.headers['x-compute-request-id'], 'test-req-id') diff --git a/nova/tests/api/openstack/test_wsgi.py b/nova/tests/api/openstack/test_wsgi.py index 018ee55f89..db3fcd9aed 100644 --- a/nova/tests/api/openstack/test_wsgi.py +++ b/nova/tests/api/openstack/test_wsgi.py @@ -335,6 +335,14 @@ class XMLDeserializerTest(test.NoDBTestCase): class ResourceTest(test.NoDBTestCase): + + def get_req_id_header_name(self, request): + header_name = 'x-openstack-request-id' + if utils.get_api_version(request) < 3: + header_name = 'x-compute-request-id' + + return header_name + def test_resource_call_with_method_get(self): class Controller(object): def index(self, req): @@ -639,8 +647,6 @@ class ResourceTest(test.NoDBTestCase): context = req.environ['nova.context'] app = fakes.TestRouter(Controller()) response = req.get_response(app) - self.assertEqual(response.headers['x-compute-request-id'], - context.request_id) self.assertEqual(response.body, '{"foo": "bar"}') self.assertEqual(response.status_int, 200) @@ -655,7 +661,8 @@ class ResourceTest(test.NoDBTestCase): # NOTE(alaski): This test is really to ensure that a str response # doesn't error. Not having a request_id header is a side effect of # our wsgi setup, ideally it would be there. - self.assertFalse(hasattr(response.headers, 'x-compute-request-id')) + expected_header = self.get_req_id_header_name(req) + self.assertFalse(hasattr(response.headers, expected_header)) self.assertEqual(response.body, 'foo') self.assertEqual(response.status_int, 200) @@ -668,8 +675,6 @@ class ResourceTest(test.NoDBTestCase): context = req.environ['nova.context'] app = fakes.TestRouter(Controller()) response = req.get_response(app) - self.assertEqual(response.headers['x-compute-request-id'], - context.request_id) self.assertEqual(response.body, '') self.assertEqual(response.status_int, 200) diff --git a/nova/tests/api/test_auth.py b/nova/tests/api/test_auth.py index 6d964369ec..f4746e5704 100644 --- a/nova/tests/api/test_auth.py +++ b/nova/tests/api/test_auth.py @@ -19,6 +19,7 @@ import webob.exc import nova.api.auth from nova.openstack.common.gettextutils import _ +from nova.openstack.common.middleware import request_id from nova import test CONF = cfg.CONF @@ -70,6 +71,14 @@ class TestNovaKeystoneContextMiddleware(test.NoDBTestCase): response = self.request.get_response(self.middleware) self.assertEqual(response.status, '500 Internal Server Error') + def test_request_id_extracted_from_env(self): + req_id = 'dummy-request-id' + self.request.headers['X_PROJECT_ID'] = 'testtenantid' + self.request.headers['X_USER_ID'] = 'testuserid' + self.request.environ[request_id.ENV_REQUEST_ID] = req_id + self.request.get_response(self.middleware) + self.assertEqual(req_id, self.context.request_id) + class TestKeystoneMiddlewareRoles(test.NoDBTestCase): diff --git a/nova/tests/api/test_compute_req_id.py b/nova/tests/api/test_compute_req_id.py new file mode 100644 index 0000000000..bbdbfab726 --- /dev/null +++ b/nova/tests/api/test_compute_req_id.py @@ -0,0 +1,40 @@ +# Copyright (c) 2014 IBM Corp. +# 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 testtools import matchers +import webob +import webob.dec + +from nova.api import compute_req_id +from nova.openstack.common import context +from nova import test + + +class RequestIdTest(test.TestCase): + def test_generate_request_id(self): + @webob.dec.wsgify + def application(req): + return req.environ[compute_req_id.ENV_REQUEST_ID] + + app = compute_req_id.ComputeReqIdMiddleware(application) + req = webob.Request.blank('/test') + req_id = context.generate_request_id() + req.environ[compute_req_id.ENV_REQUEST_ID] = req_id + res = req.get_response(app) + + res_id = res.headers.get(compute_req_id.HTTP_RESP_HEADER_REQUEST_ID) + self.assertThat(res_id, matchers.StartsWith('req-')) + self.assertEqual(res_id, res.body) diff --git a/nova/tests/utils.py b/nova/tests/utils.py index ef1e6b6d5a..da8fd5a153 100644 --- a/nova/tests/utils.py +++ b/nova/tests/utils.py @@ -206,3 +206,9 @@ def is_ipv6_supported(): has_ipv6_support = False return has_ipv6_support + + +def get_api_version(request): + api_version = 2 + if request.path[2:3].isdigit(): + return int(request.path[2:3])