diff options
| author | Justin Santa Barbara <justin@fathomdb.com> | 2011-03-29 19:09:42 -0700 |
|---|---|---|
| committer | Justin Santa Barbara <justin@fathomdb.com> | 2011-03-29 19:09:42 -0700 |
| commit | 93b43cfcaeffa93b2f8ce50f473840c77be532c9 (patch) | |
| tree | ab3c9f97697c157be300b58ee702e8f4ffab7fb4 /nova/api | |
| parent | 2315682856f420ff0b781bead142e1aff82071a4 (diff) | |
| parent | e5f108058f9b085571330dff3c3e3e3e57d2e5ed (diff) | |
Merged with trunk
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/openstack/__init__.py | 11 | ||||
| -rw-r--r-- | nova/api/openstack/images.py | 307 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 98 |
3 files changed, 197 insertions, 219 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 8b9acc11d..7545eb0c9 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -111,9 +111,6 @@ class APIRouter(wsgi.Router): parent_resource=dict(member_name='server', collection_name='servers')) - mapper.resource("image", "images", controller=images.Controller(), - collection={'detail': 'GET'}) - _limits = limits.LimitsController() mapper.resource("limit", "limits", controller=_limits) @@ -130,6 +127,10 @@ class APIRouterV10(APIRouter): collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("image", "images", + controller=images.ControllerV10(), + collection={'detail': 'GET'}) + mapper.resource("flavor", "flavors", controller=flavors.ControllerV10(), collection={'detail': 'GET'}) @@ -154,6 +155,10 @@ class APIRouterV11(APIRouter): collection={'detail': 'GET'}, member=self.server_members) + mapper.resource("image", "images", + controller=images.ControllerV11(), + collection={'detail': 'GET'}) + mapper.resource("image_meta", "meta", controller=image_metadata.Controller(), parent_resource=dict(member_name='image', diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 79852ecc6..e77100d7b 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -1,6 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -17,7 +15,7 @@ import datetime -from webob import exc +import webob.exc from nova import compute from nova import exception @@ -25,238 +23,133 @@ from nova import flags from nova import log from nova import utils from nova import wsgi -import nova.api.openstack from nova.api.openstack import common from nova.api.openstack import faults -import nova.image.service +from nova.api.openstack.views import images as images_view LOG = log.getLogger('nova.api.openstack.images') - FLAGS = flags.FLAGS -def _translate_keys(item): - """ - Maps key names to Rackspace-like attributes for return - also pares down attributes to those we want - item is a dict - - Note: should be removed when the set of keys expected by the api - and the set of keys returned by the image service are equivalent - - """ - # TODO(tr3buchet): this map is specific to s3 object store, - # replace with a list of keys for _filter_keys later - mapped_keys = {'status': 'imageState', - 'id': 'imageId', - 'name': 'imageLocation'} - - mapped_item = {} - # TODO(tr3buchet): - # this chunk of code works with s3 and the local image service/glance - # when we switch to glance/local image service it can be replaced with - # a call to _filter_keys, and mapped_keys can be changed to a list - try: - for k, v in mapped_keys.iteritems(): - # map s3 fields - mapped_item[k] = item[v] - except KeyError: - # return only the fields api expects - mapped_item = _filter_keys(item, mapped_keys.keys()) - - return mapped_item - - -def _translate_status(item): - """ - Translates status of image to match current Rackspace api bindings - item is a dict - - Note: should be removed when the set of statuses expected by the api - and the set of statuses returned by the image service are equivalent - - """ - status_mapping = { - 'pending': 'queued', - 'decrypting': 'preparing', - 'untarring': 'saving', - 'available': 'active'} - try: - item['status'] = status_mapping[item['status']] - except KeyError: - # TODO(sirp): Performing translation of status (if necessary) here for - # now. Perhaps this should really be done in EC2 API and - # S3ImageService - pass - - -def _filter_keys(item, keys): - """ - Filters all model attributes except for keys - item is a dict - - """ - return dict((k, v) for k, v in item.iteritems() if k in keys) - - -def _convert_image_id_to_hash(image): - if 'imageId' in image: - # Convert EC2-style ID (i-blah) to Rackspace-style (int) - image_id = abs(hash(image['imageId'])) - image['imageId'] = image_id - image['id'] = image_id - - -def _translate_s3_like_images(image_metadata): - """Work-around for leaky S3ImageService abstraction""" - api_metadata = image_metadata.copy() - _convert_image_id_to_hash(api_metadata) - api_metadata = _translate_keys(api_metadata) - _translate_status(api_metadata) - return api_metadata - - -def _translate_from_image_service_to_api(image_metadata): - """Translate from ImageService to OpenStack API style attribute names - - This involves 4 steps: - - 1. Filter out attributes that the OpenStack API doesn't need - - 2. Translate from base image attributes from names used by - BaseImageService to names used by OpenStack API - - 3. Add in any image properties - - 4. Format values according to API spec (for example dates must - look like "2010-08-10T12:00:00Z") - """ - service_metadata = image_metadata.copy() - properties = service_metadata.pop('properties', {}) - - # 1. Filter out unecessary attributes - api_keys = ['id', 'name', 'updated_at', 'created_at', 'status'] - api_metadata = utils.subset_dict(service_metadata, api_keys) - - # 2. Translate base image attributes - api_map = {'updated_at': 'updated', 'created_at': 'created'} - api_metadata = utils.map_dict_keys(api_metadata, api_map) - - # 3. Add in any image properties - # 3a. serverId is used for backups and snapshots - try: - api_metadata['serverId'] = int(properties['instance_id']) - except KeyError: - pass # skip if it's not present - except ValueError: - pass # skip if it's not an integer - - # 3b. Progress special case - # TODO(sirp): ImageService doesn't have a notion of progress yet, so for - # now just fake it - if service_metadata['status'] == 'saving': - api_metadata['progress'] = 0 - - # 4. Format values - # 4a. Format Image Status (API requires uppercase) - api_metadata['status'] = _format_status_for_api(api_metadata['status']) - - # 4b. Format timestamps - for attr in ('created', 'updated'): - if attr in api_metadata: - api_metadata[attr] = _format_datetime_for_api( - api_metadata[attr]) - - return api_metadata - - -def _format_status_for_api(status): - """Return status in a format compliant with OpenStack API""" - mapping = {'queued': 'QUEUED', - 'preparing': 'PREPARING', - 'saving': 'SAVING', - 'active': 'ACTIVE', - 'killed': 'FAILED'} - return mapping[status] - - -def _format_datetime_for_api(datetime_): - """Stringify datetime objects in a format compliant with OpenStack API""" - API_DATETIME_FMT = '%Y-%m-%dT%H:%M:%SZ' - return datetime_.strftime(API_DATETIME_FMT) - - -def _safe_translate(image_metadata): - """Translate attributes for OpenStack API, temporary workaround for - S3ImageService attribute leakage. - """ - # FIXME(sirp): The S3ImageService appears to be leaking implementation - # details, including its internal attribute names, and internal - # `status` values. Working around it for now. - s3_like_image = ('imageId' in image_metadata) - if s3_like_image: - translate = _translate_s3_like_images - else: - translate = _translate_from_image_service_to_api - return translate(image_metadata) - - class Controller(wsgi.Controller): + """Base `wsgi.Controller` for retrieving/displaying images.""" _serialization_metadata = { 'application/xml': { "attributes": { "image": ["id", "name", "updated", "created", "status", - "serverId", "progress"]}}} + "serverId", "progress"], + "link": ["rel", "type", "href"], + }, + }, + } + + def __init__(self, image_service=None, compute_service=None): + """Initialize new `ImageController`. - def __init__(self): - self._service = utils.import_object(FLAGS.image_service) + :param compute_service: `nova.compute.api:API` + :param image_service: `nova.image.service:BaseImageService` + """ + _default_service = utils.import_object(flags.FLAGS.image_service) + + self._compute_service = compute_service or compute.API() + self._image_service = image_service or _default_service def index(self, req): - """Return all public images in brief""" + """Return an index listing of images available to the request. + + :param req: `wsgi.Request` object + """ context = req.environ['nova.context'] - image_metas = self._service.index(context) - image_metas = common.limited(image_metas, req) - return dict(images=image_metas) + images = self._image_service.index(context) + images = common.limited(images, req) + builder = self.get_builder(req).build + return dict(images=[builder(image, detail=False) for image in images]) def detail(self, req): - """Return all public images in detail""" + """Return a detailed index listing of images available to the request. + + :param req: `wsgi.Request` object. + """ context = req.environ['nova.context'] - image_metas = self._service.detail(context) - image_metas = common.limited(image_metas, req) - api_image_metas = [_safe_translate(image_meta) - for image_meta in image_metas] - return dict(images=api_image_metas) + images = self._image_service.detail(context) + images = common.limited(images, req) + builder = self.get_builder(req).build + return dict(images=[builder(image, detail=True) for image in images]) def show(self, req, id): - """Return data about the given image id""" + """Return detailed information about a specific image. + + :param req: `wsgi.Request` object + :param id: Image identifier (integer) + """ context = req.environ['nova.context'] + + try: + image_id = int(id) + except ValueError: + explanation = _("Image not found.") + raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation)) + try: - image_id = common.get_image_id_from_image_hash( - self._service, context, id) + image = self._image_service.show(context, image_id) except exception.NotFound: - raise faults.Fault(exc.HTTPNotFound()) + explanation = _("Image '%d' not found.") % (image_id) + raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation)) - image_meta = self._service.show(context, image_id) - api_image_meta = _safe_translate(image_meta) - return dict(image=api_image_meta) + return dict(image=self.get_builder(req).build(image, detail=True)) def delete(self, req, id): - # Only public images are supported for now. - raise faults.Fault(exc.HTTPNotFound()) + """Delete an image, if allowed. + + :param req: `wsgi.Request` object + :param id: Image identifier (integer) + """ + image_id = id + context = req.environ['nova.context'] + self._image_service.delete(context, image_id) + return webob.exc.HTTPNoContent() def create(self, req): + """Snapshot a server instance and save the image. + + :param req: `wsgi.Request` object + """ context = req.environ['nova.context'] - env = self._deserialize(req.body, req.get_content_type()) - instance_id = env["image"]["serverId"] - name = env["image"]["name"] - image_meta = compute.API().snapshot( - context, instance_id, name) - api_image_meta = _safe_translate(image_meta) - return dict(image=api_image_meta) - - def update(self, req, id): - # Users may not modify public images, and that's all that - # we support for now. - raise faults.Fault(exc.HTTPNotFound()) + content_type = req.get_content_type() + image = self._deserialize(req.body, content_type) + + if not image: + raise webob.exc.HTTPBadRequest() + + try: + server_id = image["image"]["serverId"] + image_name = image["image"]["name"] + except KeyError: + raise webob.exc.HTTPBadRequest() + + image = self._compute_service.snapshot(context, server_id, image_name) + return self.get_builder(req).build(image, detail=True) + + def get_builder(self, request): + """Indicates that you must use a Controller subclass.""" + raise NotImplementedError + + +class ControllerV10(Controller): + """Version 1.0 specific controller logic.""" + + def get_builder(self, request): + """Property to get the ViewBuilder class we need to use.""" + base_url = request.application_url + return images_view.ViewBuilderV10(base_url) + + +class ControllerV11(Controller): + """Version 1.1 specific controller logic.""" + + def get_builder(self, request): + """Property to get the ViewBuilder class we need to use.""" + base_url = request.application_url + return images_view.ViewBuilderV11(base_url) diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index a6c6ad7d1..3807fa95f 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -15,20 +15,100 @@ # License for the specific language governing permissions and limitations # under the License. -from nova.api.openstack import common +import os.path class ViewBuilder(object): - def __init__(self): - pass + """Base class for generating responses to OpenStack API image requests.""" - def build(self, image_obj): - raise NotImplementedError() + def __init__(self, base_url): + """Initialize new `ViewBuilder`.""" + self._url = base_url + def _format_dates(self, image): + """Update all date fields to ensure standardized formatting.""" + for attr in ['created_at', 'updated_at', 'deleted_at']: + if image.get(attr) is not None: + image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ') -class ViewBuilderV11(ViewBuilder): - def __init__(self, base_url): - self.base_url = base_url + def _format_status(self, image): + """Update the status field to standardize format.""" + status_mapping = { + 'pending': 'queued', + 'decrypting': 'preparing', + 'untarring': 'saving', + 'available': 'active', + 'killed': 'failed', + } + + try: + image['status'] = status_mapping[image['status']].upper() + except KeyError: + image['status'] = image['status'].upper() def generate_href(self, image_id): - return "%s/images/%s" % (self.base_url, image_id) + """Return an href string pointing to this object.""" + return os.path.join(self._url, "images", str(image_id)) + + def build(self, image_obj, detail=False): + """Return a standardized image structure for display by the API.""" + properties = image_obj.get("properties", {}) + + self._format_dates(image_obj) + + if "status" in image_obj: + self._format_status(image_obj) + + image = { + "id": image_obj["id"], + "name": image_obj["name"], + } + + if "instance_id" in properties: + try: + image["serverId"] = int(properties["instance_id"]) + except ValueError: + pass + + if detail: + image.update({ + "created": image_obj["created_at"], + "updated": image_obj["updated_at"], + "status": image_obj["status"], + }) + + if image["status"] == "SAVING": + image["progress"] = 0 + + return image + + +class ViewBuilderV10(ViewBuilder): + """OpenStack API v1.0 Image Builder""" + pass + + +class ViewBuilderV11(ViewBuilder): + """OpenStack API v1.1 Image Builder""" + + def build(self, image_obj, detail=False): + """Return a standardized image structure for display by the API.""" + image = ViewBuilder.build(self, image_obj, detail) + href = self.generate_href(image_obj["id"]) + + image["links"] = [{ + "rel": "self", + "href": href, + }, + { + "rel": "bookmark", + "type": "application/json", + "href": href, + }, + { + "rel": "bookmark", + "type": "application/xml", + "href": href, + }] + + return image |
