merged glance trunk

This commit is contained in:
Isaku Yamahata 2011-07-08 18:46:06 +09:00
commit 3eeb9fec9c
28 changed files with 1906 additions and 389 deletions

View File

@ -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 "
@ -551,7 +551,8 @@ def print_help(options, args):
def user_confirm(prompt, default=False):
"""Yes/No question dialog with user.
"""
Yes/No question dialog with user.
:param prompt: question/statement to present to user (string)
:param default: boolean value to return if empty string

View File

@ -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
------------------------------------------------

View File

@ -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
------------------------------------------------

View File

@ -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``
----------------

View File

@ -74,7 +74,7 @@ class VersionNegotiationFilter(wsgi.Middleware):
req.environ['api.minor_version'])
return self.versions_app
accept = req.headers['Accept']
accept = str(req.accept)
if accept.startswith('application/vnd.openstack.images-'):
token_loc = len('application/vnd.openstack.images-')
accept_version = accept[token_loc:]

View File

@ -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,15 +94,12 @@ class Controller(object):
'size': <SIZE>}, ...
]}
"""
params = {'filters': self._get_filters(req)}
params = self._get_query_params(req)
try:
images = registry.get_images_list(self.options, **params)
except exception.Invalid, e:
raise HTTPBadRequest(explanation=str(e))
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')
images = registry.get_images_list(self.options, **params)
return dict(images=images)
def detail(self, req):
@ -125,17 +124,26 @@ 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')
images = registry.get_images_detail(self.options, **params)
params = self._get_query_params(req)
try:
images = registry.get_images_detail(self.options, **params)
except exception.Invalid, e:
raise HTTPBadRequest(explanation=str(e))
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

View File

@ -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"""
@ -199,7 +44,6 @@ class V1Client(BaseClient):
:param use_ssl: Should we use HTTPS? (defaults to False)
:param doc_root: Prefix for all URLs we request from host
"""
port = port or self.DEFAULT_PORT
self.doc_root = doc_root
super(Client, self).__init__(host, port, use_ssl)
@ -209,7 +53,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 +61,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 +77,10 @@ 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 +98,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):
"""
@ -286,7 +124,6 @@ class V1Client(BaseClient):
:retval The newly-stored image's metadata.
"""
headers = utils.image_meta_to_http_headers(image_meta or {})
if image_data:

181
glance/common/client.py Normal file
View File

@ -0,0 +1,181 @@
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:
# remove any params that are None
for (key, value) in params.items():
if value is None:
del params[key]
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
"""
try:
# expect 'filters' param to be a dict here
result = dict(actual_params.get('filters'))
except TypeError:
result = {}
for allowed_param in allowed_params:
if allowed_param in actual_params:
result[allowed_param] = actual_params[allowed_param]
return result

View File

@ -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:

View File

@ -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):
@ -40,46 +41,35 @@ class RegistryClient(BaseClient):
:param port: The port where Glance resides (defaults to 9191)
:param use_ssl: Should we use HTTPS? (defaults to False)
"""
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

View File

@ -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
@ -97,10 +97,10 @@ def image_create(context, values):
def image_update(context, image_id, values, purge_props=False):
"""Set the given properties on an image and update it.
Raises NotFound if image does not exist.
"""
Set the given properties on an image and update it.
:raises NotFound if image does not exist.
"""
return _image_update(context, values, image_id, purge_props)
@ -129,15 +129,18 @@ 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):
"""Get all public images that match zero or more filters.
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'
key is present, it is treated as a dict of key/value
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 +149,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'])
@ -179,7 +190,8 @@ def image_get_all_public(context, filters=None, marker=None, limit=None):
def _drop_protected_attrs(model_class, values):
"""Removed protected attributes from values dictionary using the models
"""
Removed protected attributes from values dictionary using the models
__protected_attributes__ field.
"""
for attr in model_class.__protected_attributes__:
@ -194,7 +206,6 @@ def validate_image(values):
:param values: Mapping of image metadata to check
"""
status = values.get('status')
disk_format = values.get('disk_format')
container_format = values.get('container_format')
@ -227,13 +238,13 @@ def validate_image(values):
def _image_update(context, values, image_id, purge_props=False):
"""Used internally by image_create and image_update
"""
Used internally by image_create and image_update
:param context: Request context
:param values: A dict of attributes to set
:param image_id: If None, create the image, otherwise, find and update it
"""
session = get_session()
with session.begin():
@ -315,7 +326,8 @@ def image_property_update(context, prop_ref, values, session=None):
def _image_property_update(context, prop_ref, values, session=None):
"""Used internally by image_property_create and image_property_update
"""
Used internally by image_property_create and image_property_update
"""
_drop_protected_attrs(models.ImageProperty, values)
values["deleted"] = False
@ -325,7 +337,8 @@ def _image_property_update(context, prop_ref, values, session=None):
def image_property_delete(context, prop_ref, session=None):
"""Used internally by image_property_create and image_property_update
"""
Used internally by image_property_create and image_property_update
"""
prop_ref.update(dict(deleted=True))
prop_ref.save(session=session)
@ -334,8 +347,8 @@ def image_property_delete(context, prop_ref, session=None):
# pylint: disable-msg=C0111
def _deleted(context):
"""Calculates whether to include deleted objects based on context.
"""
Calculates whether to include deleted objects based on context.
Currently just looks for a flag called deleted in the context dict.
"""
if not hasattr(context, 'get'):

View File

@ -51,7 +51,8 @@ BigInteger = lambda: sqlalchemy.types.BigInteger()
def from_migration_import(module_name, fromlist):
"""Import a migration file and return the module
"""
Import a migration file and return the module
:param module_name: name of migration module to import from
(ex: 001_add_images_table)
@ -84,7 +85,6 @@ def from_migration_import(module_name, fromlist):
images = define_images_table(meta)
# Refer to images table
"""
module_path = 'glance.registry.db.migrate_repo.versions.%s' % module_name
module = __import__(module_path, globals(), locals(), fromlist, -1)

