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:
parent
b8885203c1
commit
4b52b1481e
cinder
etc/cinder
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user