summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-09-18 17:11:52 +0000
committerGerrit Code Review <review@openstack.org>2012-09-18 17:11:52 +0000
commit12f3e259157475eb22a1c282ecff6a4dd5acf4d4 (patch)
tree148e62e96894ef8d6db3cc26d82a8db0d6ae8b42 /nova/api
parentafefc88389d4041ddce737f423c0a23c883ff98d (diff)
parentde09c1866b9138610914ddaaebb9b030884d1e28 (diff)
Merge "Adds new volume API extensions"
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/ec2/cloud.py10
-rw-r--r--nova/api/openstack/extensions.py3
-rw-r--r--nova/api/openstack/volume/contrib/image_create.py31
-rw-r--r--nova/api/openstack/volume/contrib/volume_actions.py131
-rw-r--r--nova/api/openstack/volume/volumes.py41
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}