]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Count Snapshots towards volume/gigabyte quotas.
authorJohn Griffith <john.griffith@solidfire.com>
Mon, 11 Mar 2013 15:21:56 +0000 (09:21 -0600)
committerJohn Griffith <john.griffith@solidfire.com>
Thu, 14 Mar 2013 17:19:54 +0000 (11:19 -0600)
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

cinder/exception.py
cinder/quota.py
cinder/tests/api/contrib/test_backups.py
cinder/tests/test_quota.py
cinder/volume/api.py
etc/cinder/cinder.conf.sample

index c5dd7e61ad9867344755fa845e9ef5161bd48884..5aaf6537a7ee103dab02756e1afd6ffc2cdf33e2 100644 (file)
@@ -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")
 
index a8c99bb6e1b3f457e822a43c6c9a776b6b134baf..2dd21875227b54ad8a53bfba8681eaef6e9c64d2 100644 (file)
@@ -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'), ]
 
 
index bc192cb26b4e2e050a56b98a642ad7064e1290ec..b5c8d1c531a1fd8363fa59ee09eaa0c72bb1882d 100644 (file)
@@ -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):
 
index fc7395b98b8d3886539b6fcd259d492d92a64436..f6c81837b57c5502f272ce36cec7ba8174992c78 100644 (file)
@@ -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):
index 2a7a61c8647d92be8197c5347559b799506af5fc..a6fb584b18d005c631381dd7414ff2eefe873288 100644 (file)
@@ -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
index 4b54fc84d0af8228480c99c6e0037f27e8d55fd8..d0bd2f4da11b1063331ba3c569a6910bc67258bf 100644 (file)
 # 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