Return request ID to callers

Currently, calls (create, get, etc.) return only the Image to a
caller. In order to log a mapping the request IDs of both glanceclient
and the caller, the x-openstack-request-id header value is needed on
the server side. This change allows that value to be bubbled up and
returned to the caller so that the appropriate logging can occur.
The return_req_id parameter can be set by services that are logging
request IDs. Glance's request ID will then be returned via the
return_req_id parameter.

This is a prerequisite for Log Request ID Mappings nova-spec to
be completed; Change Ib9b820a0feeb0c0e828ed3e4fab8261f8761ba9a

Change I43be05c351f901cee5509c76cff6d69f060c0b3f is an example of
a caller using this.

Implements: blueprint return-req-id
Change-Id: Ia82aa14db5f0e453010514fffb9a25d7b0fc2fd1
This commit is contained in:
Chris Buccella
2014-01-27 20:05:36 +00:00
parent 5528ba536f
commit f0635ecf39
3 changed files with 173 additions and 12 deletions

View File

@@ -57,10 +57,12 @@ class Manager(object):
obj_class = self.resource_class obj_class = self.resource_class
data = body[response_key] 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): 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): def _update(self, url, body, response_key=None):
resp, body = self.api.json_request('PUT', url, body=body) resp, body = self.api.json_request('PUT', url, body=body)

View File