View File

@ -33,7 +33,8 @@ logger = logging.getLogger('glance.registry.db.migration')
def db_version(options):
"""Return the database's current migration number
"""
Return the database's current migration number
:param options: options dict
:retval version number
@ -49,7 +50,8 @@ def db_version(options):
def upgrade(options, version=None):
"""Upgrade the database's current migration level
"""
Upgrade the database's current migration level
:param options: options dict
:param version: version to upgrade (defaults to latest)
@ -65,7 +67,8 @@ def upgrade(options, version=None):
def downgrade(options, version):
"""Downgrade the database's current migration level
"""
Downgrade the database's current migration level
:param options: options dict
:param version: version to downgrade to
@ -80,7 +83,8 @@ def downgrade(options, version):
def version_control(options):
"""Place a database under migration control
"""
Place a database under migration control
:param options: options dict
"""
@ -94,7 +98,8 @@ def version_control(options):
def _version_control(options):
"""Place a database under migration control
"""
Place a database under migration control
:param options: options dict
"""
@ -104,7 +109,8 @@ def _version_control(options):
def db_sync(options, version=None):
"""Place a database under migration control and perform an upgrade
"""
Place a database under migration control and perform an upgrade
:param options: options dict
:retval version number

View File

@ -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"""
@ -50,7 +57,8 @@ class Controller(object):
db_api.configure_db(options)
def index(self, req):
"""Return a basic filtered list of public, non-deleted images
"""
Return a basic filtered list of public, non-deleted images
:param req: the Request object coming from the wsgi layer
:retval a mapping of the following form::
@ -67,17 +75,13 @@ class Controller(object):
'container_format': <CONTAINER_FORMAT>,
'checksum': <CHECKSUM>
}
"""
params = {
'filters': self._get_filters(req),
'limit': self._get_limit(req),
}
if 'marker' in req.str_params:
params['marker'] = self._get_marker(req)
images = db_api.image_get_all_public(None, **params)
params = self._get_query_params(req)
try:
images = db_api.image_get_all_public(None, **params)
except exception.NotFound, e:
msg = "Invalid marker. Image could not be found."
raise exc.HTTPBadRequest(explanation=msg)
results = []
for image in images:
@ -88,7 +92,8 @@ class Controller(object):
return dict(images=results)
def detail(self, req):
"""Return a filtered list of public, non-deleted images in detail
"""
Return a filtered list of public, non-deleted images in detail
:param req: the Request object coming from the wsgi layer
:retval a mapping of the following form::
@ -97,27 +102,44 @@ class Controller(object):
Where image_list is a sequence of mappings containing
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)
images = db_api.image_get_all_public(None, **params)
params = self._get_query_params(req)
try:
images = db_api.image_get_all_public(None, **params)
except exception.NotFound, e:
msg = "Invalid marker. Image could not be found."
raise exc.HTTPBadRequest(explanation=msg)
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
"""
Return a dictionary of query param filters from the request
:param req: the Request object coming from the wsgi layer
:retval a dict of key/value filters
"""
filters = {}
properties = {}
@ -148,12 +170,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:
@ -171,7 +216,6 @@ class Controller(object):
:param id: The opaque internal identifier for the image
:retval Returns 200 if delete was successful, a fault if not.
"""
context = None
try:
@ -189,7 +233,6 @@ class Controller(object):
:retval Returns the newly-created image information as a mapping,
which will include the newly-created image's internal id
in the 'id' field
"""
image_data = body['image']
@ -210,14 +253,14 @@ class Controller(object):
return exc.HTTPBadRequest(msg)
def update(self, req, id, body):
"""Updates an existing image with the registry.
"""
Updates an existing image with the registry.
:param req: wsgi Request object
:param body: Dictionary of information about the image
:param id: The opaque internal identifier for the image
:retval Returns the updated image information as a mapping,
"""
image_data = body['image']

View File

@ -116,7 +116,6 @@ def parse_uri_tokens(parsed_uri, example_url):
1) urlparse to split the tokens
2) use RE to split on @ and /
3) reassemble authurl
"""
path = parsed_uri.path.lstrip('//')
netloc = parsed_uri.netloc

