merge trunk

This commit is contained in:
Jason Koelker 2011-07-15 16:32:53 -05:00
commit 6ac1d5a332
37 changed files with 2759 additions and 614 deletions

View File

@ -6,10 +6,12 @@ Dan Prince <dan.prince@rackspace.com>
Donal Lafferty <donal.lafferty@citrix.com>
Eldar Nugaev <enugaev@griddynamics.com>
Ewan Mellor <ewan.mellor@citrix.com>
Isaku Yamahata <yamahata@valinux.co.jp>
Jason Koelker <jason@koelker.net>
Jay Pipes <jaypipes@gmail.com>
Jinwoo 'Joseph' Suh <jsuh@isi.edu>
Josh Kearney <josh@jk0.org>
Justin Shepherd <jshepher@rackspace.com>
Ken Pepple <ken.pepple@gmail.com>
Matt Dietz <matt.dietz@rackspace.com>
Monty Taylor <mordred@inaugust.com>
@ -19,3 +21,4 @@ Soren Hansen <soren.hansen@rackspace.com>
Taku Fukushima <tfukushima@dcl.info.waseda.ac.jp>
Thierry Carrez <thierry@openstack.org>
Vishvananda Ishaya <vishvananda@gmail.com>
Yuriy Taraday <yorik.sar@gmail.com>

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

