]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Implement capability to extend existing volume.
authorJohn Griffith <john.griffith@solidfire.com>
Mon, 10 Jun 2013 20:26:57 +0000 (14:26 -0600)
committerJohn Griffith <john.griffith@solidfire.com>
Fri, 14 Jun 2013 05:47:39 +0000 (23:47 -0600)
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 <vol-id> <new-size>

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
cinder/tests/api/contrib/test_volume_actions.py
cinder/tests/policy.json
cinder/tests/test_volume.py
cinder/volume/api.py
cinder/volume/driver.py
cinder/volume/manager.py
cinder/volume/rpcapi.py
etc/cinder/policy.json

index 7ab19d710e6a0a750a7c0a9e008bd3c776f28087..f48773f24b9108c3bca2ddbde145ec207afeb632 100644 (file)
@@ -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
index 38b747f75e0141e4b1ceacf88d91c000f71b55aa..1b8be5b05c01f8ab62df1847e2ffa0c0e4397b55 100644 (file)
@@ -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)
index 86ee69dc566e6e311230d2dbead5693d4d5f1a4a..da0920fadea20f63917d58469d2f4558d344e033 100644 (file)
@@ -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"]],
index 64761e83100be3e2e3cd94fd7977c2606d9c11b3..cd952519ef6603674a22666bfb50a36d485eb873 100644 (file)
@@ -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."""
index 7d1510e4088c9e408dd2f75059c9b7aa1927c9f2..bdec96107b8f4ad7b9d636ea5a4fabc27fef21c6 100644 (file)
@@ -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):
index a6b4455719eda63ac66b6d274e5a35cf625b1a5f..8ada531826fd537b1c2b5aa83cfd6867c715ca79 100644 (file)
@@ -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.
index e986b7cd0ee671690b8abe263cfca79deb52efb1..fe225a01a49d2a06a41f2159adb60871c61ac815 100644 (file)
@@ -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)
index e3ab89e00336fcd28353508c31bb6f50a1124608..c6bbdd438c7d1f78612c1cbcd91aae3f0b32e314 100644 (file)
@@ -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')
index 63196f8de00d15a018b183088a266a1e61b6c9b3..da120983d6190c15c1de9d70b68f47e57408e70e 100644 (file)
@@ -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"]],