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