Count Snapshots towards volume/gigabyte quotas.

Cinder has quotas and limits for volume-count and Gigabytes used,
however we were only counting volumes against these quotas.

This change introduces a snapshot-count limit and also counts
snapshots against this Gigabytes quota allowed for a Tenant.

Fixed bug: 1137927

Change-Id: Ib9b00b84b05597de9b5725a7f5898fe10a20b9d9
This commit is contained in:
John Griffith 2013-03-11 09:21:56 -06:00
parent b8885203c1
commit 4b52b1481e
6 changed files with 88 additions and 10 deletions

@ -450,17 +450,22 @@ class QuotaError(CinderException):
class VolumeSizeExceedsAvailableQuota(QuotaError):
message = _("Requested volume exceeds allowed volume size quota")
message = _("Requested volume or snapshot exceeds "
"allowed Gigabytes quota")
class VolumeSizeExceedsQuota(QuotaError):
message = _("Maximum volume size exceeded")
message = _("Maximum volume/snapshot size exceeded")
class VolumeLimitExceeded(QuotaError):
message = _("Maximum number of volumes allowed (%(allowed)d) exceeded")
class SnapshotLimitExceeded(QuotaError):
message = _("Maximum number of snapshots allowed (%(allowed)d) exceeded")
class DuplicateSfVolumeNames(Duplicate):
message = _("Detected more than one volume with name %(vol_name)s")

@ -35,9 +35,13 @@ quota_opts = [
cfg.IntOpt('quota_volumes',
default=10,
help='number of volumes allowed per project'),
cfg.IntOpt('quota_snapshots',
default=10,
help='number of volume snapshots allowed per project'),
cfg.IntOpt('quota_gigabytes',
default=1000,
help='number of volume gigabytes allowed per project'),
help='number of volume gigabytes (snapshots are also included) '
'allowed per project'),
cfg.IntOpt('reservation_expire',
default=86400,
help='number of seconds until a reservation expires'),
@ -732,11 +736,19 @@ def _sync_volumes(context, project_id, session):
session=session)))
def _sync_snapshots(context, project_id, session):
return dict(zip(('snapshots', 'gigabytes'),
db.volume_data_get_for_project(context,
project_id,
session=session)))
QUOTAS = QuotaEngine()
resources = [
ReservableResource('volumes', _sync_volumes, 'quota_volumes'),
ReservableResource('snapshots', _sync_snapshots, 'quota_snapshots'),
ReservableResource('gigabytes', _sync_volumes, 'quota_gigabytes'), ]

@ -773,7 +773,8 @@ class BackupsAPITestCase(test.TestCase):
self.assertEqual(res.status_int, 413)
self.assertEqual(res_dict['overLimit']['code'], 413)
self.assertEqual(res_dict['overLimit']['message'],
'Requested volume exceeds allowed volume size quota')
'Requested volume or snapshot exceeds allowed '
'Gigabytes quota')
def test_restore_backup_with_VolumeLimitExceeded(self):

