From 0b2c628666983ae5f1ad9962d16d27a3a34c220f Mon Sep 17 00:00:00 2001 From: mjbright Date: Mon, 14 Oct 2013 11:12:19 -0700 Subject: [PATCH] Moved quota headroom calculations into quota_reserve Moved headroom calculations into quota_reserve and modified headroom calculations to take into account -ve quota limits (unlimited) on cores and ram. Updated test cases to generate OverQuota exceptions with new headroom parameter. Added new tests for 'unlimited' quotas to test_quota.py in QuotaIntegrationTestCase and DbQuotaDriverTestCase classes. Add Exception kwargs checking for OverQuota and QuotaError exceptions. Change-Id: I855b0cad495182c5c4ee42b2acf5eedd198557b5 Closes-Bug: #1224453 min_count ignored for instance create (cherry picked from commit 30fa37e7776831d6f8022f52d3d92f62189fb702) --- nova/compute/api.py | 10 +-- nova/db/sqlalchemy/api.py | 23 ++++++- nova/quota.py | 15 +++- nova/tests/api/openstack/fakes.py | 6 +- nova/tests/compute/test_compute_api.py | 26 ++++--- nova/tests/test_quota.py | 95 +++++++++++++++++++++----- 6 files changed, 137 insertions(+), 38 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 419dca23260b..fb0f610ea826 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -330,10 +330,7 @@ class API(base.Base): quotas = exc.kwargs['quotas'] usages = exc.kwargs['usages'] overs = exc.kwargs['overs'] - - headroom = dict((res, quotas[res] - - (usages[res]['in_use'] + usages[res]['reserved'])) - for res in quotas.keys()) + headroom = exc.kwargs['headroom'] allowed = headroom['instances'] # Reduce 'allowed' instances in line with the cores & ram headroom @@ -2292,10 +2289,7 @@ class API(base.Base): quotas = exc.kwargs['quotas'] usages = exc.kwargs['usages'] overs = exc.kwargs['overs'] - - headroom = dict((res, quotas[res] - - (usages[res]['in_use'] + usages[res]['reserved'])) - for res in quotas.keys()) + headroom = exc.kwargs['headroom'] resource = overs[0] used = quotas[resource] - headroom[resource] diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index f364c963564e..c7a4bc59b2f9 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3209,8 +3209,29 @@ def quota_reserve(context, resources, project_quotas, user_quotas, deltas, usages = user_usages usages = dict((k, dict(in_use=v['in_use'], reserved=v['reserved'])) for k, v in usages.items()) + headroom = dict((res, user_quotas[res] - + (usages[res]['in_use'] + usages[res]['reserved'])) + for res in user_quotas.keys()) + + # If quota_cores is unlimited [-1]: + # - set cores headroom based on instances headroom: + if user_quotas.get('cores') == -1: + if deltas['cores']: + hc = headroom['instances'] * deltas['cores'] + headroom['cores'] = hc / deltas['instances'] + else: + headroom['cores'] = headroom['instances'] + + # If quota_ram is unlimited [-1]: + # - set ram headroom based on instances headroom: + if user_quotas.get('ram') == -1: + if deltas['ram']: + hr = headroom['instances'] * deltas['ram'] + headroom['ram'] = hr / deltas['instances'] + else: + headroom['ram'] = headroom['instances'] raise exception.OverQuota(overs=sorted(overs), quotas=user_quotas, - usages=usages) + usages=usages, headroom=headroom) return reservations diff --git a/nova/quota.py b/nova/quota.py index 96eb5444849b..c9756fb59911 100644 --- a/nova/quota.py +++ b/nova/quota.py @@ -405,11 +405,22 @@ class DbQuotaDriver(object): # Check the quotas and construct a list of the resources that # would be put over limit by the desired values overs = [key for key, val in values.items() - if (quotas[key] >= 0 and quotas[key] < val) or + if quotas[key] >= 0 and quotas[key] < val or (user_quotas[key] >= 0 and user_quotas[key] < val)] if overs: + headroom = {} + # Check project_quotas: + for key in quotas: + if quotas[key] >= 0 and quotas[key] < val: + headroom[key] = quotas[key] + # Check user quotas: + for key in user_quotas: + if (user_quotas[key] >= 0 and user_quotas[key] < val and + headroom.get(key) > user_quotas[key]): + headroom[key] = user_quotas[key] + raise exception.OverQuota(overs=sorted(overs), quotas=quotas, - usages={}) + usages={}, headroom=headroom) def reserve(self, context, resources, deltas, expire=None, project_id=None, user_id=None): diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index d3499971cd46..67f9953893b9 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -171,8 +171,12 @@ def stub_out_instance_quota(stubs, allowed, quota, resource='instances'): usages[resource]['in_use'] = (quotas[resource] * 0.9 - allowed) usages[resource]['reserved'] = quotas[resource] * 0.1 + headroom = dict( + (res, value - (usages[res]['in_use'] + usages[res]['reserved'])) + for res, value in quotas.iteritems() + ) raise exc.OverQuota(overs=[resource], quotas=quotas, - usages=usages) + usages=usages, headroom=headroom) stubs.Set(QUOTAS, 'reserve', fake_reserve) diff --git a/nova/tests/compute/test_compute_api.py b/nova/tests/compute/test_compute_api.py index 024d4a2fa3fc..5226244a0894 100644 --- a/nova/tests/compute/test_compute_api.py +++ b/nova/tests/compute/test_compute_api.py @@ -134,11 +134,14 @@ class _ComputeAPIUnitTestMixIn(object): self.mox.StubOutWithMock(quota.QUOTAS, "limit_check") self.mox.StubOutWithMock(quota.QUOTAS, "reserve") - quota_exception = exception.OverQuota( - quotas={'instances': 1, 'cores': 1, 'ram': 1}, - usages=dict((r, {'in_use': 1, 'reserved': 1}) for r in - ['instances', 'cores', 'ram']), - overs=['instances']) + quotas = {'instances': 1, 'cores': 1, 'ram': 1} + usages = dict((r, {'in_use': 1, 'reserved': 1}) for r in + ['instances', 'cores', 'ram']) + headroom = dict((res, quotas[res] - + (usages[res]['in_use'] + usages[res]['reserved'])) + for res in quotas.keys()) + quota_exception = exception.OverQuota(quotas=quotas, + usages=usages, overs=['instances'], headroom=headroom) for _unused in range(2): self.compute_api._get_image(self.context, image_href).AndReturn( @@ -986,9 +989,16 @@ class _ComputeAPIUnitTestMixIn(object): self.context, fake_flavor, current_flavor).AndReturn(deltas) usage = dict(in_use=0, reserved=0) - over_quota_args = dict(quotas={'resource': 0}, - usages={'resource': usage}, - overs=['resource']) + quotas = {'resource': 0} + usages = {'resource': usage} + overs = ['resource'] + headroom = {'resource': quotas['resource'] - + (usages['resource']['in_use'] + usages['resource']['reserved'])} + over_quota_args = dict(quotas=quotas, + usages=usages, + overs=overs, + headroom=headroom) + self.compute_api._reserve_quota_delta(self.context, deltas, project_id=fake_inst['project_id']).AndRaise( exception.OverQuota(**over_quota_args)) diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py index 854fbb005355..d826daa394ee 100644 --- a/nova/tests/test_quota.py +++ b/nova/tests/test_quota.py @@ -79,12 +79,15 @@ class QuotaIntegrationTestCase(test.TestCase): instance_uuids.append(instance['uuid']) inst_type = flavors.get_flavor_by_name('m1.small') image_uuid = 'cedef40a-ed67-4d10-800e-17455edce175' - self.assertRaises(exception.QuotaError, compute.API().create, - self.context, - min_count=1, - max_count=1, - instance_type=inst_type, - image_href=image_uuid) + try: + compute.API().create(self.context, min_count=1, max_count=1, + instance_type=inst_type, image_href=image_uuid) + except exception.QuotaError, e: + expected_kwargs = {'code': 413, 'resource': 'cores', 'req': 1, + 'used': 4, 'allowed': 4, 'overs': 'cores,instances'} + self.assertEqual(e.kwargs, expected_kwargs) + else: + self.fail('Expected QuotaError exception') for instance_uuid in instance_uuids: db.instance_destroy(self.context, instance_uuid) @@ -92,12 +95,23 @@ class QuotaIntegrationTestCase(test.TestCase): instance = self._create_instance(cores=4) inst_type = flavors.get_flavor_by_name('m1.small') image_uuid = 'cedef40a-ed67-4d10-800e-17455edce175' - self.assertRaises(exception.QuotaError, compute.API().create, - self.context, - min_count=1, - max_count=1, - instance_type=inst_type, - image_href=image_uuid) + try: + compute.API().create(self.context, min_count=1, max_count=1, + instance_type=inst_type, image_href=image_uuid) + except exception.QuotaError, e: + expected_kwargs = {'code': 413, 'resource': 'cores', 'req': 1, + 'used': 4, 'allowed': 4, 'overs': 'cores'} + self.assertEqual(e.kwargs, expected_kwargs) + else: + self.fail('Expected QuotaError exception') + db.instance_destroy(self.context, instance['uuid']) + + def test_many_cores_with_unlimited_quota(self): + # Setting cores quota to unlimited: + self.flags(quota_cores=-1) + instance = self._create_instance(cores=4) + inst_type = flavors.get_flavor_by_name('m1.small') + image_uuid = 'cedef40a-ed67-4d10-800e-17455edce175' db.instance_destroy(self.context, instance['uuid']) def test_too_many_addresses(self): @@ -2293,12 +2307,23 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): def test_quota_reserve_overs(self): context = self._init_usages(4, 8, 10 * 1024, 4) - self.assertRaises(exception.OverQuota, - sqa_api.quota_reserve, - context, self.resources, self.quotas, - self.quotas, self.deltas, self.expire, - 0, 0) - + try: + sqa_api.quota_reserve(context, self.resources, self.quotas, + self.quotas, self.deltas, self.expire, 0, 0) + except exception.OverQuota, e: + expected_kwargs = {'code': 500, + 'usages': {'instances': {'reserved': 0, 'in_use': 4}, + 'ram': {'reserved': 0, 'in_use': 10240}, + 'fixed_ips': {'reserved': 0, 'in_use': 4}, + 'cores': {'reserved': 0, 'in_use': 8}}, + 'headroom': {'cores': 2, 'ram': 0, 'fixed_ips': 1, + 'instances': 1}, + 'overs': ['cores', 'fixed_ips', 'instances', 'ram'], + 'quotas': {'cores': 10, 'ram': 10240, + 'fixed_ips': 5, 'instances': 5}} + self.assertEqual(e.kwargs, expected_kwargs) + else: + self.fail('Expected OverQuota failure') self.assertEqual(self.sync_called, set([])) self.usages_list[0]["in_use"] = 4 self.usages_list[0]["reserved"] = 0 @@ -2312,6 +2337,40 @@ class QuotaReserveSqlAlchemyTestCase(test.TestCase): self.assertEqual(self.usages_created, {}) self.assertEqual(self.reservations_created, {}) + def test_quota_reserve_cores_unlimited(self): + # Requesting 8 cores, quota_cores set to unlimited: + self.flags(quota_cores=-1) + context = self._init_usages(1, 8, 1 * 1024, 1) + self.assertEqual(self.sync_called, set([])) + self.usages_list[0]["in_use"] = 1 + self.usages_list[0]["reserved"] = 0 + self.usages_list[1]["in_use"] = 8 + self.usages_list[1]["reserved"] = 0 + self.usages_list[2]["in_use"] = 1 * 1024 + self.usages_list[2]["reserved"] = 0 + self.usages_list[3]["in_use"] = 1 + self.usages_list[3]["reserved"] = 0 + self.compare_usage(self.usages, self.usages_list) + self.assertEqual(self.usages_created, {}) + self.assertEqual(self.reservations_created, {}) + + def test_quota_reserve_ram_unlimited(self): + # Requesting 10*1024 ram, quota_ram set to unlimited: + self.flags(quota_ram=-1) + context = self._init_usages(1, 1, 10 * 1024, 1) + self.assertEqual(self.sync_called, set([])) + self.usages_list[0]["in_use"] = 1 + self.usages_list[0]["reserved"] = 0 + self.usages_list[1]["in_use"] = 1 + self.usages_list[1]["reserved"] = 0 + self.usages_list[2]["in_use"] = 10 * 1024 + self.usages_list[2]["reserved"] = 0 + self.usages_list[3]["in_use"] = 1 + self.usages_list[3]["reserved"] = 0 + self.compare_usage(self.usages, self.usages_list) + self.assertEqual(self.usages_created, {}) + self.assertEqual(self.reservations_created, {}) + def test_quota_reserve_reduction(self): context = self._init_usages(10, 20, 20 * 1024, 10) self.deltas["instances"] = -2