Handle pagination for glance images

The default glance image list pagination seems to be about 20, which
means for v2 you really need to deal with pagination every time. It also
seems that the limit parameter does _not_ allow you to get more items
than the server default, so you can't just say "limit 100000" and be
done with it.

In order to accomplish this, we need to have the adapter stop trying to
return only the image list when there are other top level keys (so the
code can read the next link) and then do a loop requesting the next
link.

To make us even happier, glance returns the next link as '/v2/images' but
we have already set the adapter to 'https://example.com/v2' due to
version discovery. Since we're setting the endpoint_override on the
adapater, it treats that as the root, leaving us with
https://example.com/v2/v2/images. To deal with that, introduce a 'raw'
adapter which is bound to whatever is in the catalog, rather than
whatever we found through version discovery.

Change-Id: I030147e0275d0c4ee89588e21b5970f7d81800d3
Story: 2000837
This commit is contained in:
Monty Taylor 2017-01-06 09:51:28 -06:00
parent 197ca1b8c7
commit 68a8d513dd
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
4 changed files with 46 additions and 14 deletions

View File

@ -0,0 +1,4 @@
---
issues:
- Fixed an issue where glance image list pagination was being ignored,
leading to truncated image lists.

View File

@ -121,18 +121,10 @@ class ShadeAdapter(adapter.Adapter):
elif len(json_keys) == 1:
result = result_json[json_keys[0]]
else:
# Yay for inferrence!
path = urllib.parse.urlparse(response.url).path.strip()
object_type = path.split('/')[-1]
if object_type in json_keys:
result = result_json[object_type]
elif (object_type.startswith('os-')
and object_type[3:] in json_keys):
result = result_json[object_type[3:]]
else:
# Passthrough the whole body - sometimes (hi glance) things
# come through without a top-level container
result = result_json
# Passthrough the whole body - sometimes (hi glance) things
# come through without a top-level container. Also, sometimes
# you need to deal with pagination
result = result_json
if task_manager._is_listlike(result):
return meta.obj_list_to_dict(result, request_id=request_id)

View File

@ -387,6 +387,13 @@ class OpenStackCloud(_normalize.Normalizer):
self._raw_clients['object-store'] = raw_client
return self._raw_clients['object-store']
@property
def _raw_image_client(self):
if 'raw-image' not in self._raw_clients:
image_client = self._get_raw_client('image')
self._raw_clients['raw-image'] = image_client
return self._raw_clients['raw-image']
@property
def _image_client(self):
if 'image' not in self._raw_clients:
@ -1773,18 +1780,29 @@ class OpenStackCloud(_normalize.Normalizer):
"""
# First, try to actually get images from glance, it's more efficient
images = []
image_list = []
try:
if self.cloud_config.get_api_version('image') == '2':
endpoint = '/images'
else:
endpoint = '/images/detail'
image_list = self._image_client.get(endpoint)
response = self._image_client.get(endpoint)
except keystoneauth1.exceptions.catalog.EndpointNotFound:
# We didn't have glance, let's try nova
# If this doesn't work - we just let the exception propagate
image_list = self._compute_client.get('/images/detail')
response = self._compute_client.get('/images/detail')
while 'next' in response:
image_list.extend(meta.obj_list_to_dict(response['images']))
endpoint = response['next']
# Use the raw endpoint from the catalog not the one from
# version discovery so that the next links will work right
response = self._raw_image_client.get(endpoint)
if 'images' in response:
image_list.extend(meta.obj_list_to_dict(response['images']))
else:
image_list.extend(response)
for image in image_list:
# The cloud might return DELETED for invalid images.

View File

@ -147,6 +147,24 @@ class TestImage(base.RequestsMockTestCase):
self.cloud._normalize_images([self.fake_image_dict]),
self.cloud.list_images())
def test_list_images_paginated(self):
marker = str(uuid.uuid4())
self.adapter.register_uri(
'GET', 'https://image.example.com/v2/images',
json={
'images': [self.fake_image_dict],
'next': '/v2/images?marker={marker}'.format(marker=marker),
})
self.adapter.register_uri(
'GET',
'https://image.example.com/v2/images?marker={marker}'.format(
marker=marker),
json=self.fake_search_return)
self.assertEqual(
self.cloud._normalize_images([
self.fake_image_dict, self.fake_image_dict]),
self.cloud.list_images())
def test_create_image_put_v2(self):
self.cloud.image_api_use_tasks = False