From 2833754310d19584c3fc95b01a3c1eeccf1a3620 Mon Sep 17 00:00:00 2001 From: John Griffith Date: Mon, 10 Jun 2013 14:26:57 -0600 Subject: [PATCH] Implement capability to extend existing volume. This patch adds the core components to implement extending the size of an existing available volume. Volume status must be available, and the format is: extend where new-size must be > current size. Adding support to drivers will be handled in follow up patches for each of the existing drivers. Implements blueprint: volume-resize Change-Id: I40026083e564ea2074757e11e13cd07cdae3e6cc --- cinder/api/contrib/volume_actions.py | 15 ++++++ .../tests/api/contrib/test_volume_actions.py | 15 ++++++ cinder/tests/policy.json | 1 + cinder/tests/test_volume.py | 33 +++++++++++++ cinder/volume/api.py | 49 +++++++++++++++++++ cinder/volume/driver.py | 4 ++ cinder/volume/manager.py | 6 ++- cinder/volume/rpcapi.py | 9 ++++ etc/cinder/policy.json | 1 + 9 files changed, 132 insertions(+), 1 deletion(-) diff --git a/cinder/api/contrib/volume_actions.py b/cinder/api/contrib/volume_actions.py index 7ab19d710..f48773f24 100644 --- a/cinder/api/contrib/volume_actions.py +++ b/cinder/api/contrib/volume_actions.py @@ -186,6 +186,21 @@ class VolumeActionsController(wsgi.Controller): raise webob.exc.HTTPBadRequest(explanation=msg) return {'os-volume_upload_image': response} + @wsgi.action('os-extend') + def _extend(self, req, id, body): + """Extend size of volume.""" + context = req.environ['cinder.context'] + volume = self.volume_api.get(context, id) + try: + val = int(body['os-extend']['new_size']) + except ValueError: + msg = _("New volume size must be specified as an integer.") + raise webob.exc.HTTPBadRequest(explanation=msg) + + size = body['os-extend']['new_size'] + self.volume_api.extend(context, volume, size) + return webob.Response(status_int=202) + class Volume_actions(extensions.ExtensionDescriptor): """Enable volume actions diff --git a/cinder/tests/api/contrib/test_volume_actions.py b/cinder/tests/api/contrib/test_volume_actions.py index 38b747f75..1b8be5b05 100644 --- a/cinder/tests/api/contrib/test_volume_actions.py +++ b/cinder/tests/api/contrib/test_volume_actions.py @@ -104,6 +104,21 @@ class VolumeActionsTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 202) + def test_extend_volume(self): + def fake_extend_volume(*args, **kwargs): + return {} + self.stubs.Set(volume.API, 'extend', + fake_extend_volume) + + body = {'os-extend': {'new_size': 5}} + req = webob.Request.blank('/v2/fake/volumes/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + def stub_volume_get(self, context, volume_id): volume = stubs.stub_volume(volume_id) diff --git a/cinder/tests/policy.json b/cinder/tests/policy.json index 86ee69dc5..da0920fad 100644 --- a/cinder/tests/policy.json +++ b/cinder/tests/policy.json @@ -25,6 +25,7 @@ "volume:get_snapshot": [], "volume:get_all_snapshots": [], "volume:update_snapshot": [], + "volume:extend": [], "volume_extension:volume_admin_actions:reset_status": [["rule:admin_api"]], "volume_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]], diff --git a/cinder/tests/test_volume.py b/cinder/tests/test_volume.py index 64761e831..cd952519e 100644 --- a/cinder/tests/test_volume.py +++ b/cinder/tests/test_volume.py @@ -1176,6 +1176,39 @@ class VolumeTestCase(test.TestCase): self.assertEqual(snapshots[1].id, u'3') self.assertEqual(snapshots[2].id, u'4') + def test_extend_volume(self): + """Test volume can be extended.""" + # create a volume and assign to host + volume = self._create_volume(2) + self.volume.create_volume(self.context, volume['id']) + volume['status'] = 'available' + volume['host'] = 'fakehost' + + volume_api = cinder.volume.api.API() + + # Extend fails when new_size < orig_size + self.assertRaises(exception.InvalidInput, + volume_api.extend, + self.context, + volume, + 1) + + # Extend fails when new_size == orig_size + self.assertRaises(exception.InvalidInput, + volume_api.extend, + self.context, + volume, + 2) + + # works when new_size > orig_size + volume_api.extend(self.context, volume, 3) + + volume = db.volume_get(context.get_admin_context(), volume['id']) + self.assertEquals(volume['size'], 3) + + # clean up + self.volume.delete_volume(self.context, volume['id']) + class DriverTestCase(test.TestCase): """Base Test class for Drivers.""" diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 7d1510e40..bdec96107 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -780,6 +780,55 @@ class API(base.Base): "image_name": recv_metadata.get('name', None)} return response + @wrap_check_policy + def extend(self, context, volume, new_size): + if volume['status'] != 'available': + msg = _('Volume status must be available to extend.') + raise exception.InvalidVolume(reason=msg) + + size_increase = (int(new_size)) - volume['size'] + if size_increase <= 0: + msg = (_("New size for extend must be greater " + "than current size. (current: %(size)s, " + "extended: %(new_size)s)") % {'new_size': new_size, + 'size': volume['size']}) + raise exception.InvalidInput(reason=msg) + try: + reservations = QUOTAS.reserve(context, gigabytes=+size_increase) + except exception.OverQuota as exc: + overs = exc.kwargs['overs'] + usages = exc.kwargs['usages'] + quotas = exc.kwargs['quotas'] + + def _consumed(name): + return (usages[name]['reserved'] + usages[name]['in_use']) + + if 'gigabytes' in overs: + msg = _("Quota exceeded for %(s_pid)s, " + "tried to extend volume by " + "%(s_size)sG, (%(d_consumed)dG of %(d_quota)dG " + "already consumed)") + LOG.warn(msg % {'s_pid': context.project_id, + 's_size': size_increase, + 'd_consumed': _consumed('gigabytes'), + 'd_quota': quotas['gigabytes']}) + raise exception.VolumeSizeExceedsAvailableQuota() + + self.update(context, volume, {'status': 'extending'}) + + try: + self.volume_rpcapi.extend_volume(context, volume, new_size) + except Exception: + with excutils.save_and_reraise_exception(): + try: + self.update(context, volume, {'status': 'error_extending'}) + finally: + QUOTAS.rollback(context, reservations) + + self.update(context, volume, {'size': new_size}) + QUOTAS.commit(context, reservations) + self.update(context, volume, {'status': 'available'}) + class HostAPI(base.Base): def __init__(self): diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index a6b445571..8ada53182 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -195,6 +195,10 @@ class VolumeDriver(object): """Clean up after an interrupted image copy.""" pass + def extend_volume(self, volume, new_size): + msg = _("Extend volume not implemented") + raise NotImplementedError(msg) + class ISCSIDriver(VolumeDriver): """Executes commands relating to ISCSI volumes. diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index e986b7cd0..fe225a01a 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -108,7 +108,7 @@ MAPPING = { class VolumeManager(manager.SchedulerDependentManager): """Manages attachable block storage devices.""" - RPC_API_VERSION = '1.4' + RPC_API_VERSION = '1.6' def __init__(self, volume_driver=None, service_name=None, *args, **kwargs): @@ -747,3 +747,7 @@ class VolumeManager(manager.SchedulerDependentManager): volume_utils.notify_about_snapshot_usage( context, snapshot, event_suffix, extra_usage_info=extra_usage_info, host=self.host) + + def extend_volume(self, context, volume_id, new_size): + volume_ref = self.db.volume_get(context, volume_id) + self.driver.extend_volume(volume_ref, new_size) diff --git a/cinder/volume/rpcapi.py b/cinder/volume/rpcapi.py index e3ab89e00..c6bbdd438 100644 --- a/cinder/volume/rpcapi.py +++ b/cinder/volume/rpcapi.py @@ -40,6 +40,7 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy): 1.4 - Add request_spec, filter_properties and allow_reschedule arguments to create_volume(). 1.5 - Add accept_transfer + 1.6 - Add extend_volume ''' BASE_RPC_API_VERSION = '1.0' @@ -137,3 +138,11 @@ class VolumeAPI(cinder.openstack.common.rpc.proxy.RpcProxy): volume_id=volume['id']), topic=rpc.queue_get_for(ctxt, self.topic, volume['host']), version='1.5') + + def extend_volume(self, ctxt, volume, new_size): + self.cast(ctxt, + self.make_msg('extend_volume', + volume_id=volume['id'], + new_size=new_size), + topic=rpc.queue_get_for(ctxt, self.topic, volume['host']), + version='1.6') diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index 63196f8de..da120983d 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -10,6 +10,7 @@ "volume:get_volume_metadata": [], "volume:get_snapshot": [], "volume:get_all_snapshots": [], + "volume:extend": [], "volume_extension:types_manage": [["rule:admin_api"]], "volume_extension:types_extra_specs": [["rule:admin_api"]], -- 2.45.2