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 <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
This commit is contained in:
parent
28c6255fc7
commit
2833754310
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"]],
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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"]],
|
||||
|
Loading…
x
Reference in New Issue
Block a user