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:
parent
51504e713e
commit
2220c56375
@ -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)
|
||||
|
@ -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 []
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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',
|
||||
|
@ -622,7 +622,7 @@ class ServerManager(base.BootingManagerWithFind):
|
||||
if detailed:
|
||||
detail = "/detail"
|
||||
|
||||
result = []
|
||||
result = base.ListWithMeta([], None)
|
||||
while True:
|
||||
if marker:
|
||||
qparams['marker'] = marker
|
||||
|
Loading…
x
Reference in New Issue
Block a user