@ -32,11 +32,11 @@ class API(wsgi.Router):
def __init__(self, options):
self.options = options
mapper = routes.Mapper()
controller = images.Controller(options)
mapper.resource("image", "images", controller=controller,
resource = images.create_resource(options)
mapper.resource("image", "images", controller=resource,
collection={'detail': 'GET'})
mapper.connect("/", controller=controller, action="index")
mapper.connect("/images/{id}", controller=controller, action="meta",
mapper.connect("/", controller=resource, action="index")
mapper.connect("/images/{id}", controller=resource, action="meta",
conditions=dict(method=["HEAD"]))
super(API, self).__init__(mapper)

View File

@ -24,7 +24,7 @@ import json
import logging
import sys
from webob import Response
import webob
from webob.exc import (HTTPNotFound,
HTTPConflict,
HTTPBadRequest)
@ -45,8 +45,10 @@ 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(wsgi.Controller):
class Controller(object):
"""
WSGI controller for images resource in Glance v1 API
@ -80,7 +82,7 @@ class Controller(wsgi.Controller):
* checksum -- MD5 checksum of the image data
* size -- Size of image data in bytes
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:retval The response body is a mapping of the following form::
{'images': [
@ -92,22 +94,19 @@ class Controller(wsgi.Controller):
'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):
"""
Returns detailed information for all public, available images
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:retval The response body is a mapping of the following form::
{'images': [
@ -125,17 +124,26 @@ class Controller(wsgi.Controller):
'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
@ -155,29 +163,22 @@ class Controller(wsgi.Controller):
Returns metadata about an image in the HTTP headers of the
response object
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param id: The opaque image identifier
:retval similar to 'show' method but without image_data
:raises HTTPNotFound if image metadata is not available to user
"""
image = self.get_image_meta_or_404(req, id)
res = Response(request=req)
utils.inject_image_meta_into_headers(res, image)
res.headers.add('Location', "/v1/images/%s" % id)
res.headers.add('ETag', image['checksum'])
return req.get_response(res)
return {
'image_meta': self.get_image_meta_or_404(req, id),
}
def show(self, req, id):
"""
Returns an iterator as a Response object that
can be used to retrieve an image's data. The
content-type of the response is the content-type
of the image, or application/octet-stream if none
is known or found.
Returns an iterator that can be used to retrieve an image's
data along with the image metadata.
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HTTPNotFound if image is not available to user
@ -192,31 +193,26 @@ class Controller(wsgi.Controller):
for chunk in chunks:
yield chunk
res = Response(app_iter=image_iterator(),
content_type="application/octet-stream")
# Using app_iter blanks content-length, so we set it here...
res.headers.add('Content-Length', image['size'])
utils.inject_image_meta_into_headers(res, image)
res.headers.add('Location', "/v1/images/%s" % id)
res.headers.add('ETag', image['checksum'])
return req.get_response(res)
return {
'image_iterator': image_iterator(),
'image_meta': image,
}
def _reserve(self, req):
def _reserve(self, req, image_meta):
"""
Adds the image metadata to the registry and assigns
an image identifier if one is not supplied in the request
headers. Sets the image's status to `queued`
headers. Sets the image's status to `queued`.
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HTTPConflict if image already exists
:raises HTTPBadRequest if image metadata is not valid
"""
image_meta = utils.get_image_meta_from_headers(req)
if 'location' in image_meta:
store = get_store_from_location(image_meta['location'])
location = image_meta.get('location')
if location:
store = get_store_from_location(location)
# check the store exists before we hit the registry, but we
# don't actually care what it is at this point
self.get_store_or_400(req, store)
@ -250,28 +246,27 @@ class Controller(wsgi.Controller):
will attempt to use that store, if not, Glance will use the
store set by the flag `default_store`.
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:raises HTTPConflict if image already exists
:retval The location where the image was stored
"""
image_id = image_meta['id']
content_type = req.headers.get('content-type', 'notset')
if content_type != 'application/octet-stream':
self._safe_kill(req, image_id)
msg = ("Content-Type must be application/octet-stream")
try:
req.get_content_type('application/octet-stream')
except exception.InvalidContentType:
self._safe_kill(req, image_meta['id'])
msg = "Content-Type must be application/octet-stream"
logger.error(msg)
raise HTTPBadRequest(msg, content_type="text/plain",
request=req)
raise HTTPBadRequest(explanation=msg)
store_name = req.headers.get(
'x-image-meta-store', self.options['default_store'])
store_name = req.headers.get('x-image-meta-store',
self.options['default_store'])
store = self.get_store_or_400(req, store_name)
logger.debug("Setting image %s to status 'saving'"
% image_id)
image_id = image_meta['id']
logger.debug("Setting image %s to status 'saving'" % image_id)
registry.update_image_metadata(self.options, image_id,
{'status': 'saving'})
try:
@ -304,11 +299,13 @@ class Controller(wsgi.Controller):
'size': size})
return location
except exception.Duplicate, e:
msg = ("Attempt to upload duplicate image: %s") % str(e)
logger.error(msg)
self._safe_kill(req, image_id)
raise HTTPConflict(msg, request=req)
except Exception, e:
msg = ("Error uploading image: %s") % str(e)
logger.error(msg)
@ -320,8 +317,8 @@ class Controller(wsgi.Controller):
Sets the image status to `active` and the image's location
attribute.
:param request: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:param req: The WSGI/Webob Request object
:param image_id: Opaque image identifier
:param location: Location of where Glance stored this image
"""
image_meta = {}
@ -333,9 +330,9 @@ class Controller(wsgi.Controller):
def _kill(self, req, image_id):
"""
Marks the image status to `killed`
Marks the image status to `killed`.
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param image_id: Opaque image identifier
"""
registry.update_image_metadata(self.options,
@ -349,7 +346,7 @@ class Controller(wsgi.Controller):
Since _kill is meant to be called from exceptions handlers, it should
not raise itself, rather it should just log its error.
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param image_id: Opaque image identifier
"""
try:
@ -364,7 +361,7 @@ class Controller(wsgi.Controller):
and activates the image in the registry after a successful
upload.
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:retval Mapping of updated image data
@ -373,7 +370,7 @@ class Controller(wsgi.Controller):
location = self._upload(req, image_meta)
return self._activate(req, image_id, location)
def create(self, req):
def create(self, req, image_meta, image_data):
"""
Adds a new image to Glance. Three scenarios exist when creating an
image:
@ -399,32 +396,27 @@ class Controller(wsgi.Controller):
containing metadata about the image is returned, including its
opaque identifier.
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param image_meta: Mapping of metadata about image
:param image_data: Actual image data that is to be stored
:raises HTTPBadRequest if no x-image-meta-location is missing
:raises HTTPBadRequest if x-image-meta-location is missing
and the request body is not application/octet-stream
image data.
"""
image_meta = self._reserve(req)
image_meta = self._reserve(req, image_meta)
image_id = image_meta['id']
if utils.has_body(req):
if image_data is not None:
image_meta = self._upload_and_activate(req, image_meta)
else:
if 'x-image-meta-location' in req.headers:
location = req.headers['x-image-meta-location']
location = image_meta.get('location')
if location:
image_meta = self._activate(req, image_id, location)
# APP states we should return a Location: header with the edit
# URI of the resource newly-created.
res = Response(request=req, body=json.dumps(dict(image=image_meta)),
status=httplib.CREATED, content_type="text/plain")
res.headers.add('Location', "/v1/images/%s" % image_id)
res.headers.add('ETag', image_meta['checksum'])
return {'image_meta': image_meta}
return req.get_response(res)
def update(self, req, id):
def update(self, req, id, image_meta, image_data):
"""
Updates an existing image with the registry.
@ -433,29 +425,17 @@ class Controller(wsgi.Controller):
:retval Returns the updated image information as a mapping
"""
has_body = utils.has_body(req)
orig_image_meta = self.get_image_meta_or_404(req, id)
orig_status = orig_image_meta['status']
if has_body and orig_status != 'queued':
if image_data is not None and orig_status != 'queued':
raise HTTPConflict("Cannot upload to an unqueued image")
new_image_meta = utils.get_image_meta_from_headers(req)
try:
image_meta = registry.update_image_metadata(self.options,
id,
new_image_meta,
True)
if has_body:
image_meta = registry.update_image_metadata(self.options, id,
image_meta, True)
if image_data is not None:
image_meta = self._upload_and_activate(req, image_meta)
res = Response(request=req,
body=json.dumps(dict(image=image_meta)),
content_type="text/plain")
res.headers.add('Location', "/images/%s" % id)
res.headers.add('ETag', image_meta['checksum'])
return res
except exception.Invalid, e:
msg = ("Failed to update image metadata. Got error: %(e)s"
% locals())
@ -463,11 +443,13 @@ class Controller(wsgi.Controller):
logger.error(line)
raise HTTPBadRequest(msg, request=req, content_type="text/plain")
return {'image_meta': image_meta}
def delete(self, req, id):
"""
Deletes the image and all its chunks from the Glance
:param request: The WSGI/Webob Request object
:param req: The WSGI/Webob Request object
:param id: The opaque image identifier
:raises HttpBadRequest if image registry is invalid
@ -521,3 +503,97 @@ class Controller(wsgi.Controller):
logger.error(msg)
raise HTTPBadRequest(msg, request=request,
content_type='text/plain')
class ImageDeserializer(wsgi.JSONRequestDeserializer):
"""Handles deserialization of specific controller method requests."""
def _deserialize(self, request):
result = {}
result['image_meta'] = utils.get_image_meta_from_headers(request)
data = request.body_file if self.has_body(request) else None
result['image_data'] = data
return result
def create(self, request):
return self._deserialize(request)
def update(self, request):
return self._deserialize(request)
class ImageSerializer(wsgi.JSONResponseSerializer):
"""Handles serialization of specific controller method responses."""
def _inject_location_header(self, response, image_meta):
location = self._get_image_location(image_meta)
response.headers['Location'] = location
def _inject_checksum_header(self, response, image_meta):
response.headers['ETag'] = image_meta['checksum']
def _inject_image_meta_headers(self, response, image_meta):
"""
Given a response and mapping of image metadata, injects
the Response with a set of HTTP headers for the image
metadata. Each main image metadata field is injected
as a HTTP header with key 'x-image-meta-<FIELD>' except
for the properties field, which is further broken out
into a set of 'x-image-meta-property-<KEY>' headers
:param response: The Webob Response object
:param image_meta: Mapping of image metadata
"""
headers = utils.image_meta_to_http_headers(image_meta)
for k, v in headers.items():
response.headers[k] = v
def _get_image_location(self, image_meta):
"""Build a relative url to reach the image defined by image_meta."""
return "/v1/images/%s" % image_meta['id']
def meta(self, response, result):
image_meta = result['image_meta']
self._inject_image_meta_headers(response, image_meta)
self._inject_location_header(response, image_meta)
self._inject_checksum_header(response, image_meta)
return response
def show(self, response, result):
image_meta = result['image_meta']
response.app_iter = result['image_iterator']
# Using app_iter blanks content-length, so we set it here...
response.headers['Content-Length'] = image_meta['size']
response.headers['Content-Type'] = 'application/octet-stream'
self._inject_image_meta_headers(response, image_meta)
self._inject_location_header(response, image_meta)
self._inject_checksum_header(response, image_meta)
return response
def update(self, response, result):
image_meta = result['image_meta']
response.body = self.to_json(dict(image=image_meta))
response.headers['Content-Type'] = 'application/json'
self._inject_location_header(response, image_meta)
self._inject_checksum_header(response, image_meta)
return response
def create(self, response, result):
image_meta = result['image_meta']
response.status = httplib.CREATED
response.headers['Content-Type'] = 'application/json'
response.body = self.to_json(dict(image=image_meta))
self._inject_location_header(response, image_meta)
self._inject_checksum_header(response, image_meta)
return response
def create_resource(options):
"""Images resource factory method"""
deserializer = ImageDeserializer()
serializer = ImageSerializer()
return wsgi.Resource(Controller(options), deserializer, serializer)

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:
@ -96,3 +101,29 @@ def wrap_exception(f):
raise
_wrap.func_name = f.func_name
return _wrap
class GlanceException(Exception):
"""
Base Glance Exception
To correctly use this class, inherit from it and define
a 'message' property. That message will get printf'd
with the keyword arguments provided to the constructor.
"""
message = "An unknown exception occurred"
def __init__(self, **kwargs):
try:
self._error_string = self.message % kwargs
except Exception:
# at least get the core message out if something happened
self._error_string = self.message
def __str__(self):
return self._error_string
class InvalidContentType(GlanceException):
message = "Invalid content type %(content_type)s"

View File

@ -34,6 +34,8 @@ import routes.middleware
import webob.dec
import webob.exc
from glance.common import exception
class WritableLogger(object):
"""A thin wrapper that responds to `write` and logs."""
@ -205,73 +207,55 @@ class Router(object):
return app
class Controller(object):
"""
WSGI app that reads routing information supplied by RoutesMiddleware
and calls the requested action method upon itself. All action methods
must, in addition to their normal parameters, accept a 'req' argument
which is the incoming webob.Request. They raise a webob.exc exception,
or return a dict which will be serialized by requested content type.
"""
class Request(webob.Request):
"""Add some Openstack API-specific logic to the base webob.Request."""
@webob.dec.wsgify
def __call__(self, req):
"""
Call the method specified in req.environ by RoutesMiddleware.
"""
arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict['action']
method = getattr(self, action)
del arg_dict['controller']
del arg_dict['action']
arg_dict['req'] = req
result = method(**arg_dict)
if type(result) is dict:
return self._serialize(result, req)
def best_match_content_type(self):
"""Determine the requested response content-type."""
supported = ('application/json',)
bm = self.accept.best_match(supported)
return bm or 'application/json'
def get_content_type(self, allowed_content_types):
"""Determine content type of the request body."""
if not "Content-Type" in self.headers:
raise exception.InvalidContentType(content_type=None)
content_type = self.content_type
if content_type not in allowed_content_types:
raise exception.InvalidContentType(content_type=content_type)
else:
return result
def _serialize(self, data, request):
"""
Serialize the given dict to the response type requested in request.
Uses self._serialization_metadata if it exists, which is a dict mapping
MIME types to information needed to serialize to that type.
"""
_metadata = getattr(type(self), "_serialization_metadata", {})
serializer = Serializer(request.environ, _metadata)
return serializer.to_content_type(data)
return content_type
class Serializer(object):
"""
Serializes a dictionary to a Content Type specified by a WSGI environment.
"""
class JSONRequestDeserializer(object):
def has_body(self, request):
"""
Returns whether a Webob.Request object will possess an entity body.
def __init__(self, environ, metadata=None):
:param request: Webob.Request object
"""
Create a serializer based on the given WSGI environment.
'metadata' is an optional dict mapping MIME types to information
needed to serialize a dictionary to that type.
"""
self.environ = environ
self.metadata = metadata or {}
self._methods = {
'application/json': self._to_json,
'application/xml': self._to_xml}
if 'transfer-encoding' in request.headers:
return True
elif request.content_length > 0:
return True
def to_content_type(self, data):
"""
Serialize a dictionary into a string. The format of the string
will be decided based on the Content Type requested in self.environ:
by Accept: header, or by URL suffix.
"""
# FIXME(sirp): for now, supporting json only
#mimetype = 'application/xml'
mimetype = 'application/json'
# TODO(gundlach): determine mimetype from request
return self._methods.get(mimetype, repr)(data)
return False
def _to_json(self, data):
def from_json(self, datastring):
return json.loads(datastring)
def default(self, request):
if self.has_body(request):
return {'body': self.from_json(request.body)}
else:
return {}
class JSONResponseSerializer(object):
def to_json(self, data):
def sanitizer(obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
@ -279,37 +263,85 @@ class Serializer(object):
return json.dumps(data, default=sanitizer)
def _to_xml(self, data):
metadata = self.metadata.get('application/xml', {})
# We expect data to contain a single key which is the XML root.
root_key = data.keys()[0]
from xml.dom import minidom
doc = minidom.Document()
node = self._to_xml_node(doc, metadata, root_key, data[root_key])
return node.toprettyxml(indent=' ')
def default(self, response, result):
response.headers.add('Content-Type', 'application/json')
response.body = self.to_json(result)
def _to_xml_node(self, doc, metadata, nodename, data):
"""Recursive method to convert data members to XML nodes."""
result = doc.createElement(nodename)
if type(data) is list:
singular = metadata.get('plurals', {}).get(nodename, None)
if singular is None:
if nodename.endswith('s'):
singular = nodename[:-1]
else:
singular = 'item'
for item in data:
node = self._to_xml_node(doc, metadata, singular, item)
result.appendChild(node)
elif type(data) is dict:
attrs = metadata.get('attributes', {}).get(nodename, {})
for k, v in data.items():
if k in attrs:
result.setAttribute(k, str(v))
else:
node = self._to_xml_node(doc, metadata, k, v)
result.appendChild(node)
else: # atom
node = doc.createTextNode(str(data))
result.appendChild(node)
return result
class Resource(object):
"""
WSGI app that handles (de)serialization and controller dispatch.
Reads routing information supplied by RoutesMiddleware and calls
the requested action method upon its deserializer, controller,
and serializer. Those three objects may implement any of the basic
controller action methods (create, update, show, index, delete)
along with any that may be specified in the api router. A 'default'
method may also be implemented to be used in place of any
non-implemented actions. Deserializer methods must accept a request
argument and return a dictionary. Controller methods must accept a
request argument. Additionally, they must also accept keyword
arguments that represent the keys returned by the Deserializer. They
may raise a webob.exc exception or return a dict, which will be
serialized by requested content type.
"""
def __init__(self, controller, deserializer, serializer):
"""
:param controller: object that implement methods created by routes lib
:param deserializer: object that supports webob request deserialization
through controller-like actions
:param serializer: object that supports webob response serialization
through controller-like actions
"""
self.controller = controller
self.serializer = serializer
self.deserializer = deserializer
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
"""WSGI method that controls (de)serialization and method dispatch."""
action_args = self.get_action_args(request.environ)
action = action_args.pop('action', None)
deserialized_request = self.dispatch(self.deserializer,
action, request)
action_args.update(deserialized_request)
action_result = self.dispatch(self.controller, action,
request, **action_args)
try:
response = webob.Response()
self.dispatch(self.serializer, action, response, action_result)
return response
# return unserializable result (typically a webob exc)
except Exception:
return action_result
def dispatch(self, obj, action, *args, **kwargs):
"""Find action-specific method on self and call it."""
try:
method = getattr(obj, action)
except AttributeError:
method = getattr(obj, 'default')
return method(*args, **kwargs)
def get_action_args(self, request_environment):
"""Parse dictionary created by routes library."""
try:
args = request_environment['wsgiorg.routing_args'][1].copy()
except Exception:
return {}
try:
del args['controller']
except KeyError:
pass
try:
del args['format']
except KeyError:
pass
return args

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
@ -94,10 +84,16 @@ class RegistryClient(BaseClient):
"""
Tells registry about an image's metadata
"""
headers = {
'Content-Type': 'application/json',
}
if 'image' not in image_metadata.keys():
image_metadata = dict(image=image_metadata)
body = json.dumps(image_metadata)
res = self.do_request("POST", "/images", body)
res = self.do_request("POST", "/images", body, headers=headers)
# Registry returns a JSONified dict(image=image_info)
data = json.loads(res.read())
return data['image']
@ -111,9 +107,13 @@ class RegistryClient(BaseClient):
body = json.dumps(image_metadata)
headers = {}
headers = {
'Content-Type': 'application/json',
}
if purge_props:
headers["X-Glance-Registry-Purge-Props"] = "true"
res = self.do_request("PUT", "/images/%s" % image_id, body, headers)
data = json.loads(res.read())
image = data['image']

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
@ -33,7 +33,6 @@ from sqlalchemy.sql import or_, and_
from glance.common import config
from glance.common import exception
from glance.common import utils
from glance.registry.db import migration
from glance.registry.db import models
_ENGINE = None
@ -78,7 +77,7 @@ def configure_db(options):
elif verbose:
logger.setLevel(logging.INFO)
migration.db_sync(options)
models.register_models(_ENGINE)
def get_session(autocommit=True, expire_on_commit=False):
@ -98,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)
@ -134,7 +133,6 @@ def image_get_all_pending_delete(context, delete_time=None, limit=None):
"""Get all images that are pending deletion
:param limit: maximum number of images to return
"""
session = get_session()
query = session.query(models.Image).\
@ -154,15 +152,18 @@ def image_get_all_pending_delete(context, delete_time=None, limit=None):
return query.all()
def image_get_all_public(context, filters=None, marker=None, limit=None):
def image_get_all_public(context, filters=None, marker=None, limit=None,
sort_key='created_at', sort_dir='desc'):
"""Get all public images that match zero or more filters.
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 {}
@ -171,9 +172,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'])
@ -204,7 +213,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__:
@ -219,7 +229,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')
@ -252,13 +261,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():
@ -340,7 +349,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
@ -350,7 +360,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)
@ -359,8 +370,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

@ -118,3 +118,12 @@ class ImageProperty(BASE, ModelBase):
name = Column(String(255), index=True, nullable=False)
value = Column(Text)
def register_models(engine):
"""
Creates database tables for all models with the given engine
"""
models = (Image, ImageProperty)
for model in models:
model.metadata.create_all(engine)

View File

@ -39,10 +39,17 @@ 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(wsgi.Controller):
class Controller(object):
"""Controller for the reference implementation registry server"""
def __init__(self, options):
@ -50,7 +57,8 @@ class Controller(wsgi.Controller):
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(wsgi.Controller):
'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(wsgi.Controller):
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(wsgi.Controller):
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(wsgi.Controller):
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:
@ -167,11 +212,10 @@ class Controller(wsgi.Controller):
"""
Deletes an existing image with the registry.
:param req: Request body. Ignored.
:param req: wsgi Request object
:param id: The opaque internal identifier for the image
:retval Returns 200 if delete was successful, a fault if not.
"""
context = None
try:
@ -179,19 +223,18 @@ class Controller(wsgi.Controller):
except exception.NotFound:
return exc.HTTPNotFound()
def create(self, req):
def create(self, req, body):
"""
Registers a new image with the registry.
:param req: Request body. A JSON-ified dict of information about
the image.
:param req: wsgi Request object
:param body: Dictionary of information about the image
: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 = json.loads(req.body)['image']
image_data = body['image']
# Ensure the image has a status set
image_data.setdefault('status', 'active')
@ -209,18 +252,17 @@ class Controller(wsgi.Controller):
logger.error(msg)
return exc.HTTPBadRequest(msg)
def update(self, req, id):
"""Updates an existing image with the registry.
def update(self, req, id, body):
"""
Updates an existing image with the registry.
:param req: Request body. A JSON-ified dict of information about
the image. This will replace the information in the
registry about this image
: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 = json.loads(req.body)['image']
image_data = body['image']
purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false")
context = None
@ -244,15 +286,22 @@ class Controller(wsgi.Controller):
content_type='text/plain')
def create_resource(options):
"""Images resource factory method."""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
return wsgi.Resource(Controller(options), deserializer, serializer)
class API(wsgi.Router):
"""WSGI entry point for all Registry requests."""
def __init__(self, options):
mapper = routes.Mapper()
controller = Controller(options)
mapper.resource("image", "images", controller=controller,
resource = create_resource(options)
mapper.resource("image", "images", controller=resource,
collection={'detail': 'GET'})
mapper.connect("/", controller=controller, action="index")
mapper.connect("/", controller=resource, action="index")
super(API, self).__init__(mapper)

View File

@ -121,7 +121,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

@ -43,24 +43,6 @@ def image_meta_to_http_headers(image_meta):
return headers
def inject_image_meta_into_headers(response, image_meta):
"""
Given a response and mapping of image metadata, injects
the Response with a set of HTTP headers for the image
metadata. Each main image metadata field is injected
as a HTTP header with key 'x-image-meta-<FIELD>' except
for the properties field, which is further broken out
into a set of 'x-image-meta-property-<KEY>' headers
:param response: The Webob Response object
:param image_meta: Mapping of image metadata
"""
headers = image_meta_to_http_headers(image_meta)
for k, v in headers.items():
response.headers.add(k, v)
def get_image_meta_from_headers(response):
"""
Processes HTTP headers from a supplied response that

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
@ -51,6 +52,7 @@ To run a single test module::
"""
import gettext
import logging
import os
import unittest
import sys
@ -178,7 +180,8 @@ class GlanceTestResult(result.TextTestResult):
self._last_case = None
self.colorizer = None
# NOTE(vish, tfukushima): reset stdout for the terminal check
stdout = sys.__stdout__
stdout = sys.stdout
sys.stdout = sys.__stdout__
for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
if colorizer.supported():
self.colorizer = colorizer(self.stream)
@ -210,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.
"""
@ -269,9 +273,17 @@ class GlanceTestRunner(core.TextTestRunner):
if __name__ == '__main__':
logger = logging.getLogger()
hdlr = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(logging.DEBUG)
c = config.Config(stream=sys.stdout,
env=os.environ,
verbosity=3)
verbosity=3,
plugins=core.DefaultPluginManager())
runner = GlanceTestRunner(stream=c.stream,
verbosity=c.verbosity,

View File

@ -177,11 +177,7 @@ class RegistryServer(Server):
super(RegistryServer, self).__init__(test_dir, port)
self.server_name = 'registry'
# NOTE(sirp): in-memory DBs don't play well with sqlalchemy migrate
# (see http://code.google.com/p/sqlalchemy-migrate/
# issues/detail?id=72)
self.db_file = os.path.join(self.test_dir, 'test_glance_api.sqlite')
default_sql_connection = 'sqlite:///%s' % self.db_file
default_sql_connection = 'sqlite:///'
self.sql_connection = os.environ.get('GLANCE_TEST_SQL_CONNECTION',
default_sql_connection)

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()
@ -773,7 +768,7 @@ class TestCurlApi(functional.FunctionalTest):
with tempfile.NamedTemporaryFile() as test_data_file:
test_data_file.write("XXX")
test_data_file.flush()
cmd = ("curl -i -X POST --upload-file %s "
cmd = ("curl -i -X POST --upload-file %s -H 'Expect: ' "
"http://0.0.0.0:%d/v1/images") % (test_data_file.name,
api_port)
@ -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

@ -0,0 +1,592 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack, LLC
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Functional test case that utilizes httplib2 against the API server"""
import hashlib
import httplib2
import json
import os
from tests import functional
from tests.utils import execute
FIVE_KB = 5 * 1024
FIVE_GB = 5 * 1024 * 1024 * 1024
class TestApiHttplib2(functional.FunctionalTest):
"""Functional tests using httplib2 against the API server"""
def test_get_head_simple_post(self):
"""
We test the following sequential series of actions:
0. GET /images
- Verify no public images
1. GET /images/detail
- Verify no public images
2. HEAD /images/1
- Verify 404 returned
3. POST /images with public image named Image1 with a location
attribute and no custom properties
- Verify 201 returned
4. HEAD /images/1
- Verify HTTP headers have correct information we just added
5. GET /images/1
- Verify all information on image we just added is correct
6. GET /images
- Verify the image we just added is returned
7. GET /images/detail
- Verify the image we just added is returned
8. PUT /images/1 with custom properties of "distro" and "arch"
- Verify 200 returned
9. GET /images/1
- Verify updated information about image was stored
10. PUT /images/1
- Remove a previously existing property.
11. PUT /images/1
- Add a previously deleted property.
"""
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. GET /images/detail
# Verify no public images
path = "http://%s:%d/v1/images/detail" % ("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": []}')
# 2. HEAD /images/1
# Verify 404 returned
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, 404)
# 3. POST /images with public image named Image1
# attribute and no custom properties. Verify a 200 OK is returned
image_data = "*" * FIVE_KB
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,
body=image_data)
self.assertEqual(response.status, 201)
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)
# 4. HEAD /images/1
# Verify image found now
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")
# 5. GET /images/1
# Verify all information on image we just added is correct
path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
expected_image_headers = {
'x-image-meta-id': '1',
'x-image-meta-name': 'Image1',
'x-image-meta-is_public': 'True',
'x-image-meta-status': 'active',
'x-image-meta-disk_format': '',
'x-image-meta-container_format': '',
'x-image-meta-size': str(FIVE_KB),
'x-image-meta-location': 'file://%s/1' % self.api_server.image_dir}
expected_std_headers = {
'content-length': str(FIVE_KB),
'content-type': 'application/octet-stream'}
for expected_key, expected_value in expected_image_headers.items():
self.assertEqual(response[expected_key], expected_value,
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key, expected_value,
response[expected_key]))
for expected_key, expected_value in expected_std_headers.items():
self.assertEqual(response[expected_key], expected_value,
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key,
expected_value,
response[expected_key]))
self.assertEqual(content, "*" * FIVE_KB)
self.assertEqual(hashlib.md5(content).hexdigest(),
hashlib.md5("*" * FIVE_KB).hexdigest())
# 6. 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)
expected_result = {"images": [
{"container_format": None,
"disk_format": None,
"id": 1,
"name": "Image1",
"checksum": "c2e5db72bd7fd153f53ede5da5a06de3",
"size": 5120}]}
self.assertEqual(json.loads(content), expected_result)
# 7. GET /images/detail
# Verify image and all its metadata
path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
expected_image = {
"status": "active",
"name": "Image1",
"deleted": False,
"container_format": None,
"disk_format": None,
"id": 1,
"location": "file://%s/1" % self.api_server.image_dir,
"is_public": True,
"deleted_at": None,
"properties": {},
"size": 5120}
image = json.loads(content)
for expected_key, expected_value in expected_image.items():
self.assertEqual(expected_value, expected_image[expected_key],
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key,
expected_value,
image['images'][0][expected_key]))
# 8. PUT /images/1 with custom properties of "distro" and "arch"
# Verify 200 returned
headers = {'X-Image-Meta-Property-Distro': 'Ubuntu',
'X-Image-Meta-Property-Arch': 'x86_64'}
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)
self.assertEqual(response.status, 200)
data = json.loads(content)
self.assertEqual(data['image']['properties']['arch'], "x86_64")
self.assertEqual(data['image']['properties']['distro'], "Ubuntu")
# 9. GET /images/detail
# Verify image and all its metadata
path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
http = httplib2.Http()
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
expected_image = {
"status": "active",
"name": "Image1",
"deleted": False,
"container_format": None,
"disk_format": None,
"id": 1,
"location": "file://%s/1" % self.api_server.image_dir,
"is_public": True,
"deleted_at": None,
"properties": {'distro': 'Ubuntu', 'arch': 'x86_64'},
"size": 5120}
image = json.loads(content)
for expected_key, expected_value in expected_image.items():
self.assertEqual(expected_value, expected_image[expected_key],
"For key '%s' expected header value '%s'. Got '%s'"
% (expected_key,
expected_value,
image['images'][0][expected_key]))
# 10. PUT /images/1 and remove a previously existing property.
headers = {'X-Image-Meta-Property-Arch': 'x86_64'}
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)
self.assertEqual(response.status, 200)
path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
data = json.loads(content)['images'][0]
self.assertEqual(len(data['properties']), 1)
self.assertEqual(data['properties']['arch'], "x86_64")
# 11. PUT /images/1 and add a previously deleted property.
headers = {'X-Image-Meta-Property-Distro': 'Ubuntu',
'X-Image-Meta-Property-Arch': 'x86_64'}
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)
self.assertEqual(response.status, 200)
data = json.loads(content)
path = "http://%s:%d/v1/images/detail" % ("0.0.0.0", self.api_port)
response, content = http.request(path, 'GET')
self.assertEqual(response.status, 200)
data = json.loads(content)['images'][0]
self.assertEqual(len(data['properties']), 2)
self.assertEqual(data['properties']['arch'], "x86_64")
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 = [
@ -398,8 +400,8 @@ def stub_out_registry_db_image_api(stubs):
f['deleted_at'] <= delete_time]
return images
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:
@ -424,16 +426,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()
@ -447,6 +457,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',
@ -678,6 +1126,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -706,6 +1155,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -723,6 +1173,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -730,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'}
@ -739,6 +1192,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -758,6 +1212,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images')
req.method = 'POST'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -772,6 +1227,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images/2')
req.method = 'PUT'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -784,13 +1240,16 @@ 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')
req.method = 'PUT'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -804,6 +1263,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images/2')
req.method = 'PUT'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -817,6 +1277,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images/2')
req.method = 'PUT'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -830,6 +1291,7 @@ class TestRegistryAPI(unittest.TestCase):
req = webob.Request.blank('/images/2')
req.method = 'PUT'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -837,13 +1299,16 @@ 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'
req.method = 'PUT'
req.content_type = 'application/json'
req.body = json.dumps(dict(image=fixture))
res = req.get_response(self.api)
@ -881,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'
@ -972,6 +1438,21 @@ class TestGlanceAPI(unittest.TestCase):
res_body = json.loads(res.body)['image']
self.assertEquals('queued', res_body['status'])
def test_add_image_no_location_no_content_type(self):
"""Tests creates a queued image for no body and no loc header"""
fixture_headers = {'x-image-meta-store': 'file',
'x-image-meta-disk-format': 'vhd',
'x-image-meta-container-format': 'ovf',
'x-image-meta-name': 'fake image #3'}
req = webob.Request.blank("/images")
req.method = 'POST'
req.body = "chunk00000remainder"
for k, v in fixture_headers.iteritems():
req.headers[k] = v
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_add_image_bad_store(self):
"""Tests raises BadRequest for invalid store header"""
fixture_headers = {'x-image-meta-store': 'bad',
@ -1016,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',
@ -1122,6 +1642,8 @@ class TestGlanceAPI(unittest.TestCase):
def test_show_image_basic(self):
req = webob.Request.blank("/images/2")
res = req.get_response(self.api)
self.assertEqual(res.status_int, 200)
self.assertEqual(res.content_type, 'application/octet-stream')
self.assertEqual('chunk00000remainder', res.body)
def test_show_non_exists_image(self):
@ -1178,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

@ -40,7 +40,7 @@ class TestConfig(unittest.TestCase):
# of typed values
parser = optparse.OptionParser()
config.add_common_options(parser)
parsed_options, args = config.parse_options(parser)
parsed_options, args = config.parse_options(parser, [])
expected_options = {'verbose': False, 'debug': False,
'config_file': None}

184
tests/unit/test_wsgi.py Normal file
View File

@ -0,0 +1,184 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack, LLC
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import webob
from glance.common import wsgi
from glance.common import exception
class RequestTest(unittest.TestCase):
def test_content_type_missing(self):
request = wsgi.Request.blank('/tests/123')
self.assertRaises(exception.InvalidContentType,
request.get_content_type, ('application/xml'))
def test_content_type_unsupported(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Content-Type"] = "text/html"
self.assertRaises(exception.InvalidContentType,
request.get_content_type, ('application/xml'))
def test_content_type_with_charset(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Content-Type"] = "application/json; charset=UTF-8"
result = request.get_content_type(('application/json'))
self.assertEqual(result, "application/json")
def test_content_type_from_accept_xml(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Accept"] = "application/xml"
result = request.best_match_content_type()
self.assertEqual(result, "application/json")
def test_content_type_from_accept_json(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Accept"] = "application/json"
result = request.best_match_content_type()
self.assertEqual(result, "application/json")
def test_content_type_from_accept_xml_json(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Accept"] = "application/xml, application/json"
result = request.best_match_content_type()
self.assertEqual(result, "application/json")
def test_content_type_from_accept_json_xml_quality(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Accept"] = \
"application/json; q=0.3, application/xml; q=0.9"
result = request.best_match_content_type()
self.assertEqual(result, "application/json")
def test_content_type_accept_default(self):
request = wsgi.Request.blank('/tests/123.unsupported')
request.headers["Accept"] = "application/unsupported1"
result = request.best_match_content_type()
self.assertEqual(result, "application/json")
class ResourceTest(unittest.TestCase):
def test_get_action_args(self):
env = {
'wsgiorg.routing_args': [
None,
{
'controller': None,
'format': None,
'action': 'update',
'id': 12,
},
],
}
expected = {'action': 'update', 'id': 12}
actual = wsgi.Resource(None, None, None).get_action_args(env)
self.assertEqual(actual, expected)
def test_dispatch(self):
class Controller(object):
def index(self, shirt, pants=None):
return (shirt, pants)
resource = wsgi.Resource(None, None, None)
actual = resource.dispatch(Controller(), 'index', 'on', pants='off')
expected = ('on', 'off')
self.assertEqual(actual, expected)
def test_dispatch_default(self):
class Controller(object):
def default(self, shirt, pants=None):
return (shirt, pants)
resource = wsgi.Resource(None, None, None)
actual = resource.dispatch(Controller(), 'index', 'on', pants='off')
expected = ('on', 'off')
self.assertEqual(actual, expected)
def test_dispatch_no_default(self):
class Controller(object):
def show(self, shirt, pants=None):
return (shirt, pants)
resource = wsgi.Resource(None, None, None)
self.assertRaises(AttributeError, resource.dispatch, Controller(),
'index', 'on', pants='off')
class JSONResponseSerializerTest(unittest.TestCase):
def test_to_json(self):
fixture = {"key": "value"}
expected = '{"key": "value"}'
actual = wsgi.JSONResponseSerializer().to_json(fixture)
self.assertEqual(actual, expected)
def test_default(self):
fixture = {"key": "value"}
response = webob.Response()
wsgi.JSONResponseSerializer().default(response, fixture)
self.assertEqual(response.status_int, 200)
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.body, '{"key": "value"}')
class JSONRequestDeserializerTest(unittest.TestCase):
def test_has_body_no_content_length(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = 'asdf'
request.headers.pop('Content-Length')
self.assertFalse(wsgi.JSONRequestDeserializer().has_body(request))
def test_has_body_zero_content_length(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = 'asdf'
request.headers['Content-Length'] = 0
self.assertFalse(wsgi.JSONRequestDeserializer().has_body(request))
def test_has_body_has_content_length(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = 'asdf'
self.assertTrue('Content-Length' in request.headers)
self.assertTrue(wsgi.JSONRequestDeserializer().has_body(request))
def test_no_body_no_content_length(self):
request = wsgi.Request.blank('/')
self.assertFalse(wsgi.JSONRequestDeserializer().has_body(request))
def test_from_json(self):
fixture = '{"key": "value"}'
expected = {"key": "value"}
actual = wsgi.JSONRequestDeserializer().from_json(fixture)
self.assertEqual(actual, expected)
def test_default_no_body(self):
request = wsgi.Request.blank('/')
actual = wsgi.JSONRequestDeserializer().default(request)
expected = {}
self.assertEqual(actual, expected)
def test_default_with_body(self):
request = wsgi.Request.blank('/')
request.method = 'POST'
request.body = '{"key": "value"}'
actual = wsgi.JSONRequestDeserializer().default(request)
expected = {"body": {"key": "value"}}
self.assertEqual(actual, expected)

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...',

View File

@ -6,7 +6,7 @@ anyjson
eventlet>=0.9.12
PasteDeploy
routes
webob
webob==1.0.8
wsgiref
nose
sphinx
@ -16,3 +16,5 @@ swift
-f http://pymox.googlecode.com/files/mox-0.5.0.tar.gz
sqlalchemy-migrate>=0.6,<0.7
bzr
httplib2
hashlib