diff options
| author | Michael Gundlach <michael.gundlach@rackspace.com> | 2010-08-25 17:52:52 +0000 |
|---|---|---|
| committer | Tarmac <> | 2010-08-25 17:52:52 +0000 |
| commit | 686ad09fe4416bb578661a42f6f083528d4a7ca9 (patch) | |
| tree | b879c74a111b500d112023e1610a9bd0b24bbb73 | |
| parent | 90ca9b373935f2e2bddedf1f33befb35f89aaab4 (diff) | |
| parent | 428b3256ad7e47e9f389ac8ce0ff70cc3d720e9e (diff) | |
Initial support for Rackspace API /image requests. They will eventually be backed by Glance.
Because we don't expect Glance to support non-public images for the Austin release, all we support is index() and show().
The WSGI controller uses a service to interact with the image store (the "ImageService"). Eventually it will use a GlanceImageService, but since Glance isn't implemented yet it's hard coded to use a LocalImageService for testing.
The ImageService maps URIs to image data (because Glance will be the canonical backend and that's how Glance does it), but the Rackspace API maps ids to image data. So the images.Controller stores a mapping in the global Redis service to convert from URIs to ids.
Courtesy of whatthecommit.com:
(\ /)
(O.o)
(> <) Bunny approves these changes.
| -rw-r--r-- | nova/api/rackspace/images.py | 101 | ||||
| -rw-r--r-- | nova/api/rackspace/notes.txt | 23 | ||||
| -rw-r--r-- | nova/image/service.py | 90 | ||||
| -rw-r--r-- | nova/wsgi.py | 19 |
4 files changed, 230 insertions, 3 deletions
diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 986f11434..370980fe9 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -15,4 +15,103 @@ # License for the specific language governing permissions and limitations # under the License. -class Controller(object): pass +from nova import datastore +from nova import image +from nova.api.rackspace import base +from webob import exc + +class Controller(base.Controller): + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "image": [ "id", "name", "updated", "created", "status", + "serverId", "progress" ] + } + } + } + + def __init__(self): + self._service = image.service.ImageService.load() + self._id_translator = RackspaceAPIImageIdTranslator() + + def _to_rs_id(self, image_id): + """ + Convert an image id from the format of our ImageService strategy + to the Rackspace API format (an int). + """ + strategy = self._service.__class__.__name__ + return self._id_translator.to_rs_id(strategy, image_id) + + def _from_rs_id(self, rs_image_id): + """ + Convert an image id from the Rackspace API format (an int) to the + format of our ImageService strategy. + """ + strategy = self._service.__class__.__name__ + return self._id_translator.from_rs_id(strategy, rs_image_id) + + def index(self, req): + """Return all public images.""" + data = self._service.index() + for img in data: + img['id'] = self._to_rs_id(img['id']) + return dict(images=data) + + def show(self, req, id): + """Return data about the given image id.""" + opaque_id = self._from_rs_id(id) + img = self._service.show(opaque_id) + img['id'] = id + return dict(image=img) + + def delete(self, req, id): + # Only public images are supported for now. + raise exc.HTTPNotFound() + + def create(self, req): + # Only public images are supported for now, so a request to + # make a backup of a server cannot be supproted. + raise exc.HTTPNotFound() + + def update(self, req, id): + # Users may not modify public images, and that's all that + # we support for now. + raise exc.HTTPNotFound() + + +class RackspaceAPIImageIdTranslator(object): + """ + Converts Rackspace API image ids to and from the id format for a given + strategy. + """ + + def __init__(self): + self._store = datastore.Redis.instance() + self._key_template = "rsapi.idstrategies.image.%s.%s" + + def to_rs_id(self, strategy_name, opaque_id): + """Convert an id from a strategy-specific one to a Rackspace one.""" + key = self._key_template % (strategy_name, "fwd") + result = self._store.hget(key, str(opaque_id)) + if result: # we have a mapping from opaque to RS for this strategy + return int(result) + else: + # Store the mapping. + nextid = self._store.incr("%s.lastid" % key) + if self._store.hsetnx(key, str(opaque_id), nextid): + # If someone else didn't beat us to it, store the reverse + # mapping as well. + key = self._key_template % (strategy_name, "rev") + self._store.hset(key, nextid, str(opaque_id)) + return nextid + else: + # Someone beat us to it; use their number instead, and + # discard nextid (which is OK -- we don't require that + # every int id be used.) + return int(self._store.hget(key, str(opaque_id))) + + def from_rs_id(self, strategy_name, rs_id): + """Convert a Rackspace id to a strategy-specific one.""" + key = self._key_template % (strategy_name, "rev") + return self._store.hget(key, rs_id) diff --git a/nova/api/rackspace/notes.txt b/nova/api/rackspace/notes.txt new file mode 100644 index 000000000..e133bf5ea --- /dev/null +++ b/nova/api/rackspace/notes.txt @@ -0,0 +1,23 @@ +We will need: + +ImageService +a service that can do crud on image information. not user-specific. opaque +image ids. + +GlanceImageService(ImageService): +image ids are URIs. + +LocalImageService(ImageService): +image ids are random strings. + +RackspaceAPITranslationStore: +translates RS server/images/flavor/etc ids into formats required +by a given ImageService strategy. + +api.rackspace.images.Controller: +uses an ImageService strategy behind the scenes to do its fetching; it just +converts int image id into a strategy-specific image id. + +who maintains the mapping from user to [images he owns]? nobody, because +we have no way of enforcing access to his images, without kryptex which +won't be in Austin. diff --git a/nova/image/service.py b/nova/image/service.py new file mode 100644 index 000000000..1a7a258b7 --- /dev/null +++ b/nova/image/service.py @@ -0,0 +1,90 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 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 cPickle as pickle +import os.path +import random +import string + +class ImageService(object): + """Provides storage and retrieval of disk image objects.""" + + @staticmethod + def load(): + """Factory method to return image service.""" + #TODO(gundlach): read from config. + class_ = LocalImageService + return class_() + + def index(self): + """ + Return a dict from opaque image id to image data. + """ + + def show(self, id): + """ + Returns a dict containing image data for the given opaque image id. + """ + + +class GlanceImageService(ImageService): + """Provides storage and retrieval of disk image objects within Glance.""" + # TODO(gundlach): once Glance has an API, build this. + pass + + +class LocalImageService(ImageService): + """Image service storing images to local disk.""" + + def __init__(self): + self._path = "/tmp/nova/images" + try: + os.makedirs(self._path) + except OSError: # exists + pass + + def _path_to(self, image_id=''): + return os.path.join(self._path, image_id) + + def _ids(self): + """The list of all image ids.""" + return os.listdir(self._path) + + def index(self): + return [ self.show(id) for id in self._ids() ] + + def show(self, id): + return pickle.load(open(self._path_to(id))) + + def create(self, data): + """ + Store the image data and return the new image id. + """ + id = ''.join(random.choice(string.letters) for _ in range(20)) + data['id'] = id + self.update(id, data) + return id + + def update(self, image_id, data): + """Replace the contents of the given image with the new data.""" + pickle.dump(data, open(self._path_to(image_id), 'w')) + + def delete(self, image_id): + """ + Delete the given image. Raises OSError if the image does not exist. + """ + os.unlink(self._path_to(image_id)) diff --git a/nova/wsgi.py b/nova/wsgi.py index baf6cccd9..bec0a7b1c 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -196,7 +196,8 @@ 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. + which is the incoming webob.Request. They raise a webob.exc exception, + or return a dict which will be serialized by requested content type. """ @webob.dec.wsgify @@ -210,7 +211,21 @@ class Controller(object): del arg_dict['controller'] del arg_dict['action'] arg_dict['req'] = req - return method(**arg_dict) + result = method(**arg_dict) + if type(result) is dict: + return self._serialize(result, req) + 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) class Serializer(object): |
