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
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)
"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"]],
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."""
"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):
"""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.
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):
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)
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'
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')
"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"]],