Blob Blame History Raw
From 19631f3b4708443486950b602b6b95d13d6d7e74 Mon Sep 17 00:00:00 2001
From: Eoghan Glynn <eglynn@redhat.com>
Date: Wed, 4 Jul 2012 19:14:00 +0100
Subject: [PATCH] Distinguish over-quota for volume size and number.

Fixes LP 1020634

Ensure that exceeding the allowed number of volumes is not
mis-represented in log messages and exception handling as
excessive space usage.

Change-Id: I71ec995c77bc447bfc9221084b057bd8d69a4513
---
 nova/quota.py             |   35 +++++++++++++++++++---------
 nova/tests/test_quota.py  |   55 ++++++++++++++++++++++++--------------------
 nova/tests/test_volume.py |   30 ++++++++++++++++++++++++
 nova/volume/api.py        |   26 ++++++++++++++++++---
 4 files changed, 106 insertions(+), 40 deletions(-)

diff --git a/nova/quota.py b/nova/quota.py
index 12dd146..505dce0 100644
--- a/nova/quota.py
+++ b/nova/quota.py
@@ -129,23 +129,36 @@ def allowed_instances(context, requested_instances, instance_type):
 
 
 def allowed_volumes(context, requested_volumes, size):
-    """Check quota and return min(requested_volumes, allowed_volumes)."""
+    """Check volume quotas and return breached if any."""
     project_id = context.project_id
     context = context.elevated()
     size = int(size)
-    requested_gigabytes = requested_volumes * size
+
+    allowed = {}
+    overs = []
+
     used_volumes, used_gigabytes = db.volume_data_get_for_project(context,
                                                                   project_id)
-    quota = get_project_quotas(context, project_id)
-    allowed_volumes = _get_request_allotment(requested_volumes, used_volumes,
-                                             quota['volumes'])
-    allowed_gigabytes = _get_request_allotment(requested_gigabytes,
-                                               used_gigabytes,
-                                               quota['gigabytes'])
+    usages = dict(volumes=used_volumes, gigabytes=used_gigabytes)
+
+    quotas = get_project_quotas(context, project_id)
+
+    def _check_allowed(resource, requested):
+        allow = _get_request_allotment(requested,
+                                       usages[resource],
+                                       quotas[resource])
+        if requested and allow < requested:
+            overs.append(resource)
+        allowed[resource] = allow
+
+    _check_allowed('volumes', requested_volumes)
+    _check_allowed('gigabytes', requested_volumes * size)
+
     if size != 0:
