diff --git a/cinderclient/openstack/common/apiclient/base.py b/cinderclient/openstack/common/apiclient/base.py index 82670aa56..460e7b995 100644 --- a/cinderclient/openstack/common/apiclient/base.py +++ b/cinderclient/openstack/common/apiclient/base.py @@ -26,6 +26,7 @@ Base utilities to build API operation managers and objects on top of. import abc import copy +from requests import Response import six from six.moves.urllib import parse @@ -409,7 +410,43 @@ class Extension(HookableMixin): return "<Extension '%s'>" % self.name -class Resource(object): +class RequestIdMixin(object): + """Wrapper class to expose x-openstack-request-id to the caller.""" + def 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: list, Response object or string + """ + if resp is None: + return + + 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) + else: + # 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') + self.x_openstack_request_ids.append(request_id) + else: + # If resp is of type string (in case of encryption type list) + self.x_openstack_request_ids.append(resp) + + +class Resource(RequestIdMixin): """Base class for OpenStack resources (tenant, user, etc.). This is pretty much just a bag for attributes. @@ -418,22 +455,26 @@ 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.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', 'x_openstack_request_ids']) info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) @@ -493,3 +534,26 @@ class Resource(object): def to_dict(self): return copy.deepcopy(self._info) + + +class ListWithMeta(list, RequestIdMixin): + def __init__(self, values, resp): + super(ListWithMeta, self).__init__(values) + self.setup() + self.append_request_ids(resp) + + +class DictWithMeta(dict, RequestIdMixin): + def __init__(self, values, resp): + super(DictWithMeta, self).__init__(values) + self.setup() + self.append_request_ids(resp) + + +class TupleWithMeta(tuple, RequestIdMixin): + def __new__(cls, resp, values): + return super(TupleWithMeta, cls).__new__(cls, (resp, values)) + + def __init__(self, resp, values): + self.setup() + self.append_request_ids(resp) diff --git a/cinderclient/tests/unit/test_base.py b/cinderclient/tests/unit/test_base.py index 105d9b7c0..7d329dedc 100644 --- a/cinderclient/tests/unit/test_base.py +++ b/cinderclient/tests/unit/test_base.py @@ -11,8 +11,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from requests import Response + from cinderclient import base from cinderclient import exceptions +from cinderclient.openstack.common.apiclient import base as common_base from cinderclient.v1 import volumes from cinderclient.tests.unit import utils from cinderclient.tests.unit.v1 import fakes @@ -21,11 +24,21 @@ from cinderclient.tests.unit.v1 import fakes cs = fakes.FakeClient() +REQUEST_ID = 'req-test-request-id' + + +def create_response_obj_with_header(): + resp = Response() + resp.headers['x-openstack-request-id'] = REQUEST_ID + return resp + + class BaseTest(utils.TestCase): def test_resource_repr(self): r = base.Resource(None, dict(foo="bar", baz="spam")) self.assertEqual("<Resource baz=spam, foo=bar>", repr(r)) + self.assertNotIn("x_openstack_request_ids", repr(r)) def test_getid(self): self.assertEqual(4, base.getid(4)) @@ -63,3 +76,38 @@ class BaseTest(utils.TestCase): def test_to_dict(self): r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) self.assertEqual({'id': 1, 'name': 'hi'}, r1.to_dict()) + + 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([REQUEST_ID], r.request_ids) + + +class ListWithMetaTest(utils.TestCase): + def test_list_with_meta(self): + resp = create_response_obj_with_header() + obj = common_base.ListWithMeta([], resp) + self.assertEqual([], obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual([REQUEST_ID], obj.request_ids) + + +class DictWithMetaTest(utils.TestCase): + def test_dict_with_meta(self): + resp = create_response_obj_with_header() + obj = common_base.DictWithMeta([], resp) + self.assertEqual({}, obj) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual([REQUEST_ID], obj.request_ids) + + +class TupleWithMetaTest(utils.TestCase): + def test_tuple_with_meta(self): + resp = create_response_obj_with_header() + obj = common_base.TupleWithMeta(resp, None) + self.assertIsInstance(obj, tuple) + # Check request_ids attribute is added to obj + self.assertTrue(hasattr(obj, 'request_ids')) + self.assertEqual([REQUEST_ID], obj.request_ids)