From: John Griffith Date: Mon, 11 Mar 2013 15:21:56 +0000 (-0600) Subject: Count Snapshots towards volume/gigabyte quotas. X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=4b52b1481e3cb6c358252826785228638b0f717d;p=openstack-build%2Fcinder-build.git 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 --- diff --git a/cinder/exception.py b/cinder/exception.py index c5dd7e61a..5aaf6537a 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -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") diff --git a/cinder/quota.py b/cinder/quota.py index a8c99bb6e..2dd218752 100644 --- a/cinder/quota.py +++ b/cinder/quota.py @@ -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'), ] diff --git a/cinder/tests/api/contrib/test_backups.py b/cinder/tests/api/contrib/test_backups.py index bc192cb26..b5c8d1c53 100644 --- a/cinder/tests/api/contrib/test_backups.py +++ b/cinder/tests/api/contrib/test_backups.py @@ -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): diff --git a/cinder/tests/test_quota.py b/cinder/tests/test_quota.py index fc7395b98..f6c81837b 100644 --- a/cinder/tests/test_quota.py +++ b/cinder/tests/test_quota.py @@ -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): diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 2a7a61c86..a6fb584b1 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -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 diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 4b54fc84d..d0bd2f4da 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -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