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:
John Griffith 2013-06-10 14:26:57 -06:00
parent 28c6255fc7
commit 2833754310
9 changed files with 132 additions and 1 deletions

View 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

View 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)

View 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"]],

View 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."""

View 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):

View 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.

View 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)

View 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')

View 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"]],