diff --git a/api-ref/source/v1/index.rst b/api-ref/source/v1/index.rst index 283ee6a4..a87665bb 100644 --- a/api-ref/source/v1/index.rst +++ b/api-ref/source/v1/index.rst @@ -10,3 +10,4 @@ Blazar project. .. include:: leases.inc .. include:: hosts.inc .. include:: floatingips.inc +.. include:: request-ids.inc diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index 22304c83..0cf988b3 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -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: | diff --git a/api-ref/source/v1/request-ids.inc b/api-ref/source/v1/request-ids.inc new file mode 100644 index 00000000..27b294fe --- /dev/null +++ b/api-ref/source/v1/request-ids.inc @@ -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 diff --git a/blazar/api/context.py b/blazar/api/context.py index 218f901b..e193ed4b 100644 --- a/blazar/api/context.py +++ b/blazar/api/context.py @@ -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) diff --git a/blazar/api/v1/app.py b/blazar/api/v1/app.py index a2ab347b..3ddaae92 100644 --- a/blazar/api/v1/app.py +++ b/blazar/api/v1/app.py @@ -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 diff --git a/blazar/api/v1/request_id.py b/blazar/api/v1/request_id.py new file mode 100644 index 00000000..9e7d444f --- /dev/null +++ b/blazar/api/v1/request_id.py @@ -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] diff --git a/blazar/api/v1/request_log.py b/blazar/api/v1/request_log.py new file mode 100644 index 00000000..e3870831 --- /dev/null +++ b/blazar/api/v1/request_log.py @@ -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) diff --git a/blazar/context.py b/blazar/context.py index 69871d8a..524ec6e5 100644 --- a/blazar/context.py +++ b/blazar/context.py @@ -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): diff --git a/blazar/tests/api/test_context.py b/blazar/tests/api/test_context.py index 96e089bd..1c658fd8 100644 --- a/blazar/tests/api/test_context.py +++ b/blazar/tests/api/test_context.py @@ -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') diff --git a/blazar/tests/api/v1/leases/test_v1_0.py b/blazar/tests/api/v1/leases/test_v1_0.py index 782ba623..4ee413c1 100644 --- a/blazar/tests/api/v1/leases/test_v1_0.py +++ b/blazar/tests/api/v1/leases/test_v1_0.py @@ -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-')) diff --git a/blazar/tests/api/v1/oshosts/test_v1_0.py b/blazar/tests/api/v1/oshosts/test_v1_0.py index 869d5624..1e4afbde 100644 --- a/blazar/tests/api/v1/oshosts/test_v1_0.py +++ b/blazar/tests/api/v1/oshosts/test_v1_0.py @@ -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') diff --git a/blazar/tests/test_context.py b/blazar/tests/test_context.py index 93187306..57d1be30 100644 --- a/blazar/tests/test_context.py +++ b/blazar/tests/test_context.py @@ -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) diff --git a/blazar/tests/utils/test_trusts.py b/blazar/tests/utils/test_trusts.py index 5ade2334..31a58626 100644 --- a/blazar/tests/utils/test_trusts.py +++ b/blazar/tests/utils/test_trusts.py @@ -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): diff --git a/blazar/utils/trusts.py b/blazar/utils/trusts.py index 9ab87441..543a81d5 100644 --- a/blazar/utils/trusts.py +++ b/blazar/utils/trusts.py @@ -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 ) diff --git a/releasenotes/notes/request_id-0ebc34f09c6d01f2.yaml b/releasenotes/notes/request_id-0ebc34f09c6d01f2.yaml new file mode 100644 index 00000000..6b5616d9 --- /dev/null +++ b/releasenotes/notes/request_id-0ebc34f09c6d01f2.yaml @@ -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