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:
openstack 2018-12-14 11:52:44 +00:00 committed by Pierre Riteau
parent fedc289059
commit 438585e3d7
15 changed files with 597 additions and 226 deletions

View File

@ -10,3 +10,4 @@ Blazar project.
.. include:: leases.inc
.. include:: hosts.inc
.. include:: floatingips.inc
.. include:: request-ids.inc

View File

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

View 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

View File

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

View File

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

View 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]

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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