diff options
| author | Unmesh Gurjar <unmesh.gurjar@vertex.co.in> | 2012-08-11 10:31:51 -0700 |
|---|---|---|
| committer | Josh Durgin <josh.durgin@inktank.com> | 2012-09-18 08:54:25 -0700 |
| commit | de09c1866b9138610914ddaaebb9b030884d1e28 (patch) | |
| tree | 3a26edadd5d7a794b7c46dd2f30d08285878f3c1 /nova/volume | |
| parent | f615e9c22c4c003ac1cd3d01ec8f7cbabd76b96d (diff) | |
| download | nova-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.py | 70 | ||||
| -rw-r--r-- | nova/volume/cinder.py | 24 | ||||
| -rw-r--r-- | nova/volume/driver.py | 22 | ||||
| -rw-r--r-- | nova/volume/manager.py | 59 |
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. |
