From de09c1866b9138610914ddaaebb9b030884d1e28 Mon Sep 17 00:00:00 2001 From: Unmesh Gurjar Date: Sat, 11 Aug 2012 10:31:51 -0700 Subject: Adds new volume API extensions Adds following extensions: 1. Create volume from image 2. Copy volume to image Added unit tests. Implements: blueprint create-volume-from-image Conflicts: cinder/api/openstack/volume/contrib/volume_actions.py cinder/tests/api/openstack/fakes.py cinder/tests/api/openstack/volume/contrib/test_volume_actions.py cinder/tests/policy.json nova/api/openstack/volume/volumes.py nova/flags.py nova/tests/api/openstack/volume/test_volumes.py nova/tests/test_volume.py nova/utils.py nova/volume/api.py nova/volume/manager.py This is based on a cherry-pick of cinder commit 2f5360753308eb8b10581fc3c026c1b66f42ebdc with bug fixes 8c30edff982042d2533a810709308b586267c0e9 and ffe5036fa0e63ccde2d19aa0f425ec43de338dd7 squashed in. Change-Id: I9c73bd3fa2fa2e0648c01ff3f4fc66f757d7bc3f --- nova/api/ec2/cloud.py | 10 +- nova/api/openstack/extensions.py | 3 + nova/api/openstack/volume/contrib/image_create.py | 31 +++++ .../api/openstack/volume/contrib/volume_actions.py | 131 +++++++++++++++++++++ nova/api/openstack/volume/volumes.py | 41 ++++++- 5 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 nova/api/openstack/volume/contrib/image_create.py create mode 100644 nova/api/openstack/volume/contrib/volume_actions.py (limited to 'nova/api') 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} -- cgit