@ -40,6 +40,7 @@ class QuotaIntegrationTestCase(test.TestCase):
def setUp(self):
super(QuotaIntegrationTestCase, self).setUp()
self.flags(quota_volumes=2,
quota_snapshots=2,
quota_gigabytes=20)
# Apparently needed by the RPC tests...
@ -568,6 +569,7 @@ class DbQuotaDriverTestCase(test.TestCase):
super(DbQuotaDriverTestCase, self).setUp()
self.flags(quota_volumes=10,
quota_snapshots=10,
quota_gigabytes=1000,
reservation_expire=86400,
until_refresh=0,
@ -592,6 +594,7 @@ class DbQuotaDriverTestCase(test.TestCase):
result,
dict(
volumes=10,
snapshots=10,
gigabytes=1000, ))
def _stub_quota_class_get_all_by_name(self):
@ -599,7 +602,7 @@ class DbQuotaDriverTestCase(test.TestCase):
def fake_qcgabn(context, quota_class):
self.calls.append('quota_class_get_all_by_name')
self.assertEqual(quota_class, 'test_class')
return dict(gigabytes=500, volumes=10, )
return dict(gigabytes=500, volumes=10, snapshots=10, )
self.stubs.Set(db, 'quota_class_get_all_by_name', fake_qcgabn)
def test_get_class_quotas(self):
@ -608,7 +611,9 @@ class DbQuotaDriverTestCase(test.TestCase):
'test_class')
self.assertEqual(self.calls, ['quota_class_get_all_by_name'])
self.assertEqual(result, dict(volumes=10, gigabytes=500, ))
self.assertEqual(result, dict(volumes=10,
gigabytes=500,
snapshots=10))
def test_get_class_quotas_no_defaults(self):
self._stub_quota_class_get_all_by_name()
@ -616,18 +621,21 @@ class DbQuotaDriverTestCase(test.TestCase):
'test_class', False)
self.assertEqual(self.calls, ['quota_class_get_all_by_name'])
self.assertEqual(result, dict(volumes=10, gigabytes=500, ))
self.assertEqual(result, dict(volumes=10,
gigabytes=500,
snapshots=10))
def _stub_get_by_project(self):
def fake_qgabp(context, project_id):
self.calls.append('quota_get_all_by_project')
self.assertEqual(project_id, 'test_project')
return dict(volumes=10, gigabytes=50, reserved=0)
return dict(volumes=10, gigabytes=50, reserved=0, snapshots=10)
def fake_qugabp(context, project_id):
self.calls.append('quota_usage_get_all_by_project')
self.assertEqual(project_id, 'test_project')
return dict(volumes=dict(in_use=2, reserved=0),
snapshots=dict(in_use=2, reserved=0),
gigabytes=dict(in_use=10, reserved=0), )
self.stubs.Set(db, 'quota_get_all_by_project', fake_qgabp)
@ -647,6 +655,9 @@ class DbQuotaDriverTestCase(test.TestCase):
self.assertEqual(result, dict(volumes=dict(limit=10,
in_use=2,
reserved=0, ),
snapshots=dict(limit=10,
in_use=2,
reserved=0, ),
gigabytes=dict(limit=50,
in_use=10,
reserved=0, ), ))
@ -662,6 +673,9 @@ class DbQuotaDriverTestCase(test.TestCase):
self.assertEqual(result, dict(volumes=dict(limit=10,
in_use=2,
reserved=0, ),
snapshots=dict(limit=10,
in_use=2,
reserved=0, ),
gigabytes=dict(limit=50,
in_use=10,
reserved=0, ), ))
@ -678,6 +692,9 @@ class DbQuotaDriverTestCase(test.TestCase):
self.assertEqual(result, dict(volumes=dict(limit=10,
in_use=2,
reserved=0, ),
snapshots=dict(limit=10,
in_use=2,
reserved=0, ),
gigabytes=dict(limit=50,
in_use=10,
reserved=0, ), ))
@ -695,6 +712,9 @@ class DbQuotaDriverTestCase(test.TestCase):
dict(gigabytes=dict(limit=50,
in_use=10,
reserved=0, ),
snapshots=dict(limit=10,
in_use=2,
reserved=0, ),
volumes=dict(limit=10,
in_use=2,
reserved=0, ), ))
@ -708,6 +728,7 @@ class DbQuotaDriverTestCase(test.TestCase):
self.assertEqual(self.calls, ['quota_get_all_by_project',
'quota_class_get_all_by_name', ])
self.assertEqual(result, dict(volumes=dict(limit=10, ),
snapshots=dict(limit=10, ),
gigabytes=dict(limit=50, ), ))
def _stub_get_project_quotas(self):

@ -490,6 +490,33 @@ class API(base.Base):
msg = _("must be available")
raise exception.InvalidVolume(reason=msg)
try:
reservations = QUOTAS.reserve(context, snapshots=1,
gigabytes=volume['size'])
except exception.OverQuota as e:
overs = e.kwargs['overs']
usages = e.kwargs['usages']
quotas = e.kwargs['quotas']
def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use'])
pid = context.project_id
if 'gigabytes' in overs:
consumed = _consumed('gigabytes')
quota = quotas['gigabytes']
LOG.warn(_("Quota exceeded for %(pid)s, tried to create "
"%(size)sG volume (%(consumed)dG of %(quota)dG "
"already consumed)") % locals())
raise exception.VolumeSizeExceedsAvailableQuota()
elif 'snapshots' in overs:
consumed = _consumed('snapshots')
LOG.warn(_("Quota exceeded for %(pid)s, tried to create "
"snapshot (%(consumed)d snapshots "
"already consumed)") % locals())
raise exception.SnapshotLimitExceeded(
allowed=quotas['snapshots'])
self._check_metadata_properties(context, metadata)
options = {'volume_id': volume['id'],
'user_id': context.user_id,
@ -501,7 +528,16 @@ class API(base.Base):
'display_description': description,
'metadata': metadata}
snapshot = self.db.snapshot_create(context, options)
try:
snapshot = self.db.snapshot_create(context, options)
QUOTAS.commit(context, reservations)
except Exception:
with excutils.save_and_reraise_exception():
try:
self.db.snapshot_destroy(context, volume['id'])
finally:
QUOTAS.rollback(context, reservations)
self.volume_rpcapi.create_snapshot(context, volume, snapshot)
return snapshot

@ -226,7 +226,10 @@
# number of volumes allowed per project (integer value)
#quota_volumes=10
# number of volume gigabytes allowed per project (integer
# number of volume snapshots allowed per project (integer value)
#quota_snapshots=10
# number of volume and snapshot gigabytes allowed per project (integer
# value)
#quota_gigabytes=1000