View File

@ -65,11 +65,11 @@ class ChunkedFile(object):
class FilesystemBackend(glance.store.Backend):
@classmethod
def get(cls, parsed_uri, expected_size=None, options=None):
""" Filesystem-based backend
"""
Filesystem-based backend
file:///path/to/file.tar.gz.0
"""
filepath = parsed_uri.path
if not os.path.exists(filepath):
raise exception.NotFound("Image file %s not found" % filepath)

View File

@ -25,10 +25,10 @@ class HTTPBackend(glance.store.Backend):
@classmethod
def get(cls, parsed_uri, expected_size, options=None, conn_class=None):
"""Takes a parsed uri for an HTTP resource, fetches it, and yields the
data.
"""
Takes a parsed uri for an HTTP resource, fetches it, and
yields the data.
"""
if conn_class:
pass # use the conn_class passed in
elif parsed_uri.scheme == "http":

View File

@ -32,9 +32,8 @@ logger = logging.getLogger('glance.store.swift')
class SwiftBackend(glance.store.Backend):
"""
An implementation of the swift backend adapter.
"""
"""An implementation of the swift backend adapter."""
EXAMPLE_URL = "swift://<USER>:<KEY>@<AUTH_ADDRESS>/<CONTAINER>/<FILE>"
CHUNKSIZE = 65536

View File

@ -38,7 +38,8 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Unittest runner for glance
"""
Unittest runner for glance
To run all test::
python run_tests.py
@ -212,7 +213,8 @@ class GlanceTestResult(result.TextTestResult):
# NOTE(vish, tfukushima): copied from unittest with edit to add color
def addError(self, test, err):
"""Overrides normal addError to add support for errorClasses.
"""
Overrides normal addError to add support for errorClasses.
If the exception is a registered class, the error will be added
to the list for that class, not errors.
"""

View File

@ -40,7 +40,6 @@ class TestBinGlance(functional.FunctionalTest):
3. Delete the image
4. Verify no longer in index
"""
self.cleanup()
self.start_servers()
@ -106,7 +105,6 @@ class TestBinGlance(functional.FunctionalTest):
5. Update the image's Name attribute
6. Verify the updated name is shown
"""
self.cleanup()
self.start_servers()
@ -192,7 +190,6 @@ class TestBinGlance(functional.FunctionalTest):
3. Verify the status of the image is displayed in the show output
and is in status 'killed'
"""
self.cleanup()
# Start servers with a Swift backend and a bad auth URL
@ -253,7 +250,6 @@ class TestBinGlance(functional.FunctionalTest):
3. Verify no public images found
4. Run SQL against DB to verify no undeleted properties
"""
self.cleanup()
self.start_servers()

View File