-        allowed_volumes = min(allowed_volumes,
-                              int(allowed_gigabytes // size))
-    return min(requested_volumes, allowed_volumes)
+        allowed['volumes'] = min(allowed['volumes'],
+                                 int(allowed['gigabytes'] // size))
+
+    return dict(overs=overs, usages=usages, quotas=quotas, allowed=allowed)
 
 
 def allowed_floating_ips(context, requested_floating_ips):
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index 8cc5577..286600c 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -196,31 +196,36 @@ class QuotaTestCase(test.TestCase):
                                                 instance_type)
         self.assertEqual(num_instances, 101)
 
-    def test_unlimited_volumes(self):
-        self.flags(quota_volumes=10, quota_gigabytes=-1)
-        volumes = quota.allowed_volumes(self.context, 100, 1)
-        self.assertEqual(volumes, 10)
-        db.quota_create(self.context, self.project_id, 'volumes', None)
-        volumes = quota.allowed_volumes(self.context, 100, 1)
-        self.assertEqual(volumes, 100)
-        db.quota_create(self.context, self.project_id, 'volumes', -1)
-        volumes = quota.allowed_volumes(self.context, 100, 1)
-        self.assertEqual(volumes, 100)
-        volumes = quota.allowed_volumes(self.context, 101, 1)
-        self.assertEqual(volumes, 101)
-
-    def test_unlimited_gigabytes(self):
-        self.flags(quota_volumes=-1, quota_gigabytes=10)
-        volumes = quota.allowed_volumes(self.context, 100, 1)
-        self.assertEqual(volumes, 10)
-        db.quota_create(self.context, self.project_id, 'gigabytes', None)
-        volumes = quota.allowed_volumes(self.context, 100, 1)
-        self.assertEqual(volumes, 100)
-        db.quota_create(self.context, self.project_id, 'gigabytes', -1)
-        volumes = quota.allowed_volumes(self.context, 100, 1)
-        self.assertEqual(volumes, 100)
-        volumes = quota.allowed_volumes(self.context, 101, 1)
-        self.assertEqual(volumes, 101)
+    def _do_test_volume_quota(self, resource):
+
+        def _validate(result, request, quota, allow):
+            overs = result['overs']
+            usages = result['usages']
+            quotas = result['quotas']
+            allowed = result['allowed']
+            self.assertEquals(request > allow, resource in overs)
+            self.assertEquals(usages[resource], 0)
+            self.assertEquals(quotas[resource], quota)
+            self.assertEquals(allowed[resource], allow)
+
+        quota_volumes = 10 if resource == 'volumes' else -1
+        quota_gigabytes = 10 if resource == 'gigabytes' else -1
+        self.flags(quota_volumes=quota_volumes,
+                   quota_gigabytes=quota_gigabytes)
+        _validate(quota.allowed_volumes(self.context, 100, 1), 100, 10, 10)
+
+        db.quota_create(self.context, self.project_id, resource, None)
+        _validate(quota.allowed_volumes(self.context, 100, 1), 100, None, 100)
+
+        db.quota_create(self.context, self.project_id, resource, -1)
+        _validate(quota.allowed_volumes(self.context, 100, 1), 100, None, 100)
+        _validate(quota.allowed_volumes(self.context, 101, 1), 101, None, 101)
+
+    def test_volumes_quota(self):
+        self._do_test_volume_quota('volumes')
+
+    def test_gigabytes_quota(self):
+        self._do_test_volume_quota('gigabytes')
 
     def test_unlimited_floating_ips(self):
         self.flags(quota_floating_ips=10)
diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py
index fca593c..58a6715 100644
--- a/nova/tests/test_volume.py
+++ b/nova/tests/test_volume.py
@@ -81,6 +81,36 @@ class VolumeTestCase(test.TestCase):
                           self.context,
                           volume_id)
 
+    def _do_test_create_over_quota(self, resource, expected):
+        """Test volume creation over quota."""
+
+        def fake_allowed_volumes(context, requested_volumes, size):
+            return dict(overs=[resource],
+                        usages=dict(gigabytes=999, volumes=9),
+                        quotas=dict(gigabytes=1000, volumes=10),
+                        allowed=dict(gigabytes=0, volumes=0))
+
+        self.stubs.Set(nova.quota, 'allowed_volumes', fake_allowed_volumes)
+
+        volume_api = nova.volume.api.API()
+
+        try:
+            volume_api.create(self.context,
+                              2,
+                             'name',
+                             'description')
+            self.fail('expected QuotaError')
+        except exception.QuotaError as qe:
+            self.assertTrue(str(qe).endswith('code=%s' % expected))
+
+    def test_create_volumes_over_quota(self):
+        self._do_test_create_over_quota('volumes',
+                                        'VolumeLimitExceeded')
+
+    def test_create_gigabytes_over_quota(self):
+        self._do_test_create_over_quota('gigabytes',
+                                        'VolumeSizeTooLarge')
+
     def test_delete_busy_volume(self):
         """Test volume survives deletion if driver reports it as busy."""
         volume = self._create_volume()
diff --git a/nova/volume/api.py b/nova/volume/api.py
index 76de551..c5faf4a 100644
--- a/nova/volume/api.py
+++ b/nova/volume/api.py
@@ -80,11 +80,29 @@ class API(base.Base):
         else:
             snapshot_id = None
 
-        if quota.allowed_volumes(context, 1, size) < 1:
+        result = quota.allowed_volumes(context, 1, size)
+
+        overs = result['overs']
+        usages = result['usages']
+        quotas = result['quotas']
+        allowed = result['allowed']
+
+        if allowed['volumes'] < 1:
             pid = context.project_id
-            LOG.warn(_("Quota exceeded for %(pid)s, tried to create"
-                    " %(size)sG volume") % locals())
-            raise exception.QuotaError(code="VolumeSizeTooLarge")
+            if 'gigabytes' in overs:
+                consumed = usages['gigabytes']
+                limit = quotas['gigabytes']
+                LOG.warn(_("Quota exceeded for %(pid)s, tried to create "
+                           "%(size)sG volume (%(consumed)dG of %(limit)dG "
+                           "already consumed)") % locals())
+                code = "VolumeSizeTooLarge"
+            elif 'volumes' in overs:
+                consumed = usages['volumes']
+                LOG.warn(_("Quota exceeded for %(pid)s, tried to create "
+                           "volume (%(consumed)d volumes already consumed)")
+                           % locals())
+                code = "VolumeLimitExceeded"
+            raise exception.QuotaError(code=code)
 
         if availability_zone is None:
             availability_zone = FLAGS.storage_availability_zone