diff options
24 files changed, 916 insertions, 48 deletions
diff --git a/etc/nova/rootwrap.d/volume.filters b/etc/nova/rootwrap.d/volume.filters index e2b0f6203..574fef7c2 100644 --- a/etc/nova/rootwrap.d/volume.filters +++ b/etc/nova/rootwrap.d/volume.filters @@ -31,3 +31,6 @@ iscsiadm_usr: CommandFilter, /usr/bin/iscsiadm, root # nova/volume/driver.py dmsetup: CommandFilter, /sbin/dmsetup, root dmsetup_usr: CommandFilter, /usr/sbin/dmsetup, root + +#nova/volume/.py: utils.temporary_chown(path, 0), ... +chown: CommandFilter, /bin/chown, root 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} diff --git a/nova/scheduler/chance.py b/nova/scheduler/chance.py index e44e9d7d2..b8dd468f0 100644 --- a/nova/scheduler/chance.py +++ b/nova/scheduler/chance.py @@ -93,10 +93,10 @@ class ChanceScheduler(driver.Scheduler): self.compute_rpcapi.prep_resize(context, image, instance, instance_type, host, reservations) - def schedule_create_volume(self, context, volume_id, snapshot_id, + def schedule_create_volume(self, context, volume_id, snapshot_id, image_id, reservations): """Picks a host that is up at random.""" host = self._schedule(context, FLAGS.volume_topic, None, {}) driver.cast_to_host(context, FLAGS.volume_topic, host, 'create_volume', volume_id=volume_id, snapshot_id=snapshot_id, - reservations=reservations) + image_id=image_id, reservations=reservations) diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index ee7db02b9..df49acfae 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -207,7 +207,7 @@ class Scheduler(object): msg = _("Driver must implement schedule_run_instance") raise NotImplementedError(msg) - def schedule_create_volume(self, context, volume_id, snapshot_id, + def schedule_create_volume(self, context, volume_id, snapshot_id, image_id, reservations): msg = _("Driver must implement schedule_create_volune") raise NotImplementedError(msg) diff --git a/nova/scheduler/filter_scheduler.py b/nova/scheduler/filter_scheduler.py index a3e3e1354..371aebf53 100644 --- a/nova/scheduler/filter_scheduler.py +++ b/nova/scheduler/filter_scheduler.py @@ -42,7 +42,7 @@ class FilterScheduler(driver.Scheduler): self.cost_function_cache = {} self.options = scheduler_options.SchedulerOptions() - def schedule_create_volume(self, context, volume_id, snapshot_id, + def schedule_create_volume(self, context, volume_id, snapshot_id, image_id, reservations): # NOTE: We're only focused on compute instances right now, # so this method will always raise NoValidHost(). diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py index dca8c0cea..647c2c780 100644 --- a/nova/scheduler/manager.py +++ b/nova/scheduler/manager.py @@ -69,10 +69,11 @@ class SchedulerManager(manager.Manager): self.driver.update_service_capabilities(service_name, host, capabilities) - def create_volume(self, context, volume_id, snapshot_id, reservations): + def create_volume(self, context, volume_id, snapshot_id, image_id, + reservations): try: self.driver.schedule_create_volume( - context, volume_id, snapshot_id, reservations) + context, volume_id, snapshot_id, image_id, reservations) except Exception as ex: with excutils.save_and_reraise_exception(): self._set_vm_state_and_notify('create_volume', diff --git a/nova/scheduler/rpcapi.py b/nova/scheduler/rpcapi.py index 940fe315e..894131179 100644 --- a/nova/scheduler/rpcapi.py +++ b/nova/scheduler/rpcapi.py @@ -93,11 +93,12 @@ class SchedulerAPI(nova.openstack.common.rpc.proxy.RpcProxy): disk_over_commit=disk_over_commit, instance=instance_p, dest=dest)) - def create_volume(self, ctxt, volume_id, snapshot_id, reservations): + def create_volume(self, ctxt, volume_id, snapshot_id, image_id, + reservations): self.cast(ctxt, self.make_msg('create_volume', volume_id=volume_id, snapshot_id=snapshot_id, - reservations=reservations)) + image_id=image_id, reservations=reservations)) def update_service_capabilities(self, ctxt, service_name, host, capabilities): diff --git a/nova/scheduler/simple.py b/nova/scheduler/simple.py index 78578dbd0..5eee0b136 100644 --- a/nova/scheduler/simple.py +++ b/nova/scheduler/simple.py @@ -56,7 +56,7 @@ class SimpleScheduler(chance.ChanceScheduler): request_spec, admin_password, injected_files, requested_networks, is_first_time, filter_properties) - def schedule_create_volume(self, context, volume_id, snapshot_id, + def schedule_create_volume(self, context, volume_id, snapshot_id, image_id, reservations): """Picks a host that is up and has the fewest volumes.""" deprecated.warn(_('nova-volume functionality is deprecated in Folsom ' @@ -76,7 +76,7 @@ class SimpleScheduler(chance.ChanceScheduler): raise exception.WillNotSchedule(host=host) driver.cast_to_volume_host(context, host, 'create_volume', volume_id=volume_id, snapshot_id=snapshot_id, - reservations=reservations) + image_id=image_id, reservations=reservations) return None results = db.service_get_all_volume_sorted(elevated) @@ -91,7 +91,8 @@ class SimpleScheduler(chance.ChanceScheduler): if utils.service_is_up(service) and not service['disabled']: driver.cast_to_volume_host(context, service['host'], 'create_volume', volume_id=volume_id, - snapshot_id=snapshot_id, reservations=reservations) + snapshot_id=snapshot_id, image_id=image_id, + reservations=reservations) return None msg = _("Is the appropriate service running?") raise exception.NoValidHost(reason=msg) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 6a6f85f68..18407b6a7 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -542,6 +542,18 @@ def stub_volume_create(self, context, size, name, description, snapshot, return vol +def stub_volume_create_from_image(self, context, size, name, description, + snapshot, volume_type, metadata, + availability_zone): + vol = stub_volume('1') + vol['status'] = 'creating' + vol['size'] = size + vol['display_name'] = name + vol['display_description'] = description + vol['availability_zone'] = 'nova' + return vol + + def stub_volume_update(self, context, *args, **param): pass diff --git a/nova/tests/api/openstack/volume/contrib/test_volume_actions.py b/nova/tests/api/openstack/volume/contrib/test_volume_actions.py new file mode 100644 index 000000000..4dd79d366 --- /dev/null +++ b/nova/tests/api/openstack/volume/contrib/test_volume_actions.py @@ -0,0 +1,162 @@ +# 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 datetime +import webob + +from nova.api.openstack.volume.contrib import volume_actions +from nova import exception +from nova.openstack.common.rpc import common as rpc_common +from nova import test +from nova.tests.api.openstack import fakes +from nova.volume import api as volume_api + + +def stub_volume_get(self, context, volume_id): + volume = fakes.stub_volume(volume_id) + if volume_id == 5: + volume['status'] = 'in-use' + else: + volume['status'] = 'available' + return volume + + +def stub_upload_volume_to_image_service(self, context, volume, metadata, + force): + ret = {"id": volume['id'], + "updated_at": datetime.datetime(1, 1, 1, 1, 1, 1), + "status": 'uploading', + "display_description": volume['display_description'], + "size": volume['size'], + "volume_type": volume['volume_type'], + "image_id": 1, + "container_format": 'bare', + "disk_format": 'raw', + "image_name": 'image_name'} + return ret + + +class VolumeImageActionsTest(test.TestCase): + def setUp(self): + super(VolumeImageActionsTest, self).setUp() + self.controller = volume_actions.VolumeActionsController() + + self.stubs.Set(volume_api.API, 'get', stub_volume_get) + + def test_copy_volume_to_image(self): + self.stubs.Set(volume_api.API, + "copy_volume_to_image", + stub_upload_volume_to_image_service) + + id = 1 + vol = {"container_format": 'bare', + "disk_format": 'raw', + "image_name": 'image_name', + "force": True} + body = {"os-volume_upload_image": vol} + req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) + res_dict = self.controller._volume_upload_image(req, id, body) + expected = {'os-volume_upload_image': {'id': id, + 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'status': 'uploading', + 'display_description': 'displaydesc', + 'size': 1, + 'volume_type': {'name': 'vol_type_name'}, + 'image_id': 1, + 'container_format': 'bare', + 'disk_format': 'raw', + 'image_name': 'image_name'}} + self.assertDictMatch(res_dict, expected) + + def test_copy_volume_to_image_volumenotfound(self): + def stub_volume_get_raise_exc(self, context, volume_id): + raise exception.VolumeNotFound(volume_id=volume_id) + + self.stubs.Set(volume_api.API, 'get', stub_volume_get_raise_exc) + + id = 1 + vol = {"container_format": 'bare', + "disk_format": 'raw', + "image_name": 'image_name', + "force": True} + body = {"os-volume_upload_image": vol} + req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) + self.assertRaises(webob.exc.HTTPNotFound, + self.controller._volume_upload_image, + req, + id, + body) + + def test_copy_volume_to_image_invalidvolume(self): + def stub_upload_volume_to_image_service_raise(self, context, volume, + metadata, force): + raise exception.InvalidVolume + self.stubs.Set(volume_api.API, + "copy_volume_to_image", + stub_upload_volume_to_image_service_raise) + + id = 1 + vol = {"container_format": 'bare', + "disk_format": 'raw', + "image_name": 'image_name', + "force": True} + body = {"os-volume_upload_image": vol} + req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._volume_upload_image, + req, + id, + body) + + def test_copy_volume_to_image_valueerror(self): + def stub_upload_volume_to_image_service_raise(self, context, volume, + metadata, force): + raise ValueError + self.stubs.Set(volume_api.API, + "copy_volume_to_image", + stub_upload_volume_to_image_service_raise) + + id = 1 + vol = {"container_format": 'bare', + "disk_format": 'raw', + "image_name": 'image_name', + "force": True} + body = {"os-volume_upload_image": vol} + req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._volume_upload_image, + req, + id, + body) + + def test_copy_volume_to_image_remoteerror(self): + def stub_upload_volume_to_image_service_raise(self, context, volume, + metadata, force): + raise rpc_common.RemoteError + self.stubs.Set(volume_api.API, + "copy_volume_to_image", + stub_upload_volume_to_image_service_raise) + + id = 1 + vol = {"container_format": 'bare', + "disk_format": 'raw', + "image_name": 'image_name', + "force": True} + body = {"os-volume_upload_image": vol} + req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller._volume_upload_image, + req, + id, + body) diff --git a/nova/tests/api/openstack/volume/test_router.py b/nova/tests/api/openstack/volume/test_router.py index 97900bd6d..5934a21e5 100644 --- a/nova/tests/api/openstack/volume/test_router.py +++ b/nova/tests/api/openstack/volume/test_router.py @@ -44,12 +44,16 @@ def create_resource(ext_mgr): return wsgi.Resource(FakeController(ext_mgr)) +def create_volume_resource(ext_mgr): + return wsgi.Resource(FakeController(ext_mgr)) + + class VolumeRouterTestCase(test.TestCase): def setUp(self): super(VolumeRouterTestCase, self).setUp() # NOTE(vish): versions is just returning text so, no need to stub. self.stubs.Set(snapshots, 'create_resource', create_resource) - self.stubs.Set(volumes, 'create_resource', create_resource) + self.stubs.Set(volumes, 'create_resource', create_volume_resource) self.app = volume.APIRouter() def test_versions(self): diff --git a/nova/tests/api/openstack/volume/test_volumes.py b/nova/tests/api/openstack/volume/test_volumes.py index ddd5d0d2d..e7a0e66f3 100644 --- a/nova/tests/api/openstack/volume/test_volumes.py +++ b/nova/tests/api/openstack/volume/test_volumes.py @@ -26,11 +26,29 @@ from nova import flags from nova.openstack.common import timeutils from nova import test from nova.tests.api.openstack import fakes +from nova.tests.image import fake as fake_image from nova.volume import api as volume_api FLAGS = flags.FLAGS +TEST_SNAPSHOT_UUID = '00000000-0000-0000-0000-000000000001' + + +def stub_snapshot_get(self, context, snapshot_id): + if snapshot_id != TEST_SNAPSHOT_UUID: + raise exception.NotFound + + return { + 'id': snapshot_id, + 'volume_id': 12, + 'status': 'available', + 'volume_size': 100, + 'created_at': None, + 'display_name': 'Default name', + 'display_description': 'Default description', + } + class VolumeApiTest(test.TestCase): def setUp(self): @@ -86,6 +104,92 @@ class VolumeApiTest(test.TestCase): req, body) + def test_volume_create_no_body(self): + body = {} + req = fakes.HTTPRequest.blank('/v1/volumes') + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller.create, + req, + body) + + def test_volume_create_with_image_id(self): + self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) + self.ext_mgr.extensions = {'os-image-create': 'fake'} + vol = {"size": '1', + "display_name": "Volume Test Name", + "display_description": "Volume Test Desc", + "availability_zone": "nova", + "imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'} + expected = {'volume': {'status': 'fakestatus', + 'display_description': 'Volume Test Desc', + 'availability_zone': 'nova', + 'display_name': 'Volume Test Name', + 'attachments': [{'device': '/', + 'server_id': 'fakeuuid', + 'id': '1', + 'volume_id': '1'}], + 'volume_type': 'vol_type_name', + 'image_id': 'c905cedb-7281-47e4-8a62-f26bc5fc4c77', + 'snapshot_id': None, + 'metadata': {}, + 'id': '1', + 'created_at': datetime.datetime(1999, 1, 1, + 1, 1, 1), + 'size': '1'} + } + body = {"volume": vol} + req = fakes.HTTPRequest.blank('/v1/volumes') + res = self.controller.create(req, body) + self.maxDiff = 4096 + self.assertEqual(res.obj, expected) + + def test_volume_create_with_image_id_and_snapshot_id(self): + self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) + self.stubs.Set(volume_api.API, "get_snapshot", stub_snapshot_get) + self.ext_mgr.extensions = {'os-image-create': 'fake'} + vol = {"size": '1', + "display_name": "Volume Test Name", + "display_description": "Volume Test Desc", + "availability_zone": "nova", + "imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77', + "snapshot_id": TEST_SNAPSHOT_UUID} + body = {"volume": vol} + req = fakes.HTTPRequest.blank('/v1/volumes') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + req, + body) + + def test_volume_create_with_image_id_is_integer(self): + self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) + self.ext_mgr.extensions = {'os-image-create': 'fake'} + vol = {"size": '1', + "display_name": "Volume Test Name", + "display_description": "Volume Test Desc", + "availability_zone": "nova", + "imageRef": 1234} + body = {"volume": vol} + req = fakes.HTTPRequest.blank('/v1/volumes') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + req, + body) + + def test_volume_create_with_image_id_not_uuid_format(self): + self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) + self.ext_mgr.extensions = {'os-image-create': 'fake'} + vol = {"size": '1', + "display_name": "Volume Test Name", + "display_description": "Volume Test Desc", + "availability_zone": "nova", + "imageRef": '12345'} + body = {"volume": vol} + req = fakes.HTTPRequest.blank('/v1/volumes') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + req, + body) + def test_volume_list(self): self.stubs.Set(volume_api.API, 'get_all', fakes.stub_volume_get_all_by_project) @@ -475,7 +579,9 @@ class VolumesUnprocessableEntityTestCase(test.TestCase): def setUp(self): super(VolumesUnprocessableEntityTestCase, self).setUp() - self.controller = volumes.VolumeController() + self.ext_mgr = extensions.ExtensionManager() + self.ext_mgr.extensions = {} + self.controller = volumes.VolumeController(self.ext_mgr) def _unprocessable_volume_create(self, body): req = fakes.HTTPRequest.blank('/v2/fake/volumes') diff --git a/nova/tests/policy.json b/nova/tests/policy.json index a6330c9e5..b856da58a 100644 --- a/nova/tests/policy.json +++ b/nova/tests/policy.json @@ -154,6 +154,7 @@ "volume_extension:volume_admin_actions:reset_status": [["rule:admin_api"]], "volume_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]], "volume_extension:volume_admin_actions:force_delete": [["rule:admin_api"]], + "volume_extension:volume_actions:upload_image": [], "volume_extension:types_manage": [], "volume_extension:types_extra_specs": [], diff --git a/nova/tests/scheduler/test_rpcapi.py b/nova/tests/scheduler/test_rpcapi.py index 1ff278a20..62147ce25 100644 --- a/nova/tests/scheduler/test_rpcapi.py +++ b/nova/tests/scheduler/test_rpcapi.py @@ -94,4 +94,5 @@ class SchedulerRpcAPITestCase(test.TestCase): def test_create_volume(self): self._test_scheduler_api('create_volume', rpc_method='cast', volume_id="fake_volume", - snapshot_id="fake_snapshots", reservations=list('fake_res')) + snapshot_id="fake_snapshots", image_id="fake_image", + reservations=list('fake_res')) diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py index 6de3d380e..0c5328456 100644 --- a/nova/tests/test_volume.py +++ b/nova/tests/test_volume.py @@ -21,8 +21,10 @@ Tests for Volume Code. """ import cStringIO +import datetime import mox +import os import shutil import tempfile @@ -37,9 +39,13 @@ from nova.openstack.common import rpc import nova.policy from nova import quota from nova import test +from nova.tests.image import fake as fake_image +import nova.volume.api from nova.volume import iscsi QUOTAS = quota.QUOTAS + + FLAGS = flags.FLAGS @@ -60,11 +66,12 @@ class VolumeTestCase(test.TestCase): self.instance_id = instance['id'] self.instance_uuid = instance['uuid'] test_notifier.NOTIFICATIONS = [] + fake_image.stub_out_image_service(self.stubs) def tearDown(self): try: shutil.rmtree(FLAGS.volumes_dir) - except OSError, e: + except OSError: pass db.instance_destroy(self.context, self.instance_uuid) notifier_api._reset_drivers() @@ -74,11 +81,12 @@ class VolumeTestCase(test.TestCase): return 1 @staticmethod - def _create_volume(size=0, snapshot_id=None, metadata=None): + def _create_volume(size=0, snapshot_id=None, image_id=None, metadata=None): """Create a volume object.""" vol = {} vol['size'] = size vol['snapshot_id'] = snapshot_id + vol['image_id'] = image_id vol['user_id'] = 'fake' vol['project_id'] = 'fake' vol['availability_zone'] = FLAGS.storage_availability_zone @@ -137,7 +145,7 @@ class VolumeTestCase(test.TestCase): def test_create_delete_volume_with_metadata(self): """Test volume can be created and deleted.""" test_meta = {'fake_key': 'fake_value'} - volume = self._create_volume('0', None, test_meta) + volume = self._create_volume('0', None, metadata=test_meta) volume_id = volume['id'] self.volume.create_volume(self.context, volume_id) result_meta = { @@ -564,6 +572,231 @@ class VolumeTestCase(test.TestCase): volume = db.volume_get(self.context, volume['id']) self.assertEqual(volume['status'], "in-use") + def _create_volume_from_image(self, expected_status, + fakeout_copy_image_to_volume=False): + """Call copy image to volume, Test the status of volume after calling + copying image to volume.""" + def fake_local_path(volume): + return dst_path + + def fake_copy_image_to_volume(context, volume, image_id): + pass + + dst_fd, dst_path = tempfile.mkstemp() + os.close(dst_fd) + self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) + if fakeout_copy_image_to_volume: + self.stubs.Set(self.volume, '_copy_image_to_volume', + fake_copy_image_to_volume) + + image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' + volume_id = 1 + # creating volume testdata + db.volume_create(self.context, {'id': volume_id, + 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'display_description': 'Test Desc', + 'size': 20, + 'status': 'creating', + 'instance_uuid': None, + 'host': 'dummy'}) + try: + self.volume.create_volume(self.context, + volume_id, + image_id=image_id) + + volume = db.volume_get(self.context, volume_id) + self.assertEqual(volume['status'], expected_status) + finally: + # cleanup + db.volume_destroy(self.context, volume_id) + os.unlink(dst_path) + + def test_create_volume_from_image_status_downloading(self): + """Verify that before copying image to volume, it is in downloading + state.""" + self._create_volume_from_image('downloading', True) + + def test_create_volume_from_image_status_available(self): + """Verify that before copying image to volume, it is in available + state.""" + self._create_volume_from_image('available') + + def test_create_volume_from_image_exception(self): + """Verify that create volume from image, the volume status is + 'downloading'.""" + dst_fd, dst_path = tempfile.mkstemp() + os.close(dst_fd) + + self.stubs.Set(self.volume.driver, 'local_path', lambda x: dst_path) + + image_id = 'aaaaaaaa-0000-0000-0000-000000000000' + # creating volume testdata + volume_id = 1 + db.volume_create(self.context, {'id': volume_id, + 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'display_description': 'Test Desc', + 'size': 20, + 'status': 'creating', + 'host': 'dummy'}) + + self.assertRaises(exception.ImageNotFound, + self.volume.create_volume, + self.context, + volume_id, + None, + image_id) + volume = db.volume_get(self.context, volume_id) + self.assertEqual(volume['status'], "error") + # cleanup + db.volume_destroy(self.context, volume_id) + os.unlink(dst_path) + + def test_copy_volume_to_image_status_available(self): + dst_fd, dst_path = tempfile.mkstemp() + os.close(dst_fd) + + def fake_local_path(volume): + return dst_path + + self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) + + image_id = '70a599e0-31e7-49b7-b260-868f441e862b' + # creating volume testdata + volume_id = 1 + db.volume_create(self.context, {'id': volume_id, + 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'display_description': 'Test Desc', + 'size': 20, + 'status': 'uploading', + 'instance_uuid': None, + 'host': 'dummy'}) + + try: + # start test + self.volume.copy_volume_to_image(self.context, + volume_id, + image_id) + + volume = db.volume_get(self.context, volume_id) + self.assertEqual(volume['status'], 'available') + finally: + # cleanup + db.volume_destroy(self.context, volume_id) + os.unlink(dst_path) + + def test_copy_volume_to_image_status_use(self): + dst_fd, dst_path = tempfile.mkstemp() + os.close(dst_fd) + + def fake_local_path(volume): + return dst_path + + self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) + + #image_id = '70a599e0-31e7-49b7-b260-868f441e862b' + image_id = 'a440c04b-79fa-479c-bed1-0b816eaec379' + # creating volume testdata + volume_id = 1 + db.volume_create(self.context, + {'id': volume_id, + 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'display_description': 'Test Desc', + 'size': 20, + 'status': 'uploading', + 'instance_uuid': + 'b21f957d-a72f-4b93-b5a5-45b1161abb02', + 'host': 'dummy'}) + + try: + # start test + self.volume.copy_volume_to_image(self.context, + volume_id, + image_id) + + volume = db.volume_get(self.context, volume_id) + self.assertEqual(volume['status'], 'in-use') + finally: + # cleanup + db.volume_destroy(self.context, volume_id) + os.unlink(dst_path) + + def test_copy_volume_to_image_exception(self): + dst_fd, dst_path = tempfile.mkstemp() + os.close(dst_fd) + + def fake_local_path(volume): + return dst_path + + self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) + + image_id = 'aaaaaaaa-0000-0000-0000-000000000000' + # creating volume testdata + volume_id = 1 + db.volume_create(self.context, {'id': volume_id, + 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), + 'display_description': 'Test Desc', + 'size': 20, + 'status': 'in-use', + 'host': 'dummy'}) + + try: + # start test + self.assertRaises(exception.ImageNotFound, + self.volume.copy_volume_to_image, + self.context, + volume_id, + image_id) + + volume = db.volume_get(self.context, volume_id) + self.assertEqual(volume['status'], 'available') + finally: + # cleanup + db.volume_destroy(self.context, volume_id) + os.unlink(dst_path) + + def test_create_volume_from_exact_sized_image(self): + """Verify that an image which is exactly the same size as the + volume, will work correctly.""" + class _FakeImageService: + def __init__(self, db_driver=None, image_service=None): + pass + + def show(self, context, image_id): + return {'size': 2 * 1024 * 1024 * 1024} + + image_id = '70a599e0-31e7-49b7-b260-868f441e862b' + + try: + volume_id = None + volume_api = nova.volume.api.API( + image_service=_FakeImageService()) + volume = volume_api.create(self.context, 2, 'name', 'description', + image_id=1) + volume_id = volume['id'] + self.assertEqual(volume['status'], 'creating') + + finally: + # cleanup + db.volume_destroy(self.context, volume_id) + + def test_create_volume_from_oversized_image(self): + """Verify that an image which is too big will fail correctly.""" + class _FakeImageService: + def __init__(self, db_driver=None, image_service=None): + pass + + def show(self, context, image_id): + return {'size': 2 * 1024 * 1024 * 1024 + 1} + + image_id = '70a599e0-31e7-49b7-b260-868f441e862b' + + volume_api = nova.volume.api.API(image_service=_FakeImageService()) + + self.assertRaises(exception.InvalidInput, + volume_api.create, + self.context, 2, + 'name', 'description', image_id=1) + class DriverTestCase(test.TestCase): """Base Test class for Drivers.""" @@ -591,7 +824,7 @@ class DriverTestCase(test.TestCase): def tearDown(self): try: shutil.rmtree(FLAGS.volumes_dir) - except OSError, e: + except OSError: pass super(DriverTestCase, self).tearDown() diff --git a/nova/utils.py b/nova/utils.py index 41b268c2e..f2642f07c 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -1135,6 +1135,18 @@ def read_cached_file(filename, cache_info, reload_func=None): return cache_info['data'] +def file_open(*args, **kwargs): + """Open file + + see built-in file() documentation for more details + + Note: The reason this is kept in a separate module is to easily + be able to provide a stub module that doesn't alter system + state at all (for unit tests) + """ + return file(*args, **kwargs) + + def hash_file(file_like_object): """Generate a hash for the contents of a file.""" checksum = hashlib.sha1() diff --git a/nova/volume/api.py b/nova/volume/api.py index 0c18e4a83..0342c0ac2 100644 --- a/nova/volume/api.py +++ b/nova/volume/api.py @@ -25,6 +25,7 @@ import functools from nova.db import base from nova import exception from nova import flags +from nova.image import glance from nova.openstack.common import cfg from nova.openstack.common import log as logging from nova.openstack.common import rpc @@ -42,6 +43,7 @@ FLAGS.register_opt(volume_host_opt) flags.DECLARE('storage_availability_zone', 'nova.volume.manager') LOG = logging.getLogger(__name__) +GB = 1048576 * 1024 QUOTAS = quota.QUOTAS @@ -73,12 +75,15 @@ def check_policy(context, action, target_obj=None): class API(base.Base): """API for interacting with the volume manager.""" - def __init__(self, **kwargs): + def __init__(self, image_service=None, **kwargs): + self.image_service = (image_service or + glance.get_default_image_service()) self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI() super(API, self).__init__(**kwargs) def create(self, context, size, name, description, snapshot=None, - volume_type=None, metadata=None, availability_zone=None): + image_id=None, volume_type=None, metadata=None, + availability_zone=None): check_policy(context, 'create') if snapshot is not None: if snapshot['status'] != "available": @@ -129,6 +134,15 @@ class API(base.Base): % locals()) raise exception.VolumeLimitExceeded(allowed=quotas['volumes']) + if image_id: + # check image existence + image_meta = self.image_service.show(context, image_id) + image_size_in_gb = (int(image_meta['size']) + GB - 1) / GB + #check image size is not larger than volume size. + if image_size_in_gb > size: + msg = _('Size of specified image is larger than volume size.') + raise exception.InvalidInput(reason=msg) + if availability_zone is None: availability_zone = FLAGS.storage_availability_zone @@ -150,14 +164,13 @@ class API(base.Base): 'volume_type_id': volume_type_id, 'metadata': metadata, } - volume = self.db.volume_create(context, options) self._cast_create_volume(context, volume['id'], - snapshot_id, reservations) + snapshot_id, image_id, reservations) return volume def _cast_create_volume(self, context, volume_id, - snapshot_id, reservations): + snapshot_id, image_id, reservations): # NOTE(Rongze Zhu): It is a simple solution for bug 1008866 # If snapshot_id is set, make the call create volume directly to @@ -176,10 +189,12 @@ class API(base.Base): {"method": "create_volume", "args": {"volume_id": volume_id, "snapshot_id": snapshot_id, - "reservations": reservations}}) + "reservations": reservations, + "image_id": image_id}}) + else: self.scheduler_rpcapi.create_volume( - context, volume_id, snapshot_id, reservations) + context, volume_id, snapshot_id, image_id, reservations) @wrap_check_policy def delete(self, context, volume, force=False): @@ -206,6 +221,10 @@ class API(base.Base): {"method": "delete_volume", "args": {"volume_id": volume_id}}) + @wrap_check_policy + def update(self, context, volume, fields): + self.db.volume_update(context, volume['id'], fields) + def get(self, context, volume_id): rv = self.db.volume_get(context, volume_id) volume = dict(rv.iteritems()) @@ -437,3 +456,40 @@ class API(base.Base): if i['key'] == key: return i['value'] return None + + def _check_volume_availability(self, context, volume, force): + """Check if the volume can be used.""" + if volume['status'] not in ['available', 'in-use']: + msg = _('Volume status must be available/in-use.') + raise exception.InvalidVolume(reason=msg) + if not force and 'in-use' == volume['status']: + msg = _('Volume status is in-use.') + raise exception.InvalidVolume(reason=msg) + + @wrap_check_policy + def copy_volume_to_image(self, context, volume, metadata, force): + """Create a new image from the specified volume.""" + self._check_volume_availability(context, volume, force) + + recv_metadata = self.image_service.create(context, metadata) + self.update(context, volume, {'status': 'uploading'}) + rpc.cast(context, + rpc.queue_get_for(context, + FLAGS.volume_topic, + volume['host']), + {"method": "copy_volume_to_image", + "args": {"volume_id": volume['id'], + "image_id": recv_metadata['id']}}) + + response = {"id": volume['id'], + "updated_at": volume['updated_at'], + "status": 'uploading', + "display_description": volume['display_description'], + "size": volume['size'], + "volume_type": volume['volume_type'], + "image_id": recv_metadata['id'], + "container_format": recv_metadata['container_format'], + "disk_format": recv_metadata['disk_format'], + "image_name": recv_metadata.get('name', None) + } + return response diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py index 616fdea2f..951607efb 100644 --- a/nova/volume/cinder.py +++ b/nova/volume/cinder.py @@ -197,21 +197,25 @@ class API(base.Base): volumes.terminate_connection(volume['id'], connector) def create(self, context, size, name, description, snapshot=None, - volume_type=None, metadata=None, availability_zone=None): + image_id=None, volume_type=None, metadata=None, + availability_zone=None): if snapshot is not None: snapshot_id = snapshot['id'] else: snapshot_id = None - item = cinderclient(context).volumes.create(size, - snapshot_id, - name, - description, - volume_type, - context.user_id, - context.project_id, - availability_zone, - metadata) + + kwargs = dict(snapshot_id=snapshot_id, + display_name=name, + display_description=description, + volume_type=volume_type, + user_id=context.user_id, + project_id=context.project_id, + availability_zone=availability_zone, + metadata=metadata, + imageRef=image_id) + + item = cinderclient(context).volumes.create(size, **kwargs) return _untranslate_volume_summary_view(context, item) diff --git a/nova/volume/driver.py b/nova/volume/driver.py index bb80d9f93..0a124923b 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -261,6 +261,14 @@ class VolumeDriver(object): """Any initialization the volume driver does while starting""" pass + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + raise NotImplementedError() + + def copy_volume_to_image(self, context, volume, image_service, image_id): + """Copy the volume to the specified image.""" + raise NotImplementedError() + class ISCSIDriver(VolumeDriver): """Executes commands relating to ISCSI volumes. @@ -541,6 +549,20 @@ class ISCSIDriver(VolumeDriver): "id:%(volume_id)s.") % locals()) raise + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + volume_path = self.local_path(volume) + with utils.temporary_chown(volume_path): + with utils.file_open(volume_path, "wb") as image_file: + image_service.download(context, image_id, image_file) + + def copy_volume_to_image(self, context, volume, image_service, image_id): + """Copy the volume to the specified image.""" + volume_path = self.local_path(volume) + with utils.temporary_chown(volume_path): + with utils.file_open(volume_path) as volume_file: + image_service.update(context, image_id, {}, volume_file) + class FakeISCSIDriver(ISCSIDriver): """Logs calls instead of executing.""" diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 0631925ad..cd3471146 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -41,6 +41,7 @@ intact. from nova import context from nova import exception from nova import flags +from nova.image import glance from nova import manager from nova.openstack.common import cfg from nova.openstack.common import excutils @@ -112,7 +113,7 @@ class VolumeManager(manager.SchedulerDependentManager): self.delete_volume(ctxt, volume['id']) def create_volume(self, context, volume_id, snapshot_id=None, - reservations=None): + image_id=None, reservations=None): """Creates and exports the volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) @@ -126,6 +127,11 @@ class VolumeManager(manager.SchedulerDependentManager): # before passing it to the driver. volume_ref['host'] = self.host + if image_id: + status = 'downloading' + else: + status = 'available' + try: vol_name = volume_ref['name'] vol_size = volume_ref['size'] @@ -158,11 +164,15 @@ class VolumeManager(manager.SchedulerDependentManager): now = timeutils.utcnow() volume_ref = self.db.volume_update(context, - volume_ref['id'], {'status': 'available', + volume_ref['id'], {'status': status, 'launched_at': now}) LOG.debug(_("volume %s: created successfully"), volume_ref['name']) self._reset_stats() self._notify_about_volume_usage(context, volume_ref, "create.end") + + if image_id: + #copy the image onto the volume. + self._copy_image_to_volume(context, volume_ref, image_id) return volume_id def delete_volume(self, context, volume_id): @@ -174,7 +184,7 @@ class VolumeManager(manager.SchedulerDependentManager): raise exception.VolumeAttached(volume_id=volume_id) if volume_ref['host'] != self.host: raise exception.InvalidVolume( - reason=_("Volume is not local to this node")) + reason=_("Volume is not local to this node")) self._notify_about_volume_usage(context, volume_ref, "delete.start") self._reset_stats() @@ -183,7 +193,7 @@ class VolumeManager(manager.SchedulerDependentManager): self.driver.remove_export(context, volume_ref) LOG.debug(_("volume %s: deleting"), volume_ref['name']) self.driver.delete_volume(volume_ref) - except exception.VolumeIsBusy, e: + except exception.VolumeIsBusy: LOG.debug(_("volume %s: volume is busy"), volume_ref['name']) self.driver.ensure_export(context, volume_ref) self.db.volume_update(context, volume_ref['id'], @@ -298,6 +308,47 @@ class VolumeManager(manager.SchedulerDependentManager): self.db.volume_detached(context, volume_id) + def _copy_image_to_volume(self, context, volume, image_id): + """Downloads Glance image to the specified volume. """ + volume_id = volume['id'] + payload = {'volume_id': volume_id, 'image_id': image_id} + try: + image_service, image_id = glance.get_remote_image_service(context, + image_id) + self.driver.copy_image_to_volume(context, volume, image_service, + image_id) + LOG.debug(_("Downloaded image %(image_id)s to %(volume_id)s " + "successfully") % locals()) + self.db.volume_update(context, volume_id, + {'status': 'available'}) + except Exception, error: + with excutils.save_and_reraise_exception(): + payload['message'] = unicode(error) + self.db.volume_update(context, volume_id, {'status': 'error'}) + + def copy_volume_to_image(self, context, volume_id, image_id): + """Uploads the specified volume to Glance.""" + payload = {'volume_id': volume_id, 'image_id': image_id} + try: + volume = self.db.volume_get(context, volume_id) + self.driver.ensure_export(context.elevated(), volume) + image_service, image_id = glance.get_remote_image_service(context, + image_id) + self.driver.copy_volume_to_image(context, volume, image_service, + image_id) + LOG.debug(_("Uploaded volume %(volume_id)s to " + "image (%(image_id)s) successfully") % locals()) + except Exception, error: + with excutils.save_and_reraise_exception(): + payload['message'] = unicode(error) + finally: + if volume['instance_uuid'] is None: + self.db.volume_update(context, volume_id, + {'status': 'available'}) + else: + self.db.volume_update(context, volume_id, + {'status': 'in-use'}) + def initialize_connection(self, context, volume_id, connector): """Prepare volume for connection from host represented by connector. |