diff options
| author | Ken Pepple <ken.pepple@gmail.com> | 2011-04-04 08:15:51 -0700 |
|---|---|---|
| committer | Ken Pepple <ken.pepple@gmail.com> | 2011-04-04 08:15:51 -0700 |
| commit | 871836ed8609ee5178f983a33ba4b431f16bd647 (patch) | |
| tree | a246fdfb7731555c50d7d65175cfee679d6ca48b /nova/api | |
| parent | 5c74862a08a82b7db3e11fbcbec63293ea903e00 (diff) | |
| parent | 0ec4352046939785b3ffa390e6d8264ce4d99f98 (diff) | |
merge trunk
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/ec2/cloud.py | 9 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 13 | ||||
| -rw-r--r-- | nova/api/openstack/common.py | 6 | ||||
| -rw-r--r-- | nova/api/openstack/contrib/__init__.py | 22 | ||||
| -rw-r--r-- | nova/api/openstack/contrib/volumes.py | 336 | ||||
| -rw-r--r-- | nova/api/openstack/extensions.py | 204 | ||||
| -rw-r--r-- | nova/api/openstack/faults.py | 1 | ||||
| -rw-r--r-- | nova/api/openstack/images.py | 307 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 85 | ||||
| -rw-r--r-- | nova/api/openstack/versions.py | 10 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 98 |
11 files changed, 793 insertions, 298 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 9e34d3317..425784e8a 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -536,6 +536,13 @@ class CloudController(object): return self.compute_api.get_ajax_console(context, instance_id=instance_id) + def get_vnc_console(self, context, instance_id, **kwargs): + """Returns vnc browser url. Used by OS dashboard.""" + ec2_id = instance_id + instance_id = ec2utils.ec2_id_to_id(ec2_id) + return self.compute_api.get_vnc_console(context, + instance_id=instance_id) + def describe_volumes(self, context, volume_id=None, **kwargs): if volume_id: volumes = [] @@ -750,6 +757,8 @@ class CloudController(object): iterator = db.floating_ip_get_all_by_project(context, context.project_id) for floating_ip_ref in iterator: + if floating_ip_ref['project_id'] is None: + continue address = floating_ip_ref['address'] ec2_id = None if (floating_ip_ref['fixed_ip'] diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 283cebb4d..7545eb0c9 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -111,12 +111,11 @@ 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) + super(APIRouter, self).__init__(mapper) + class APIRouterV10(APIRouter): """Define routes specific to OpenStack API V1.0.""" @@ -128,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'}) @@ -152,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/common.py b/nova/api/openstack/common.py index 8cad1273a..75aeb0a5f 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -21,6 +21,11 @@ import webob from nova import exception from nova import flags +from nova import log as logging + + +LOG = logging.getLogger('common') + FLAGS = flags.FLAGS @@ -121,4 +126,5 @@ def get_id_from_href(href): try: return int(urlparse(href).path.split('/')[-1]) except: + LOG.debug(_("Error extracting id from href: %s") % href) raise webob.exc.HTTPBadRequest(_('could not parse id from href')) diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py new file mode 100644 index 000000000..b42a1d89d --- /dev/null +++ b/nova/api/openstack/contrib/__init__.py @@ -0,0 +1,22 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# 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 datetime + +"""Contrib contains extensions that are shipped with nova. + +It can't be called 'extensions' because that causes namespacing problems. + +""" diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py new file mode 100644 index 000000000..6efacce52 --- /dev/null +++ b/nova/api/openstack/contrib/volumes.py @@ -0,0 +1,336 @@ +# Copyright 2011 Justin Santa Barbara +# 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. + +"""The volumes extension.""" + +from webob import exc + +from nova import compute +from nova import exception +from nova import flags +from nova import log as logging +from nova import volume +from nova import wsgi +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults + + +LOG = logging.getLogger("nova.api.volumes") + + +FLAGS = flags.FLAGS + + +def _translate_volume_detail_view(context, vol): + """Maps keys for volumes details view.""" + + d = _translate_volume_summary_view(context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_volume_summary_view(context, vol): + """Maps keys for volumes summary view.""" + d = {} + + d['id'] = vol['id'] + d['status'] = vol['status'] + d['size'] = vol['size'] + d['availabilityZone'] = vol['availability_zone'] + d['createdAt'] = vol['created_at'] + + if vol['attach_status'] == 'attached': + d['attachments'] = [_translate_attachment_detail_view(context, vol)] + else: + d['attachments'] = [{}] + + d['displayName'] = vol['display_name'] + d['displayDescription'] = vol['display_description'] + return d + + +class VolumeController(wsgi.Controller): + """The Volumes API controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "volume": [ + "id", + "status", + "size", + "availabilityZone", + "createdAt", + "displayName", + "displayDescription", + ]}}} + + def __init__(self): + self.volume_api = volume.API() + super(VolumeController, self).__init__() + + def show(self, req, id): + """Return data about the given volume.""" + context = req.environ['nova.context'] + + try: + vol = self.volume_api.get(context, id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + return {'volume': _translate_volume_detail_view(context, vol)} + + def delete(self, req, id): + """Delete a volume.""" + context = req.environ['nova.context'] + + LOG.audit(_("Delete volume with id: %s"), id, context=context) + + try: + self.volume_api.delete(context, volume_id=id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + def index(self, req): + """Returns a summary list of volumes.""" + return self._items(req, entity_maker=_translate_volume_summary_view) + + def detail(self, req): + """Returns a detailed list of volumes.""" + return self._items(req, entity_maker=_translate_volume_detail_view) + + def _items(self, req, entity_maker): + """Returns a list of volumes, transformed through entity_maker.""" + context = req.environ['nova.context'] + + volumes = self.volume_api.get_all(context) + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumes': res} + + def create(self, req): + """Creates a new volume.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + vol = env['volume'] + size = vol['size'] + LOG.audit(_("Create volume of %s GB"), size, context=context) + new_volume = self.volume_api.create(context, size, + vol.get('display_name'), + vol.get('display_description')) + + # Work around problem that instance is lazy-loaded... + new_volume['instance'] = None + + retval = _translate_volume_detail_view(context, new_volume) + + return {'volume': retval} + + +def _translate_attachment_detail_view(_context, vol): + """Maps keys for attachment details view.""" + + d = _translate_attachment_summary_view(_context, vol) + + # No additional data / lookups at the moment + + return d + + +def _translate_attachment_summary_view(_context, vol): + """Maps keys for attachment summary view.""" + d = {} + + volume_id = vol['id'] + + # NOTE(justinsb): We use the volume id as the id of the attachment object + d['id'] = volume_id + + d['volumeId'] = volume_id + if vol.get('instance_id'): + d['serverId'] = vol['instance_id'] + if vol.get('mountpoint'): + d['device'] = vol['mountpoint'] + + return d + + +class VolumeAttachmentController(wsgi.Controller): + """The volume attachment API controller for the Openstack API. + + A child resource of the server. Note that we use the volume id + as the ID of the attachment (though this is not guaranteed externally) + + """ + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'volumeAttachment': ['id', + 'serverId', + 'volumeId', + 'device']}}} + + def __init__(self): + self.compute_api = compute.API() + self.volume_api = volume.API() + super(VolumeAttachmentController, self).__init__() + + def index(self, req, server_id): + """Returns the list of volume attachments for a given instance.""" + return self._items(req, server_id, + entity_maker=_translate_attachment_summary_view) + + def show(self, req, server_id, id): + """Return data about the given volume attachment.""" + context = req.environ['nova.context'] + + volume_id = id + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + LOG.debug("volume_id not found") + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + return {'volumeAttachment': _translate_attachment_detail_view(context, + vol)} + + def create(self, req, server_id): + """Attach a volume to an instance.""" + context = req.environ['nova.context'] + + env = self._deserialize(req.body, req.get_content_type()) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + instance_id = server_id + volume_id = env['volumeAttachment']['volumeId'] + device = env['volumeAttachment']['device'] + + msg = _("Attach volume %(volume_id)s to instance %(server_id)s" + " at %(device)s") % locals() + LOG.audit(msg, context=context) + + try: + self.compute_api.attach_volume(context, + instance_id=instance_id, + volume_id=volume_id, + device=device) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + # The attach is async + attachment = {} + attachment['id'] = volume_id + attachment['volumeId'] = volume_id + + # NOTE(justinsb): And now, we have a problem... + # The attach is async, so there's a window in which we don't see + # the attachment (until the attachment completes). We could also + # get problems with concurrent requests. I think we need an + # attachment state, and to write to the DB here, but that's a bigger + # change. + # For now, we'll probably have to rely on libraries being smart + + # TODO(justinsb): How do I return "accepted" here? + return {'volumeAttachment': attachment} + + def update(self, _req, _server_id, _id): + """Update a volume attachment. We don't currently support this.""" + return faults.Fault(exc.HTTPBadRequest()) + + def delete(self, req, server_id, id): + """Detach a volume from an instance.""" + context = req.environ['nova.context'] + + volume_id = id + LOG.audit(_("Detach volume %s"), volume_id, context=context) + + try: + vol = self.volume_api.get(context, volume_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + if str(vol['instance_id']) != server_id: + LOG.debug("instance_id != server_id") + return faults.Fault(exc.HTTPNotFound()) + + self.compute_api.detach_volume(context, + volume_id=volume_id) + + return exc.HTTPAccepted() + + def _items(self, req, server_id, entity_maker): + """Returns a list of attachments, transformed through entity_maker.""" + context = req.environ['nova.context'] + + try: + instance = self.compute_api.get(context, server_id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + volumes = instance['volumes'] + limited_list = common.limited(volumes, req) + res = [entity_maker(context, vol) for vol in limited_list] + return {'volumeAttachments': res} + + +class Volumes(extensions.ExtensionDescriptor): + def get_name(self): + return "Volumes" + + def get_alias(self): + return "VOLUMES" + + def get_description(self): + return "Volumes support" + + def get_namespace(self): + return "http://docs.openstack.org/ext/volumes/api/v1.1" + + def get_updated(self): + return "2011-03-25T00:00:00+00:00" + + def get_resources(self): + resources = [] + + # NOTE(justinsb): No way to provide singular name ('volume') + # Does this matter? + res = extensions.ResourceExtension('volumes', + VolumeController(), + collection_actions={'detail': 'GET'} + ) + resources.append(res) + + res = extensions.ResourceExtension('volume_attachments', + VolumeAttachmentController(), + parent=dict( + member_name='server', + collection_name='servers')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index b9b7f998d..fb1dccb28 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack LLC. +# Copyright 2011 Justin Santa Barbara # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,12 +17,14 @@ # under the License. import imp +import inspect import os import sys import routes import webob.dec import webob.exc +from nova import exception from nova import flags from nova import log as logging from nova import wsgi @@ -34,6 +37,84 @@ LOG = logging.getLogger('extensions') FLAGS = flags.FLAGS +class ExtensionDescriptor(object): + """Base class that defines the contract for extensions. + + Note that you don't have to derive from this class to have a valid + extension; it is purely a convenience. + + """ + + def get_name(self): + """The name of the extension. + + e.g. 'Fox In Socks' + + """ + raise NotImplementedError() + + def get_alias(self): + """The alias for the extension. + + e.g. 'FOXNSOX' + + """ + raise NotImplementedError() + + def get_description(self): + """Friendly description for the extension. + + e.g. 'The Fox In Socks Extension' + + """ + raise NotImplementedError() + + def get_namespace(self): + """The XML namespace for the extension. + + e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0' + + """ + raise NotImplementedError() + + def get_updated(self): + """The timestamp when the extension was last updated. + + e.g. '2011-01-22T13:25:27-06:00' + + """ + # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS + raise NotImplementedError() + + def get_resources(self): + """List of extensions.ResourceExtension extension objects. + + Resources define new nouns, and are accessible through URLs. + + """ + resources = [] + return resources + + def get_actions(self): + """List of extensions.ActionExtension extension objects. + + Actions are verbs callable from the API. + + """ + actions = [] + return actions + + def get_response_extensions(self): + """List of extensions.ResponseExtension extension objects. + + Response extensions are used to insert information into existing + response data. + + """ + response_exts = [] + return response_exts + + class ActionExtensionController(wsgi.Controller): def __init__(self, application): @@ -94,45 +175,38 @@ class ExtensionController(wsgi.Controller): ext_data['description'] = ext.get_description() ext_data['namespace'] = ext.get_namespace() ext_data['updated'] = ext.get_updated() - ext_data['links'] = [] # TODO: implement extension links + ext_data['links'] = [] # TODO(dprince): implement extension links return ext_data def index(self, req): extensions = [] - for alias, ext in self.extension_manager.extensions.iteritems(): + for _alias, ext in self.extension_manager.extensions.iteritems(): extensions.append(self._translate(ext)) return dict(extensions=extensions) def show(self, req, id): - # NOTE: the extensions alias is used as the 'id' for show + # NOTE(dprince): the extensions alias is used as the 'id' for show ext = self.extension_manager.extensions[id] return self._translate(ext) def delete(self, req, id): - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(webob.exc.HTTPNotFound()) def create(self, req): - raise faults.Fault(exc.HTTPNotFound()) - - def delete(self, req, id): - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(webob.exc.HTTPNotFound()) class ExtensionMiddleware(wsgi.Middleware): - """ - Extensions middleware that intercepts configured routes for extensions. - """ + """Extensions middleware for WSGI.""" @classmethod def factory(cls, global_config, **local_config): - """ paste factory """ + """Paste factory.""" def _factory(app): return cls(app, **local_config) return _factory def _action_ext_controllers(self, application, ext_mgr, mapper): - """ - Return a dict of ActionExtensionController objects by collection - """ + """Return a dict of ActionExtensionController-s by collection.""" action_controllers = {} for action in ext_mgr.get_actions(): if not action.collection in action_controllers.keys(): @@ -151,9 +225,7 @@ class ExtensionMiddleware(wsgi.Middleware): return action_controllers def _response_ext_controllers(self, application, ext_mgr, mapper): - """ - Return a dict of ResponseExtensionController objects by collection - """ + """Returns a dict of ResponseExtensionController-s by collection.""" response_ext_controllers = {} for resp_ext in ext_mgr.get_response_extensions(): if not resp_ext.key in response_ext_controllers.keys(): @@ -212,18 +284,18 @@ class ExtensionMiddleware(wsgi.Middleware): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): - """ - Route the incoming request with router. - """ + """Route the incoming request with router.""" req.environ['extended.app'] = self.application return self._router @staticmethod @webob.dec.wsgify(RequestClass=wsgi.Request) def _dispatch(req): - """ + """Dispatch the request. + Returns the routed WSGI app's response or defers to the extended application. + """ match = req.environ['wsgiorg.routing_args'][1] if not match: @@ -233,10 +305,11 @@ class ExtensionMiddleware(wsgi.Middleware): class ExtensionManager(object): - """ - Load extensions from the configured extension path. - See nova/tests/api/openstack/extensions/foxinsocks.py for an example - extension implementation. + """Load extensions from the configured extension path. + + See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an + example extension implementation. + """ def __init__(self, path): @@ -244,12 +317,10 @@ class ExtensionManager(object): self.path = path self.extensions = {} - self._load_extensions() + self._load_all_extensions() def get_resources(self): - """ - returns a list of ResourceExtension objects - """ + """Returns a list of ResourceExtension objects.""" resources = [] resources.append(ResourceExtension('extensions', ExtensionController(self))) @@ -257,40 +328,37 @@ class ExtensionManager(object): try: resources.extend(ext.get_resources()) except AttributeError: - # NOTE: Extension aren't required to have resource extensions + # NOTE(dprince): Extension aren't required to have resource + # extensions pass return resources def get_actions(self): - """ - returns a list of ActionExtension objects - """ + """Returns a list of ActionExtension objects.""" actions = [] for alias, ext in self.extensions.iteritems(): try: actions.extend(ext.get_actions()) except AttributeError: - # NOTE: Extension aren't required to have action extensions + # NOTE(dprince): Extension aren't required to have action + # extensions pass return actions def get_response_extensions(self): - """ - returns a list of ResponseExtension objects - """ + """Returns a list of ResponseExtension objects.""" response_exts = [] for alias, ext in self.extensions.iteritems(): try: response_exts.extend(ext.get_response_extensions()) except AttributeError: - # NOTE: Extension aren't required to have response extensions + # NOTE(dprince): Extension aren't required to have response + # extensions pass return response_exts def _check_extension(self, extension): - """ - Checks for required methods in extension objects. - """ + """Checks for required methods in extension objects.""" try: LOG.debug(_('Ext name: %s'), extension.get_name()) LOG.debug(_('Ext alias: %s'), extension.get_alias()) @@ -300,43 +368,59 @@ class ExtensionManager(object): except AttributeError as ex: LOG.exception(_("Exception loading extension: %s"), unicode(ex)) - def _load_extensions(self): - """ + def _load_all_extensions(self): + """Load extensions from the configured path. + Load extensions from the configured path. The extension name is constructed from the module_name. If your extension module was named widgets.py the extension class within that module should be 'Widgets'. + In addition, extensions are loaded from the 'contrib' directory. + See nova/tests/api/openstack/extensions/foxinsocks.py for an example extension implementation. + """ - if not os.path.exists(self.path): - return + if os.path.exists(self.path): + self._load_all_extensions_from_path(self.path) - for f in os.listdir(self.path): + contrib_path = os.path.join(os.path.dirname(__file__), "contrib") + if os.path.exists(contrib_path): + self._load_all_extensions_from_path(contrib_path) + + def _load_all_extensions_from_path(self, path): + for f in os.listdir(path): LOG.audit(_('Loading extension file: %s'), f) mod_name, file_ext = os.path.splitext(os.path.split(f)[-1]) - ext_path = os.path.join(self.path, f) + ext_path = os.path.join(path, f) if file_ext.lower() == '.py' and not mod_name.startswith('_'): mod = imp.load_source(mod_name, ext_path) ext_name = mod_name[0].upper() + mod_name[1:] new_ext_class = getattr(mod, ext_name, None) if not new_ext_class: LOG.warn(_('Did not find expected name ' - '"%(ext_name)" in %(file)s'), + '"%(ext_name)s" in %(file)s'), {'ext_name': ext_name, - 'file': ext_path}) + 'file': ext_path}) continue new_ext = new_ext_class() self._check_extension(new_ext) - self.extensions[new_ext.get_alias()] = new_ext + self._add_extension(new_ext) + + def _add_extension(self, ext): + alias = ext.get_alias() + LOG.audit(_('Loaded extension: %s'), alias) + + self._check_extension(ext) + + if alias in self.extensions: + raise exception.Error("Found duplicate extension: %s" % alias) + self.extensions[alias] = ext class ResponseExtension(object): - """ - ResponseExtension objects can be used to add data to responses from - core nova OpenStack API controllers. - """ + """Add data to responses from core nova OpenStack API controllers.""" def __init__(self, method, url_route, handler): self.url_route = url_route @@ -346,10 +430,7 @@ class ResponseExtension(object): class ActionExtension(object): - """ - ActionExtension objects can be used to add custom actions to core nova - nova OpenStack API controllers. - """ + """Add custom actions to core nova OpenStack API controllers.""" def __init__(self, collection, action_name, handler): self.collection = collection @@ -358,10 +439,7 @@ class ActionExtension(object): class ResourceExtension(object): - """ - ResourceExtension objects can be used to add top level resources - to the OpenStack API in nova. - """ + """Add top level resources to the OpenStack API in nova.""" def __init__(self, collection, controller, parent=None, collection_actions={}, member_actions={}): diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 0e9c4b26f..940bd8771 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -60,6 +60,7 @@ class Fault(webob.exc.HTTPException): serializer = wsgi.Serializer(metadata) content_type = req.best_match_content_type() self.wrapped_exc.body = serializer.serialize(fault_data, content_type) + self.wrapped_exc.content_type = content_type return self.wrapped_exc 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/servers.py b/nova/api/openstack/servers.py index 75a305a14..4e2ebb2bd 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -150,6 +150,15 @@ class Controller(wsgi.Controller): injected_files = self._get_injected_files(personality) flavor_id = self._flavor_id_from_req_data(env) + + if not 'name' in env['server']: + msg = _("Server name is not defined") + return exc.HTTPBadRequest(msg) + + name = env['server']['name'] + self._validate_server_name(name) + name = name.strip() + try: (inst,) = self.compute_api.create( context, @@ -157,8 +166,8 @@ class Controller(wsgi.Controller): image_id, kernel_id=kernel_id, ramdisk_id=ramdisk_id, - display_name=env['server']['name'], - display_description=env['server']['name'], + display_name=name, + display_description=name, key_name=key_name, key_data=key_data, metadata=metadata, @@ -246,31 +255,45 @@ class Controller(wsgi.Controller): ctxt = req.environ['nova.context'] update_dict = {} - if 'adminPass' in inst_dict['server']: - update_dict['admin_pass'] = inst_dict['server']['adminPass'] - try: - self.compute_api.set_admin_password(ctxt, id) - except exception.TimeoutException: - return exc.HTTPRequestTimeout() + if 'name' in inst_dict['server']: - update_dict['display_name'] = inst_dict['server']['name'] + name = inst_dict['server']['name'] + self._validate_server_name(name) + update_dict['display_name'] = name.strip() + + self._parse_update(ctxt, id, inst_dict, update_dict) + try: self.compute_api.update(ctxt, id, **update_dict) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPNoContent() + def _validate_server_name(self, value): + if not isinstance(value, basestring): + msg = _("Server name is not a string or unicode") + raise exc.HTTPBadRequest(msg) + + if value.strip() == '': + msg = _("Server name is an empty string") + raise exc.HTTPBadRequest(msg) + + def _parse_update(self, context, id, inst_dict, update_dict): + pass + @scheduler_api.redirect_handler def action(self, req, id): """Multi-purpose method used to reboot, rebuild, or resize a server""" actions = { - 'reboot': self._action_reboot, - 'resize': self._action_resize, + 'changePassword': self._action_change_password, + 'reboot': self._action_reboot, + 'resize': self._action_resize, 'confirmResize': self._action_confirm_resize, - 'revertResize': self._action_revert_resize, - 'rebuild': self._action_rebuild, + 'revertResize': self._action_revert_resize, + 'rebuild': self._action_rebuild, } input_dict = self._deserialize(req.body, req.get_content_type()) @@ -279,6 +302,9 @@ class Controller(wsgi.Controller): return actions[key](input_dict, req, id) return faults.Fault(exc.HTTPNotImplemented()) + def _action_change_password(self, input_dict, req, id): + return exc.HTTPNotImplemented() + def _action_confirm_resize(self, input_dict, req, id): try: self.compute_api.confirm_resize(req.environ['nova.context'], id) @@ -477,7 +503,7 @@ class Controller(wsgi.Controller): @scheduler_api.redirect_handler def get_ajax_console(self, req, id): - """ Returns a url to an instance's ajaxterm console. """ + """Returns a url to an instance's ajaxterm console.""" try: self.compute_api.get_ajax_console(req.environ['nova.context'], int(id)) @@ -486,6 +512,16 @@ class Controller(wsgi.Controller): return exc.HTTPAccepted() @scheduler_api.redirect_handler + def get_vnc_console(self, req, id): + """Returns a url to an instance's ajaxterm console.""" + try: + self.compute_api.get_vnc_console(req.environ['nova.context'], + int(id)) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() + + @scheduler_api.redirect_handler def diagnostics(self, req, id): """Permit Admins to retrieve server diagnostics.""" ctxt = req.environ["nova.context"] @@ -566,6 +602,14 @@ class ControllerV10(Controller): def _limit_items(self, items, req): return common.limited(items, req) + def _parse_update(self, context, server_id, inst_dict, update_dict): + if 'adminPass' in inst_dict['server']: + update_dict['admin_pass'] = inst_dict['server']['adminPass'] + try: + self.compute_api.set_admin_password(context, server_id) + except exception.TimeoutException: + return exc.HTTPRequestTimeout() + class ControllerV11(Controller): def _image_id_from_req_data(self, data): @@ -589,6 +633,19 @@ class ControllerV11(Controller): def _get_addresses_view_builder(self, req): return nova.api.openstack.views.addresses.ViewBuilderV11(req) + def _action_change_password(self, input_dict, req, id): + context = req.environ['nova.context'] + if (not 'changePassword' in input_dict + or not 'adminPass' in input_dict['changePassword']): + msg = _("No adminPass was specified") + return exc.HTTPBadRequest(msg) + password = input_dict['changePassword']['adminPass'] + if not isinstance(password, basestring) or password == '': + msg = _("Invalid adminPass") + return exc.HTTPBadRequest(msg) + self.compute_api.set_admin_password(context, id, password) + return exc.HTTPAccepted() + def _limit_items(self, items, req): return common.limited_by_marker(items, req) diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py index 33f1dd628..3f9d91934 100644 --- a/nova/api/openstack/versions.py +++ b/nova/api/openstack/versions.py @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import webob import webob.dec -import webob.exc from nova import wsgi import nova.api.openstack.views.versions @@ -51,4 +51,10 @@ class Versions(wsgi.Application): } content_type = req.best_match_content_type() - return wsgi.Serializer(metadata).serialize(response, content_type) + body = wsgi.Serializer(metadata).serialize(response, content_type) + + response = webob.Response() + response.content_type = content_type + response.body = body + + return response 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 |
