merged trunk

This commit is contained in:
Justin Shepherd 2011-06-22 09:19:33 -05:00
commit 96acd63435
11 changed files with 551 additions and 217 deletions

View File

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

View File

@ -24,7 +24,7 @@ import json
import logging
import sys
from webob import Response
import webob
from webob.exc import (HTTPNotFound,
HTTPConflict,
HTTPBadRequest)
@ -46,7 +46,7 @@ SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
'size_min', 'size_max']
class Controller(wsgi.Controller):
class Controller(object):
"""
WSGI controller for images resource in Glance v1 API
@ -80,7 +80,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': [
@ -107,7 +107,7 @@ class Controller(wsgi.Controller):
"""
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': [
@ -155,29 +155,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 +185,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 +238,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 +291,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 +309,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 +322,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 +338,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 +353,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 +362,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 +388,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 +417,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 +435,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
@ -527,3 +501,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 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.add('Location', location)
def _inject_checksum_header(self, response, image_meta):
response.headers.add('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.add(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.add('Content-Length', image_meta['size'])
response.headers.add('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.add('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.add('Content-Type', 'application/json')
response.body = self.to_json(dict(image=image_meta))
self._inject_location_header(response, image_meta)
self._inject_checksum_header(response, image_meta)
return response
def create_resource(options):
"""Images resource factory method"""
deserializer = ImageDeserializer()
serializer = ImageSerializer()
return wsgi.Resource(Controller(options), deserializer, serializer)

View File

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

View File

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

View File

@ -94,10 +94,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 +117,13 @@ class RegistryClient(BaseClient):
body = json.dumps(image_metadata)
headers = {}
headers = {
'Content-Type': 'application/json',
}
if purge_props:
headers["X-Glance-Registry-Purge-Props"] = "true"
res = self.do_request("PUT", "/images/%s" % image_id, body, headers)
data = json.loads(res.read())
image = data['image']

View File

@ -42,7 +42,7 @@ SUPPORTED_FILTERS = ['name', 'status', 'container_format', 'disk_format',
MAX_ITEM_LIMIT = 25
class Controller(wsgi.Controller):
class Controller(object):
"""Controller for the reference implementation registry server"""
def __init__(self, options):
@ -167,7 +167,7 @@ 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.
@ -179,19 +179,19 @@ 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 +209,17 @@ class Controller(wsgi.Controller):
logger.error(msg)
return exc.HTTPBadRequest(msg)
def update(self, req, id):
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 +243,22 @@ class Controller(wsgi.Controller):
content_type='text/plain')
def create_resource(options):
"""Images resource factory method."""
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
return wsgi.Resource(Controller(options), deserializer, serializer)
class API(wsgi.Router):
"""WSGI entry point for all Registry requests."""
def __init__(self, options):
mapper = routes.Mapper()
controller = Controller(options)
mapper.resource("image", "images", controller=controller,
resource = create_resource(options)
mapper.resource("image", "images", controller=resource,
collection={'detail': 'GET'})
mapper.connect("/", controller=controller, action="index")
mapper.connect("/", controller=resource, action="index")
super(API, self).__init__(mapper)

View File

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

View File

@ -773,7 +773,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)

View File

@ -678,6 +678,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 +707,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 +725,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)
@ -739,6 +742,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 +762,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 +777,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)
@ -791,6 +797,7 @@ class TestRegistryAPI(unittest.TestCase):
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 +811,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 +825,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 +839,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)
@ -844,6 +854,7 @@ class TestRegistryAPI(unittest.TestCase):
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)
@ -972,6 +983,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',

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

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

View File

@ -6,7 +6,7 @@ anyjson
eventlet>=0.9.12
PasteDeploy
routes
webob
webob==1.0.8
wsgiref
nose
sphinx