summaryrefslogtreecommitdiffstats
path: root/nova/volume
diff options
context:
space:
mode:
authorUnmesh Gurjar <unmesh.gurjar@vertex.co.in>2012-08-11 10:31:51 -0700
committerJosh Durgin <josh.durgin@inktank.com>2012-09-18 08:54:25 -0700
commitde09c1866b9138610914ddaaebb9b030884d1e28 (patch)
tree3a26edadd5d7a794b7c46dd2f30d08285878f3c1 /nova/volume
parentf615e9c22c4c003ac1cd3d01ec8f7cbabd76b96d (diff)
downloadnova-de09c1866b9138610914ddaaebb9b030884d1e28.tar.gz
nova-de09c1866b9138610914ddaaebb9b030884d1e28.tar.xz
nova-de09c1866b9138610914ddaaebb9b030884d1e28.zip
Adds new volume API extensions
Adds following extensions: 1. Create volume from image 2. Copy volume to image Added unit tests. Implements: blueprint create-volume-from-image Conflicts: cinder/api/openstack/volume/contrib/volume_actions.py cinder/tests/api/openstack/fakes.py cinder/tests/api/openstack/volume/contrib/test_volume_actions.py cinder/tests/policy.json nova/api/openstack/volume/volumes.py nova/flags.py nova/tests/api/openstack/volume/test_volumes.py nova/tests/test_volume.py nova/utils.py nova/volume/api.py nova/volume/manager.py This is based on a cherry-pick of cinder commit 2f5360753308eb8b10581fc3c026c1b66f42ebdc with bug fixes 8c30edff982042d2533a810709308b586267c0e9 and ffe5036fa0e63ccde2d19aa0f425ec43de338dd7 squashed in. Change-Id: I9c73bd3fa2fa2e0648c01ff3f4fc66f757d7bc3f
Diffstat (limited to 'nova/volume')
-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
4 files changed, 154 insertions, 21 deletions
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.