summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
Diffstat (limited to 'nova')
-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
-rw-r--r--nova/scheduler/chance.py4
-rw-r--r--nova/scheduler/driver.py2
-rw-r--r--nova/scheduler/filter_scheduler.py2
-rw-r--r--nova/scheduler/manager.py5
-rw-r--r--nova/scheduler/rpcapi.py5
-rw-r--r--nova/scheduler/simple.py7
-rw-r--r--nova/tests/api/openstack/fakes.py12
-rw-r--r--nova/tests/api/openstack/volume/contrib/test_volume_actions.py162
-rw-r--r--nova/tests/api/openstack/volume/test_router.py6
-rw-r--r--nova/tests/api/openstack/volume/test_volumes.py108
-rw-r--r--nova/tests/policy.json1
-rw-r--r--nova/tests/scheduler/test_rpcapi.py3
-rw-r--r--nova/tests/test_volume.py241
-rw-r--r--nova/utils.py12
-rw-r--r--nova/volume/api.py70
-rw-r--r--nova/volume/cinder.py24
-rw-r--r--nova/volume/driver.py22
-rw-r--r--nova/volume/manager.py59
23 files changed, 913 insertions, 48 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}
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.