Add wrapper classes for return-request-id-to-caller

Added wrapper classes which are inherited from base data types str,
list, tuple and dict. Each of these wrapper classes and
the Resource class contain a 'request_ids' attribute
which is populated with a 'x-compute-request-id' or
a 'x-openstack-request-id' received in a header from a response body.

This change is required to return 'request_id' from
client to log request_id mappings of cross projects.

This patch is one of a series of patches for implementing
return-request-id-to-caller.

Co-authored-by: Ankit Agrawal <ankit11.agrawal@nttdata.com>
Change-Id: I422c4f4ee59991ca89a0a16f548b537c8b61bb97
Implements: blueprint return-request-id-to-caller
This commit is contained in:
Takashi NATSUME 2015-12-25 11:26:40 +09:00
parent 51504e713e
commit 2220c56375
7 changed files with 246 additions and 23 deletions

View File

@ -28,6 +28,7 @@ import os
import threading
from oslo_utils import strutils
from requests import Response
import six
from novaclient import exceptions
@ -78,7 +79,43 @@ class HookableMixin(object):
hook_func(*args, **kwargs)
class Resource(object):
class RequestIdMixin(object):
"""Wrapper class to expose x-openstack-request-id to the caller.
"""
def request_ids_setup(self):
self.x_openstack_request_ids = []
@property
def request_ids(self):
return self.x_openstack_request_ids
def append_request_ids(self, resp):
"""Add request_ids as an attribute to the object
:param resp: Response object or list of Response objects
"""
if isinstance(resp, list):
# Add list of request_ids if response is of type list.
for resp_obj in resp:
self._append_request_id(resp_obj)
elif resp is not None:
# Add request_ids if response contains single object.
self._append_request_id(resp)
def _append_request_id(self, resp):
if isinstance(resp, Response):
# Extract 'x-openstack-request-id' from headers if
# response is a Response object.
request_id = (resp.headers.get('x-openstack-request-id') or
resp.headers.get('x-compute-request-id'))
else:
# If resp is of type string or None.
request_id = resp
if request_id not in self.x_openstack_request_ids:
self.x_openstack_request_ids.append(request_id)
class Resource(RequestIdMixin):
"""Base class for OpenStack resources (tenant, user, etc.).
This is pretty much just a bag for attributes.
@ -87,22 +124,27 @@ class Resource(object):
HUMAN_ID = False
NAME_ATTR = 'name'
def __init__(self, manager, info, loaded=False):
def __init__(self, manager, info, loaded=False, resp=None):
"""Populate and bind to a manager.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
:param resp: Response or list of Response objects
"""
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
self.request_ids_setup()
self.append_request_ids(resp)
def __repr__(self):
reprkeys = sorted(k
for k in self.__dict__.keys()
if k[0] != '_' and k != 'manager')
if k[0] != '_' and
k not in ['manager', 'request_ids',
'x_openstack_request_ids'])
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
@ -150,9 +192,9 @@ class Resource(object):
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
if self.manager.client.last_request_id:
self._add_details(
{'x_request_id': self.manager.client.last_request_id})
# The 'request_ids' attribute has been added,
# so store the request id to it instead of _info
self.append_request_ids(new.request_ids)
def __eq__(self, other):
if not isinstance(other, Resource):
@ -196,9 +238,9 @@ class Manager(HookableMixin):
def _list(self, url, response_key, obj_class=None, body=None):
if body:
_resp, body = self.api.client.post(url, body=body)
resp, body = self.api.client.post(url, body=body)
else:
_resp, body = self.api.client.get(url)
resp, body = self.api.client.get(url)
if obj_class is None:
obj_class = self.resource_class
@ -214,8 +256,9 @@ class Manager(HookableMixin):
with self.completion_cache('human_id', obj_class, mode="w"):
with self.completion_cache('uuid', obj_class, mode="w"):
return [obj_class(self, res, loaded=True)
for res in data if res]
items = [obj_class(self, res, loaded=True)
for res in data if res]
return ListWithMeta(items, resp)
@contextlib.contextmanager
def alternate_service_type(self, default, allowed_types=()):
@ -294,30 +337,51 @@ class Manager(HookableMixin):
cache.write("%s\n" % val)
def _get(self, url, response_key):
_resp, body = self.api.client.get(url)
return self.resource_class(self, body[response_key], loaded=True)
resp, body = self.api.client.get(url)
return self.resource_class(self, body[response_key], loaded=True,
resp=resp)
def _create(self, url, body, response_key, return_raw=False, **kwargs):
self.run_hooks('modify_body_for_create', body, **kwargs)
_resp, body = self.api.client.post(url, body=body)
resp, body = self.api.client.post(url, body=body)
if return_raw:
return body[response_key]
return self.convert_into_with_meta(body[response_key], resp)
with self.completion_cache('human_id', self.resource_class, mode="a"):
with self.completion_cache('uuid', self.resource_class, mode="a"):
return self.resource_class(self, body[response_key])
return self.resource_class(self, body[response_key], resp=resp)
def _delete(self, url):
_resp, _body = self.api.client.delete(url)
resp, body = self.api.client.delete(url)
return self.convert_into_with_meta(body, resp)
def _update(self, url, body, response_key=None, **kwargs):
self.run_hooks('modify_body_for_update', body, **kwargs)
_resp, body = self.api.client.put(url, body=body)
resp, body = self.api.client.put(url, body=body)
if body:
if response_key:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body[response_key], resp=resp)
else:
return self.resource_class(self, body)
return self.resource_class(self, body, resp=resp)
else:
return StrWithMeta(body, resp)
def convert_into_with_meta(self, item, resp):
if isinstance(item, six.string_types):
if six.PY2 and isinstance(item, six.text_type):
return UnicodeWithMeta(item, resp)
else:
return StrWithMeta(item, resp)
elif isinstance(item, six.binary_type):
return BytesWithMeta(item, resp)
elif isinstance(item, list):
return ListWithMeta(item, resp)
elif isinstance(item, tuple):
return TupleWithMeta(item, resp)
elif item is None:
return TupleWithMeta((), resp)
else:
return DictWithMeta(item, resp)
@six.add_metaclass(abc.ABCMeta)
@ -338,11 +402,12 @@ class ManagerWithFind(Manager):
elif num_matches > 1:
raise exceptions.NoUniqueMatch
else:
matches[0].append_request_ids(matches.request_ids)
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``."""
found = []
found = ListWithMeta([], None)
searches = kwargs.items()
detailed = True
@ -382,6 +447,7 @@ class ManagerWithFind(Manager):
searches = [(k, v) for k, v in searches if k != 'all_tenants']
listing = self.list(**list_kwargs)
found.append_request_ids(listing.request_ids)
for obj in listing:
try:
@ -433,3 +499,54 @@ class BootingManagerWithFind(ManagerWithFind):
bdm.append(bdm_dict)
return bdm
class ListWithMeta(list, RequestIdMixin):
def __init__(self, values, resp):
super(ListWithMeta, self).__init__(values)
self.request_ids_setup()
self.append_request_ids(resp)
class DictWithMeta(dict, RequestIdMixin):
def __init__(self, values, resp):
super(DictWithMeta, self).__init__(values)
self.request_ids_setup()
self.append_request_ids(resp)
class TupleWithMeta(tuple, RequestIdMixin):
def __new__(cls, values, resp):
return super(TupleWithMeta, cls).__new__(cls, values)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)
class StrWithMeta(str, RequestIdMixin):
def __new__(cls, value, resp):
return super(StrWithMeta, cls).__new__(cls, value)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)
class BytesWithMeta(six.binary_type, RequestIdMixin):
def __new__(cls, value, resp):
return super(BytesWithMeta, cls).__new__(cls, value)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)
if six.PY2:
class UnicodeWithMeta(six.text_type, RequestIdMixin):
def __new__(cls, value, resp):
return super(UnicodeWithMeta, cls).__new__(cls, value)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)

