merge trunk
This commit is contained in:
commit
6ac1d5a332
3
Authors
3
Authors
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
------------------------------------------------
|
||||
|
@ -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
|
||||
------------------------------------------------
|
||||
|
||||
|
@ -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``
|
||||
----------------
|
||||
|
||||
|
@ -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:]
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
189
glance/client.py
189
glance/client.py
@ -19,172 +19,17 @@
|
||||
Client classes for callers of a Glance system
|
||||
"""
|
||||
|
||||
import httplib
|
||||
import json
|
||||
import logging
|
||||
import urlparse
|
||||
import socket
|
||||
import sys
|
||||
import urllib
|
||||
|
||||
from glance import utils
|
||||
from glance.api.v1 import images as v1_images
|
||||
from glance.common import client as base_client
|
||||
from glance.common import exception
|
||||
from glance import utils
|
||||
|
||||
#TODO(jaypipes) Allow a logger param for client classes
|
||||
|
||||
|
||||
class ClientConnectionError(Exception):
|
||||
"""Error resulting from a client connecting to a server"""
|
||||
pass
|
||||
|
||||
|
||||
class ImageBodyIterator(object):
|
||||
|
||||
"""
|
||||
A class that acts as an iterator over an image file's
|
||||
chunks of data. This is returned as part of the result
|
||||
tuple from `glance.client.Client.get_image`
|
||||
"""
|
||||
|
||||
CHUNKSIZE = 65536
|
||||
|
||||
def __init__(self, response):
|
||||
"""
|
||||
Constructs the object from an HTTPResponse object
|
||||
"""
|
||||
self.response = response
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Exposes an iterator over the chunks of data in the
|
||||
image file.
|
||||
"""
|
||||
while True:
|
||||
chunk = self.response.read(ImageBodyIterator.CHUNKSIZE)
|
||||
if chunk:
|
||||
yield chunk
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
class BaseClient(object):
|
||||
|
||||
"""A base client class"""
|
||||
|
||||
CHUNKSIZE = 65536
|
||||
|
||||
def __init__(self, host, port, use_ssl):
|
||||
"""
|
||||
Creates a new client to some service.
|
||||
|
||||
:param host: The host where service resides
|
||||
:param port: The port where service resides
|
||||
:param use_ssl: Should we use HTTPS?
|
||||
"""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.use_ssl = use_ssl
|
||||
self.connection = None
|
||||
|
||||
def get_connection_type(self):
|
||||
"""
|
||||
Returns the proper connection type
|
||||
"""
|
||||
if self.use_ssl:
|
||||
return httplib.HTTPSConnection
|
||||
else:
|
||||
return httplib.HTTPConnection
|
||||
|
||||
def do_request(self, method, action, body=None, headers=None,
|
||||
params=None):
|
||||
"""
|
||||
Connects to the server and issues a request. Handles converting
|
||||
any returned HTTP error status codes to OpenStack/Glance exceptions
|
||||
and closing the server connection. Returns the result data, or
|
||||
raises an appropriate exception.
|
||||
|
||||
:param method: HTTP method ("GET", "POST", "PUT", etc...)
|
||||
:param action: part of URL after root netloc
|
||||
:param body: string of data to send, or None (default)
|
||||
:param headers: mapping of key/value pairs to add as headers
|
||||
:param params: dictionary of key/value pairs to add to append
|
||||
to action
|
||||
|
||||
:note
|
||||
|
||||
If the body param has a read attribute, and method is either
|
||||
POST or PUT, this method will automatically conduct a chunked-transfer
|
||||
encoding and use the body as a file object, transferring chunks
|
||||
of data using the connection's send() method. This allows large
|
||||
objects to be transferred efficiently without buffering the entire
|
||||
body in memory.
|
||||
"""
|
||||
if type(params) is dict:
|
||||
action += '?' + urllib.urlencode(params)
|
||||
|
||||
try:
|
||||
connection_type = self.get_connection_type()
|
||||
headers = headers or {}
|
||||
c = connection_type(self.host, self.port)
|
||||
|
||||
# Do a simple request or a chunked request, depending
|
||||
# on whether the body param is a file-like object and
|
||||
# the method is PUT or POST
|
||||
if hasattr(body, 'read') and method.lower() in ('post', 'put'):
|
||||
# Chunk it, baby...
|
||||
c.putrequest(method, action)
|
||||
|
||||
for header, value in headers.items():
|
||||
c.putheader(header, value)
|
||||
c.putheader('Transfer-Encoding', 'chunked')
|
||||
c.endheaders()
|
||||
|
||||
chunk = body.read(self.CHUNKSIZE)
|
||||
while chunk:
|
||||
c.send('%x\r\n%s\r\n' % (len(chunk), chunk))
|
||||
chunk = body.read(self.CHUNKSIZE)
|
||||
c.send('0\r\n\r\n')
|
||||
else:
|
||||
# Simple request...
|
||||
c.request(method, action, body, headers)
|
||||
res = c.getresponse()
|
||||
status_code = self.get_status_code(res)
|
||||
if status_code in (httplib.OK,
|
||||
httplib.CREATED,
|
||||
httplib.ACCEPTED,
|
||||
httplib.NO_CONTENT):
|
||||
return res
|
||||
elif status_code == httplib.UNAUTHORIZED:
|
||||
raise exception.NotAuthorized
|
||||
elif status_code == httplib.FORBIDDEN:
|
||||
raise exception.NotAuthorized
|
||||
elif status_code == httplib.NOT_FOUND:
|
||||
raise exception.NotFound
|
||||
elif status_code == httplib.CONFLICT:
|
||||
raise exception.Duplicate(res.read())
|
||||
elif status_code == httplib.BAD_REQUEST:
|
||||
raise exception.Invalid(res.read())
|
||||
elif status_code == httplib.INTERNAL_SERVER_ERROR:
|
||||
raise Exception("Internal Server error: %s" % res.read())
|
||||
else:
|
||||
raise Exception("Unknown error occurred! %s" % res.read())
|
||||
|
||||
except (socket.error, IOError), e:
|
||||
raise ClientConnectionError("Unable to connect to "
|
||||
"server. Got error: %s" % e)
|
||||
|
||||
def get_status_code(self, response):
|
||||
"""
|
||||
Returns the integer status code from the response, which
|
||||
can be either a Webob.Response (used in testing) or httplib.Response
|
||||
"""
|
||||
if hasattr(response, 'status_int'):
|
||||
return response.status_int
|
||||
else:
|
||||
return response.status
|
||||
|
||||
|
||||
class V1Client(BaseClient):
|
||||
class V1Client(base_client.BaseClient):
|
||||
|
||||
"""Main client class for accessing Glance resources"""
|
||||
|
||||
@ -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
181
glance/common/client.py
Normal 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
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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'):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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":
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
20
run_tests.py
20
run_tests.py
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
592
tests/functional/test_httplib2_api.py
Normal file
592
tests/functional/test_httplib2_api.py
Normal 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()
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
184
tests/unit/test_wsgi.py
Normal 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)
|
@ -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...',
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user