diff --git a/bin/glance b/bin/glance index 6b74e11b1c..6c690c73c4 100755 --- a/bin/glance +++ b/bin/glance @@ -194,7 +194,7 @@ EXAMPLES print "Returned the following metadata for the new image:" for k, v in sorted(image_meta.items()): print " %(k)30s => %(v)s" % locals() - except client.ClientConnectionError, e: + except exception.ClientConnectionError, e: host = options.host port = options.port print ("Failed to connect to the Glance API server " diff --git a/doc/source/client.rst b/doc/source/client.rst index 247840bf02..bbda9a0987 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -113,7 +113,36 @@ in size and in the `saving` status. c = Client("glance.example.com", 9292) filters = {'status': 'saving', 'size_max': (5 * 1024 * 1024 * 1024)} - print c.get_images_detailed(filters) + print c.get_images_detailed(filters=filters) + +Sorting Images Returned via ``get_images()`` and ``get_images_detailed()`` +-------------------------------------------------------------------------- + +Two parameters are available to sort the list of images returned by +these methods. + +* ``sort_key: KEY`` + + Images can be ordered by the image attribute ``KEY``. Acceptable values: + ``id``, ``name``, ``status``, ``container_format``, ``disk_format``, + ``created_at`` (default) and ``updated_at``. + +* ``sort_dir: DIR`` + + The direction of the sort may be defined by ``DIR``. Accepted values: + ``asc`` for ascending or ``desc`` (default) for descending. + +The following example will return a list of images sorted alphabetically +by name in ascending order. + +.. code-block:: python + + from glance.client import Client + + c = Client("glance.example.com", 9292) + + print c.get_images(sort_key='name', sort_dir='asc') + Requesting Detailed Metadata on a Specific Image ------------------------------------------------ diff --git a/doc/source/glanceapi.rst b/doc/source/glanceapi.rst index b978e9832d..beec85bd79 100644 --- a/doc/source/glanceapi.rst +++ b/doc/source/glanceapi.rst @@ -129,6 +129,20 @@ list details these query parameters. Filters images having a ``size`` attribute less than or equal to ``BYTES`` +These two resources also accept sort parameters: + +* ``sort_key=KEY`` + + Results will be ordered by the specified image attribute ``KEY``. Accepted + values include ``id``, ``name``, ``status``, ``disk_format``, + ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``. + +* ``sort_dir=DIR`` + + Results will be sorted in the direction ``DIR``. Accepted values are ``asc`` + for ascending or ``desc`` (default) for descending. + + Requesting Detailed Metadata on a Specific Image ------------------------------------------------ diff --git a/doc/source/registries.rst b/doc/source/registries.rst index a949bbe253..b59ad41c68 100644 --- a/doc/source/registries.rst +++ b/doc/source/registries.rst @@ -83,6 +83,20 @@ list details these query parameters. Filters images having a ``size`` attribute less than or equal to ``BYTES`` +These two resources also accept sort parameters: + +* ``sort_key=KEY`` + + Results will be ordered by the specified image attribute ``KEY``. Accepted + values include ``id``, ``name``, ``status``, ``disk_format``, + ``container_format``, ``size``, ``created_at`` (default) and ``updated_at``. + +* ``sort_dir=DIR`` + + Results will be sorted in the direction ``DIR``. Accepted values are ``asc`` + for ascending or ``desc`` (default) for descending. + + ``POST /images`` ---------------- diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index b47277faca..36fd77e5eb 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -45,6 +45,8 @@ logger = logging.getLogger('glance.api.v1.images') SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', 'size_min', 'size_max'] +SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') + class Controller(object): @@ -92,14 +94,7 @@ class Controller(object): 'size': }, ... ]} """ - params = {'filters': self._get_filters(req)} - - if 'limit' in req.str_params: - params['limit'] = req.str_params.get('limit') - - if 'marker' in req.str_params: - params['marker'] = req.str_params.get('marker') - + params = self._get_query_params(req) images = registry.get_images_list(self.options, **params) return dict(images=images) @@ -125,17 +120,23 @@ class Controller(object): 'properties': {'distro': 'Ubuntu 10.04 LTS', ...}}, ... ]} """ - params = {'filters': self._get_filters(req)} - - if 'limit' in req.str_params: - params['limit'] = req.str_params.get('limit') - - if 'marker' in req.str_params: - params['marker'] = req.str_params.get('marker') - + params = self._get_query_params(req) images = registry.get_images_detail(self.options, **params) return dict(images=images) + def _get_query_params(self, req): + """ + Extracts necessary query params from request. + + :param req: the WSGI Request object + :retval dict of parameters that can be used by registry client + """ + params = {'filters': self._get_filters(req)} + for PARAM in SUPPORTED_PARAMS: + if PARAM in req.str_params: + params[PARAM] = req.str_params.get(PARAM) + return params + def _get_filters(self, req): """ Return a dictionary of query param filters from the request diff --git a/glance/client.py b/glance/client.py index 7a01c7d141..df150f0365 100644 --- a/glance/client.py +++ b/glance/client.py @@ -19,172 +19,17 @@ Client classes for callers of a Glance system """ -import httplib import json -import logging -import urlparse -import socket -import sys -import urllib -from glance import utils +from glance.api.v1 import images as v1_images +from glance.common import client as base_client from glance.common import exception +from glance import utils #TODO(jaypipes) Allow a logger param for client classes -class ClientConnectionError(Exception): - """Error resulting from a client connecting to a server""" - pass - - -class ImageBodyIterator(object): - - """ - A class that acts as an iterator over an image file's - chunks of data. This is returned as part of the result - tuple from `glance.client.Client.get_image` - """ - - CHUNKSIZE = 65536 - - def __init__(self, response): - """ - Constructs the object from an HTTPResponse object - """ - self.response = response - - def __iter__(self): - """ - Exposes an iterator over the chunks of data in the - image file. - """ - while True: - chunk = self.response.read(ImageBodyIterator.CHUNKSIZE) - if chunk: - yield chunk - else: - break - - -class BaseClient(object): - - """A base client class""" - - CHUNKSIZE = 65536 - - def __init__(self, host, port, use_ssl): - """ - Creates a new client to some service. - - :param host: The host where service resides - :param port: The port where service resides - :param use_ssl: Should we use HTTPS? - """ - self.host = host - self.port = port - self.use_ssl = use_ssl - self.connection = None - - def get_connection_type(self): - """ - Returns the proper connection type - """ - if self.use_ssl: - return httplib.HTTPSConnection - else: - return httplib.HTTPConnection - - def do_request(self, method, action, body=None, headers=None, - params=None): - """ - Connects to the server and issues a request. Handles converting - any returned HTTP error status codes to OpenStack/Glance exceptions - and closing the server connection. Returns the result data, or - raises an appropriate exception. - - :param method: HTTP method ("GET", "POST", "PUT", etc...) - :param action: part of URL after root netloc - :param body: string of data to send, or None (default) - :param headers: mapping of key/value pairs to add as headers - :param params: dictionary of key/value pairs to add to append - to action - - :note - - If the body param has a read attribute, and method is either - POST or PUT, this method will automatically conduct a chunked-transfer - encoding and use the body as a file object, transferring chunks - of data using the connection's send() method. This allows large - objects to be transferred efficiently without buffering the entire - body in memory. - """ - if type(params) is dict: - action += '?' + urllib.urlencode(params) - - try: - connection_type = self.get_connection_type() - headers = headers or {} - c = connection_type(self.host, self.port) - - # Do a simple request or a chunked request, depending - # on whether the body param is a file-like object and - # the method is PUT or POST - if hasattr(body, 'read') and method.lower() in ('post', 'put'): - # Chunk it, baby... - c.putrequest(method, action) - - for header, value in headers.items(): - c.putheader(header, value) - c.putheader('Transfer-Encoding', 'chunked') - c.endheaders() - - chunk = body.read(self.CHUNKSIZE) - while chunk: - c.send('%x\r\n%s\r\n' % (len(chunk), chunk)) - chunk = body.read(self.CHUNKSIZE) - c.send('0\r\n\r\n') - else: - # Simple request... - c.request(method, action, body, headers) - res = c.getresponse() - status_code = self.get_status_code(res) - if status_code in (httplib.OK, - httplib.CREATED, - httplib.ACCEPTED, - httplib.NO_CONTENT): - return res - elif status_code == httplib.UNAUTHORIZED: - raise exception.NotAuthorized - elif status_code == httplib.FORBIDDEN: - raise exception.NotAuthorized - elif status_code == httplib.NOT_FOUND: - raise exception.NotFound - elif status_code == httplib.CONFLICT: - raise exception.Duplicate(res.read()) - elif status_code == httplib.BAD_REQUEST: - raise exception.Invalid(res.read()) - elif status_code == httplib.INTERNAL_SERVER_ERROR: - raise Exception("Internal Server error: %s" % res.read()) - else: - raise Exception("Unknown error occurred! %s" % res.read()) - - except (socket.error, IOError), e: - raise ClientConnectionError("Unable to connect to " - "server. Got error: %s" % e) - - def get_status_code(self, response): - """ - Returns the integer status code from the response, which - can be either a Webob.Response (used in testing) or httplib.Response - """ - if hasattr(response, 'status_int'): - return response.status_int - else: - return response.status - - -class V1Client(BaseClient): +class V1Client(base_client.BaseClient): """Main client class for accessing Glance resources""" @@ -209,7 +54,7 @@ class V1Client(BaseClient): return super(V1Client, self).do_request(method, action, body, headers, params) - def get_images(self, filters=None, marker=None, limit=None): + def get_images(self, **kwargs): """ Returns a list of image id/name mappings from Registry @@ -217,18 +62,15 @@ class V1Client(BaseClient): collection of images should be filtered :param marker: id after which to start the page of images :param limit: maximum number of items to return + :param sort_key: results will be ordered by this image attribute + :param sort_dir: direction in which to to order results (asc, desc) """ - - params = filters or {} - if marker: - params['marker'] = marker - if limit: - params['limit'] = limit + params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS) res = self.do_request("GET", "/images", params=params) data = json.loads(res.read())['images'] return data - def get_images_detailed(self, filters=None, marker=None, limit=None): + def get_images_detailed(self, **kwargs): """ Returns a list of detailed image data mappings from Registry @@ -236,13 +78,11 @@ class V1Client(BaseClient): collection of images should be filtered :param marker: id after which to start the page of images :param limit: maximum number of items to return + :param sort_key: results will be ordered by this image attribute + :param sort_dir: direction in which to to order results (asc, desc) """ - params = filters or {} - if marker: - params['marker'] = marker - if limit: - params['limit'] = limit + params = self._extract_params(kwargs, v1_images.SUPPORTED_PARAMS) res = self.do_request("GET", "/images/detail", params=params) data = json.loads(res.read())['images'] return data @@ -260,7 +100,7 @@ class V1Client(BaseClient): res = self.do_request("GET", "/images/%s" % image_id) image = utils.get_image_meta_from_headers(res) - return image, ImageBodyIterator(res) + return image, base_client.ImageBodyIterator(res) def get_image_meta(self, image_id): """ diff --git a/glance/common/client.py b/glance/common/client.py new file mode 100644 index 0000000000..f8ed40efd5 --- /dev/null +++ b/glance/common/client.py @@ -0,0 +1,169 @@ +import httplib +import logging +import socket +import urllib + +from glance.common import exception + + +class ImageBodyIterator(object): + + """ + A class that acts as an iterator over an image file's + chunks of data. This is returned as part of the result + tuple from `glance.client.Client.get_image` + """ + + CHUNKSIZE = 65536 + + def __init__(self, response): + """ + Constructs the object from an HTTPResponse object + """ + self.response = response + + def __iter__(self): + """ + Exposes an iterator over the chunks of data in the + image file. + """ + while True: + chunk = self.response.read(ImageBodyIterator.CHUNKSIZE) + if chunk: + yield chunk + else: + break + + +class BaseClient(object): + + """A base client class""" + + CHUNKSIZE = 65536 + + def __init__(self, host, port, use_ssl): + """ + Creates a new client to some service. + + :param host: The host where service resides + :param port: The port where service resides + :param use_ssl: Should we use HTTPS? + """ + self.host = host + self.port = port + self.use_ssl = use_ssl + self.connection = None + + def get_connection_type(self): + """ + Returns the proper connection type + """ + if self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection + + def do_request(self, method, action, body=None, headers=None, + params=None): + """ + Connects to the server and issues a request. Handles converting + any returned HTTP error status codes to OpenStack/Glance exceptions + and closing the server connection. Returns the result data, or + raises an appropriate exception. + + :param method: HTTP method ("GET", "POST", "PUT", etc...) + :param action: part of URL after root netloc + :param body: string of data to send, or None (default) + :param headers: mapping of key/value pairs to add as headers + :param params: dictionary of key/value pairs to add to append + to action + + :note + + If the body param has a read attribute, and method is either + POST or PUT, this method will automatically conduct a chunked-transfer + encoding and use the body as a file object, transferring chunks + of data using the connection's send() method. This allows large + objects to be transferred efficiently without buffering the entire + body in memory. + """ + if type(params) is dict: + action += '?' + urllib.urlencode(params) + + try: + connection_type = self.get_connection_type() + headers = headers or {} + c = connection_type(self.host, self.port) + + # Do a simple request or a chunked request, depending + # on whether the body param is a file-like object and + # the method is PUT or POST + if hasattr(body, 'read') and method.lower() in ('post', 'put'): + # Chunk it, baby... + c.putrequest(method, action) + + for header, value in headers.items(): + c.putheader(header, value) + c.putheader('Transfer-Encoding', 'chunked') + c.endheaders() + + chunk = body.read(self.CHUNKSIZE) + while chunk: + c.send('%x\r\n%s\r\n' % (len(chunk), chunk)) + chunk = body.read(self.CHUNKSIZE) + c.send('0\r\n\r\n') + else: + # Simple request... + c.request(method, action, body, headers) + res = c.getresponse() + status_code = self.get_status_code(res) + if status_code in (httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT): + return res + elif status_code == httplib.UNAUTHORIZED: + raise exception.NotAuthorized + elif status_code == httplib.FORBIDDEN: + raise exception.NotAuthorized + elif status_code == httplib.NOT_FOUND: + raise exception.NotFound + elif status_code == httplib.CONFLICT: + raise exception.Duplicate(res.read()) + elif status_code == httplib.BAD_REQUEST: + raise exception.Invalid(res.read()) + elif status_code == httplib.INTERNAL_SERVER_ERROR: + raise Exception("Internal Server error: %s" % res.read()) + else: + raise Exception("Unknown error occurred! %s" % res.read()) + + except (socket.error, IOError), e: + raise exception.ClientConnectionError("Unable to connect to " + "server. Got error: %s" % e) + + def get_status_code(self, response): + """ + Returns the integer status code from the response, which + can be either a Webob.Response (used in testing) or httplib.Response + """ + if hasattr(response, 'status_int'): + return response.status_int + else: + return response.status + + def _extract_params(self, actual_params, allowed_params): + """ + Extract a subset of keys from a dictionary. The filters key + will also be extracted, and each of its values will be returned + as an individual param. + + :param actual_params: dict of keys to filter + :param allowed_params: list of keys that 'actual_params' will be + reduced to + :retval subset of 'params' dict + """ + result = actual_params.get('filters', {}) + for allowed_param in allowed_params: + if allowed_param in actual_params: + result[allowed_param] = actual_params[allowed_param] + return result diff --git a/glance/common/exception.py b/glance/common/exception.py index e2ad116f2f..351c3e276a 100644 --- a/glance/common/exception.py +++ b/glance/common/exception.py @@ -83,6 +83,11 @@ class DatabaseMigrationError(Error): pass +class ClientConnectionError(Exception): + """Error resulting from a client connecting to a server""" + pass + + def wrap_exception(f): def _wrap(*args, **kw): try: diff --git a/glance/registry/client.py b/glance/registry/client.py index 97dd30785d..ff84f8b143 100644 --- a/glance/registry/client.py +++ b/glance/registry/client.py @@ -23,7 +23,8 @@ the Glance Registry API import json import urllib -from glance.client import BaseClient +from glance.common.client import BaseClient +from glance.registry import server class RegistryClient(BaseClient): @@ -44,42 +45,32 @@ class RegistryClient(BaseClient): port = port or self.DEFAULT_PORT super(RegistryClient, self).__init__(host, port, use_ssl) - def get_images(self, filters=None, marker=None, limit=None): + def get_images(self, **kwargs): """ Returns a list of image id/name mappings from Registry :param filters: dict of keys & expected values to filter results :param marker: image id after which to start page :param limit: max number of images to return + :param sort_key: results will be ordered by this image attribute + :param sort_dir: direction in which to to order results (asc, desc) """ - params = filters or {} - - if marker != None: - params['marker'] = marker - - if limit != None: - params['limit'] = limit - + params = self._extract_params(kwargs, server.SUPPORTED_PARAMS) res = self.do_request("GET", "/images", params=params) data = json.loads(res.read())['images'] return data - def get_images_detailed(self, filters=None, marker=None, limit=None): + def get_images_detailed(self, **kwargs): """ Returns a list of detailed image data mappings from Registry :param filters: dict of keys & expected values to filter results :param marker: image id after which to start page :param limit: max number of images to return + :param sort_key: results will be ordered by this image attribute + :param sort_dir: direction in which to to order results (asc, desc) """ - params = filters or {} - - if marker != None: - params['marker'] = marker - - if limit != None: - params['limit'] = limit - + params = self._extract_params(kwargs, server.SUPPORTED_PARAMS) res = self.do_request("GET", "/images/detail", params=params) data = json.loads(res.read())['images'] return data diff --git a/glance/registry/db/api.py b/glance/registry/db/api.py index 3e8106e4cf..cae03a0111 100644 --- a/glance/registry/db/api.py +++ b/glance/registry/db/api.py @@ -23,7 +23,7 @@ Defines interface for DB access import logging -from sqlalchemy import create_engine, desc +from sqlalchemy import asc, create_engine, desc from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import exc from sqlalchemy.orm import joinedload @@ -129,7 +129,8 @@ def image_get(context, image_id, session=None): raise exception.NotFound("No image found with ID %s" % image_id) -def image_get_all_public(context, filters=None, marker=None, limit=None): +def image_get_all_public(context, filters=None, marker=None, limit=None, + sort_key='created_at', sort_dir='desc'): """Get all public images that match zero or more filters. :param filters: dict of filter keys and values. If a 'properties' @@ -137,7 +138,8 @@ def image_get_all_public(context, filters=None, marker=None, limit=None): filters on the image properties attribute :param marker: image id after which to start page :param limit: maximum number of images to return - + :param sort_key: image attribute by which results should be sorted + :param sort_dir: direction in which results should be sorted (asc, desc) """ filters = filters or {} @@ -146,9 +148,17 @@ def image_get_all_public(context, filters=None, marker=None, limit=None): options(joinedload(models.Image.properties)).\ filter_by(deleted=_deleted(context)).\ filter_by(is_public=True).\ - filter(models.Image.status != 'killed').\ - order_by(desc(models.Image.created_at)).\ - order_by(desc(models.Image.id)) + filter(models.Image.status != 'killed') + + sort_dir_func = { + 'asc': asc, + 'desc': desc, + }[sort_dir] + + sort_key_attr = getattr(models.Image, sort_key) + + query = query.order_by(sort_dir_func(sort_key_attr)).\ + order_by(sort_dir_func(models.Image.id)) if 'size_min' in filters: query = query.filter(models.Image.size >= filters['size_min']) diff --git a/glance/registry/server.py b/glance/registry/server.py index f6501ed3ec..012268140f 100644 --- a/glance/registry/server.py +++ b/glance/registry/server.py @@ -39,8 +39,15 @@ DISPLAY_FIELDS_IN_INDEX = ['id', 'name', 'size', SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format', 'size_min', 'size_max'] +SUPPORTED_SORT_KEYS = ('name', 'status', 'container_format', 'disk_format', + 'size', 'id', 'created_at', 'updated_at') + +SUPPORTED_SORT_DIRS = ('asc', 'desc') + MAX_ITEM_LIMIT = 25 +SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir') + class Controller(object): """Controller for the reference implementation registry server""" @@ -69,14 +76,7 @@ class Controller(object): } """ - params = { - 'filters': self._get_filters(req), - 'limit': self._get_limit(req), - } - - if 'marker' in req.str_params: - params['marker'] = self._get_marker(req) - + params = self._get_query_params(req) images = db_api.image_get_all_public(None, **params) results = [] @@ -99,19 +99,33 @@ class Controller(object): all image model fields. """ - params = { - 'filters': self._get_filters(req), - 'limit': self._get_limit(req), - } - - if 'marker' in req.str_params: - params['marker'] = self._get_marker(req) - + params = self._get_query_params(req) images = db_api.image_get_all_public(None, **params) image_dicts = [make_image_dict(i) for i in images] return dict(images=image_dicts) + def _get_query_params(self, req): + """ + Extract necessary query parameters from http request. + + :param req: the Request object coming from the wsgi layer + :retval dictionary of filters to apply to list of images + """ + params = { + 'filters': self._get_filters(req), + 'limit': self._get_limit(req), + 'sort_key': self._get_sort_key(req), + 'sort_dir': self._get_sort_dir(req), + 'marker': self._get_marker(req), + } + + for key, value in params.items(): + if value is None: + del params[key] + + return params + def _get_filters(self, req): """Return a dictionary of query param filters from the request @@ -148,12 +162,35 @@ class Controller(object): def _get_marker(self, req): """Parse a marker query param into something usable.""" + marker = req.str_params.get('marker', None) + + if marker is None: + return None + try: - marker = int(req.str_params.get('marker', None)) + marker = int(marker) except ValueError: raise exc.HTTPBadRequest("marker param must be an integer") return marker + def _get_sort_key(self, req): + """Parse a sort key query param from the request object.""" + sort_key = req.str_params.get('sort_key', None) + if sort_key is not None and sort_key not in SUPPORTED_SORT_KEYS: + _keys = ', '.join(SUPPORTED_SORT_KEYS) + msg = "Unsupported sort_key. Acceptable values: %s" % (_keys,) + raise exc.HTTPBadRequest(explanation=msg) + return sort_key + + def _get_sort_dir(self, req): + """Parse a sort direction query param from the request object.""" + sort_dir = req.str_params.get('sort_dir', None) + if sort_dir is not None and sort_dir not in SUPPORTED_SORT_DIRS: + _keys = ', '.join(SUPPORTED_SORT_DIRS) + msg = "Unsupported sort_dir. Acceptable values: %s" % (_keys,) + raise exc.HTTPBadRequest(explanation=msg) + return sort_dir + def show(self, req, id): """Return data about the given image id.""" try: diff --git a/tests/functional/test_curl_api.py b/tests/functional/test_curl_api.py index 5adf918d1a..e17ef42a12 100644 --- a/tests/functional/test_curl_api.py +++ b/tests/functional/test_curl_api.py @@ -1117,3 +1117,118 @@ class TestCurlApi(functional.FunctionalTest): self.assertEqual(int(images[0]['id']), 2) self.stop_servers() + + def test_ordered_images(self): + """ + Set up three test images and ensure each query param filter works + """ + self.cleanup() + self.start_servers() + + api_port = self.api_port + registry_port = self.registry_port + + # 0. GET /images + # Verify no public images + cmd = "curl http://0.0.0.0:%d/v1/images" % api_port + + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + self.assertEqual('{"images": []}', out.strip()) + + # 1. POST /images with three public images with various attributes + cmd = ("curl -i -X POST " + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue + "-H 'X-Image-Meta-Name: Image1' " + "-H 'X-Image-Meta-Status: active' " + "-H 'X-Image-Meta-Container-Format: ovf' " + "-H 'X-Image-Meta-Disk-Format: vdi' " + "-H 'X-Image-Meta-Size: 19' " + "-H 'X-Image-Meta-Is-Public: True' " + "http://0.0.0.0:%d/v1/images") % api_port + + exitcode, out, err = execute(cmd) + self.assertEqual(0, exitcode) + + lines = out.split("\r\n") + status_line = lines[0] + + self.assertEqual("HTTP/1.1 201 Created", status_line) + + cmd = ("curl -i -X POST " + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue + "-H 'X-Image-Meta-Name: ASDF' " + "-H 'X-Image-Meta-Status: active' " + "-H 'X-Image-Meta-Container-Format: bare' " + "-H 'X-Image-Meta-Disk-Format: iso' " + "-H 'X-Image-Meta-Size: 2' " + "-H 'X-Image-Meta-Is-Public: True' " + "http://0.0.0.0:%d/v1/images") % api_port + + exitcode, out, err = execute(cmd) + self.assertEqual(0, exitcode) + + lines = out.split("\r\n") + status_line = lines[0] + + self.assertEqual("HTTP/1.1 201 Created", status_line) + cmd = ("curl -i -X POST " + "-H 'Expect: ' " # Necessary otherwise sends 100 Continue + "-H 'X-Image-Meta-Name: XYZ' " + "-H 'X-Image-Meta-Status: saving' " + "-H 'X-Image-Meta-Container-Format: ami' " + "-H 'X-Image-Meta-Disk-Format: ami' " + "-H 'X-Image-Meta-Size: 5' " + "-H 'X-Image-Meta-Is-Public: True' " + "http://0.0.0.0:%d/v1/images") % api_port + + exitcode, out, err = execute(cmd) + self.assertEqual(0, exitcode) + + lines = out.split("\r\n") + status_line = lines[0] + + self.assertEqual("HTTP/1.1 201 Created", status_line) + + # 2. GET /images with no query params + # Verify three public images sorted by created_at desc + cmd = "curl http://0.0.0.0:%d/v1/images" % api_port + + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + images = json.loads(out.strip())['images'] + + self.assertEqual(len(images), 3) + self.assertEqual(images[0]['id'], 3) + self.assertEqual(images[1]['id'], 2) + self.assertEqual(images[2]['id'], 1) + + # 3. GET /images sorted by name asc + params = 'sort_key=name&sort_dir=asc' + cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params) + + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + images = json.loads(out.strip())['images'] + + self.assertEqual(len(images), 3) + self.assertEqual(images[0]['id'], 2) + self.assertEqual(images[1]['id'], 1) + self.assertEqual(images[2]['id'], 3) + + # 4. GET /images sorted by size desc + params = 'sort_key=size&sort_dir=desc' + cmd = "curl 'http://0.0.0.0:%d/v1/images?%s'" % (api_port, params) + + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + images = json.loads(out.strip())['images'] + + self.assertEqual(len(images), 3) + self.assertEqual(images[0]['id'], 1) + self.assertEqual(images[1]['id'], 3) + self.assertEqual(images[2]['id'], 2) diff --git a/tests/stubs.py b/tests/stubs.py index 635fd2ea07..f281a2473b 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -28,6 +28,7 @@ import sys import stubout import webob +import glance.common.client from glance.common import exception from glance.registry import server as rserver from glance.api import v1 as server @@ -254,9 +255,9 @@ def stub_out_registry_and_store_server(stubs): for i in self.response.app_iter: yield i - stubs.Set(glance.client.BaseClient, 'get_connection_type', + stubs.Set(glance.common.client.BaseClient, 'get_connection_type', fake_get_connection_type) - stubs.Set(glance.client.ImageBodyIterator, '__iter__', + stubs.Set(glance.common.client.ImageBodyIterator, '__iter__', fake_image_iter) @@ -388,8 +389,8 @@ def stub_out_registry_db_image_api(stubs): else: return images[0] - def image_get_all_public(self, _context, filters=None, - marker=None, limit=1000): + def image_get_all_public(self, _context, filters=None, marker=None, + limit=1000, sort_key=None, sort_dir=None): images = [f for f in self.images if f['is_public'] == True] if 'size_min' in filters: @@ -414,16 +415,24 @@ def stub_out_registry_db_image_api(stubs): for k, v in filters.items(): images = [f for f in images if f[k] == v] + # sorted func expects func that compares in descending order def image_cmp(x, y): - if x['created_at'] > y['created_at']: - return 1 - elif x['created_at'] == y['created_at']: + _sort_dir = sort_dir or 'desc' + multiplier = { + 'asc': -1, + 'desc': 1, + }[_sort_dir] + + _sort_key = sort_key or 'created_at' + if x[_sort_key] > y[_sort_key]: + return 1 * multiplier + elif x[_sort_key] == y[_sort_key]: if x['id'] > y['id']: - return 1 + return 1 * multiplier else: - return -1 + return -1 * multiplier else: - return -1 + return -1 * multiplier images = sorted(images, cmp=image_cmp) images.reverse() diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 106e763757..43f31b29ce 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -285,6 +285,398 @@ class TestRegistryAPI(unittest.TestCase): for image in images: self.assertEqual('new name! #123', image['name']) + def test_get_index_sort_default_created_at_desc(self): + """ + Tests that the /images registry API returns list of + public images that conforms to a default sort key/dir + """ + time1 = datetime.datetime.utcnow() + datetime.timedelta(seconds=5) + time2 = datetime.datetime.utcnow() + + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 19, + 'checksum': None, + 'created_at': time1} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 20, + 'checksum': None, + 'created_at': time1} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 5, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 20, + 'checksum': None, + 'created_at': time2} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images') + res = req.get_response(self.api) + res_dict = json.loads(res.body) + self.assertEquals(res.status_int, 200) + + images = res_dict['images'] + self.assertEquals(len(images), 4) + self.assertEquals(int(images[0]['id']), 4) + self.assertEquals(int(images[1]['id']), 3) + self.assertEquals(int(images[2]['id']), 5) + self.assertEquals(int(images[3]['id']), 2) + + def test_get_index_bad_sort_key(self): + """Ensure a 400 is returned when a bad sort_key is provided.""" + req = webob.Request.blank('/images?sort_key=asdf') + res = req.get_response(self.api) + self.assertEqual(400, res.status_int) + + def test_get_index_bad_sort_dir(self): + """Ensure a 400 is returned when a bad sort_dir is provided.""" + req = webob.Request.blank('/images?sort_dir=asdf') + res = req.get_response(self.api) + self.assertEqual(400, res.status_int) + + def test_get_index_sort_id_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted by id in descending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images?sort_key=id&sort_dir=desc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 4) + self.assertEquals(int(images[1]['id']), 3) + self.assertEquals(int(images[2]['id']), 2) + + def test_get_index_sort_name_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by name in + ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images?sort_key=name&sort_dir=asc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 2) + self.assertEquals(int(images[2]['id']), 4) + + def test_get_index_sort_status_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by status in + descending order. + """ + extra_fixture = {'id': 3, + 'status': 'killed', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images?sort_key=status&sort_dir=desc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 2) + + def test_get_index_sort_disk_format_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by disk_format in + ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vdi', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images?sort_key=disk_format&sort_dir=asc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 2) + + def test_get_index_sort_container_format_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by container_format in + descending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'iso', + 'container_format': 'bare', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + url = '/images?sort_key=container_format&sort_dir=desc' + req = webob.Request.blank(url) + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 2) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 3) + + def test_get_index_sort_size_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted by size in ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 100, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'iso', + 'container_format': 'bare', + 'name': 'xyz', + 'size': 2, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + url = '/images?sort_key=size&sort_dir=asc' + req = webob.Request.blank(url) + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 4) + self.assertEquals(int(images[1]['id']), 2) + self.assertEquals(int(images[2]['id']), 3) + + def test_get_index_sort_created_at_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted by created_at in ascending order. + """ + now = datetime.datetime.utcnow() + time1 = now + datetime.timedelta(seconds=5) + time2 = now + + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 19, + 'checksum': None, + 'created_at': time1} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 20, + 'checksum': None, + 'created_at': time2} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images?sort_key=created_at&sort_dir=asc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 2) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 3) + + def test_get_index_sort_updated_at_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted by updated_at in descending order. + """ + now = datetime.datetime.utcnow() + time1 = now + datetime.timedelta(seconds=5) + time2 = now + + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 19, + 'checksum': None, + 'created_at': None, + 'created_at': time1} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 20, + 'checksum': None, + 'created_at': None, + 'updated_at': time2} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images?sort_key=updated_at&sort_dir=desc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 2) + def test_get_details(self): """Tests that the /images/detail registry API returns a mapping containing a list of detailed image information @@ -668,6 +1060,45 @@ class TestRegistryAPI(unittest.TestCase): for image in images: self.assertEqual('v a', image['properties']['prop_123']) + def test_get_details_sort_name_asc(self): + """ + Tests that the /images/details registry API returns list of + public images sorted alphabetically by name in + ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images/detail?sort_key=name&sort_dir=asc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 2) + self.assertEquals(int(images[2]['id']), 4) + def test_create_image(self): """Tests that the /images POST registry API creates the image""" fixture = {'name': 'fake public image', @@ -1042,6 +1473,45 @@ class TestGlanceAPI(unittest.TestCase): "res.headerlist = %r" % res.headerlist) self.assertTrue('/images/3' in res.headers['location']) + def test_get_index_sort_name_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by name in + ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images?sort_key=name&sort_dir=asc') + res = req.get_response(self.api) + self.assertEquals(res.status_int, 200) + res_dict = json.loads(res.body) + + images = res_dict['images'] + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 2) + self.assertEquals(int(images[2]['id']), 4) + def test_image_is_checksummed(self): """Test that the image contents are checksummed properly""" fixture_headers = {'x-image-meta-store': 'file', diff --git a/tests/unit/test_clients.py b/tests/unit/test_clients.py index 2be1a3ad9e..58be704ee2 100644 --- a/tests/unit/test_clients.py +++ b/tests/unit/test_clients.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime import json import os import stubout @@ -37,7 +38,7 @@ class TestBadClients(unittest.TestCase): def test_bad_address(self): """Test ClientConnectionError raised""" c = client.Client("127.999.1.1") - self.assertRaises(client.ClientConnectionError, + self.assertRaises(exception.ClientConnectionError, c.get_image, 1) @@ -70,6 +71,298 @@ class TestRegistryClient(unittest.TestCase): for k, v in fixture.items(): self.assertEquals(v, images[0][k]) + def test_get_index_sort_id_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted by id in descending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='id', sort_dir='desc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 4) + self.assertEquals(int(images[1]['id']), 3) + self.assertEquals(int(images[2]['id']), 2) + + def test_get_index_sort_name_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by name in + ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='name', sort_dir='asc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 2) + self.assertEquals(int(images[2]['id']), 4) + + def test_get_index_sort_status_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by status in + descending order. + """ + extra_fixture = {'id': 3, + 'status': 'killed', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='status', sort_dir='desc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 2) + + def test_get_index_sort_disk_format_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by disk_format in + ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vdi', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='disk_format', + sort_dir='asc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 2) + + def test_get_index_sort_container_format_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted alphabetically by container_format in + descending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'iso', + 'container_format': 'bare', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='container_format', + sort_dir='desc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 2) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 3) + + def test_get_index_sort_size_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted by size in ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 100, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'iso', + 'container_format': 'bare', + 'name': 'xyz', + 'size': 2, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='size', sort_dir='asc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 4) + self.assertEquals(int(images[1]['id']), 2) + self.assertEquals(int(images[2]['id']), 3) + + def test_get_index_sort_created_at_asc(self): + """ + Tests that the /images registry API returns list of + public images sorted by created_at in ascending order. + """ + now = datetime.datetime.utcnow() + time1 = now + datetime.timedelta(seconds=5) + time2 = now + + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 19, + 'checksum': None, + 'created_at': time1} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 20, + 'checksum': None, + 'created_at': time2} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='created_at', sort_dir='asc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 2) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 3) + + def test_get_index_sort_updated_at_desc(self): + """ + Tests that the /images registry API returns list of + public images sorted by updated_at in descending order. + """ + now = datetime.datetime.utcnow() + time1 = now + datetime.timedelta(seconds=5) + time2 = now + + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 19, + 'checksum': None, + 'created_at': None, + 'created_at': time1} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 20, + 'checksum': None, + 'created_at': None, + 'updated_at': time2} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='updated_at', sort_dir='desc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 2) + def test_get_image_index_marker(self): """Test correct set of images returned with marker param.""" extra_fixture = {'id': 3, @@ -170,7 +463,7 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images({'name': 'new name! #123'}) + images = self.client.get_images(filters={'name': 'new name! #123'}) self.assertEquals(len(images), 1) for image in images: @@ -236,7 +529,8 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'name': 'new name! #123'}) + filters = {'name': 'new name! #123'} + images = self.client.get_images_detailed(filters=filters) self.assertEquals(len(images), 1) for image in images: @@ -255,7 +549,7 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'status': 'saving'}) + images = self.client.get_images_detailed(filters={'status': 'saving'}) self.assertEquals(len(images), 1) for image in images: @@ -274,7 +568,8 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'container_format': 'ovf'}) + filters = {'container_format': 'ovf'} + images = self.client.get_images_detailed(filters=filters) self.assertEquals(len(images), 2) for image in images: @@ -293,7 +588,8 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'disk_format': 'vhd'}) + filters = {'disk_format': 'vhd'} + images = self.client.get_images_detailed(filters=filters) self.assertEquals(len(images), 2) for image in images: @@ -312,7 +608,7 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'size_max': 20}) + images = self.client.get_images_detailed(filters={'size_max': 20}) self.assertEquals(len(images), 1) for image in images: @@ -331,7 +627,7 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'size_min': 20}) + images = self.client.get_images_detailed(filters={'size_min': 20}) self.assertEquals(len(images), 1) for image in images: @@ -351,12 +647,49 @@ class TestRegistryClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'property-p a': 'v a'}) + filters = {'property-p a': 'v a'} + images = self.client.get_images_detailed(filters=filters) self.assertEquals(len(images), 1) for image in images: self.assertEquals('v a', image['properties']['p a']) + def test_get_image_details_sort_disk_format_asc(self): + """ + Tests that a detailed call returns list of + public images sorted alphabetically by disk_format in + ascending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vdi', + 'container_format': 'ovf', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images_detailed(sort_key='disk_format', + sort_dir='asc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 3) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 2) + def test_get_image(self): """Tests that the detailed info about an image returned""" fixture = {'id': 1, @@ -562,6 +895,42 @@ class TestClient(unittest.TestCase): self.client.get_image, 3) + def test_get_image_index_sort_container_format_desc(self): + """ + Tests that the client returns list of public images + sorted alphabetically by container_format in + descending order. + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'asdf', + 'size': 19, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'iso', + 'container_format': 'bare', + 'name': 'xyz', + 'size': 20, + 'checksum': None} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images(sort_key='container_format', + sort_dir='desc') + + self.assertEquals(len(images), 3) + self.assertEquals(int(images[0]['id']), 2) + self.assertEquals(int(images[1]['id']), 4) + self.assertEquals(int(images[2]['id']), 3) + def test_get_image_index(self): """Test correct set of public image returned""" fixture = {'id': 2, @@ -671,7 +1040,8 @@ class TestClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images({'name': 'new name! #123'}) + filters = {'name': 'new name! #123'} + images = self.client.get_images(filters=filters) self.assertEquals(len(images), 1) self.assertEquals('new name! #123', images[0]['name']) @@ -690,7 +1060,8 @@ class TestClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images({'property-p a': 'v a'}) + filters = {'property-p a': 'v a'} + images = self.client.get_images(filters=filters) self.assertEquals(len(images), 1) self.assertEquals(3, images[0]['id']) @@ -765,7 +1136,8 @@ class TestClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'name': 'new name! #123'}) + filters = {'name': 'new name! #123'} + images = self.client.get_images_detailed(filters=filters) self.assertEquals(len(images), 1) for image in images: @@ -785,7 +1157,8 @@ class TestClient(unittest.TestCase): glance.registry.db.api.image_create(None, extra_fixture) - images = self.client.get_images_detailed({'property-p a': 'v a'}) + filters = {'property-p a': 'v a'} + images = self.client.get_images_detailed(filters=filters) self.assertEquals(len(images), 1) for image in images: