Add request_id middleware support
'x-openstack-request-id' is the common header name for request ID which is implemented in most of the OpenStack services. Using the oslo middleware `request_id` middleware is a convenient way to generate request ID and also include it in the response. The request ID will be generated by the oslo middleware and is inserted into the request environment. 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. Sample log output of blazar-api service for ``GET v1/os-hosts`` API which logs local request ID `req-de45521c-2a04-4e7d-809a-960e782eb1e7` is shown below: http://paste.openstack.org/show/753805/ Note: For v2 apis, the request_id is not returned in response header but it will be logged in logs. APIImpact: responses of the API will include 'x-openstack-request-id' header. Implements: blueprint oslo-middleware-request-id Change-Id: I437f783787514ff1add2d7f0059cb27addd12c3e
This commit is contained in:
parent
fedc289059
commit
438585e3d7
@ -10,3 +10,4 @@ Blazar project.
|
||||
.. include:: leases.inc
|
||||
.. include:: hosts.inc
|
||||
.. include:: floatingips.inc
|
||||
.. include:: request-ids.inc
|
||||
|
@ -1,3 +1,15 @@
|
||||
# variables in headers
|
||||
x-openstack-request-id_resp:
|
||||
description: |
|
||||
The local request ID, which is a unique ID generated automatically
|
||||
for tracking each request to blazar.
|
||||
It is associated with the request and appears in the log lines
|
||||
for that request.
|
||||
in: header
|
||||
required: true
|
||||
type: string
|
||||
|
||||
|
||||
# variables in path
|
||||
floatingip_id_path:
|
||||
description: |
|
||||
|
23
api-ref/source/v1/request-ids.inc
Normal file
23
api-ref/source/v1/request-ids.inc
Normal file
@ -0,0 +1,23 @@
|
||||
.. -*- rst -*-
|
||||
|
||||
===========
|
||||
Request ID
|
||||
===========
|
||||
|
||||
For each REST API request, a local request ID is returned as a header in the response.
|
||||
|
||||
**Response**
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- X-Openstack-Request-Id: x-openstack-request-id_resp
|
||||
|
||||
**Response Header**
|
||||
|
||||
For each REST API request, the response contains a ``X-Openstack-Request-Id`` header.
|
||||
|
||||
The value of the ``X-Openstack-Request-Id`` header is the local request ID assigned to the request.
|
||||
|
||||
Response header example::
|
||||
|
||||
X-Openstack-Request-Id: req-d7bc29d0-7b99-4aeb-a356-89975043ab5e
|
@ -28,12 +28,16 @@ def ctx_from_headers(headers):
|
||||
except TypeError:
|
||||
raise exceptions.WrongFormat()
|
||||
|
||||
return context.BlazarContext(
|
||||
user_id=headers['X-User-Id'],
|
||||
project_id=headers['X-Project-Id'],
|
||||
auth_token=headers['X-Auth-Token'],
|
||||
service_catalog=service_catalog,
|
||||
user_name=headers['X-User-Name'],
|
||||
project_name=headers['X-Project-Name'],
|
||||
roles=list(map(six.text_type.strip, headers['X-Roles'].split(','))),
|
||||
)
|
||||
kwargs = {"user_id": headers['X-User-Id'],
|
||||
"project_id": headers['X-Project-Id'],
|
||||
"auth_token": headers['X-Auth-Token'],
|
||||
"service_catalog": service_catalog,
|
||||
"user_name": headers['X-User-Name'],
|
||||
"project_name": headers['X-Project-Name'],
|
||||
"roles": list(
|
||||
map(six.text_type.strip, headers['X-Roles'].split(',')))}
|
||||
|
||||
# For v1 only, request_id and global_request_id will be available.
|
||||
if headers.environ['PATH_INFO'].startswith('/v1'):
|
||||
kwargs['request_id'] = headers.environ['openstack.request_id']
|
||||
return context.BlazarContext(**kwargs)
|
||||
|
@ -25,6 +25,8 @@ from oslo_middleware import debug
|
||||
from stevedore import enabled
|
||||
from werkzeug import exceptions as werkzeug_exceptions
|
||||
|
||||
from blazar.api.v1 import request_id
|
||||
from blazar.api.v1 import request_log
|
||||
from blazar.api.v1 import utils as api_utils
|
||||
|
||||
|
||||
@ -92,6 +94,8 @@ def make_app():
|
||||
if cfg.CONF.log_exchange:
|
||||
app.wsgi_app = debug.Debug.factory(app.config)(app.wsgi_app)
|
||||
|
||||
app.wsgi_app = request_id.BlazarReqIdMiddleware(app.wsgi_app)
|
||||
app.wsgi_app = request_log.RequestLog(app.wsgi_app)
|
||||
app.wsgi_app = auth_token.filter_factory(app.config)(app.wsgi_app)
|
||||
|
||||
return app
|
||||
|
20
blazar/api/v1/request_id.py
Normal file
20
blazar/api/v1/request_id.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2019 NTT DATA.
|
||||
#
|
||||
# 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_middleware import request_id
|
||||
|
||||
|
||||
class BlazarReqIdMiddleware(request_id.RequestId):
|
||||
compat_headers = [request_id.HTTP_RESP_HEADER_REQUEST_ID]
|
82
blazar/api/v1/request_log.py
Normal file
82
blazar/api/v1/request_log.py
Normal file
@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
"""Simple middleware for request logging."""
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestLog(object):
|
||||
"""Middleware to write a simple request log to.
|
||||
|
||||
Borrowed from Paste Translogger
|
||||
"""
|
||||
|
||||
format = ('%(REMOTE_ADDR)s "%(REQUEST_METHOD)s %(REQUEST_URI)s" '
|
||||
'status: %(status)s len: %(bytes)s ')
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
LOG.debug('Starting request: %s "%s %s"',
|
||||
environ['REMOTE_ADDR'], environ['REQUEST_METHOD'],
|
||||
self._get_uri(environ))
|
||||
# Set the accept header if it is not otherwise set or is '*/*'. This
|
||||
# ensures that error responses will be in JSON.
|
||||
accept = environ.get('HTTP_ACCEPT')
|
||||
if accept:
|
||||
environ['HTTP_ACCEPT'] = 'application/json'
|
||||
if LOG.isEnabledFor(logging.INFO):
|
||||
return self._log_app(environ, start_response)
|
||||
|
||||
@staticmethod
|
||||
def _get_uri(environ):
|
||||
req_uri = (environ.get('SCRIPT_NAME', '')
|
||||
+ environ.get('PATH_INFO', ''))
|
||||
if environ.get('QUERY_STRING'):
|
||||
req_uri += '?' + environ['QUERY_STRING']
|
||||
return req_uri
|
||||
|
||||
def _log_app(self, environ, start_response):
|
||||
req_uri = self._get_uri(environ)
|
||||
|
||||
def replacement_start_response(status, headers, exc_info=None):
|
||||
"""We need to gaze at the content-length, if set to write log info.
|
||||
|
||||
"""
|
||||
size = None
|
||||
for name, value in headers:
|
||||
if name.lower() == 'content-length':
|
||||
size = value
|
||||
self.write_log(environ, req_uri, status, size)
|
||||
return start_response(status, headers, exc_info)
|
||||
|
||||
return self.application(environ, replacement_start_response)
|
||||
|
||||
def write_log(self, environ, req_uri, status, size):
|
||||
"""Write the log info out in a formatted form to ``LOG.info``.
|
||||
|
||||
"""
|
||||
if size is None:
|
||||
size = '-'
|
||||
log_format = {
|
||||
'REMOTE_ADDR': environ.get('REMOTE_ADDR', '-'),
|
||||
'REQUEST_METHOD': environ['REQUEST_METHOD'],
|
||||
'REQUEST_URI': req_uri,
|
||||
'status': status.split(None, 1)[0],
|
||||
'bytes': size
|
||||
}
|
||||
LOG.info(self.format, log_format)
|
@ -15,41 +15,35 @@
|
||||
|
||||
import threading
|
||||
|
||||
from oslo_context import context
|
||||
|
||||
class BaseContext(object):
|
||||
|
||||
_elements = set()
|
||||
class BlazarContext(context.RequestContext):
|
||||
|
||||
_context_stack = threading.local()
|
||||
|
||||
def __init__(self, __mapping=None, **kwargs):
|
||||
if __mapping is None:
|
||||
self.__values = dict(**kwargs)
|
||||
else:
|
||||
if isinstance(__mapping, BaseContext):
|
||||
__mapping = __mapping.__values
|
||||
self.__values = dict(__mapping)
|
||||
self.__values.update(**kwargs)
|
||||
not_supported_keys = set(self.__values) - self._elements
|
||||
for k in not_supported_keys:
|
||||
del self.__values[k]
|
||||
def __init__(self, user_id=None, project_id=None, project_name=None,
|
||||
service_catalog=None, user_name=None, **kwargs):
|
||||
# NOTE(neha-alhat): During serializing/deserializing context object
|
||||
# over the RPC layer, below extra parameters which are passed by
|
||||
# `oslo.messaging` are popped as these parameters are not required.
|
||||
kwargs.pop('client_timeout', None)
|
||||
kwargs.pop('user_identity', None)
|
||||
kwargs.pop('project', None)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.__values[name]
|
||||
except KeyError:
|
||||
if name in self._elements:
|
||||
return None
|
||||
else:
|
||||
raise AttributeError(name)
|
||||
if user_id:
|
||||
kwargs['user_id'] = user_id
|
||||
if project_id:
|
||||
kwargs['project_id'] = project_id
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# NOTE(yorik-sar): only the very first assignment for __values is
|
||||
# allowed. All context arguments should be set at the time the context
|
||||
# object is being created.
|
||||
if not self.__dict__:
|
||||
super(BaseContext, self).__setattr__(name, value)
|
||||
else:
|
||||
raise Exception(self.__dict__, name, value)
|
||||
super(BlazarContext, self).__init__(**kwargs)
|
||||
|
||||
self.project_name = project_name
|
||||
self.user_name = user_name
|
||||
self.service_catalog = service_catalog or []
|
||||
|
||||
if self.is_admin and 'admin' not in self.roles:
|
||||
self.roles.append('admin')
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
@ -73,21 +67,13 @@ class BaseContext(object):
|
||||
|
||||
# NOTE(yorik-sar): as long as oslo.rpc requires this
|
||||
def to_dict(self):
|
||||
return self.__values
|
||||
|
||||
|
||||
class BlazarContext(BaseContext):
|
||||
|
||||
_elements = set([
|
||||
"user_id",
|
||||
"project_id",
|
||||
"auth_token",
|
||||
"service_catalog",
|
||||
"user_name",
|
||||
"project_name",
|
||||
"roles",
|
||||
"is_admin",
|
||||
])
|
||||
result = super(BlazarContext, self).to_dict()
|
||||
result['user_id'] = self.user_id
|
||||
result['user_name'] = self.user_name
|
||||
result['project_id'] = self.project_id
|
||||
result['project_name'] = self.project_name
|
||||
result['service_catalog'] = self.service_catalog
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def elevated(cls):
|
||||
|
@ -14,6 +14,9 @@
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils.fixture import uuidsentinel
|
||||
import webob
|
||||
from werkzeug import wrappers
|
||||
|
||||
from blazar.api import context as api_context
|
||||
from blazar import context
|
||||
@ -22,30 +25,18 @@ from blazar import tests
|
||||
|
||||
|
||||
class ContextTestCase(tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ContextTestCase, self).setUp()
|
||||
|
||||
self.fake_headers = {u'X-User-Id': u'1',
|
||||
u'X-Project-Id': u'1',
|
||||
self.fake_headers = {u'X-User-Id': uuidsentinel.user_id,
|
||||
u'X-Project-Id': uuidsentinel.project_id,
|
||||
u'X-Auth-Token': u'111-111-111',
|
||||
u'X-User-Name': u'user_name',
|
||||
u'X-Project-Name': u'project_name',
|
||||
u'X-Roles': u'user_name0, user_name1'}
|
||||
|
||||
def test_ctx_from_headers(self):
|
||||
self.context = self.patch(context, 'BlazarContext')
|
||||
catalog = jsonutils.dump_as_bytes({'nova': 'catalog'})
|
||||
self.fake_headers[u'X-Service-Catalog'] = catalog
|
||||
api_context.ctx_from_headers(self.fake_headers)
|
||||
self.context.assert_called_once_with(user_id=u'1',
|
||||
roles=[u'user_name0',
|
||||
u'user_name1'],
|
||||
project_name=u'project_name',
|
||||
auth_token=u'111-111-111',
|
||||
service_catalog={
|
||||
u'nova': u'catalog'},
|
||||
project_id=u'1',
|
||||
user_name=u'user_name')
|
||||
self.catalog = jsonutils.dump_as_bytes({'nova': 'catalog'})
|
||||
|
||||
def test_ctx_from_headers_no_catalog(self):
|
||||
self.assertRaises(
|
||||
@ -60,3 +51,46 @@ class ContextTestCase(tests.TestCase):
|
||||
exceptions.WrongFormat,
|
||||
api_context.ctx_from_headers,
|
||||
self.fake_headers)
|
||||
|
||||
|
||||
class ContextTestCaseV1(ContextTestCase):
|
||||
|
||||
def test_ctx_from_headers(self):
|
||||
self.fake_headers[u'X-Service-Catalog'] = self.catalog
|
||||
environ_base = {
|
||||
'openstack.request_id': 'req-' + uuidsentinel.reqid}
|
||||
req = wrappers.Request.from_values(
|
||||
'/v1/leases',
|
||||
headers=self.fake_headers,
|
||||
environ_base=environ_base)
|
||||
api_context.ctx_from_headers(req.headers)
|
||||
|
||||
self.context.assert_called_once_with(
|
||||
user_id=uuidsentinel.user_id,
|
||||
roles=[u'user_name0',
|
||||
u'user_name1'],
|
||||
project_name=u'project_name',
|
||||
auth_token=u'111-111-111',
|
||||
service_catalog={u'nova': u'catalog'},
|
||||
project_id=uuidsentinel.project_id,
|
||||
user_name=u'user_name',
|
||||
request_id='req-' + uuidsentinel.reqid)
|
||||
|
||||
|
||||
class ContextTestCaseV2(ContextTestCase):
|
||||
|
||||
def test_ctx_from_headers(self):
|
||||
self.fake_headers[u'X-Service-Catalog'] = self.catalog
|
||||
req = webob.Request.blank('/v2/leases')
|
||||
req.headers = self.fake_headers
|
||||
api_context.ctx_from_headers(req.headers)
|
||||
|
||||
self.context.assert_called_once_with(
|
||||
user_id=uuidsentinel.user_id,
|
||||
roles=[u'user_name0',
|
||||
u'user_name1'],
|
||||
project_name=u'project_name',
|
||||
auth_token=u'111-111-111',
|
||||
service_catalog={u'nova': u'catalog'},
|
||||
project_id=uuidsentinel.project_id,
|
||||
user_name=u'user_name')
|
||||
|
@ -13,44 +13,130 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import flask
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
from testtools import matchers
|
||||
|
||||
from oslo_middleware import request_id as id
|
||||
|
||||
from blazar.api import context as api_context
|
||||
from blazar.api.v1.leases import service as service_api
|
||||
from blazar.api.v1.leases import v1_0 as api
|
||||
from blazar.api.v1 import utils as utils_api
|
||||
from blazar.api.v1.leases import v1_0 as leases_api_v1_0
|
||||
from blazar.api.v1 import request_id
|
||||
from blazar.api.v1 import request_log
|
||||
from blazar import context
|
||||
from blazar import tests
|
||||
|
||||
|
||||
class RESTApiTestCase(tests.TestCase):
|
||||
def make_app():
|
||||
"""App builder (wsgi).
|
||||
|
||||
Entry point for Blazar REST API server.
|
||||
"""
|
||||
app = flask.Flask('blazar.api')
|
||||
|
||||
app.register_blueprint(leases_api_v1_0.rest, url_prefix='/v1')
|
||||
app.wsgi_app = request_id.BlazarReqIdMiddleware(app.wsgi_app)
|
||||
app.wsgi_app = request_log.RequestLog(app.wsgi_app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def fake_lease(**kw):
|
||||
return {
|
||||
u'id': kw.get('id', u'2bb8720a-0873-4d97-babf-0d906851a1eb'),
|
||||
u'name': kw.get('name', u'lease_test'),
|
||||
u'start_date': kw.get('start_date', u'2014-01-01 01:23'),
|
||||
u'end_date': kw.get('end_date', u'2014-02-01 13:37'),
|
||||
u'trust_id': kw.get('trust_id',
|
||||
u'35b17138b3644e6aa1318f3099c5be68'),
|
||||
u'user_id': kw.get('user_id', u'efd8780712d24b389c705f5c2ac427ff'),
|
||||
u'project_id': kw.get('project_id',
|
||||
u'bd9431c18d694ad3803a8d4a6b89fd36'),
|
||||
u'reservations': kw.get('reservations', [
|
||||
{
|
||||
u'resource_id': u'1234',
|
||||
u'resource_type': u'virtual:instance'
|
||||
}
|
||||
]),
|
||||
u'events': kw.get('events', []),
|
||||
u'status': kw.get('status', 'ACTIVE'),
|
||||
}
|
||||
|
||||
|
||||
def fake_lease_request_body(exclude=None, **kw):
|
||||
default_exclude = set(['id', 'trust_id', 'user_id', 'project_id',
|
||||
'status'])
|
||||
exclude = exclude or set()
|
||||
exclude |= default_exclude
|
||||
lease_body = fake_lease(**kw)
|
||||
return dict((key, lease_body[key])
|
||||
for key in lease_body if key not in exclude)
|
||||
|
||||
|
||||
class LeaseAPITestCase(tests.TestCase):
|
||||
def setUp(self):
|
||||
super(RESTApiTestCase, self).setUp()
|
||||
self.api = api
|
||||
self.u_api = utils_api
|
||||
self.s_api = service_api
|
||||
super(LeaseAPITestCase, self).setUp()
|
||||
self.app = make_app()
|
||||
self.headers = {'Accept': 'application/json'}
|
||||
self.lease_uuid = six.text_type(uuidutils.generate_uuid())
|
||||
self.mock_ctx = self.patch(api_context, 'ctx_from_headers')
|
||||
self.mock_ctx.return_value = context.BlazarContext(
|
||||
user_id='fake', project_id='fake', roles=['member'])
|
||||
self.create_lease = self.patch(service_api.API, 'create_lease')
|
||||
self.get_leases = self.patch(service_api.API, 'get_leases')
|
||||
self.get_lease = self.patch(service_api.API, 'get_lease')
|
||||
self.update_lease = self.patch(service_api.API, 'update_lease')
|
||||
self.delete_lease = self.patch(service_api.API, 'delete_lease')
|
||||
|
||||
self.render = self.patch(self.u_api, "render")
|
||||
self.get_leases = self.patch(self.s_api.API, 'get_leases')
|
||||
self.create_lease = self.patch(self.s_api.API, 'create_lease')
|
||||
self.get_lease = self.patch(self.s_api.API, 'get_lease')
|
||||
self.update_lease = self.patch(self.s_api.API, 'update_lease')
|
||||
self.delete_lease = self.patch(self.s_api.API, 'delete_lease')
|
||||
def _assert_response(self, actual_resp, expected_status_code,
|
||||
expected_resp_body, key='lease'):
|
||||
res_id = actual_resp.headers.get(id.HTTP_RESP_HEADER_REQUEST_ID)
|
||||
self.assertIn(id.HTTP_RESP_HEADER_REQUEST_ID, actual_resp.headers)
|
||||
self.assertThat(res_id, matchers.StartsWith('req-'))
|
||||
self.assertEqual(expected_status_code, actual_resp.status_code)
|
||||
self.assertEqual(expected_resp_body, actual_resp.get_json()[key])
|
||||
|
||||
self.fake_id = '1'
|
||||
def test_list(self):
|
||||
with self.app.test_client() as c:
|
||||
self.get_leases.return_value = []
|
||||
res = c.get('/v1/leases', headers=self.headers)
|
||||
self._assert_response(res, 200, [], key='leases')
|
||||
|
||||
def test_lease_list(self):
|
||||
self.api.leases_list(query={})
|
||||
self.render.assert_called_once_with(leases=self.get_leases(query={}))
|
||||
def test_create(self):
|
||||
with self.app.test_client() as c:
|
||||
self.create_lease.return_value = fake_lease(id=self.lease_uuid)
|
||||
res = c.post('/v1/leases', json=fake_lease_request_body(
|
||||
id=self.lease_uuid), headers=self.headers)
|
||||
self._assert_response(res, 201, fake_lease(id=self.lease_uuid))
|
||||
|
||||
def test_leases_create(self):
|
||||
self.api.leases_create(data=None)
|
||||
self.render.assert_called_once_with(lease=self.create_lease())
|
||||
def test_get(self):
|
||||
with self.app.test_client() as c:
|
||||
self.get_lease.return_value = fake_lease(id=self.lease_uuid)
|
||||
res = c.get('/v1/leases/{0}'.format(self.lease_uuid),
|
||||
headers=self.headers)
|
||||
self._assert_response(res, 200, fake_lease(id=self.lease_uuid))
|
||||
|
||||
def test_leases_get(self):
|
||||
self.api.leases_get(lease_id=self.fake_id)
|
||||
self.render.assert_called_once_with(lease=self.get_lease())
|
||||
def test_update(self):
|
||||
with self.app.test_client() as c:
|
||||
self.fake_lease = fake_lease(id=self.lease_uuid, name='updated')
|
||||
self.fake_lease_body = fake_lease_request_body(
|
||||
exclude=set(['reservations', 'events']),
|
||||
id=self.lease_uuid,
|
||||
name='updated'
|
||||
)
|
||||
self.update_lease.return_value = self.fake_lease
|
||||
|
||||
def test_leases_update(self):
|
||||
self.api.leases_update(lease_id=self.fake_id, data=self.fake_id)
|
||||
self.render.assert_called_once_with(lease=self.update_lease())
|
||||
res = c.put('/v1/leases/{0}'.format(self.lease_uuid),
|
||||
json=self.fake_lease_body, headers=self.headers)
|
||||
self._assert_response(res, 200, self.fake_lease)
|
||||
|
||||
def test_leases_delete(self):
|
||||
self.api.leases_delete(lease_id=self.fake_id)
|
||||
self.render.assert_called_once_with()
|
||||
def test_delete(self):
|
||||
with self.app.test_client() as c:
|
||||
res = c.delete('/v1/leases/{0}'.format(self.lease_uuid),
|
||||
headers=self.headers)
|
||||
res_id = res.headers.get(id.HTTP_RESP_HEADER_REQUEST_ID)
|
||||
self.assertEqual(204, res.status_code)
|
||||
self.assertIn(id.HTTP_RESP_HEADER_REQUEST_ID, res.headers)
|
||||
self.assertThat(res_id, matchers.StartsWith('req-'))
|
||||
|
@ -13,59 +13,175 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import ddt
|
||||
import flask
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
from testtools import matchers
|
||||
|
||||
from oslo_middleware import request_id as id
|
||||
|
||||
from blazar.api import context as api_context
|
||||
from blazar.api.v1.oshosts import service as service_api
|
||||
from blazar.api.v1.oshosts import v1_0 as api
|
||||
from blazar.api.v1 import utils as utils_api
|
||||
from blazar.api.v1.oshosts import v1_0 as hosts_api_v1_0
|
||||
from blazar.api.v1 import request_id
|
||||
from blazar.api.v1 import request_log
|
||||
from blazar import context
|
||||
from blazar import tests
|
||||
|
||||
|
||||
class RESTApiTestCase(tests.TestCase):
|
||||
def make_app():
|
||||
"""App builder (wsgi).
|
||||
|
||||
Entry point for Blazar REST API server.
|
||||
"""
|
||||
app = flask.Flask('blazar.api')
|
||||
|
||||
app.register_blueprint(hosts_api_v1_0.rest, url_prefix='/v1')
|
||||
app.wsgi_app = request_id.BlazarReqIdMiddleware(app.wsgi_app)
|
||||
app.wsgi_app = request_log.RequestLog(app.wsgi_app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def fake_computehost(**kw):
|
||||
return {
|
||||
u'id': kw.get('id', u'1'),
|
||||
u'hypervisor_hostname': kw.get('hypervisor_hostname', u'host01'),
|
||||
u'hypervisor_type': kw.get('hypervisor_type', u'QEMU'),
|
||||
u'vcpus': kw.get('vcpus', 1),
|
||||
u'hypervisor_version': kw.get('hypervisor_version', 1000000),
|
||||
u'trust_id': kw.get('trust_id',
|
||||
u'35b17138-b364-4e6a-a131-8f3099c5be68'),
|
||||
u'memory_mb': kw.get('memory_mb', 8192),
|
||||
u'local_gb': kw.get('local_gb', 50),
|
||||
u'cpu_info': kw.get('cpu_info',
|
||||
u"{\"vendor\": \"Intel\", \"model\": \"qemu32\", "
|
||||
"\"arch\": \"x86_64\", \"features\": [],"
|
||||
" \"topology\": {\"cores\": 1}}",
|
||||
),
|
||||
u'extra_capas': kw.get('extra_capas',
|
||||
{u'vgpus': 2, u'fruits': u'bananas'}),
|
||||
}
|
||||
|
||||
|
||||
def fake_computehost_request_body(include=None, **kw):
|
||||
computehost_body = fake_computehost(**kw)
|
||||
computehost_body['name'] = kw.get('name',
|
||||
computehost_body['hypervisor_hostname'])
|
||||
default_include = set(['name', 'extra_capas'])
|
||||
include = include or set()
|
||||
include |= default_include
|
||||
return dict((key, computehost_body[key])
|
||||
for key in computehost_body if key in include)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class OsHostAPITestCase(tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RESTApiTestCase, self).setUp()
|
||||
self.api = api
|
||||
self.u_api = utils_api
|
||||
self.s_api = service_api
|
||||
|
||||
self.render = self.patch(self.u_api, "render")
|
||||
self.get_computehosts = self.patch(self.s_api.API,
|
||||
super(OsHostAPITestCase, self).setUp()
|
||||
self.app = make_app()
|
||||
self.headers = {'Accept': 'application/json'}
|
||||
self.host_id = six.text_type('1')
|
||||
self.mock_ctx = self.patch(api_context, 'ctx_from_headers')
|
||||
self.mock_ctx.return_value = context.BlazarContext(
|
||||
user_id='fake', project_id='fake', roles=['member'])
|
||||
self.get_computehosts = self.patch(service_api.API,
|
||||
'get_computehosts')
|
||||
self.create_computehost = self.patch(self.s_api.API,
|
||||
self.create_computehost = self.patch(service_api.API,
|
||||
'create_computehost')
|
||||
self.get_computehost = self.patch(self.s_api.API, 'get_computehost')
|
||||
self.update_computehost = self.patch(self.s_api.API,
|
||||
self.get_computehost = self.patch(service_api.API, 'get_computehost')
|
||||
self.update_computehost = self.patch(service_api.API,
|
||||
'update_computehost')
|
||||
self.delete_computehost = self.patch(self.s_api.API,
|
||||
self.delete_computehost = self.patch(service_api.API,
|
||||
'delete_computehost')
|
||||
self.list_allocations = self.patch(self.s_api.API, 'list_allocations')
|
||||
self.get_allocations = self.patch(self.s_api.API, 'get_allocations')
|
||||
self.fake_id = '1'
|
||||
self.list_allocations = self.patch(service_api.API,
|
||||
'list_allocations')
|
||||
self.get_allocations = self.patch(service_api.API, 'get_allocations')
|
||||
|
||||
def test_computehost_list(self):
|
||||
self.api.computehosts_list(query={})
|
||||
self.render.assert_called_once_with(
|
||||
hosts=self.get_computehosts(query={}))
|
||||
def _assert_response(self, actual_resp, expected_status_code,
|
||||
expected_resp_body, key='host'):
|
||||
res_id = actual_resp.headers.get(id.HTTP_RESP_HEADER_REQUEST_ID)
|
||||
self.assertIn(id.HTTP_RESP_HEADER_REQUEST_ID,
|
||||
actual_resp.headers)
|
||||
self.assertThat(res_id, matchers.StartsWith('req-'))
|
||||
self.assertEqual(expected_status_code, actual_resp.status_code)
|
||||
self.assertEqual(expected_resp_body, actual_resp.get_json()[key])
|
||||
|
||||
def test_computehosts_create(self):
|
||||
self.api.computehosts_create(data=None)
|
||||
self.render.assert_called_once_with(host=self.create_computehost())
|
||||
def test_list(self):
|
||||
with self.app.test_client() as c:
|
||||
self.get_computehosts.return_value = []
|
||||
res = c.get('/v1', headers=self.headers)
|
||||
self._assert_response(res, 200, [], key='hosts')
|
||||
|
||||
def test_computehosts_get(self):
|
||||
self.api.computehosts_get(host_id=self.fake_id)
|
||||
self.render.assert_called_once_with(host=self.get_computehost())
|
||||
def test_create(self):
|
||||
with self.app.test_client() as c:
|
||||
self.create_computehost.return_value = fake_computehost(
|
||||
id=self.host_id)
|
||||
res = c.post('/v1', json=fake_computehost_request_body(
|
||||
id=self.host_id), headers=self.headers)
|
||||
self._assert_response(res, 201, fake_computehost(
|
||||
id=self.host_id))
|
||||
|
||||
def test_computehosts_update(self):
|
||||
self.api.computehosts_update(host_id=self.fake_id, data=self.fake_id)
|
||||
self.render.assert_called_once_with(host=self.update_computehost())
|
||||
def test_get(self):
|
||||
with self.app.test_client() as c:
|
||||
self.get_computehost.return_value = fake_computehost(
|
||||
id=self.host_id)
|
||||
res = c.get('/v1/{0}'.format(self.host_id), headers=self.headers)
|
||||
self._assert_response(res, 200, fake_computehost(id=self.host_id))
|
||||
|
||||
def test_computehosts_delete(self):
|
||||
self.api.computehosts_delete(host_id=self.fake_id)
|
||||
self.render.assert_called_once_with()
|
||||
def test_update(self):
|
||||
with self.app.test_client() as c:
|
||||
self.fake_computehost = fake_computehost(id=self.host_id,
|
||||
name='updated')
|
||||
self.fake_computehost_body = fake_computehost_request_body(
|
||||
id=self.host_id,
|
||||
name='updated'
|
||||
)
|
||||
self.update_computehost.return_value = self.fake_computehost
|
||||
|
||||
res = c.put('/v1/{0}'.format(self.host_id),
|
||||
json=self.fake_computehost_body, headers=self.headers)
|
||||
self._assert_response(res, 200, self.fake_computehost, 'host')
|
||||
|
||||
def test_delete(self):
|
||||
with self.app.test_client() as c:
|
||||
self.get_computehosts.return_value = fake_computehost(
|
||||
id=self.host_id)
|
||||
|
||||
res = c.delete('/v1/{0}'.format(self.host_id),
|
||||
headers=self.headers)
|
||||
res_id = res.headers.get(id.HTTP_RESP_HEADER_REQUEST_ID)
|
||||
self.assertEqual(204, res.status_code)
|
||||
self.assertIn(id.HTTP_RESP_HEADER_REQUEST_ID, res.headers)
|
||||
self.assertThat(res_id, matchers.StartsWith('req-'))
|
||||
|
||||
def test_allocation_list(self):
|
||||
self.api.allocations_list(query={})
|
||||
self.render.assert_called_once_with(
|
||||
allocations=self.list_allocations())
|
||||
with self.app.test_client() as c:
|
||||
self.list_allocations.return_value = []
|
||||
res = c.get('/v1/allocations', headers=self.headers)
|
||||
self._assert_response(res, 200, [], key='allocations')
|
||||
|
||||
def test_allocation_get(self):
|
||||
self.api.allocations_get(host_id=self.fake_id, query={})
|
||||
self.render.assert_called_once_with(allocation=self.get_allocations())
|
||||
with self.app.test_client() as c:
|
||||
self.get_allocations.return_value = {}
|
||||
res = c.get('/v1/{0}/allocation'.format(self.host_id),
|
||||
headers=self.headers)
|
||||
self._assert_response(res, 200, {}, key='allocation')
|
||||
|
||||
@ddt.data({'lease_id': six.text_type(uuidutils.generate_uuid()),
|
||||
'reservation_id': six.text_type(uuidutils.generate_uuid())})
|
||||
def test_allocation_list_with_query_params(self, query_params):
|
||||
with self.app.test_client() as c:
|
||||
res = c.get('/v1/allocations?{0}'.format(query_params),
|
||||
headers=self.headers)
|
||||
self._assert_response(res, 200, {}, key='allocations')
|
||||
|
||||
@ddt.data({'lease_id': six.text_type(uuidutils.generate_uuid()),
|
||||
'reservation_id': six.text_type(uuidutils.generate_uuid())})
|
||||
def test_allocation_get_with_query_params(self, query_params):
|
||||
with self.app.test_client() as c:
|
||||
res = c.get('/v1/{0}/allocation?{1}'.format(
|
||||
self.host_id, query_params), headers=self.headers)
|
||||
self._assert_response(res, 200, {}, key='allocation')
|
||||
|
@ -13,93 +13,70 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_utils.fixture import uuidsentinel
|
||||
|
||||
from blazar import context
|
||||
from blazar import tests
|
||||
|
||||
|
||||
class TestContext(context.BaseContext):
|
||||
_elements = set(["first", "second", "third"])
|
||||
|
||||
|
||||
class TestContextCreate(tests.TestCase):
|
||||
|
||||
def test_kwargs(self):
|
||||
ctx = TestContext(first=1, second=2)
|
||||
self.assertEqual({"first": 1, "second": 2}, ctx.to_dict())
|
||||
|
||||
def test_dict(self):
|
||||
ctx = TestContext({"first": 1, "second": 2})
|
||||
self.assertEqual({"first": 1, "second": 2}, ctx.to_dict())
|
||||
|
||||
def test_mix(self):
|
||||
ctx = TestContext({"first": 1}, second=2)
|
||||
self.assertEqual({"first": 1, "second": 2}, ctx.to_dict())
|
||||
|
||||
def test_fail(self):
|
||||
ctx = TestContext({'first': 1, "forth": 4}, fifth=5)
|
||||
self.assertEqual(ctx.to_dict(), {"first": 1})
|
||||
|
||||
|
||||
class TestBaseContext(tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBaseContext, self).setUp()
|
||||
self.context = TestContext(first=1, second=2)
|
||||
|
||||
def tearDown(self):
|
||||
super(TestBaseContext, self).tearDown()
|
||||
self.assertEqual(1, self.context.first)
|
||||
|
||||
def test_get_default(self):
|
||||
self.assertIsNone(self.context.third)
|
||||
|
||||
def test_get_unexpected(self):
|
||||
self.assertRaises(AttributeError, getattr, self.context, 'forth')
|
||||
|
||||
def test_current_fails(self):
|
||||
self.assertRaises(RuntimeError, TestContext.current)
|
||||
|
||||
|
||||
class TestContextManager(tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestContextManager, self).setUp()
|
||||
self.context = TestContext(first=1, second=2)
|
||||
self.context.__enter__()
|
||||
|
||||
def tearDown(self):
|
||||
super(TestContextManager, self).tearDown()
|
||||
self.context.__exit__(None, None, None)
|
||||
try:
|
||||
stack = TestContext._context_stack.stack
|
||||
except AttributeError:
|
||||
self.fail("Context stack have never been created")
|
||||
else:
|
||||
del TestContext._context_stack.stack
|
||||
self.assertEqual(stack, [],
|
||||
"Context stack is not empty after test.")
|
||||
|
||||
def test_enter(self):
|
||||
self.assertEqual(TestContext._context_stack.stack, [self.context])
|
||||
|
||||
def test_double_enter(self):
|
||||
with self.context:
|
||||
self.assertEqual(TestContext._context_stack.stack,
|
||||
[self.context, self.context])
|
||||
|
||||
def test_current(self):
|
||||
self.assertIs(self.context, TestContext.current())
|
||||
|
||||
|
||||
class TestBlazarContext(tests.TestCase):
|
||||
|
||||
def test_to_dict(self):
|
||||
ctx = context.BlazarContext(
|
||||
user_id=111, project_id=222,
|
||||
request_id='req-679033b7-1755-4929-bf85-eb3bfaef7e0b')
|
||||
expected = {
|
||||
'auth_token': None,
|
||||
'domain': None,
|
||||
'global_request_id': None,
|
||||
'is_admin': False,
|
||||
'is_admin_project': True,
|
||||
'project': 222,
|
||||
'project_domain': None,
|
||||
'project_id': 222,
|
||||
'project_name': None,
|
||||
'read_only': False,
|
||||
'request_id': 'req-679033b7-1755-4929-bf85-eb3bfaef7e0b',
|
||||
'resource_uuid': None,
|
||||
'roles': [],
|
||||
'service_catalog': [],
|
||||
'show_deleted': False,
|
||||
'system_scope': None,
|
||||
'tenant': 222,
|
||||
'user': 111,
|
||||
'user_domain': None,
|
||||
'user_id': 111,
|
||||
'user_identity': u'111 222 - - -',
|
||||
'user_name': None}
|
||||
self.assertEqual(expected, ctx.to_dict())
|
||||
|
||||
def test_elevated_empty(self):
|
||||
ctx = context.BlazarContext.elevated()
|
||||
self.assertTrue(ctx.is_admin)
|
||||
|
||||
def test_elevated(self):
|
||||
with context.BlazarContext(user_id="user", project_id="project"):
|
||||
ctx = context.BlazarContext.elevated()
|
||||
self.assertEqual(ctx.user_id, "user")
|
||||
self.assertEqual(ctx.project_id, "project")
|
||||
self.assertTrue(ctx.is_admin)
|
||||
def test_service_catalog_default(self):
|
||||
ctxt = context.BlazarContext(user_id=uuidsentinel.user_id,
|
||||
project_id=uuidsentinel.project_id)
|
||||
self.assertEqual([], ctxt.service_catalog)
|
||||
|
||||
ctxt = context.BlazarContext(user_id=uuidsentinel.user_id,
|
||||
project_id=uuidsentinel.project_id,
|
||||
service_catalog=[])
|
||||
self.assertEqual([], ctxt.service_catalog)
|
||||
|
||||
ctxt = context.BlazarContext(user_id=uuidsentinel.user_id,
|
||||
project_id=uuidsentinel.project_id,
|
||||
service_catalog=None)
|
||||
self.assertEqual([], ctxt.service_catalog)
|
||||
|
||||
def test_blazar_context_elevated(self):
|
||||
user_context = context.BlazarContext(
|
||||
user_id=uuidsentinel.user_id,
|
||||
project_id=uuidsentinel.project_id, is_admin=False)
|
||||
self.assertFalse(user_context.is_admin)
|
||||
|
||||
admin_context = user_context.elevated()
|
||||
self.assertFalse(user_context.is_admin)
|
||||
self.assertTrue(admin_context.is_admin)
|
||||
self.assertNotIn('admin', user_context.roles)
|
||||
self.assertIn('admin', admin_context.roles)
|
||||
|
@ -58,18 +58,28 @@ class TestTrusts(tests.TestCase):
|
||||
def test_create_ctx_from_trust(self):
|
||||
self.cfg.config(os_admin_project_name='admin')
|
||||
self.cfg.config(os_admin_username='admin')
|
||||
fake_item = self.client().service_catalog.catalog.__getitem__()
|
||||
fake_ctx_dict = {'_BaseContext__values': {
|
||||
ctx = self.trusts.create_ctx_from_trust('1')
|
||||
fake_ctx_dict = {
|
||||
'auth_token': self.client().auth_token,
|
||||
'service_catalog': fake_item,
|
||||
'domain': None,
|
||||
'is_admin': False,
|
||||
'is_admin_project': True,
|
||||
'project': self.client().tenant_id,
|
||||
'project_domain': None,
|
||||
'project_id': self.client().tenant_id,
|
||||
'project_name': 'admin',
|
||||
'user_name': 'admin',
|
||||
}}
|
||||
|
||||
ctx = self.trusts.create_ctx_from_trust('1')
|
||||
|
||||
self.assertEqual(fake_ctx_dict, ctx.__dict__)
|
||||
'read_only': False,
|
||||
'request_id': ctx.request_id,
|
||||
'resource_uuid': None,
|
||||
'roles': [],
|
||||
'service_catalog': ctx.service_catalog,
|
||||
'show_deleted': False,
|
||||
'system_scope': None,
|
||||
'tenant': self.client().tenant_id,
|
||||
'user': None,
|
||||
'user_domain': None,
|
||||
'user_id': None}
|
||||
self.assertDictContainsSubset(fake_ctx_dict, ctx.to_dict())
|
||||
|
||||
def test_use_trust_auth_dict(self):
|
||||
def to_wrap(self, arg_to_update):
|
||||
|
@ -50,9 +50,12 @@ def delete_trust(lease):
|
||||
|
||||
def create_ctx_from_trust(trust_id):
|
||||
"""Return context built from given trust."""
|
||||
ctx = context.current()
|
||||
|
||||
ctx = context.BlazarContext(
|
||||
user_name=CONF.os_admin_username,
|
||||
project_name=CONF.os_admin_project_name,
|
||||
request_id=ctx.request_id
|
||||
)
|
||||
auth_url = "%s://%s:%s/%s" % (CONF.os_auth_protocol,
|
||||
CONF.os_auth_host,
|
||||
@ -67,10 +70,12 @@ def create_ctx_from_trust(trust_id):
|
||||
|
||||
# use 'with ctx' statement in the place you need context from trust
|
||||
return context.BlazarContext(
|
||||
ctx,
|
||||
user_name=ctx.user_name,
|
||||
project_name=ctx.project_name,
|
||||
auth_token=client.auth_token,
|
||||
service_catalog=client.service_catalog.catalog['catalog'],
|
||||
project_id=client.tenant_id,
|
||||
request_id=ctx.request_id
|
||||
)
|
||||
|
||||
|
||||
|
11
releasenotes/notes/request_id-0ebc34f09c6d01f2.yaml
Normal file
11
releasenotes/notes/request_id-0ebc34f09c6d01f2.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Blazar now uses oslo.middleware for request_id processing which will now
|
||||
return a new `X-OpenStack-Request-ID`_ header in the response to each Restful API request.
|
||||
Also, operators can see request_id, user_id and project_id by default in logs for better tracing
|
||||
and it is configurable via the ``[DEFAULT]/logging_context_format_string`` `option`_.
|
||||
|
||||
|
||||
.. _`X-OpenStack-Request-ID`: https://developer.openstack.org/api-ref/reservation/v1/index.html#request-id
|
||||
.. _`option`: https://docs.openstack.org/oslo.log/latest/configuration/index.html#DEFAULT.logging_context_format_string
|
Loading…
Reference in New Issue
Block a user