diff options
| author | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-08-30 16:44:30 -0700 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-08-30 16:44:30 -0700 |
| commit | ffcc019e38e61c1c3c5c2552c15259d2b8b04e5d (patch) | |
| tree | 8794f85ac3c4770e3a6ecd6a77c378636cd5e40a | |
| parent | b9aa0dae0a5a64a244f1bff95ad8af22cf87f7f6 (diff) | |
| parent | 9643d417c539f3e21beed743cb2f427c6ab5a469 (diff) | |
merged trunk, fixed a couple errors
| -rwxr-xr-x | bin/nova-api | 8 | ||||
| -rw-r--r-- | nova/api/rackspace/__init__.py | 6 | ||||
| -rw-r--r-- | nova/api/rackspace/_id_translator.py | 42 | ||||
| -rw-r--r-- | nova/api/rackspace/flavors.py | 38 | ||||
| -rw-r--r-- | nova/api/rackspace/images.py | 54 | ||||
| -rw-r--r-- | nova/api/rackspace/notes.txt | 23 | ||||
| -rw-r--r-- | nova/compute/instance_types.py | 14 | ||||
| -rw-r--r-- | nova/endpoint/cloud.py | 1 | ||||
| -rw-r--r-- | nova/flags.py | 2 | ||||
| -rw-r--r-- | nova/image/__init__.py | 0 | ||||
| -rw-r--r-- | nova/image/service.py | 90 | ||||
| -rw-r--r-- | nova/objectstore/handler.py | 135 | ||||
| -rw-r--r-- | nova/server.py | 6 | ||||
| -rw-r--r-- | nova/tests/api_unittest.py | 83 | ||||
| -rw-r--r-- | nova/tests/cloud_unittest.py | 4 | ||||
| -rw-r--r-- | nova/tests/network_unittest.py | 3 | ||||
| -rw-r--r-- | nova/twistd.py | 16 | ||||
| -rw-r--r-- | nova/utils.py | 7 | ||||
| -rw-r--r-- | nova/virt/connection.py | 6 | ||||
| -rw-r--r-- | nova/virt/libvirt_conn.py | 14 | ||||
| -rw-r--r-- | nova/virt/xenapi.py | 167 | ||||
| -rw-r--r-- | nova/wsgi.py | 19 | ||||
| -rw-r--r-- | tools/install_venv.py | 4 |
23 files changed, 613 insertions, 129 deletions
diff --git a/bin/nova-api b/bin/nova-api index 13baf22a7..a3ad5a0e1 100755 --- a/bin/nova-api +++ b/bin/nova-api @@ -26,7 +26,6 @@ from tornado import httpserver from tornado import ioloop from nova import flags -from nova import rpc from nova import server from nova import utils from nova.endpoint import admin @@ -43,14 +42,7 @@ def main(_argv): 'Admin': admin.AdminController()} _app = api.APIServerApplication(controllers) - conn = rpc.Connection.instance() - consumer = rpc.AdapterConsumer(connection=conn, - topic=FLAGS.cloud_topic, - proxy=controllers['Cloud']) - io_inst = ioloop.IOLoop.instance() - _injected = consumer.attach_to_tornado(io_inst) - http_server = httpserver.HTTPServer(_app) http_server.listen(FLAGS.cc_port) logging.debug('Started HTTP server on %s', FLAGS.cc_port) diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index 27e78f801..b4d666d63 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -74,8 +74,10 @@ class APIRouter(wsgi.Router): def __init__(self): mapper = routes.Mapper() mapper.resource("server", "servers", controller=servers.Controller()) - mapper.resource("image", "images", controller=images.Controller()) - mapper.resource("flavor", "flavors", controller=flavors.Controller()) + mapper.resource("image", "images", controller=images.Controller(), + collection={'detail': 'GET'}) + mapper.resource("flavor", "flavors", controller=flavors.Controller(), + collection={'detail': 'GET'}) mapper.resource("sharedipgroup", "sharedipgroups", controller=sharedipgroups.Controller()) super(APIRouter, self).__init__(mapper) diff --git a/nova/api/rackspace/_id_translator.py b/nova/api/rackspace/_id_translator.py new file mode 100644 index 000000000..aec5fb6a5 --- /dev/null +++ b/nova/api/rackspace/_id_translator.py @@ -0,0 +1,42 @@ +from nova import datastore + +class RackspaceAPIIdTranslator(object): + """ + Converts Rackspace API ids to and from the id format for a given + strategy. + """ + + def __init__(self, id_type, service_name): + """ + Creates a translator for ids of the given type (e.g. 'flavor'), for the + given storage service backend class name (e.g. 'LocalFlavorService'). + """ + + self._store = datastore.Redis.instance() + key_prefix = "rsapi.idtranslator.%s.%s" % (id_type, service_name) + # Forward (strategy format -> RS format) and reverse translation keys + self._fwd_key = "%s.fwd" % key_prefix + self._rev_key = "%s.rev" % key_prefix + + def to_rs_id(self, opaque_id): + """Convert an id from a strategy-specific one to a Rackspace one.""" + result = self._store.hget(self._fwd_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" % self._fwd_key) + if self._store.hsetnx(self._fwd_key, str(opaque_id), nextid): + # If someone else didn't beat us to it, store the reverse + # mapping as well. + self._store.hset(self._rev_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(self._fwd_key, str(opaque_id))) + + def from_rs_id(self, strategy_name, rs_id): + """Convert a Rackspace id to a strategy-specific one.""" + return self._store.hget(self._rev_key, rs_id) diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py index 986f11434..60b35c939 100644 --- a/nova/api/rackspace/flavors.py +++ b/nova/api/rackspace/flavors.py @@ -15,4 +15,40 @@ # License for the specific language governing permissions and limitations # under the License. -class Controller(object): pass +from nova.api.rackspace import base +from nova.compute import instance_types +from webob import exc + +class Controller(base.Controller): + """Flavor controller for the Rackspace API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "flavor": [ "id", "name", "ram", "disk" ] + } + } + } + + def index(self, req): + """Return all flavors in brief.""" + return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) + for flavor in self.detail(req)['flavors']]) + + def detail(self, req): + """Return all flavors in detail.""" + items = [self.show(req, id)['flavor'] for id in self._all_ids()] + return dict(flavors=items) + + def show(self, req, id): + """Return data about the given flavor id.""" + for name, val in instance_types.INSTANCE_TYPES.iteritems(): + if val['flavorid'] == int(id): + item = dict(ram=val['memory_mb'], disk=val['local_gb'], + id=val['flavorid'], name=name) + return dict(flavor=item) + raise exc.HTTPNotFound() + + def _all_ids(self): + """Return the list of all flavorids.""" + return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()] diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 986f11434..2f3e928b9 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -15,4 +15,56 @@ # License for the specific language governing permissions and limitations # under the License. -class Controller(object): pass +import nova.image.service +from nova.api.rackspace import base +from nova.api.rackspace import _id_translator +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 = nova.image.service.ImageService.load() + self._id_translator = _id_translator.RackspaceAPIIdTranslator( + "image", self._service.__class__.__name__) + + def index(self, req): + """Return all public images in brief.""" + return dict(images=[dict(id=img['id'], name=img['name']) + for img in self.detail(req)['images']]) + + def detail(self, req): + """Return all public images in detail.""" + data = self._service.index() + for img in data: + img['id'] = self._id_translator.to_rs_id(img['id']) + return dict(images=data) + + def show(self, req, id): + """Return data about the given image id.""" + opaque_id = self._id_translator.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() 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/compute/instance_types.py b/nova/compute/instance_types.py index 439be3c7d..0102bae54 100644 --- a/nova/compute/instance_types.py +++ b/nova/compute/instance_types.py @@ -21,10 +21,10 @@ The built-in instance properties. """ -INSTANCE_TYPES = {} -INSTANCE_TYPES['m1.tiny'] = {'memory_mb': 512, 'vcpus': 1, 'local_gb': 0} -INSTANCE_TYPES['m1.small'] = {'memory_mb': 1024, 'vcpus': 1, 'local_gb': 10} -INSTANCE_TYPES['m1.medium'] = {'memory_mb': 2048, 'vcpus': 2, 'local_gb': 10} -INSTANCE_TYPES['m1.large'] = {'memory_mb': 4096, 'vcpus': 4, 'local_gb': 10} -INSTANCE_TYPES['m1.xlarge'] = {'memory_mb': 8192, 'vcpus': 4, 'local_gb': 10} -INSTANCE_TYPES['c1.medium'] = {'memory_mb': 2048, 'vcpus': 4, 'local_gb': 10} +INSTANCE_TYPES = { + 'm1.tiny': dict(memory_mb=512, vcpus=1, local_gb=0, flavorid=1), + 'm1.small': dict(memory_mb=1024, vcpus=1, local_gb=10, flavorid=2), + 'm1.medium': dict(memory_mb=2048, vcpus=2, local_gb=10, flavorid=3), + 'm1.large': dict(memory_mb=4096, vcpus=4, local_gb=10, flavorid=4), + 'm1.xlarge': dict(memory_mb=8192, vcpus=4, local_gb=10, flavorid=5), + 'c1.medium': dict(memory_mb=2048, vcpus=4, local_gb=10, flavorid=6)} diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 4f7f1c605..8e459c935 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -41,7 +41,6 @@ from nova.endpoint import images FLAGS = flags.FLAGS -flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') def _gen_key(user_id, key_name): diff --git a/nova/flags.py b/nova/flags.py index dfdfe9785..a99179837 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -142,6 +142,7 @@ def _wrapper(func): return _wrapped +DEFINE = _wrapper(gflags.DEFINE) DEFINE_string = _wrapper(gflags.DEFINE_string) DEFINE_integer = _wrapper(gflags.DEFINE_integer) DEFINE_bool = _wrapper(gflags.DEFINE_bool) @@ -169,7 +170,6 @@ def DECLARE(name, module_string, flag_values=FLAGS): DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') -#DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on') DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') DEFINE_string('network_topic', 'network', 'the topic network nodes listen on') diff --git a/nova/image/__init__.py b/nova/image/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/nova/image/__init__.py 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/objectstore/handler.py b/nova/objectstore/handler.py index 035e342ca..5c3dc286b 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -64,6 +64,7 @@ FLAGS = flags.FLAGS def render_xml(request, value): + """Writes value as XML string to request""" assert isinstance(value, dict) and len(value) == 1 request.setHeader("Content-Type", "application/xml; charset=UTF-8") @@ -77,12 +78,14 @@ def render_xml(request, value): def finish(request, content=None): + """Finalizer method for request""" if content: request.write(content) request.finish() def _render_parts(value, write_cb): + """Helper method to render different Python objects to XML""" if isinstance(value, basestring): write_cb(escape.xhtml_escape(value)) elif isinstance(value, int) or isinstance(value, long): @@ -102,37 +105,48 @@ def _render_parts(value, write_cb): def get_argument(request, key, default_value): + """Returns the request's value at key, or default_value + if not found + """ if key in request.args: return request.args[key][0] return default_value def get_context(request): + """Returns the supplied request's context object""" try: # Authorization Header format: 'AWS <access>:<secret>' authorization_header = request.getHeader('Authorization') if not authorization_header: raise exception.NotAuthorized - access, sep, secret = authorization_header.split(' ')[1].rpartition(':') - (user, project) = manager.AuthManager().authenticate(access, - secret, - {}, - request.method, - request.getRequestHostname(), - request.uri, - headers=request.getAllHeaders(), - check_type='s3') + auth_header_value = authorization_header.split(' ')[1] + access, _ignored, secret = auth_header_value.rpartition(':') + am = manager.AuthManager() + (user, project) = am.authenticate(access, + secret, + {}, + request.method, + request.getRequestHostname(), + request.uri, + headers=request.getAllHeaders(), + check_type='s3') return api.APIRequestContext(None, user, project) except exception.Error as ex: - logging.debug("Authentication Failure: %s" % ex) + logging.debug("Authentication Failure: %s", ex) raise exception.NotAuthorized - class ErrorHandlingResource(resource.Resource): - """Maps exceptions to 404 / 401 codes. Won't work for exceptions thrown after NOT_DONE_YET is returned.""" - # TODO(unassigned) (calling-all-twisted-experts): This needs to be plugged in to the right place in twisted... - # This doesn't look like it's the right place (consider exceptions in getChild; or after NOT_DONE_YET is returned + """Maps exceptions to 404 / 401 codes. Won't work for + exceptions thrown after NOT_DONE_YET is returned. + """ + # TODO(unassigned) (calling-all-twisted-experts): This needs to be + # plugged in to the right place in twisted... + # This doesn't look like it's the right place + # (consider exceptions in getChild; or after + # NOT_DONE_YET is returned def render(self, request): + """Renders the response as XML""" try: return resource.Resource.render(self, request) except exception.NotFound: @@ -145,7 +159,11 @@ class ErrorHandlingResource(resource.Resource): class S3(ErrorHandlingResource): """Implementation of an S3-like storage server based on local files.""" - def getChild(self, name, request): + def __init__(self): + ErrorHandlingResource.__init__(self) + + def getChild(self, name, request): # pylint: disable-msg=C0103 + """Returns either the image or bucket resource""" request.context = get_context(request) if name == '': return self @@ -154,9 +172,11 @@ class S3(ErrorHandlingResource): else: return BucketResource(name) - def render_GET(self, request): + def render_GET(self, request): # pylint: disable-msg=R0201 + """Renders the GET request for a list of buckets as XML""" logging.debug('List of buckets requested') - buckets = [b for b in bucket.Bucket.all() if b.is_authorized(request.context)] + buckets = [b for b in bucket.Bucket.all() \ + if b.is_authorized(request.context)] render_xml(request, {"ListAllMyBucketsResult": { "Buckets": {"Bucket": [b.metadata for b in buckets]}, @@ -165,22 +185,27 @@ class S3(ErrorHandlingResource): class BucketResource(ErrorHandlingResource): + """A web resource containing an S3-like bucket""" def __init__(self, name): resource.Resource.__init__(self) self.name = name def getChild(self, name, request): + """Returns the bucket resource itself, or the object resource + the bucket contains if a name is supplied + """ if name == '': return self else: return ObjectResource(bucket.Bucket(self.name), name) def render_GET(self, request): - logging.debug("List keys for bucket %s" % (self.name)) + "Returns the keys for the bucket resource""" + logging.debug("List keys for bucket %s", self.name) try: bucket_object = bucket.Bucket(self.name) - except exception.NotFound, e: + except exception.NotFound: return error.NoResource(message="No such bucket").render(request) if not bucket_object.is_authorized(request.context): @@ -191,19 +216,26 @@ class BucketResource(ErrorHandlingResource): max_keys = int(get_argument(request, "max-keys", 1000)) terse = int(get_argument(request, "terse", 0)) - results = bucket_object.list_keys(prefix=prefix, marker=marker, max_keys=max_keys, terse=terse) + results = bucket_object.list_keys(prefix=prefix, + marker=marker, + max_keys=max_keys, + terse=terse) render_xml(request, {"ListBucketResult": results}) return server.NOT_DONE_YET def render_PUT(self, request): - logging.debug("Creating bucket %s" % (self.name)) - logging.debug("calling bucket.Bucket.create(%r, %r)" % (self.name, request.context)) + "Creates the bucket resource""" + logging.debug("Creating bucket %s", self.name) + logging.debug("calling bucket.Bucket.create(%r, %r)", + self.name, + request.context) bucket.Bucket.create(self.name, request.context) request.finish() return server.NOT_DONE_YET def render_DELETE(self, request): - logging.debug("Deleting bucket %s" % (self.name)) + """Deletes the bucket resource""" + logging.debug("Deleting bucket %s", self.name) bucket_object = bucket.Bucket(self.name) if not bucket_object.is_authorized(request.context): @@ -215,25 +247,37 @@ class BucketResource(ErrorHandlingResource): class ObjectResource(ErrorHandlingResource): + """The resource returned from a bucket""" def __init__(self, bucket, name): resource.Resource.__init__(self) self.bucket = bucket self.name = name def render_GET(self, request): - logging.debug("Getting object: %s / %s" % (self.bucket.name, self.name)) + """Returns the object + + Raises NotAuthorized if user in request context is not + authorized to delete the object. + """ + logging.debug("Getting object: %s / %s", self.bucket.name, self.name) if not self.bucket.is_authorized(request.context): raise exception.NotAuthorized obj = self.bucket[urllib.unquote(self.name)] request.setHeader("Content-Type", "application/unknown") - request.setHeader("Last-Modified", datetime.datetime.utcfromtimestamp(obj.mtime)) + request.setHeader("Last-Modified", + datetime.datetime.utcfromtimestamp(obj.mtime)) request.setHeader("Etag", '"' + obj.md5 + '"') return static.File(obj.path).render_GET(request) def render_PUT(self, request): - logging.debug("Putting object: %s / %s" % (self.bucket.name, self.name)) + """Modifies/inserts the object and returns a result code + + Raises NotAuthorized if user in request context is not + authorized to delete the object. + """ + logging.debug("Putting object: %s / %s", self.bucket.name, self.name) if not self.bucket.is_authorized(request.context): raise exception.NotAuthorized @@ -246,7 +290,15 @@ class ObjectResource(ErrorHandlingResource): return server.NOT_DONE_YET def render_DELETE(self, request): - logging.debug("Deleting object: %s / %s" % (self.bucket.name, self.name)) + """Deletes the object and returns a result code + + Raises NotAuthorized if user in request context is not + authorized to delete the object. + """ + + logging.debug("Deleting object: %s / %s", + self.bucket.name, + self.name) if not self.bucket.is_authorized(request.context): raise exception.NotAuthorized @@ -257,6 +309,7 @@ class ObjectResource(ErrorHandlingResource): class ImageResource(ErrorHandlingResource): + """A web resource representing a single image""" isLeaf = True def __init__(self, name): @@ -264,17 +317,21 @@ class ImageResource(ErrorHandlingResource): self.img = image.Image(name) def render_GET(self, request): - return static.File(self.img.image_path, defaultType='application/octet-stream').render_GET(request) - + """Returns the image file""" + return static.File(self.img.image_path, + defaultType='application/octet-stream' + ).render_GET(request) class ImagesResource(resource.Resource): - def getChild(self, name, request): + """A web resource representing a list of images""" + def getChild(self, name, _request): + """Returns itself or an ImageResource if no name given""" if name == '': return self else: return ImageResource(name) - def render_GET(self, request): + def render_GET(self, request): # pylint: disable-msg=R0201 """ returns a json listing of all images that a user has permissions to see """ @@ -301,7 +358,7 @@ class ImagesResource(resource.Resource): request.finish() return server.NOT_DONE_YET - def render_PUT(self, request): + def render_PUT(self, request): # pylint: disable-msg=R0201 """ create a new registered image """ image_id = get_argument(request, 'image_id', u'') @@ -313,7 +370,6 @@ class ImagesResource(resource.Resource): raise exception.NotAuthorized bucket_object = bucket.Bucket(image_location.split("/")[0]) - manifest = image_location[len(image_location.split('/')[0])+1:] if not bucket_object.is_authorized(request.context): raise exception.NotAuthorized @@ -323,8 +379,8 @@ class ImagesResource(resource.Resource): p.start() return '' - def render_POST(self, request): - """ update image attributes: public/private """ + def render_POST(self, request): # pylint: disable-msg=R0201 + """Update image attributes: public/private""" image_id = get_argument(request, 'image_id', u'') operation = get_argument(request, 'operation', u'') @@ -338,8 +394,8 @@ class ImagesResource(resource.Resource): return '' - def render_DELETE(self, request): - """ delete a registered image """ + def render_DELETE(self, request): # pylint: disable-msg=R0201 + """Delete a registered image""" image_id = get_argument(request, "image_id", u"") image_object = image.Image(image_id) @@ -353,14 +409,19 @@ class ImagesResource(resource.Resource): def get_site(): + """Support for WSGI-like interfaces""" root = S3() site = server.Site(root) return site def get_application(): + """Support WSGI-like interfaces""" factory = get_site() application = service.Application("objectstore") + # Disabled because of lack of proper introspection in Twisted + # or possibly different versions of twisted? + # pylint: disable-msg=E1101 objectStoreService = internet.TCPServer(FLAGS.s3_port, factory) objectStoreService.setServiceParent(application) return application diff --git a/nova/server.py b/nova/server.py index 96550f078..c6b60e090 100644 --- a/nova/server.py +++ b/nova/server.py @@ -44,6 +44,8 @@ flags.DEFINE_bool('use_syslog', True, 'output to syslog when daemonizing') flags.DEFINE_string('logfile', None, 'log file to output to') flags.DEFINE_string('pidfile', None, 'pid file to output to') flags.DEFINE_string('working_directory', './', 'working directory...') +flags.DEFINE_integer('uid', os.getuid(), 'uid under which to run') +flags.DEFINE_integer('gid', os.getgid(), 'gid under which to run') def stop(pidfile): @@ -135,6 +137,8 @@ def daemonize(args, name, main): threaded=False), stdin=stdin, stdout=stdout, - stderr=stderr + stderr=stderr, + uid=FLAGS.uid, + gid=FLAGS.gid ): main(args) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 9d072866c..462d1b295 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -16,6 +16,8 @@ # License for the specific language governing permissions and limitations # under the License. +"""Unit tests for the API endpoint""" + import boto from boto.ec2 import regioninfo import httplib @@ -38,7 +40,15 @@ FLAGS = flags.FLAGS # circuit boto calls and feed them into our tornado handlers, # it's pretty damn circuitous so apologies if you have to fix # a bug in it -def boto_to_tornado(method, path, headers, data, host, connection=None): +# NOTE(jaypipes) The pylint disables here are for R0913 (too many args) which +# isn't controllable since boto's HTTPRequest needs that many +# args, and for the version-differentiated import of tornado's +# httputil. +# NOTE(jaypipes): The disable-msg=E1101 and E1103 below is because pylint is +# unable to introspect the deferred's return value properly + +def boto_to_tornado(method, path, headers, data, # pylint: disable-msg=R0913 + host, connection=None): """ translate boto requests into tornado requests connection should be a FakeTornadoHttpConnection instance @@ -46,7 +56,7 @@ def boto_to_tornado(method, path, headers, data, host, connection=None): try: headers = httpserver.HTTPHeaders() except AttributeError: - from tornado import httputil + from tornado import httputil # pylint: disable-msg=E0611 headers = httputil.HTTPHeaders() for k, v in headers.iteritems(): headers[k] = v @@ -61,57 +71,64 @@ def boto_to_tornado(method, path, headers, data, host, connection=None): return req -def raw_to_httpresponse(s): - """ translate a raw tornado http response into an httplib.HTTPResponse """ - sock = FakeHttplibSocket(s) +def raw_to_httpresponse(response_string): + """translate a raw tornado http response into an httplib.HTTPResponse""" + sock = FakeHttplibSocket(response_string) resp = httplib.HTTPResponse(sock) resp.begin() return resp class FakeHttplibSocket(object): - """ a fake socket implementation for httplib.HTTPResponse, trivial """ - def __init__(self, s): - self.fp = StringIO.StringIO(s) + """a fake socket implementation for httplib.HTTPResponse, trivial""" + def __init__(self, response_string): + self._buffer = StringIO.StringIO(response_string) - def makefile(self, mode, other): - return self.fp + def makefile(self, _mode, _other): + """Returns the socket's internal buffer""" + return self._buffer class FakeTornadoStream(object): - """ a fake stream to satisfy tornado's assumptions, trivial """ - def set_close_callback(self, f): + """a fake stream to satisfy tornado's assumptions, trivial""" + def set_close_callback(self, _func): + """Dummy callback for stream""" pass class FakeTornadoConnection(object): - """ a fake connection object for tornado to pass to its handlers + """A fake connection object for tornado to pass to its handlers web requests are expected to write to this as they get data and call finish when they are done with the request, we buffer the writes and kick off a callback when it is done so that we can feed the result back into boto. """ - def __init__(self, d): - self.d = d + def __init__(self, deferred): + self._deferred = deferred self._buffer = StringIO.StringIO() def write(self, chunk): + """Writes a chunk of data to the internal buffer""" self._buffer.write(chunk) def finish(self): - s = self._buffer.getvalue() - self.d.callback(s) + """Finalizes the connection and returns the buffered data via the + deferred callback. + """ + data = self._buffer.getvalue() + self._deferred.callback(data) xheaders = None @property - def stream(self): + def stream(self): # pylint: disable-msg=R0201 + """Required property for interfacing with tornado""" return FakeTornadoStream() class FakeHttplibConnection(object): - """ a fake httplib.HTTPConnection for boto to use + """A fake httplib.HTTPConnection for boto to use requests made via this connection actually get translated and routed into our tornado app, we then wait for the response and turn it back into @@ -123,7 +140,9 @@ class FakeHttplibConnection(object): self.deferred = defer.Deferred() def request(self, method, path, data, headers): - req = boto_to_tornado + """Creates a connection to a fake tornado and sets + up a deferred request with the supplied data and + headers""" conn = FakeTornadoConnection(self.deferred) request = boto_to_tornado(connection=conn, method=method, @@ -131,12 +150,16 @@ class FakeHttplibConnection(object): headers=headers, data=data, host=self.host) - handler = self.app(request) + self.app(request) self.deferred.addCallback(raw_to_httpresponse) def getresponse(self): + """A bit of deferred magic for catching the response + from the previously deferred request""" @defer.inlineCallbacks def _waiter(): + """Callback that simply yields the deferred's + return value.""" result = yield self.deferred defer.returnValue(result) d = _waiter() @@ -144,14 +167,16 @@ class FakeHttplibConnection(object): # this deferred has already been called by the time # we get here, we are going to cheat and return # the result of the callback - return d.result + return d.result # pylint: disable-msg=E1101 def close(self): + """Required for compatibility with boto/tornado""" pass class ApiEc2TestCase(test.BaseTestCase): - def setUp(self): + """Unit test for the cloud controller on an EC2 API""" + def setUp(self): # pylint: disable-msg=C0103,C0111 super(ApiEc2TestCase, self).setUp() self.manager = manager.AuthManager() @@ -171,12 +196,16 @@ class ApiEc2TestCase(test.BaseTestCase): self.mox.StubOutWithMock(self.ec2, 'new_http_connection') def expect_http(self, host=None, is_secure=False): + """Returns a new EC2 connection""" http = FakeHttplibConnection( self.app, '%s:%d' % (self.host, FLAGS.cc_port), False) + # pylint: disable-msg=E1103 self.ec2.new_http_connection(host, is_secure).AndReturn(http) return http def test_describe_instances(self): + """Test that, after creating a user and a project, the describe + instances call to the API works properly""" self.expect_http() self.mox.ReplayAll() user = self.manager.create_user('fake', 'fake', 'fake') @@ -187,14 +216,18 @@ class ApiEc2TestCase(test.BaseTestCase): def test_get_all_key_pairs(self): + """Test that, after creating a user and project and generating + a key pair, that the API call to list key pairs works properly""" self.expect_http() self.mox.ReplayAll() - keyname = "".join(random.choice("sdiuisudfsdcnpaqwertasd") for x in range(random.randint(4, 8))) + keyname = "".join(random.choice("sdiuisudfsdcnpaqwertasd") \ + for x in range(random.randint(4, 8))) user = self.manager.create_user('fake', 'fake', 'fake') project = self.manager.create_project('fake', 'fake', 'fake') self.manager.generate_key_pair(user.id, keyname) rv = self.ec2.get_all_key_pairs() - self.assertTrue(filter(lambda k: k.name == keyname, rv)) + results = [k for k in rv if k.name == keyname] + self.assertEquals(len(results), 1) self.manager.delete_project(project) self.manager.delete_user(user) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index df2246aae..e6796e3da 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -47,10 +47,6 @@ class CloudTestCase(test.BaseTestCase): # set up our cloud self.cloud = cloud.CloudController() - self.cloud_consumer = rpc.AdapterConsumer(connection=self.conn, - topic=FLAGS.cloud_topic, - proxy=self.cloud) - self.injected.append(self.cloud_consumer.attach_to_tornado(self.ioloop)) # set up a service self.compute = utils.import_class(FLAGS.compute_manager) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index b479f2fa4..b284e4e51 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -77,8 +77,7 @@ class NetworkTestCase(test.TrialTestCase): def _create_address(self, project_num, instance_id=None): net = db.project_get_network(None, self.projects[project_num].id) - fixed_ip = db.fixed_ip_allocate(None, net['id']) - address = fixed_ip['str_id'] + address = db.fixed_ip_allocate(None, net['id']) if instance_id is None: instance_id = self.instance_id db.fixed_ip_instance_associate(None, address, instance_id) diff --git a/nova/twistd.py b/nova/twistd.py index 8de322aa5..9511c231c 100644 --- a/nova/twistd.py +++ b/nova/twistd.py @@ -21,6 +21,7 @@ Twisted daemon helpers, specifically to parse out gFlags from twisted flags, manage pid files and support syslogging. """ +import gflags import logging import os import signal @@ -49,6 +50,14 @@ class TwistdServerOptions(ServerOptions): return +class FlagParser(object): + def __init__(self, parser): + self.parser = parser + + def Parse(self, s): + return self.parser(s) + + def WrapTwistedOptions(wrapped): class TwistedOptionsToFlags(wrapped): subCommands = None @@ -79,7 +88,12 @@ def WrapTwistedOptions(wrapped): reflect.accumulateClassList(self.__class__, 'optParameters', twistd_params) for param in twistd_params: key = param[0].replace('-', '_') - flags.DEFINE_string(key, param[2], str(param[-1])) + if len(param) > 4: + flags.DEFINE(FlagParser(param[4]), + key, param[2], str(param[3]), + serializer=gflags.ArgumentSerializer()) + else: + flags.DEFINE_string(key, param[2], str(param[3])) def _absorbHandlers(self): twistd_handlers = {} diff --git a/nova/utils.py b/nova/utils.py index 907c174cd..536d722bb 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -29,6 +29,8 @@ import subprocess import socket import sys +from twisted.internet.threads import deferToThread + from nova import exception from nova import flags @@ -157,7 +159,6 @@ def parse_isotime(timestr): return datetime.datetime.strptime(timestr, TIME_FORMAT) - class LazyPluggable(object): """A pluggable backend loaded lazily based on some value.""" @@ -188,3 +189,7 @@ class LazyPluggable(object): backend = self.__get_backend() return getattr(backend, key) +def deferredToThread(f): + def g(*args, **kwargs): + return deferToThread(f, *args, **kwargs) + return g diff --git a/nova/virt/connection.py b/nova/virt/connection.py index 90bc7fa0a..34e37adf7 100644 --- a/nova/virt/connection.py +++ b/nova/virt/connection.py @@ -17,6 +17,11 @@ # License for the specific language governing permissions and limitations # under the License. +"""Abstraction of the underlying virtualization API""" + +import logging +import sys + from nova import flags from nova.virt import fake from nova.virt import libvirt_conn @@ -35,7 +40,6 @@ def get_connection(read_only=False): Any object returned here must conform to the interface documented by FakeConnection. """ - # TODO(termie): maybe lazy load after initial check for permissions # TODO(termie): check whether we can be disconnected t = FLAGS.connection_type diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 7b16781e7..823eb1e0b 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -85,10 +85,22 @@ class LibvirtConnection(object): @property def _conn(self): - if not self._wrapped_conn: + if not self._wrapped_conn or not self._test_connection(): + logging.debug('Connecting to libvirt: %s' % self.libvirt_uri) self._wrapped_conn = self._connect(self.libvirt_uri, self.read_only) return self._wrapped_conn + def _test_connection(self): + try: + libvirt.getVersion() + return True + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_SYSTEM_ERROR and \ + e.get_error_domain() == libvirt.VIR_FROM_REMOTE: + logging.debug('Connection to libvirt broke') + return False + raise + def get_uri_and_template(self): if FLAGS.libvirt_type == 'uml': uri = FLAGS.libvirt_uri or 'uml:///system' diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 2f5994983..b44ac383a 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -16,17 +16,35 @@ """ A connection to XenServer or Xen Cloud Platform. + +The concurrency model for this class is as follows: + +All XenAPI calls are on a thread (using t.i.t.deferToThread, via the decorator +deferredToThread). They are remote calls, and so may hang for the usual +reasons. They should not be allowed to block the reactor thread. + +All long-running XenAPI calls (VM.start, VM.reboot, etc) are called async +(using XenAPI.VM.async_start etc). These return a task, which can then be +polled for completion. Polling is handled using reactor.callLater. + +This combination of techniques means that we don't block the reactor thread at +all, and at the same time we don't hold lots of threads waiting for +long-running operations. + +FIXME: get_info currently doesn't conform to these rules, and will block the +reactor thread if the VM.get_by_name_label or VM.get_record calls block. """ import logging import xmlrpclib from twisted.internet import defer +from twisted.internet import reactor from twisted.internet import task -from nova import exception from nova import flags from nova import process +from nova import utils from nova.auth.manager import AuthManager from nova.compute import power_state from nova.virt import images @@ -47,6 +65,11 @@ flags.DEFINE_string('xenapi_connection_password', None, 'Password for connection to XenServer/Xen Cloud Platform.' ' Used only if connection_type=xenapi.') +flags.DEFINE_float('xenapi_task_poll_interval', + 0.5, + 'The interval used for polling of remote tasks ' + '(Async.VM.start, etc). Used only if ' + 'connection_type=xenapi.') XENAPI_POWER_STATE = { @@ -84,9 +107,8 @@ class XenAPIConnection(object): for vm in self._conn.xenapi.VM.get_all()] @defer.inlineCallbacks - @exception.wrap_exception def spawn(self, instance): - vm = yield self.lookup(instance.name) + vm = yield self._lookup(instance.name) if vm is not None: raise Exception('Attempted to create non-unique name %s' % instance.name) @@ -105,21 +127,27 @@ class XenAPIConnection(object): user = AuthManager().get_user(instance.datamodel['user_id']) project = AuthManager().get_project(instance.datamodel['project_id']) - vdi_uuid = yield self.fetch_image( + vdi_uuid = yield self._fetch_image( instance.datamodel['image_id'], user, project, True) - kernel = yield self.fetch_image( + kernel = yield self._fetch_image( instance.datamodel['kernel_id'], user, project, False) - ramdisk = yield self.fetch_image( + ramdisk = yield self._fetch_image( instance.datamodel['ramdisk_id'], user, project, False) - vdi_ref = yield self._conn.xenapi.VDI.get_by_uuid(vdi_uuid) + vdi_ref = yield self._call_xenapi('VDI.get_by_uuid', vdi_uuid) - vm_ref = yield self.create_vm(instance, kernel, ramdisk) - yield self.create_vbd(vm_ref, vdi_ref, 0, True) + vm_ref = yield self._create_vm(instance, kernel, ramdisk) + yield self._create_vbd(vm_ref, vdi_ref, 0, True) if network_ref: yield self._create_vif(vm_ref, network_ref, mac_address) - yield self._conn.xenapi.VM.start(vm_ref, False, False) + logging.debug('Starting VM %s...', vm_ref) + yield self._call_xenapi('VM.start', vm_ref, False, False) + logging.info('Spawning VM %s created %s.', instance.name, vm_ref) - def create_vm(self, instance, kernel, ramdisk): + @defer.inlineCallbacks + def _create_vm(self, instance, kernel, ramdisk): + """Create a VM record. Returns a Deferred that gives the new + VM reference.""" + mem = str(long(instance.datamodel['memory_kb']) * 1024) vcpus = str(instance.datamodel['vcpus']) rec = { @@ -152,11 +180,15 @@ class XenAPIConnection(object): 'other_config': {}, } logging.debug('Created VM %s...', instance.name) - vm_ref = self._conn.xenapi.VM.create(rec) + vm_ref = yield self._call_xenapi('VM.create', rec) logging.debug('Created VM %s as %s.', instance.name, vm_ref) - return vm_ref + defer.returnValue(vm_ref) - def create_vbd(self, vm_ref, vdi_ref, userdevice, bootable): + @defer.inlineCallbacks + def _create_vbd(self, vm_ref, vdi_ref, userdevice, bootable): + """Create a VBD record. Returns a Deferred that gives the new + VBD reference.""" + vbd_rec = {} vbd_rec['VM'] = vm_ref vbd_rec['VDI'] = vdi_ref @@ -171,12 +203,16 @@ class XenAPIConnection(object): vbd_rec['qos_algorithm_params'] = {} vbd_rec['qos_supported_algorithms'] = [] logging.debug('Creating VBD for VM %s, VDI %s ... ', vm_ref, vdi_ref) - vbd_ref = self._conn.xenapi.VBD.create(vbd_rec) + vbd_ref = yield self._call_xenapi('VBD.create', vbd_rec) logging.debug('Created VBD %s for VM %s, VDI %s.', vbd_ref, vm_ref, vdi_ref) - return vbd_ref + defer.returnValue(vbd_ref) + @defer.inlineCallbacks def _create_vif(self, vm_ref, network_ref, mac_address): + """Create a VIF record. Returns a Deferred that gives the new + VIF reference.""" + vif_rec = {} vif_rec['device'] = '0' vif_rec['network']= network_ref @@ -188,25 +224,29 @@ class XenAPIConnection(object): vif_rec['qos_algorithm_params'] = {} logging.debug('Creating VIF for VM %s, network %s ... ', vm_ref, network_ref) - vif_ref = self._conn.xenapi.VIF.create(vif_rec) + vif_ref = yield self._call_xenapi('VIF.create', vif_rec) logging.debug('Created VIF %s for VM %s, network %s.', vif_ref, vm_ref, network_ref) - return vif_ref + defer.returnValue(vif_ref) + @defer.inlineCallbacks def _find_network_with_bridge(self, bridge): expr = 'field "bridge" = "%s"' % bridge - networks = self._conn.xenapi.network.get_all_records_where(expr) + networks = yield self._call_xenapi('network.get_all_records_where', + expr) if len(networks) == 1: - return networks.keys()[0] + defer.returnValue(networks.keys()[0]) elif len(networks) > 1: raise Exception('Found non-unique network for bridge %s' % bridge) else: raise Exception('Found no network for bridge %s' % bridge) - def fetch_image(self, image, user, project, use_sr): + @defer.inlineCallbacks + def _fetch_image(self, image, user, project, use_sr): """use_sr: True to put the image as a VDI in an SR, False to place it on dom0's filesystem. The former is for VM disks, the latter for - its kernel and ramdisk (if external kernels are being used).""" + its kernel and ramdisk (if external kernels are being used). + Returns a Deferred that gives the new VDI UUID.""" url = images.image_url(image) access = AuthManager().get_access_key(user, project) @@ -218,22 +258,28 @@ class XenAPIConnection(object): args['password'] = user.secret if use_sr: args['add_partition'] = 'true' - return self._call_plugin('objectstore', fn, args) + task = yield self._async_call_plugin('objectstore', fn, args) + uuid = yield self._wait_for_task(task) + defer.returnValue(uuid) + @defer.inlineCallbacks def reboot(self, instance): - vm = self.lookup(instance.name) + vm = yield self._lookup(instance.name) if vm is None: raise Exception('instance not present %s' % instance.name) - yield self._conn.xenapi.VM.clean_reboot(vm) + task = yield self._call_xenapi('Async.VM.clean_reboot', vm) + yield self._wait_for_task(task) + @defer.inlineCallbacks def destroy(self, instance): - vm = self.lookup(instance.name) + vm = yield self._lookup(instance.name) if vm is None: raise Exception('instance not present %s' % instance.name) - yield self._conn.xenapi.VM.destroy(vm) + task = yield self._call_xenapi('Async.VM.destroy', vm) + yield self._wait_for_task(task) def get_info(self, instance_id): - vm = self.lookup(instance_id) + vm = self._lookup_blocking(instance_id) if vm is None: raise Exception('instance not present %s' % instance_id) rec = self._conn.xenapi.VM.get_record(vm) @@ -243,7 +289,11 @@ class XenAPIConnection(object): 'num_cpu': rec['VCPUs_max'], 'cpu_time': 0} - def lookup(self, i): + @utils.deferredToThread + def _lookup(self, i): + return self._lookup_blocking(i) + + def _lookup_blocking(self, i): vms = self._conn.xenapi.VM.get_by_name_label(i) n = len(vms) if n == 0: @@ -253,9 +303,52 @@ class XenAPIConnection(object): else: return vms[0] - def _call_plugin(self, plugin, fn, args): + def _wait_for_task(self, task): + """Return a Deferred that will give the result of the given task. + The task is polled until it completes.""" + d = defer.Deferred() + reactor.callLater(0, self._poll_task, task, d) + return d + + @utils.deferredToThread + def _poll_task(self, task, deferred): + """Poll the given XenAPI task, and fire the given Deferred if we + get a result.""" + try: + #logging.debug('Polling task %s...', task) + status = self._conn.xenapi.task.get_status(task) + if status == 'pending': + reactor.callLater(FLAGS.xenapi_task_poll_interval, + self._poll_task, task, deferred) + elif status == 'success': + result = self._conn.xenapi.task.get_result(task) + logging.info('Task %s status: success. %s', task, result) + deferred.callback(_parse_xmlrpc_value(result)) + else: + error_info = self._conn.xenapi.task.get_error_info(task) + logging.warn('Task %s status: %s. %s', task, status, + error_info) + deferred.errback(XenAPI.Failure(error_info)) + #logging.debug('Polling task %s done.', task) + except Exception, exn: + logging.warn(exn) + deferred.errback(exn) + + @utils.deferredToThread + def _call_xenapi(self, method, *args): + """Call the specified XenAPI method on a background thread. Returns + a Deferred for the result.""" + f = self._conn.xenapi + for m in method.split('.'): + f = f.__getattr__(m) + return f(*args) + + @utils.deferredToThread + def _async_call_plugin(self, plugin, fn, args): + """Call Async.host.call_plugin on a background thread. Returns a + Deferred with the task reference.""" return _unwrap_plugin_exceptions( - self._conn.xenapi.host.call_plugin, + self._conn.xenapi.Async.host.call_plugin, self._get_xenapi_host(), plugin, fn, args) def _get_xenapi_host(self): @@ -281,3 +374,15 @@ def _unwrap_plugin_exceptions(func, *args, **kwargs): except xmlrpclib.ProtocolError, exn: logging.debug("Got exception: %s", exn) raise + + +def _parse_xmlrpc_value(val): + """Parse the given value as if it were an XML-RPC value. This is + sometimes used as the format for the task.result field.""" + if not val: + return val + x = xmlrpclib.loads( + '<?xml version="1.0"?><methodResponse><params><param>' + + val + + '</param></params></methodResponse>') + return x[0][0] 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): diff --git a/tools/install_venv.py b/tools/install_venv.py index 1f0fa3cc7..5d2369a96 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -95,8 +95,8 @@ def install_dependencies(venv=VENV): # Tell the virtual env how to "import nova" - pathfile=os.path.join(venv, "lib", "python2.6", "site-packages", "nova.pth") - f=open(pathfile, 'w') + pthfile = os.path.join(venv, "lib", "python2.6", "site-packages", "nova.pth") + f = open(pthfile, 'w') f.write("%s\n" % ROOT) |
