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
This commit is contained in:
Chris Buccella 2014-01-15 06:21:27 +00:00
parent 296f2fa183
commit 49d83e356a
9 changed files with 126 additions and 32 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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])