diff --git a/glanceclient/common/base.py b/glanceclient/common/base.py index 3be987b4..85229a0c 100644 --- a/glanceclient/common/base.py +++ b/glanceclient/common/base.py @@ -57,10 +57,12 @@ class Manager(object): obj_class = self.resource_class data = body[response_key] - return [obj_class(self, res, loaded=True) for res in data if res] + return ([obj_class(self, res, loaded=True) for res in data if res], + resp) def _delete(self, url): - self.api.raw_request('DELETE', url) + resp = self.api.raw_request('DELETE', url) + return resp[0] def _update(self, url, body, response_key=None): resp, body = self.api.json_request('PUT', url, body=body) diff --git a/glanceclient/v1/images.py b/glanceclient/v1/images.py index 72cebf1f..19b24329 100644 --- a/glanceclient/v1/images.py +++ b/glanceclient/v1/images.py @@ -39,6 +39,8 @@ SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format', 'size', 'id', 'created_at', 'updated_at') +OS_REQ_ID_HDR = 'x-openstack-request-id' + class Image(base.Resource): def __repr__(self): @@ -47,7 +49,7 @@ class Image(base.Resource): def update(self, **fields): self.manager.update(self, **fields) - def delete(self): + def delete(self, **kwargs): return self.manager.delete(self) def data(self, **kwargs): @@ -104,7 +106,7 @@ class ImageManager(base.Manager): pass return meta - def get(self, image): + def get(self, image, **kwargs): """Get the metadata for a specific image. :param image: image object or id to look up @@ -115,9 +117,13 @@ class ImageManager(base.Manager): resp, body = self.api.raw_request('HEAD', '/v1/images/%s' % parse.quote(str(image_id))) meta = self._image_meta_from_headers(dict(resp.getheaders())) + return_request_id = kwargs.get('return_req_id', None) + if return_request_id is not None: + return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None)) + return Image(self, meta) - def data(self, image, do_checksum=True): + def data(self, image, do_checksum=True, **kwargs): """Get the raw data for a specific image. :param image: image object or id to look up @@ -130,6 +136,10 @@ class ImageManager(base.Manager): checksum = resp.getheader('x-image-meta-checksum', None) if do_checksum and checksum is not None: body.set_checksum(checksum) + return_request_id = kwargs.get('return_req_id', None) + if return_request_id is not None: + return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None)) + return body def list(self, **kwargs): @@ -144,11 +154,14 @@ class ImageManager(base.Manager): :param owner: If provided, only images with this owner (tenant id) will be listed. An empty string ('') matches ownerless images. + :param return_request_id: If an empty list is provided, populate this + list with the request ID value from the header + x-openstack-request-id :rtype: list of :class:`Image` """ absolute_limit = kwargs.get('limit') - def paginate(qp, seen=0): + def paginate(qp, seen=0, return_request_id=None): def filter_owner(owner, image): # If client side owner 'filter' is specified # only return images that match 'owner'. @@ -173,7 +186,11 @@ class ImageManager(base.Manager): qp[param] = strutils.safe_encode(value) url = '/v1/images/detail?%s' % parse.urlencode(qp) - images = self._list(url, "images") + images, resp = self._list(url, "images") + + if return_request_id is not None: + return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None)) + for image in images: if filter_owner(owner, image): continue @@ -186,7 +203,7 @@ class ImageManager(base.Manager): if (page_size and len(images) == page_size and (absolute_limit is None or 0 < seen < absolute_limit)): qp['marker'] = image.id - for image in paginate(qp, seen): + for image in paginate(qp, seen, return_request_id): yield image params = {'limit': kwargs.get('page_size', DEFAULT_PAGE_SIZE)} @@ -221,11 +238,16 @@ class ImageManager(base.Manager): if 'is_public' in kwargs: params['is_public'] = kwargs['is_public'] - return paginate(params) + return_request_id = kwargs.get('return_req_id', None) - def delete(self, image): + return paginate(params, 0, return_request_id) + + def delete(self, image, **kwargs): """Delete an image.""" - self._delete("/v1/images/%s" % base.getid(image)) + resp = self._delete("/v1/images/%s" % base.getid(image)) + return_request_id = kwargs.get('return_req_id', None) + if return_request_id is not None: + return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None)) def create(self, **kwargs): """Create an image @@ -242,6 +264,8 @@ class ImageManager(base.Manager): for field in kwargs: if field in CREATE_PARAMS: fields[field] = kwargs[field] + elif field == 'return_req_id': + continue else: msg = 'create() got an unexpected keyword argument \'%s\'' raise TypeError(msg % field) @@ -254,6 +278,10 @@ class ImageManager(base.Manager): resp, body_iter = self.api.raw_request( 'POST', '/v1/images', headers=hdrs, body=image_data) body = json.loads(''.join([c for c in body_iter])) + return_request_id = kwargs.get('return_req_id', None) + if return_request_id is not None: + return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None)) + return Image(self, self._format_image_meta_for_user(body['image'])) def update(self, image, **kwargs): @@ -279,6 +307,8 @@ class ImageManager(base.Manager): for field in kwargs: if field in UPDATE_PARAMS: fields[field] = kwargs[field] + elif field == 'return_req_id': + continue else: msg = 'update() got an unexpected keyword argument \'%s\'' raise TypeError(msg % field) @@ -292,4 +322,8 @@ class ImageManager(base.Manager): resp, body_iter = self.api.raw_request( 'PUT', url, headers=hdrs, body=image_data) body = json.loads(''.join([c for c in body_iter])) + return_request_id = kwargs.get('return_req_id', None) + if return_request_id is not None: + return_request_id.append(resp.getheader(OS_REQ_ID_HDR, None)) + return Image(self, self._format_image_meta_for_user(body['image'])) diff --git a/tests/v1/test_images.py b/tests/v1/test_images.py index 8c00fb41..b7742749 100644 --- a/tests/v1/test_images.py +++ b/tests/v1/test_images.py @@ -33,6 +33,7 @@ fixtures = { 'POST': ( { 'location': '/v1/images/1', + 'x-openstack-request-id': 'req-1234', }, json.dumps( {'image': { @@ -71,7 +72,7 @@ fixtures = { }, '/v1/images/detail?is_public=None&limit=20': { 'GET': ( - {}, + {'x-openstack-request-id': 'req-1234'}, {'images': [ { 'id': 'a', @@ -358,6 +359,50 @@ fixtures = { 'ZZZ', ), }, + '/v1/images/4': { + 'HEAD': ( + { + 'x-image-meta-id': '4', + 'x-image-meta-name': 'image-4', + 'x-image-meta-property-arch': 'x86_64', + 'x-image-meta-is_public': 'false', + 'x-image-meta-protected': 'false', + 'x-image-meta-deleted': 'false', + 'x-openstack-request-id': 'req-1234', + }, + None), + 'GET': ( + { + 'x-openstack-request-id': 'req-1234', + }, + 'XXX', + ), + 'PUT': ( + { + 'x-openstack-request-id': 'req-1234', + }, + json.dumps( + {'image': { + 'id': '4', + 'name': 'image-4', + 'container_format': 'ovf', + 'disk_format': 'vhd', + 'owner': 'asdf', + 'size': '1024', + 'min_ram': '512', + 'min_disk': '10', + 'properties': {'a': 'b', 'c': 'd'}, + 'is_public': False, + 'protected': False, + }}, + ), + ), + 'DELETE': ( + { + 'x-openstack-request-id': 'req-1234', + }, + None), + }, '/v1/images/v2_created_img': { 'PUT': ( {}, @@ -480,6 +525,12 @@ class ImageManagerTest(testtools.TestCase): expect = [('HEAD', '/v1/images/3', {}, None)] self.assertEqual(u"ni\xf1o", image.name) + def test_get_req_id(self): + params = {'return_req_id': []} + image = self.mgr.get('4', **params) + expect_req_id = ['req-1234'] + self.assertEqual(expect_req_id, params['return_req_id']) + def test_data(self): data = ''.join([b for b in self.mgr.data('1', do_checksum=False)]) expect = [('GET', '/v1/images/1', {}, None)] @@ -508,6 +559,15 @@ class ImageManagerTest(testtools.TestCase): msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong' self.assertTrue(msg in str(e)) + def test_data_req_id(self): + params = { + 'do_checksum': False, + 'return_req_id': [], + } + data = ''.join([b for b in self.mgr.data('4', **params)]) + expect_req_id = ['req-1234'] + self.assertEqual(expect_req_id, params['return_req_id']) + def test_data_with_checksum(self): data = ''.join([b for b in self.mgr.data('3', do_checksum=False)]) expect = [('GET', '/v1/images/3', {}, None)] @@ -524,6 +584,16 @@ class ImageManagerTest(testtools.TestCase): expect = [('DELETE', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) + def test_delete_req_id(self): + params = { + 'return_req_id': [] + } + self.mgr.delete('4', **params) + expect = [('DELETE', '/v1/images/4', {}, None)] + self.assertEqual(self.api.calls, expect) + expect_req_id = ['req-1234'] + self.assertEqual(expect_req_id, params['return_req_id']) + def test_create_without_data(self): params = { 'id': '1', @@ -573,6 +643,40 @@ class ImageManagerTest(testtools.TestCase): expect = [('POST', '/v1/images', expect_headers, image_data)] self.assertEqual(expect, self.api.calls) + def test_create_req_id(self): + params = { + 'id': '4', + 'name': 'image-4', + 'container_format': 'ovf', + 'disk_format': 'vhd', + 'owner': 'asdf', + 'size': 1024, + 'min_ram': 512, + 'min_disk': 10, + 'copy_from': 'http://example.com', + 'properties': {'a': 'b', 'c': 'd'}, + 'return_req_id': [], + } + image = self.mgr.create(**params) + expect_headers = { + 'x-image-meta-id': '4', + 'x-image-meta-name': 'image-4', + 'x-image-meta-container_format': 'ovf', + 'x-image-meta-disk_format': 'vhd', + 'x-image-meta-owner': 'asdf', + 'x-image-meta-size': '1024', + 'x-image-meta-min_ram': '512', + 'x-image-meta-min_disk': '10', + 'x-glance-api-copy-from': 'http://example.com', + 'x-image-meta-property-a': 'b', + 'x-image-meta-property-c': 'd', + } + expect = [('POST', '/v1/images', expect_headers, None)] + self.assertEqual(self.api.calls, expect) + self.assertEqual(image.id, '1') + expect_req_id = ['req-1234'] + self.assertEqual(expect_req_id, params['return_req_id']) + def test_update(self): fields = { 'name': 'image-2', @@ -621,6 +725,18 @@ class ImageManagerTest(testtools.TestCase): expect = [('PUT', '/v1/images/1', expect_headers, None)] self.assertEqual(expect, self.api.calls) + def test_update_req_id(self): + fields = { + 'purge_props': True, + 'return_req_id': [], + } + self.mgr.update('4', **fields) + expect_headers = {'x-glance-registry-purge-props': 'true'} + expect = [('PUT', '/v1/images/4', expect_headers, None)] + self.assertEqual(self.api.calls, expect) + expect_req_id = ['req-1234'] + self.assertEqual(expect_req_id, fields['return_req_id']) + def test_image_meta_from_headers_encoding(self): fields = {"x-image-meta-name": "ni\xc3\xb1o"} headers = self.mgr._image_meta_from_headers(fields) @@ -633,6 +749,15 @@ class ImageManagerTest(testtools.TestCase): self.assertEqual('a', image_list[0].id) self.assertEqual(1, len(image_list)) + def test_image_list_with_owner_req_id(self): + fields = { + 'owner': 'A', + 'return_req_id': [], + } + images = self.mgr.list(**fields) + images.next() + self.assertEqual(fields['return_req_id'], ['req-1234']) + def test_image_list_with_notfound_owner(self): images = self.mgr.list(owner='X', page_size=20) self.assertEqual(0, len(list(images)))