View File

@ -21,6 +21,10 @@ places where actual behavior differs from the spec.
from novaclient import base
# fake request id
FAKE_REQUEST_ID = 'req-3fdea7c2-e3e3-48b5-a656-6b12504c49a1'
FAKE_REQUEST_ID_LIST = [FAKE_REQUEST_ID]
def assert_has_keys(dict, required=None, optional=None):
required = required or []

View File

@ -11,6 +11,9 @@
# License for the specific language governing permissions and limitations
# under the License.
from requests import Response
import six
from novaclient import base
from novaclient import exceptions
from novaclient.tests.unit import utils
@ -21,6 +24,18 @@ from novaclient.v2 import flavors
cs = fakes.FakeClient()
def create_response_obj_with_header():
resp = Response()
resp.headers['x-openstack-request-id'] = fakes.FAKE_REQUEST_ID
return resp
def create_response_obj_with_compute_header():
resp = Response()
resp.headers['x-compute-request-id'] = fakes.FAKE_REQUEST_ID
return resp
class BaseTest(utils.TestCase):
def test_resource_repr(self):
@ -67,3 +82,75 @@ class BaseTest(utils.TestCase):
self.assertRaises(exceptions.NotFound,
cs.flavors.find,
vegetable='carrot')
def test_resource_object_with_request_ids(self):
resp_obj = create_response_obj_with_header()
r = base.Resource(None, {"name": "1"}, resp=resp_obj)
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids)
def test_resource_object_with_compute_request_ids(self):
resp_obj = create_response_obj_with_compute_header()
r = base.Resource(None, {"name": "1"}, resp=resp_obj)
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids)
class ListWithMetaTest(utils.TestCase):
def test_list_with_meta(self):
resp = create_response_obj_with_header()
obj = base.ListWithMeta([], resp)
self.assertEqual([], obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class DictWithMetaTest(utils.TestCase):
def test_dict_with_meta(self):
resp = create_response_obj_with_header()
obj = base.DictWithMeta({}, resp)
self.assertEqual({}, obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class TupleWithMetaTest(utils.TestCase):
def test_tuple_with_meta(self):
resp = create_response_obj_with_header()
expected_tuple = (1, 2)
obj = base.TupleWithMeta(expected_tuple, resp)
self.assertEqual(expected_tuple, obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class StrWithMetaTest(utils.TestCase):
def test_str_with_meta(self):
resp = create_response_obj_with_header()
obj = base.StrWithMeta("test-str", resp)
self.assertEqual("test-str", obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class BytesWithMetaTest(utils.TestCase):
def test_bytes_with_meta(self):
resp = create_response_obj_with_header()
obj = base.BytesWithMeta(b'test-bytes', resp)
self.assertEqual(b'test-bytes', obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
if six.PY2:
class UnicodeWithMetaTest(utils.TestCase):
def test_unicode_with_meta(self):
resp = create_response_obj_with_header()
obj = base.UnicodeWithMeta(u'test-unicode', resp)
self.assertEqual(u'test-unicode', obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)

View File

@ -19,6 +19,7 @@ import six
from novaclient import base
from novaclient import exceptions
from novaclient.tests.unit import fakes
from novaclient.tests.unit import utils as test_utils
from novaclient import utils
@ -28,6 +29,8 @@ UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
class FakeResource(object):
NAME_ATTR = 'name'
request_ids = fakes.FAKE_REQUEST_ID_LIST
def __init__(self, _id, properties):
self.id = _id
try:
@ -35,6 +38,9 @@ class FakeResource(object):
except KeyError:
pass
def append_request_ids(self, resp):
pass
class FakeManager(base.ManagerWithFind):
@ -63,7 +69,7 @@ class FakeManager(base.ManagerWithFind):
raise exceptions.NotFound(resource_id)
def list(self):
return self.resources
return base.ListWithMeta(self.resources, fakes.FAKE_REQUEST_ID_LIST)
class FakeDisplayResource(object):
@ -76,6 +82,9 @@ class FakeDisplayResource(object):
except KeyError:
pass
def append_request_ids(self, resp):
pass
class FakeDisplayManager(FakeManager):

View File

@ -45,6 +45,10 @@ CALLBACK_RE = re.compile(r"^get_http:__nova_api:8774_v\d(_\d)?$")
FAKE_IMAGE_UUID_1 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76'
FAKE_IMAGE_UUID_2 = 'f27f479a-ddda-419a-9bbc-d6b56b210161'
# fake request id
FAKE_REQUEST_ID = fakes.FAKE_REQUEST_ID
FAKE_REQUEST_ID_LIST = fakes.FAKE_REQUEST_ID_LIST
class FakeClient(fakes.FakeClient, client.Client):

View File

@ -14,6 +14,7 @@
import mock
from novaclient import base
from novaclient import exceptions as exc
from novaclient.tests.unit import utils
from novaclient.tests.unit.v2 import fakes
@ -47,7 +48,7 @@ class VersionsTest(utils.TestCase):
None, {"links": [{"href": "http://nova-api:8774/v2.1"}]},
loaded=True)
mock_list.return_value = [
all_versions = [
versions.Version(
None, {"links": [{"href": "http://url/v1"}]}, loaded=True),
versions.Version(
@ -57,6 +58,7 @@ class VersionsTest(utils.TestCase):
current_version,
versions.Version(
None, {"links": [{"href": "http://url/v21"}]}, loaded=True)]
mock_list.return_value = base.ListWithMeta(all_versions, None)
self.assertEqual(current_version, self.cs.versions.get_current())
@mock.patch.object(versions.VersionManager, '_is_session_client',

View File

@ -622,7 +622,7 @@ class ServerManager(base.BootingManagerWithFind):
if detailed:
detail = "/detail"
result = []
result = base.ListWithMeta([], None)
while True:
if marker:
qparams['marker'] = marker