diff options
| author | Jenkins <jenkins@review.openstack.org> | 2012-09-18 17:11:52 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2012-09-18 17:11:52 +0000 |
| commit | 12f3e259157475eb22a1c282ecff6a4dd5acf4d4 (patch) | |
| tree | 148e62e96894ef8d6db3cc26d82a8db0d6ae8b42 /nova/api | |
| parent | afefc88389d4041ddce737f423c0a23c883ff98d (diff) | |
| parent | de09c1866b9138610914ddaaebb9b030884d1e28 (diff) | |
Merge "Adds new volume API extensions"
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/ec2/cloud.py | 10 | ||||
| -rw-r--r-- | nova/api/openstack/extensions.py | 3 | ||||
| -rw-r--r-- | nova/api/openstack/volume/contrib/image_create.py | 31 | ||||
| -rw-r--r-- | nova/api/openstack/volume/contrib/volume_actions.py | 131 | ||||
| -rw-r--r-- | nova/api/openstack/volume/volumes.py | 41 |
5 files changed, 207 insertions, 9 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 14035fa14..5cb07eeac 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -761,14 +761,16 @@ class CloudController(object): kwargs.get('size'), context=context) + create_kwargs = dict(snapshot=snapshot, + volume_type=kwargs.get('volume_type'), + metadata=kwargs.get('metadata'), + availability_zone=kwargs.get('availability_zone')) + volume = self.volume_api.create(context, kwargs.get('size'), kwargs.get('name'), kwargs.get('description'), - snapshot, - kwargs.get('volume_type'), - kwargs.get('metadata'), - kwargs.get('availability_zone')) + **create_kwargs) db.ec2_volume_create(context, volume['id']) # TODO(vish): Instance should be None at db layer instead of diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py index f36586443..8baa88488 100644 --- a/nova/api/openstack/extensions.py +++ b/nova/api/openstack/extensions.py @@ -189,6 +189,9 @@ class ExtensionManager(object): for _alias, ext in self.sorted_ext_list: yield ext + def is_loaded(self, alias): + return alias in self.extensions + def register(self, ext): # Do nothing if the extension doesn't check out if not self._check_extension(ext): diff --git a/nova/api/openstack/volume/contrib/image_create.py b/nova/api/openstack/volume/contrib/image_create.py new file mode 100644 index 000000000..840689799 --- /dev/null +++ b/nova/api/openstack/volume/contrib/image_create.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 NTT.
+# Copyright (c) 2012 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.
+
+"""The Create Volume from Image extension."""
+
+
+from nova.api.openstack import extensions
+
+
+class Image_create(extensions.ExtensionDescriptor):
+ """Allow creating a volume from an image in the Create Volume v1 API"""
+
+ name = "CreateVolumeExtension"
+ alias = "os-image-create"
+ namespace = "http://docs.openstack.org/volume/ext/image-create/api/v1"
+ updated = "2012-08-13T00:00:00+00:00"
diff --git a/nova/api/openstack/volume/contrib/volume_actions.py b/nova/api/openstack/volume/contrib/volume_actions.py new file mode 100644 index 000000000..8a453bfb1 --- /dev/null +++ b/nova/api/openstack/volume/contrib/volume_actions.py @@ -0,0 +1,131 @@ +# Copyright 2012 OpenStack, LLC. +# +# 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 webob +from xml.dom import minidom + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import exception +from nova import flags +from nova.openstack.common import log as logging +from nova.openstack.common.rpc import common as rpc_common +from nova import volume + + +FLAGS = flags.FLAGS +LOG = logging.getLogger(__name__) + + +def authorize(context, action_name): + action = 'volume_actions:%s' % action_name + extensions.extension_authorizer('volume', action)(context) + + +class VolumeToImageSerializer(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('os-volume_upload_image', + selector='os-volume_upload_image') + root.set('id') + root.set('updated_at') + root.set('status') + root.set('display_description') + root.set('size') + root.set('volume_type') + root.set('image_id') + root.set('container_format') + root.set('disk_format') + root.set('image_name') + return xmlutil.MasterTemplate(root, 1) + + +class VolumeToImageDeserializer(wsgi.XMLDeserializer): + """Deserializer to handle xml-formatted requests""" + def default(self, string): + dom = minidom.parseString(string) + action_node = dom.childNodes[0] + action_name = action_node.tagName + + action_data = {} + attributes = ["force", "image_name", "container_format", "disk_format"] + for attr in attributes: + if action_node.hasAttribute(attr): + action_data[attr] = action_node.getAttribute(attr) + if 'force' in action_data and action_data['force'] == 'True': + action_data['force'] = True + return {'body': {action_name: action_data}} + + +class VolumeActionsController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(VolumeActionsController, self).__init__(*args, **kwargs) + self.volume_api = volume.API() + + @wsgi.response(202) + @wsgi.action('os-volume_upload_image') + @wsgi.serializers(xml=VolumeToImageSerializer) + @wsgi.deserializers(xml=VolumeToImageDeserializer) + def _volume_upload_image(self, req, id, body): + """Uploads the specified volume to image service.""" + context = req.environ['nova.context'] + try: + params = body['os-volume_upload_image'] + except (TypeError, KeyError): + msg = _("Invalid request body") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not params.get("image_name"): + msg = _("No image_name was specified in request.") + raise webob.exc.HTTPBadRequest(explanation=msg) + + force = params.get('force', False) + try: + volume = self.volume_api.get(context, id) + except exception.VolumeNotFound, error: + raise webob.exc.HTTPNotFound(explanation=unicode(error)) + authorize(context, "upload_image") + image_metadata = {"container_format": params.get("container_format", + "bare"), + "disk_format": params.get("disk_format", "raw"), + "name": params["image_name"]} + try: + response = self.volume_api.copy_volume_to_image(context, + volume, + image_metadata, + force) + except exception.InvalidVolume, error: + raise webob.exc.HTTPBadRequest(explanation=unicode(error)) + except ValueError, error: + raise webob.exc.HTTPBadRequest(explanation=unicode(error)) + except rpc_common.RemoteError as error: + msg = "%(err_type)s: %(err_msg)s" % {'err_type': error.exc_type, + 'err_msg': error.value} + raise webob.exc.HTTPBadRequest(explanation=msg) + return {'os-volume_upload_image': response} + + +class Volume_actions(extensions.ExtensionDescriptor): + """Enable volume actions + """ + + name = "VolumeActions" + alias = "os-volume-actions" + namespace = "http://docs.openstack.org/volume/ext/volume-actions/api/v1.1" + updated = "2012-05-31T00:00:00+00:00" + + def get_controller_extensions(self): + controller = VolumeActionsController() + extension = extensions.ControllerExtension(self, 'volumes', controller) + return [extension] diff --git a/nova/api/openstack/volume/volumes.py b/nova/api/openstack/volume/volumes.py index 23a506daa..6cc4af899 100644 --- a/nova/api/openstack/volume/volumes.py +++ b/nova/api/openstack/volume/volumes.py @@ -25,6 +25,7 @@ from nova.api.openstack import xmlutil from nova import exception from nova import flags from nova.openstack.common import log as logging +from nova import utils from nova import volume from nova.volume import volume_types @@ -62,17 +63,17 @@ def _translate_attachment_summary_view(_context, vol): return d -def _translate_volume_detail_view(context, vol): +def _translate_volume_detail_view(context, vol, image_id=None): """Maps keys for volumes details view.""" - d = _translate_volume_summary_view(context, vol) + d = _translate_volume_summary_view(context, vol, image_id) # No additional data / lookups at the moment return d -def _translate_volume_summary_view(context, vol): +def _translate_volume_summary_view(context, vol, image_id=None): """Maps keys for volumes summary view.""" d = {} @@ -98,6 +99,9 @@ def _translate_volume_summary_view(context, vol): d['snapshot_id'] = vol['snapshot_id'] + if image_id: + d['image_id'] = image_id + LOG.audit(_("vol=%s"), vol, context=context) if vol.get('volume_metadata'): @@ -195,7 +199,7 @@ class CreateDeserializer(CommonDeserializer): class VolumeController(wsgi.Controller): """The Volumes API controller for the OpenStack API.""" - def __init__(self, ext_mgr=None): + def __init__(self, ext_mgr): self.volume_api = volume.API() self.ext_mgr = ext_mgr super(VolumeController, self).__init__() @@ -250,6 +254,21 @@ class VolumeController(wsgi.Controller): res = [entity_maker(context, vol) for vol in limited_list] return {'volumes': res} + def _image_uuid_from_href(self, image_href): + # If the image href was generated by nova api, strip image_href + # down to an id. + try: + image_uuid = image_href.split('/').pop() + except (TypeError, AttributeError): + msg = _("Invalid imageRef provided.") + raise exc.HTTPBadRequest(explanation=msg) + + if not utils.is_uuid_like(image_uuid): + msg = _("Invalid imageRef provided.") + raise exc.HTTPBadRequest(explanation=msg) + + return image_uuid + @wsgi.serializers(xml=VolumeTemplate) @wsgi.deserializers(xml=CreateDeserializer) def create(self, req, body): @@ -285,6 +304,17 @@ class VolumeController(wsgi.Controller): LOG.audit(_("Create volume of %s GB"), size, context=context) + image_href = None + image_uuid = None + if self.ext_mgr.is_loaded('os-image-create'): + image_href = volume.get('imageRef') + if snapshot_id and image_href: + msg = _("Snapshot and image cannot be specified together.") + raise exc.HTTPBadRequest(explanation=msg) + if image_href: + image_uuid = self._image_uuid_from_href(image_href) + kwargs['image_id'] = image_uuid + kwargs['availability_zone'] = volume.get('availability_zone', None) new_volume = self.volume_api.create(context, @@ -296,7 +326,8 @@ class VolumeController(wsgi.Controller): # TODO(vish): Instance should be None at db layer instead of # trying to lazy load, but for now we turn it into # a dict to avoid an error. - retval = _translate_volume_detail_view(context, dict(new_volume)) + retval = _translate_volume_detail_view(context, dict(new_volume), + image_uuid) result = {'volume': retval} |