@@ -39,6 +39,8 @@ SORT_DIR_VALUES = ('asc', 'desc')
SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format', SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format',
'size', 'id', 'created_at', 'updated_at') 'size', 'id', 'created_at', 'updated_at')
OS_REQ_ID_HDR = 'x-openstack-request-id'
class Image(base.Resource): class Image(base.Resource):
def __repr__(self): def __repr__(self):
@@ -47,7 +49,7 @@ class Image(base.Resource):
def update(self, **fields): def update(self, **fields):
self.manager.update(self, **fields) self.manager.update(self, **fields)
def delete(self): def delete(self, **kwargs):
return self.manager.delete(self) return self.manager.delete(self)
def data(self, **kwargs): def data(self, **kwargs):
@@ -104,7 +106,7 @@ class ImageManager(base.Manager):
pass pass
return meta return meta
def get(self, image): def get(self, image, **kwargs):
"""Get the metadata for a specific image. """Get the metadata for a specific image.
:param image: image object or id to look up :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' resp, body = self.api.raw_request('HEAD', '/v1/images/%s'
% parse.quote(str(image_id))) % parse.quote(str(image_id)))
meta = self._image_meta_from_headers(dict(resp.getheaders())) 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) 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. """Get the raw data for a specific image.
:param image: image object or id to look up :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) checksum = resp.getheader('x-image-meta-checksum', None)
if do_checksum and checksum is not None: if do_checksum and checksum is not None:
body.set_checksum(checksum) 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 return body
def list(self, **kwargs): def list(self, **kwargs):
@@ -144,11 +154,14 @@ class ImageManager(base.Manager):
:param owner: If provided, only images with this owner (tenant id) :param owner: If provided, only images with this owner (tenant id)
will be listed. An empty string ('') matches ownerless will be listed. An empty string ('') matches ownerless
images. 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` :rtype: list of :class:`Image`
""" """
absolute_limit = kwargs.get('limit') absolute_limit = kwargs.get('limit')
def paginate(qp, seen=0): def paginate(qp, seen=0, return_request_id=None):
def filter_owner(owner, image): def filter_owner(owner, image):
# If client side owner 'filter' is specified # If client side owner 'filter' is specified
# only return images that match 'owner'. # only return images that match 'owner'.
@@ -173,7 +186,11 @@ class ImageManager(base.Manager):
qp[param] = strutils.safe_encode(value) qp[param] = strutils.safe_encode(value)
url = '/v1/images/detail?%s' % parse.urlencode(qp) 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: for image in images:
if filter_owner(owner, image): if filter_owner(owner, image):
continue continue
@@ -186,7 +203,7 @@ class ImageManager(base.Manager):
if (page_size and len(images) == page_size and if (page_size and len(images) == page_size and
(absolute_limit is None or 0 < seen < absolute_limit)): (absolute_limit is None or 0 < seen < absolute_limit)):
qp['marker'] = image.id qp['marker'] = image.id
for image in paginate(qp, seen): for image in paginate(qp, seen, return_request_id):
yield image yield image
params = {'limit': kwargs.get('page_size', DEFAULT_PAGE_SIZE)} params = {'limit': kwargs.get('page_size', DEFAULT_PAGE_SIZE)}
@@ -221,11 +238,16 @@ class ImageManager(base.Manager):
if 'is_public' in kwargs: if 'is_public' in kwargs:
params['is_public'] = kwargs['is_public'] 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.""" """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): def create(self, **kwargs):
"""Create an image """Create an image
@@ -242,6 +264,8 @@ class ImageManager(base.Manager):
for field in kwargs: for field in kwargs:
if field in CREATE_PARAMS: if field in CREATE_PARAMS:
fields[field] = kwargs[field] fields[field] = kwargs[field]
elif field == 'return_req_id':
continue
else: else:
msg = 'create() got an unexpected keyword argument \'%s\'' msg = 'create() got an unexpected keyword argument \'%s\''
raise TypeError(msg % field) raise TypeError(msg % field)
@@ -254,6 +278,10 @@ class ImageManager(base.Manager):
resp, body_iter = self.api.raw_request( resp, body_iter = self.api.raw_request(
'POST', '/v1/images', headers=hdrs, body=image_data) 'POST', '/v1/images', headers=hdrs, body=image_data)
body = json.loads(''.join([c for c in body_iter])) 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'])) return Image(self, self._format_image_meta_for_user(body['image']))
def update(self, image, **kwargs): def update(self, image, **kwargs):
@@ -279,6 +307,8 @@ class ImageManager(base.Manager):
for field in kwargs: for field in kwargs:
if field in UPDATE_PARAMS: if field in UPDATE_PARAMS:
fields[field] = kwargs[field] fields[field] = kwargs[field]
elif field == 'return_req_id':
continue
else: else:
msg = 'update() got an unexpected keyword argument \'%s\'' msg = 'update() got an unexpected keyword argument \'%s\''
raise TypeError(msg % field) raise TypeError(msg % field)
@@ -292,4 +322,8 @@ class ImageManager(base.Manager):
resp, body_iter = self.api.raw_request( resp, body_iter = self.api.raw_request(
'PUT', url, headers=hdrs, body=image_data) 'PUT', url, headers=hdrs, body=image_data)
body = json.loads(''.join([c for c in body_iter])) 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'])) return Image(self, self._format_image_meta_for_user(body['image']))

View File

@@ -33,6 +33,7 @@ fixtures = {
'POST': ( 'POST': (
{ {
'location': '/v1/images/1', 'location': '/v1/images/1',
'x-openstack-request-id': 'req-1234',
}, },
json.dumps( json.dumps(
{'image': { {'image': {
@@ -71,7 +72,7 @@ fixtures = {
}, },
'/v1/images/detail?is_public=None&limit=20': { '/v1/images/detail?is_public=None&limit=20': {
'GET': ( 'GET': (
{}, {'x-openstack-request-id': 'req-1234'},
{'images': [ {'images': [
{ {
'id': 'a', 'id': 'a',
@@ -358,6 +359,50 @@ fixtures = {
'ZZZ', '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': { '/v1/images/v2_created_img': {
'PUT': ( 'PUT': (
{}, {},
@@ -480,6 +525,12 @@ class ImageManagerTest(testtools.TestCase):
expect = [('HEAD', '/v1/images/3', {}, None)] expect = [('HEAD', '/v1/images/3', {}, None)]
self.assertEqual(u"ni\xf1o", image.name) 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): def test_data(self):
data = ''.join([b for b in self.mgr.data('1', do_checksum=False)]) data = ''.join([b for b in self.mgr.data('1', do_checksum=False)])
expect = [('GET', '/v1/images/1', {}, None)] expect = [('GET', '/v1/images/1', {}, None)]
@@ -508,6 +559,15 @@ class ImageManagerTest(testtools.TestCase):
msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong' msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong'
self.assertTrue(msg in str(e)) 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): def test_data_with_checksum(self):
data = ''.join([b for b in self.mgr.data('3', do_checksum=False)]) data = ''.join([b for b in self.mgr.data('3', do_checksum=False)])
expect = [('GET', '/v1/images/3', {}, None)] expect = [('GET', '/v1/images/3', {}, None)]
@@ -524,6 +584,16 @@ class ImageManagerTest(testtools.TestCase):
expect = [('DELETE', '/v1/images/1', {}, None)] expect = [('DELETE', '/v1/images/1', {}, None)]
self.assertEqual(expect, self.api.calls) 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): def test_create_without_data(self):
params = { params = {
'id': '1', 'id': '1',
@@ -573,6 +643,40 @@ class ImageManagerTest(testtools.TestCase):
expect = [('POST', '/v1/images', expect_headers, image_data)] expect = [('POST', '/v1/images', expect_headers, image_data)]
self.assertEqual(expect, self.api.calls) 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): def test_update(self):
fields = { fields = {
'name': 'image-2', 'name': 'image-2',
@@ -621,6 +725,18 @@ class ImageManagerTest(testtools.TestCase):
expect = [('PUT', '/v1/images/1', expect_headers, None)] expect = [('PUT', '/v1/images/1', expect_headers, None)]
self.assertEqual(expect, self.api.calls) 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): def test_image_meta_from_headers_encoding(self):
fields = {"x-image-meta-name": "ni\xc3\xb1o"} fields = {"x-image-meta-name": "ni\xc3\xb1o"}
headers = self.mgr._image_meta_from_headers(fields) 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('a', image_list[0].id)
self.assertEqual(1, len(image_list)) 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): def test_image_list_with_notfound_owner(self):
images = self.mgr.list(owner='X', page_size=20) images = self.mgr.list(owner='X', page_size=20)
self.assertEqual(0, len(list(images))) self.assertEqual(0, len(list(images)))