@ -63,7 +63,6 @@ class TestCurlApi(functional.FunctionalTest):
11. PUT /images/1
- Add a previously deleted property.
"""
self.cleanup()
self.start_servers()
@ -382,7 +381,6 @@ class TestCurlApi(functional.FunctionalTest):
6. GET /images
- Verify one public image
"""
self.cleanup()
self.start_servers()
@ -502,7 +500,6 @@ class TestCurlApi(functional.FunctionalTest):
handled properly, and that usage of the Accept: header does
content negotiation properly.
"""
self.cleanup()
self.start_servers()
@ -570,7 +567,7 @@ class TestCurlApi(functional.FunctionalTest):
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 5. GET / with an Accept: application/vnd.openstack.images-v1
# 4. GET / with an Accept: application/vnd.openstack.images-v1
# Verify empty image list returned
cmd = ("curl -H 'Accept: application/vnd.openstack.images-v1' "
"http://0.0.0.0:%d/images") % api_port
@ -693,7 +690,6 @@ class TestCurlApi(functional.FunctionalTest):
:see https://bugs.launchpad.net/glance/+bug/739433
"""
self.cleanup()
self.start_servers()
@ -760,7 +756,6 @@ class TestCurlApi(functional.FunctionalTest):
:see https://bugs.launchpad.net/glance/+bug/755912
"""
self.cleanup()
self.start_servers()
@ -1117,3 +1112,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)

View File

@ -33,7 +33,7 @@ class TestApiHttplib2(functional.FunctionalTest):
"""Functional tests using httplib2 against the API server"""
def test_001_get_head_simple_post(self):
def test_get_head_simple_post(self):
"""
We test the following sequential series of actions:
@ -63,7 +63,6 @@ class TestApiHttplib2(functional.FunctionalTest):
11. PUT /images/1
- Add a previously deleted property.
"""
self.cleanup()
self.start_servers()
@ -272,3 +271,322 @@ class TestApiHttplib2(functional.FunctionalTest):
self.assertEqual(data['properties']['distro'], "Ubuntu")
self.stop_servers()
def test_queued_process_flow(self):
"""
We test the process flow where a user registers an image
with Glance but does not immediately upload an image file.
Later, the user uploads an image file using a PUT operation.
We track the changing of image status throughout this process.
0. GET /images
- Verify no public images
1. POST /images with public image named Image1 with no location
attribute and no image data.
- Verify 201 returned
2. GET /images
- Verify one public image
3. HEAD /images/1
- Verify image now in queued status
4. PUT /images/1 with image data
- Verify 200 returned
5. HEAD /images/1
- Verify image now in active status
6. GET /images
- Verify one public image
"""
self.cleanup()
self.start_servers()
# 0. GET /images
# Verify no public images
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, '{"images": []}')
# 1. POST /images with public image named Image1
# with no location or image data
headers = {'Content-Type': 'application/octet-stream',
'X-Image-Meta-Name': 'Image1',
'X-Image-Meta-Is-Public': 'True'}
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'POST', headers=headers)
self.assertEqual(response.status, 201)
data = json.loads(content)
self.assertEqual(data['image']['checksum'], None)
self.assertEqual(data['image']['size'], 0)
self.assertEqual(data['image']['container_format'], None)
self.assertEqual(data['image']['disk_format'], None)
self.assertEqual(data['image']['name'], "Image1")
self.assertEqual(data['image']['is_public'], True)
# 2. GET /images
# Verify 1 public image
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
data = json.loads(content)
self.assertEqual(data['images'][0]['id'], 1)
self.assertEqual(data['images'][0]['checksum'], None)
self.assertEqual(data['images'][0]['size'], 0)
self.assertEqual(data['images'][0]['container_format'], None)
self.assertEqual(data['images'][0]['disk_format'], None)
self.assertEqual(data['images'][0]['name'], "Image1")
# 3. HEAD /images
# Verify status is in queued
path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'HEAD')
self.assertEqual(response.status, 200)
self.assertEqual(response['x-image-meta-name'], "Image1")
self.assertEqual(response['x-image-meta-status'], "queued")
self.assertEqual(response['x-image-meta-size'], '0')
self.assertEqual(response['x-image-meta-id'], '1')
# 4. PUT /images/1 with image data, verify 200 returned
image_data = "*" * FIVE_KB
headers = {'Content-Type': 'application/octet-stream'}
path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'PUT', headers=headers,
body=image_data)
self.assertEqual(response.status, 200)
data = json.loads(content)
self.assertEqual(data['image']['checksum'],
hashlib.md5(image_data).hexdigest())
self.assertEqual(data['image']['size'], FIVE_KB)
self.assertEqual(data['image']['name'], "Image1")
self.assertEqual(data['image']['is_public'], True)
# 5. HEAD /images
# Verify status is in active
path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'HEAD')
self.assertEqual(response.status, 200)
self.assertEqual(response['x-image-meta-name'], "Image1")
self.assertEqual(response['x-image-meta-status'], "active")
# 6. GET /images
# Verify 1 public image still...
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
data = json.loads(content)
self.assertEqual(data['images'][0]['checksum'],
hashlib.md5(image_data).hexdigest())
self.assertEqual(data['images'][0]['id'], 1)
self.assertEqual(data['images'][0]['size'], FIVE_KB)
self.assertEqual(data['images'][0]['container_format'], None)
self.assertEqual(data['images'][0]['disk_format'], None)
self.assertEqual(data['images'][0]['name'], "Image1")
self.stop_servers()
def test_version_variations(self):
"""
We test that various calls to the images and root endpoints are
handled properly, and that usage of the Accept: header does
content negotiation properly.
"""
self.cleanup()
self.start_servers()
versions = {'versions': [{
"id": "v1.0",
"status": "CURRENT",
"links": [{
"rel": "self",
"href": "http://0.0.0.0:%d/v1/" % self.api_port}]}]}
versions_json = json.dumps(versions)
images = {'images': []}
images_json = json.dumps(images)
# 0. GET / with no Accept: header
# Verify version choices returned.
# Bug lp:803260 no Accept header causes a 500 in glance-api
path = "http://%s:%d/" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
# 1. GET /images with no Accept: header
# Verify version choices returned.
path = "http://%s:%d/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
# 2. GET /v1/images with no Accept: header
# Verify empty images list returned.
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, images_json)
# 3. GET / with Accept: unknown header
# Verify version choices returned. Verify message in API log about
# unknown accept header.
path = "http://%s:%d/" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
headers = {'Accept': 'unknown'}
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 4. GET / with an Accept: application/vnd.openstack.images-v1
# Verify empty image list returned
path = "http://%s:%d/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
headers = {'Accept': 'application/vnd.openstack.images-v1'}
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 200)
self.assertEqual(content, images_json)
# 5. GET /images with a Accept: application/vnd.openstack.compute-v1
# header. Verify version choices returned. Verify message in API log
# about unknown accept header.
path = "http://%s:%d/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
headers = {'Accept': 'application/vnd.openstack.compute-v1'}
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 6. GET /v1.0/images with no Accept: header
# Verify empty image list returned
path = "http://%s:%d/v1.0/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, images_json)
# 7. GET /v1.a/images with no Accept: header
# Verify empty image list returned
path = "http://%s:%d/v1.a/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
self.assertEqual(content, images_json)
# 8. GET /va.1/images with no Accept: header
# Verify version choices returned
path = "http://%s:%d/va.1/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
# 9. GET /versions with no Accept: header
# Verify version choices returned
path = "http://%s:%d/versions" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
# 10. GET /versions with a Accept: application/vnd.openstack.images-v1
# header. Verify version choices returned.
path = "http://%s:%d/versions" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
headers = {'Accept': 'application/vnd.openstack.images-v1'}
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
# 11. GET /v1/versions with no Accept: header
# Verify 404 returned
path = "http://%s:%d/v1/versions" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 404)
# 12. GET /v2/versions with no Accept: header
# Verify version choices returned
path = "http://%s:%d/v2/versions" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
# 13. GET /images with a Accept: application/vnd.openstack.compute-v2
# header. Verify version choices returned. Verify message in API log
# about unknown version in accept header.
path = "http://%s:%d/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
headers = {'Accept': 'application/vnd.openstack.images-v2'}
response, content = http.request(path, 'GET', headers=headers)
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown accept header'
in open(self.api_server.log_file).read())
# 14. GET /v1.2/images with no Accept: header
# Verify version choices returned
path = "http://%s:%d/v1.2/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 300)
self.assertEqual(content, versions_json)
self.assertTrue('Unknown version in versioned URI'
in open(self.api_server.log_file).read())
self.stop_servers()
def test_size_greater_2G_mysql(self):
"""
A test against the actual datastore backend for the registry
to ensure that the image size property is not truncated.
:see https://bugs.launchpad.net/glance/+bug/739433
"""
self.cleanup()
self.start_servers()
# 1. POST /images with public image named Image1
# attribute and a size of 5G. Use the HTTP engine with an
# X-Image-Meta-Location attribute to make Glance forego
# "adding" the image data.
# Verify a 201 OK is returned
headers = {'Content-Type': 'application/octet-stream',
'X-Image-Meta-Location': 'http://example.com/fakeimage',
'X-Image-Meta-Size': str(FIVE_GB),
'X-Image-Meta-Name': 'Image1',
'X-Image-Meta-Is-Public': 'True'}
path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'POST', headers=headers)
self.assertEqual(response.status, 201)
new_image_url = response['location']
self.assertTrue(new_image_url is not None,
"Could not find a new image URI!")
self.assertTrue("v1/images" in new_image_url,
"v1/images no in %s" % new_image_url)
# 2. HEAD /images
# Verify image size is what was passed in, and not truncated
path = new_image_url
http = httplib2.Http()
response, content = http.request(path, 'HEAD')
self.assertEqual(response.status, 200)
self.assertEqual(response['x-image-meta-size'], str(FIVE_GB))
self.assertEqual(response['x-image-meta-name'], 'Image1')
self.assertEqual(response['x-image-meta-is_public'], 'True')
self.stop_servers()

View File

@ -33,7 +33,6 @@ class TestLogging(functional.FunctionalTest):
Test logging output proper when verbose and debug
is on.
"""
self.cleanup()
self.start_servers()
@ -60,7 +59,6 @@ class TestLogging(functional.FunctionalTest):
Test logging output proper when verbose and debug
is off.
"""
self.cleanup()
self.start_servers(debug=False, verbose=False)

View File

@ -39,7 +39,6 @@ class TestMiscellaneous(functional.FunctionalTest):
We also fire the glance-upload tool against the API server
and verify that glance-upload doesn't eat the exception either...
"""
self.cleanup()
self.start_servers()

View File

@ -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
@ -43,14 +44,14 @@ DEBUG = False
def stub_out_http_backend(stubs):
"""Stubs out the httplib.HTTPRequest.getresponse to return
"""
Stubs out the httplib.HTTPRequest.getresponse to return
faked-out data instead of grabbing actual contents of a resource
The stubbed getresponse() returns an iterator over
the data "I am a teapot, short and stout\n"
:param stubs: Set of stubout stubs
"""
class FakeHTTPConnection(object):
@ -94,7 +95,6 @@ def stub_out_filesystem_backend():
//tmp/glance-tests/2 <-- file containing "chunk00000remainder"
The stubbed service yields the data in the above files.
"""
# Establish a clean faked filesystem with dummy images
@ -108,13 +108,13 @@ def stub_out_filesystem_backend():
def stub_out_s3_backend(stubs):
""" Stubs out the S3 Backend with fake data and calls.
"""
Stubs out the S3 Backend with fake data and calls.
The stubbed s3 backend provides back an iterator over
the data ""
:param stubs: Set of stubout stubs
"""
class FakeSwiftAuth(object):
@ -254,14 +254,15 @@ 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)
def stub_out_registry_db_image_api(stubs):
"""Stubs out the database set/fetch API calls for Registry
"""
Stubs out the database set/fetch API calls for Registry
so the calls are routed to an in-memory dict. This helps us
avoid having to manually clear or flush the SQLite database.
@ -269,6 +270,7 @@ def stub_out_registry_db_image_api(stubs):
:param stubs: Set of stubout stubs
"""
class FakeDatastore(object):
FIXTURES = [
@ -388,8 +390,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 +416,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()
@ -437,6 +447,9 @@ def stub_out_registry_db_image_api(stubs):
start_index = i + 1
break
if start_index == -1:
raise exception.NotFound(marker)
return images[start_index:start_index + limit]
fake_datastore = FakeDatastore()

View File

@ -50,9 +50,9 @@ class TestRegistryAPI(unittest.TestCase):
self.stubs.UnsetAll()
def test_get_root(self):
"""Tests that the root registry API returns "index",
"""
Tests that the root registry API returns "index",
which is a list of public images
"""
fixture = {'id': 2,
'name': 'fake image #2',
@ -70,9 +70,9 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEquals(v, images[0][k])
def test_get_index(self):
"""Tests that the /images registry API returns list of
"""
Tests that the /images registry API returns list of
public images
"""
fixture = {'id': 2,
'name': 'fake image #2',
@ -90,11 +90,10 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEquals(v, images[0][k])
def test_get_index_marker(self):
"""Tests that the /images registry API returns list of
public images that conforms to a marker query param
"""
Tests that the /images registry API returns list of
public images that conforms to a marker query param
"""
time1 = datetime.datetime.utcnow() + datetime.timedelta(seconds=5)
time2 = datetime.datetime.utcnow()
@ -147,10 +146,19 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEquals(int(images[1]['id']), 5)
self.assertEquals(int(images[2]['id']), 2)
def test_get_index_limit(self):
"""Tests that the /images registry API returns list of
public images that conforms to a limit query param
def test_get_index_invalid_marker(self):
"""
Tests that the /images registry API returns a 400
when an invalid marker is provided
"""
req = webob.Request.blank('/images?marker=10')
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_get_index_limit(self):
"""
Tests that the /images registry API returns list of
public images that conforms to a limit query param
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -186,27 +194,27 @@ class TestRegistryAPI(unittest.TestCase):
self.assertTrue(int(images[0]['id']), 4)
def test_get_index_limit_negative(self):
"""Tests that the /images registry API returns list of
"""
Tests that the /images registry API returns list of
public images that conforms to a limit query param
"""
req = webob.Request.blank('/images?limit=-1')
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_get_index_limit_non_int(self):
"""Tests that the /images registry API returns list of
"""
Tests that the /images registry API returns list of
public images that conforms to a limit query param
"""
req = webob.Request.blank('/images?limit=a')
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_get_index_limit_marker(self):
"""Tests that the /images registry API returns list of
"""
Tests that the /images registry API returns list of
public images that conforms to limit and marker query params
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -242,10 +250,10 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEqual(int(images[0]['id']), 2)
def test_get_index_filter_name(self):
"""Tests that the /images registry API returns list of
"""
Tests that the /images registry API returns list of
public images that have a specific name. This is really a sanity
check, filtering is tested more in-depth using /images/detail
"""
fixture = {'id': 2,
'name': 'fake image #2',
@ -285,10 +293,402 @@ class TestRegistryAPI(unittest.TestCase):
for image in images:
self.assertEqual('new name! #123', image['name'])
def test_get_details(self):
"""Tests that the /images/detail registry API returns
a mapping containing a list of detailed image information
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
"""
fixture = {'id': 2,
'name': 'fake image #2',
@ -311,11 +711,11 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEquals(v, images[0][k])
def test_get_details_limit_marker(self):
"""Tests that the /images/details registry API returns list of
"""
Tests that the /images/details registry API returns list of
public images that conforms to limit and marker query params.
This functionality is tested more thoroughly on /images, this is
just a sanity check
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -350,10 +750,19 @@ class TestRegistryAPI(unittest.TestCase):
# expect list to be sorted by created_at desc
self.assertEqual(int(images[0]['id']), 2)
def test_get_details_filter_name(self):
"""Tests that the /images/detail registry API returns list of
public images that have a specific name
def test_get_details_invalid_marker(self):
"""
Tests that the /images/detail registry API returns a 400
when an invalid marker is provided
"""
req = webob.Request.blank('/images/detail?marker=10')
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_get_details_filter_name(self):
"""
Tests that the /images/detail registry API returns list of
public images that have a specific name
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -389,9 +798,9 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEqual('new name! #123', image['name'])
def test_get_details_filter_status(self):
"""Tests that the /images/detail registry API returns list of
"""
Tests that the /images/detail registry API returns list of
public images that have a specific status
"""
extra_fixture = {'id': 3,
'status': 'saving',
@ -427,9 +836,9 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEqual('saving', image['status'])
def test_get_details_filter_container_format(self):
"""Tests that the /images/detail registry API returns list of
"""
Tests that the /images/detail registry API returns list of
public images that have a specific container_format
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -465,9 +874,9 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEqual('ovf', image['container_format'])
def test_get_details_filter_disk_format(self):
"""Tests that the /images/detail registry API returns list of
"""
Tests that the /images/detail registry API returns list of
public images that have a specific disk_format
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -503,9 +912,9 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEqual('vhd', image['disk_format'])
def test_get_details_filter_size_min(self):
"""Tests that the /images/detail registry API returns list of
"""
Tests that the /images/detail registry API returns list of
public images that have a size greater than or equal to size_min
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -541,9 +950,9 @@ class TestRegistryAPI(unittest.TestCase):
self.assertTrue(image['size'] >= 19)
def test_get_details_filter_size_max(self):
"""Tests that the /images/detail registry API returns list of
"""
Tests that the /images/detail registry API returns list of
public images that have a size less than or equal to size_max
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -579,10 +988,10 @@ class TestRegistryAPI(unittest.TestCase):
self.assertTrue(image['size'] <= 19)
def test_get_details_filter_size_min_max(self):
"""Tests that the /images/detail registry API returns list of
"""
Tests that the /images/detail registry API returns list of
public images that have a size less than or equal to size_max
and greater than or equal to size_min
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -629,9 +1038,9 @@ class TestRegistryAPI(unittest.TestCase):
self.assertTrue(image['size'] <= 19 and image['size'] >= 18)
def test_get_details_filter_property(self):
"""Tests that the /images/detail registry API returns list of
"""
Tests that the /images/detail registry API returns list of
public images that have a specific custom property
"""
extra_fixture = {'id': 3,
'status': 'active',
@ -668,6 +1077,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',
@ -733,8 +1181,10 @@ class TestRegistryAPI(unittest.TestCase):
self.assertTrue('Invalid disk format' in res.body)
def test_create_image_with_mismatched_formats(self):
"""Tests that exception raised for bad matching disk and container
formats"""
"""
Tests that exception raised for bad matching disk and
container formats
"""
fixture = {'name': 'fake public image #3',
'container_format': 'aki',
'disk_format': 'ari'}
@ -790,8 +1240,10 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEquals(v, res_dict['image'][k])
def test_update_image_not_existing(self):
"""Tests proper exception is raised if attempt to update non-existing
image"""
"""
Tests proper exception is raised if attempt to update
non-existing image
"""
fixture = {'status': 'killed'}
req = webob.Request.blank('/images/3')
@ -847,8 +1299,10 @@ class TestRegistryAPI(unittest.TestCase):
self.assertTrue('Invalid container format' in res.body)
def test_update_image_with_mismatched_formats(self):
"""Tests that exception raised for bad matching disk and container
formats"""
"""
Tests that exception raised for bad matching disk and
container formats
"""
fixture = {'container_format': 'ari'}
req = webob.Request.blank('/images/2') # Image 2 has disk format 'vhd'
@ -892,9 +1346,10 @@ class TestRegistryAPI(unittest.TestCase):
self.assertEquals(new_num_images, orig_num_images - 1)
def test_delete_image_not_existing(self):
"""Tests proper exception is raised if attempt to delete non-existing
image"""
"""
Tests proper exception is raised if attempt to delete
non-existing image
"""
req = webob.Request.blank('/images/3')
req.method = 'DELETE'
@ -1042,6 +1497,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',
@ -1206,3 +1700,12 @@ class TestGlanceAPI(unittest.TestCase):
req.method = 'DELETE'
res = req.get_response(self.api)
self.assertEquals(res.status_int, 200)
def test_get_details_invalid_marker(self):
"""
Tests that the /images/detail registry API returns a 400
when an invalid marker is provided
"""
req = webob.Request.blank('/images/detail?marker=10')
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)

View File

@ -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,
@ -100,6 +393,12 @@ class TestRegistryClient(unittest.TestCase):
for image in images:
self.assertTrue(image['id'] < 4)
def test_get_image_index_invalid_marker(self):
"""Test exception is raised when marker is invalid"""
self.assertRaises(exception.Invalid,
self.client.get_images,
marker=4)
def test_get_image_index_limit(self):
"""Test correct number of images returned with limit param."""
extra_fixture = {'id': 3,
@ -156,9 +455,38 @@ class TestRegistryClient(unittest.TestCase):
self.assertEquals(images[0]['id'], 2)
def test_get_image_index_limit_None(self):
"""Test correct set of images returned with limit param == None."""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
extra_fixture = {'id': 4,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #125',
'size': 19,
'checksum': None}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images(limit=None)
self.assertEquals(len(images), 3)
def test_get_image_index_by_name(self):
"""Test correct set of public, name-filtered image returned. This
is just a sanity check, we test the details call more in-depth."""
"""
Test correct set of public, name-filtered image returned. This
is just a sanity check, we test the details call more in-depth.
"""
extra_fixture = {'id': 3,
'status': 'active',
'is_public': True,
@ -170,7 +498,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:
@ -223,6 +551,12 @@ class TestRegistryClient(unittest.TestCase):
self.assertEquals(images[0]['id'], 2)
def test_get_image_details_invalid_marker(self):
"""Test exception is raised when marker is invalid"""
self.assertRaises(exception.Invalid,
self.client.get_images_detailed,
marker=4)
def test_get_image_details_by_name(self):
"""Tests that a detailed call can be filtered by name"""
extra_fixture = {'id': 3,
@ -236,7 +570,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 +590,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 +609,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 +629,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 +649,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 +668,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 +688,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,
@ -379,7 +753,6 @@ class TestRegistryClient(unittest.TestCase):
def test_get_image_non_existing(self):
"""Tests that NotFound is raised when getting a non-existing image"""
self.assertRaises(exception.NotFound,
self.client.get_image,
42)
@ -493,7 +866,6 @@ class TestRegistryClient(unittest.TestCase):
def test_delete_image(self):
"""Tests that image metadata is deleted properly"""
# Grab the original number of images
orig_num_images = len(self.client.get_images())
@ -507,7 +879,6 @@ class TestRegistryClient(unittest.TestCase):
def test_delete_image_not_existing(self):
"""Tests cannot delete non-existing image"""
self.assertRaises(exception.NotFound,
self.client.delete_image,
3)
@ -557,11 +928,46 @@ class TestClient(unittest.TestCase):
def test_get_image_not_existing(self):
"""Test retrieval of a non-existing image returns a 404"""
self.assertRaises(exception.NotFound,
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,
@ -602,6 +1008,12 @@ class TestClient(unittest.TestCase):
for image in images:
self.assertTrue(image['id'] < 4)
def test_get_image_index_invalid_marker(self):
"""Test exception is raised when marker is invalid"""
self.assertRaises(exception.Invalid,
self.client.get_images,
marker=4)
def test_get_image_index_limit(self):
"""Test correct number of public images returned with limit param."""
extra_fixture = {'id': 3,
@ -671,7 +1083,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 +1103,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'])
@ -752,6 +1166,12 @@ class TestClient(unittest.TestCase):
self.assertEquals(images[0]['id'], 2)
def test_get_image_details_invalid_marker(self):
"""Test exception is raised when marker is invalid"""
self.assertRaises(exception.Invalid,
self.client.get_images_detailed,
marker=4)
def test_get_image_details_by_base_attribute(self):
"""Tests that a detailed call can be filtered by a base attribute"""
extra_fixture = {'id': 3,
@ -765,7 +1185,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,12 +1206,30 @@ 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:
self.assertEquals('v a', image['properties']['p a'])
def test_get_image_bad_filters_with_other_params(self):
"""Tests that a detailed call can be filtered by a property"""
extra_fixture = {'id': 3,
'status': 'saving',
'is_public': True,
'disk_format': 'vhd',
'container_format': 'ovf',
'name': 'new name! #123',
'size': 19,
'checksum': None,
'properties': {'p a': 'v a'}}
glance.registry.db.api.image_create(None, extra_fixture)
images = self.client.get_images_detailed(filters=None, limit=1)
self.assertEquals(len(images), 1)
def test_get_image_meta(self):
"""Tests that the detailed info about an image returned"""
fixture = {'id': 2,
@ -834,7 +1273,6 @@ class TestClient(unittest.TestCase):
def test_get_image_non_existing(self):
"""Tests that NotFound is raised when getting a non-existing image"""
self.assertRaises(exception.NotFound,
self.client.get_image,
42)
@ -928,9 +1366,11 @@ class TestClient(unittest.TestCase):
self.assertEquals('active', data['status'])
def test_add_image_with_bad_iso_properties(self):
"""Verify that ISO with invalid container format is rejected.
"""
Verify that ISO with invalid container format is rejected.
Intended to exercise error path once rather than be exhaustive
set of mismatches"""
set of mismatches
"""
fixture = {'name': 'fake public iso',
'is_public': True,
'disk_format': 'iso',
@ -1113,7 +1553,6 @@ class TestClient(unittest.TestCase):
def test_delete_image(self):
"""Tests that image metadata is deleted properly"""
# Grab the original number of images
orig_num_images = len(self.client.get_images())
@ -1127,7 +1566,6 @@ class TestClient(unittest.TestCase):
def test_delete_image_not_existing(self):
"""Tests cannot delete non-existing image"""
self.assertRaises(exception.NotFound,
self.client.delete_image,
3)

View File

@ -77,7 +77,8 @@ def check_dependencies():
def create_virtualenv(venv=VENV):
"""Creates the virtual environment and installs PIP only into the
"""
Creates the virtual environment and installs PIP only into the
virtual environment
"""
print 'Creating venv...',