diff --git a/adjutant/actions/v1/resources.py b/adjutant/actions/v1/resources.py index 4e79dda..2ec47d1 100644 --- a/adjutant/actions/v1/resources.py +++ b/adjutant/actions/v1/resources.py @@ -313,6 +313,18 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin): return False return True + def _validate_quota_management_enabled_for_regions(self): + # Check that at least one region in the given list has + # quota management enabled. + default_services = CONF.quota.services.get("*", {}) + for region in self.regions: + if CONF.quota.services.get(region, default_services): + return True + self.add_note( + "Quota management is disabled for all specified regions", + ) + return False + def _set_region_quota(self, region_name, quota_size): # Set the quota for an individual region quota_config = CONF.quota.sizes.get(quota_size, {}) @@ -360,9 +372,17 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin): ) for region in self.regions: - current_size = quota_manager.get_region_quota_data( + current_quota = quota_manager.get_region_quota_data( region, include_usage=False - )["current_quota_size"] + ) + # If get_region_quota_data returns None, this region + # has quota management disabled. + if not current_quota: + self.add_note( + f"Quota management is disabled in region: {region}", + ) + continue + current_size = current_quota["current_quota_size"] region_sizes.append(current_size) self.add_note( "Project has size '%s' in region: '%s'" % (current_size, region) @@ -372,6 +392,12 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin): preapproved_quotas = [] smaller_quotas = [] + if not region_sizes: + self.add_note( + "Quota management is disabled for all specified regions", + ) + return False + # If all region sizes are the same if region_sizes.count(region_sizes[0]) == len(region_sizes): preapproved_quotas = quota_manager.get_quota_change_options(region_sizes[0]) @@ -409,6 +435,7 @@ class UpdateProjectQuotasAction(BaseAction, QuotaMixin): self._validate_project_id, self._validate_quota_size_exists, self._validate_regions_exist, + self._validate_quota_management_enabled_for_regions, self._validate_usage_lower_than_quota, ] ) diff --git a/adjutant/actions/v1/tests/test_resource_actions.py b/adjutant/actions/v1/tests/test_resource_actions.py index 4aa5621..1a3d3da 100644 --- a/adjutant/actions/v1/tests/test_resource_actions.py +++ b/adjutant/actions/v1/tests/test_resource_actions.py @@ -568,6 +568,122 @@ class QuotaActionTests(AdjutantTestCase): neutronquota = neutron_cache["RegionOne"]["test_project_id"]["quota"] self.assertEqual(neutronquota["network"], 5) + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + { + "operation": "override", + "value": { + "RegionOne": [], + "*": ["cinder", "neutron", "nova"], + }, + }, + ], + }, + ) + def test_update_quota_fail_disabled_region(self): + """ + Check that a quota update for a region for which quota management + is disabled is not valid, or performed. + """ + project = mock.Mock() + project.id = "test_project_id" + project.name = "test_project" + project.domain = "default" + project.roles = {} + + user = mock.Mock() + user.id = "user_id" + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = "default" + user.password = "test_password" + + setup_identity_cache(projects=[project], users=[user]) + setup_mock_caches("RegionOne", "test_project_id") + + # Test sending to only a single region + task = Task.objects.create(keystone_user={"roles": ["admin"]}) + + data = { + "project_id": "test_project_id", + "size": "large", + "regions": ["RegionOne"], + "user_id": user.id, + } + + action = UpdateProjectQuotasAction(data, task=task, order=1) + + action.prepare() + self.assertEqual(action.valid, False) + + action.approve() + self.assertEqual(action.valid, False) + + # check the quotas were updated + cinderquota = cinder_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(cinderquota["gigabytes"], 5000) + novaquota = nova_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(novaquota["ram"], 65536) + neutronquota = neutron_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(neutronquota["network"], 3) + + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + {"operation": "override", "value": {}}, + ], + }, + ) + def test_update_quota_fail_disabled(self): + """ + Check that a quota update tasks are not valid or performed + when quota management is disabled completely. + """ + project = mock.Mock() + project.id = "test_project_id" + project.name = "test_project" + project.domain = "default" + project.roles = {} + + user = mock.Mock() + user.id = "user_id" + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = "default" + user.password = "test_password" + + setup_identity_cache(projects=[project], users=[user]) + setup_mock_caches("RegionOne", "test_project_id") + + # Test sending to only a single region + task = Task.objects.create(keystone_user={"roles": ["admin"]}) + + data = { + "project_id": "test_project_id", + "size": "large", + "regions": ["RegionOne"], + "user_id": user.id, + } + + action = UpdateProjectQuotasAction(data, task=task, order=1) + + action.prepare() + self.assertEqual(action.valid, False) + + action.approve() + self.assertEqual(action.valid, False) + + # check the quotas were updated + cinderquota = cinder_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(cinderquota["gigabytes"], 5000) + novaquota = nova_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(novaquota["ram"], 65536) + neutronquota = neutron_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(neutronquota["network"], 3) + def test_update_quota_multi_region(self): """ Sets a new quota on all services of a project in multiple regions @@ -622,6 +738,140 @@ class QuotaActionTests(AdjutantTestCase): neutronquota = neutron_cache["RegionTwo"]["test_project_id"]["quota"] self.assertEqual(neutronquota["network"], 10) + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + { + "operation": "override", + "value": { + "RegionTwo": [], + "*": ["cinder", "neutron", "nova"], + }, + }, + ], + }, + ) + def test_update_quota_multi_region_one_disabled(self): + """ + Check that when a request to update multiple regions at once + and one of the regions have quota management disabled, + only the enabled regions have their quotas updated. + """ + project = mock.Mock() + project.id = "test_project_id" + project.name = "test_project" + project.domain = "default" + project.roles = {} + + user = mock.Mock() + user.id = "user_id" + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = "default" + user.password = "test_password" + + setup_identity_cache(projects=[project], users=[user]) + setup_mock_caches("RegionOne", project.id) + setup_mock_caches("RegionTwo", project.id) + + task = Task.objects.create(keystone_user={"roles": ["admin"]}) + + data = { + "project_id": "test_project_id", + "size": "large", + "domain_id": "default", + "regions": ["RegionOne", "RegionTwo"], + "user_id": "user_id", + } + + action = UpdateProjectQuotasAction(data, task=task, order=1) + + action.prepare() + self.assertEqual(action.valid, True) + + action.approve() + self.assertEqual(action.valid, True) + + # check the quotas were updated + cinderquota = cinder_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(cinderquota["gigabytes"], 50000) + novaquota = nova_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(novaquota["ram"], 655360) + neutronquota = neutron_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(neutronquota["network"], 10) + + cinderquota = cinder_cache["RegionTwo"]["test_project_id"]["quota"] + self.assertEqual(cinderquota["gigabytes"], 5000) + novaquota = nova_cache["RegionTwo"]["test_project_id"]["quota"] + self.assertEqual(novaquota["ram"], 65536) + neutronquota = neutron_cache["RegionTwo"]["test_project_id"]["quota"] + self.assertEqual(neutronquota["network"], 3) + + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + {"operation": "override", "value": {}}, + ], + }, + ) + def test_update_quota_multi_region_disabled(self): + """ + Check that if a task to update quotas for multiple regions at once + is initiated but quota management is disabled, no regions' quotas + are updated. + """ + project = mock.Mock() + project.id = "test_project_id" + project.name = "test_project" + project.domain = "default" + project.roles = {} + + user = mock.Mock() + user.id = "user_id" + user.name = "test@example.com" + user.email = "test@example.com" + user.domain = "default" + user.password = "test_password" + + setup_identity_cache(projects=[project], users=[user]) + setup_mock_caches("RegionOne", project.id) + setup_mock_caches("RegionTwo", project.id) + + task = Task.objects.create(keystone_user={"roles": ["admin"]}) + + data = { + "project_id": "test_project_id", + "size": "large", + "domain_id": "default", + "regions": ["RegionOne", "RegionTwo"], + "user_id": "user_id", + } + + action = UpdateProjectQuotasAction(data, task=task, order=1) + + action.prepare() + self.assertEqual(action.valid, False) + + action.approve() + self.assertEqual(action.valid, False) + + # check the quotas were updated + cinderquota = cinder_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(cinderquota["gigabytes"], 5000) + novaquota = nova_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(novaquota["ram"], 65536) + neutronquota = neutron_cache["RegionOne"]["test_project_id"]["quota"] + self.assertEqual(neutronquota["network"], 3) + + cinderquota = cinder_cache["RegionTwo"]["test_project_id"]["quota"] + self.assertEqual(cinderquota["gigabytes"], 5000) + novaquota = nova_cache["RegionTwo"]["test_project_id"]["quota"] + self.assertEqual(novaquota["ram"], 65536) + neutronquota = neutron_cache["RegionTwo"]["test_project_id"]["quota"] + self.assertEqual(neutronquota["network"], 3) + @conf_utils.modify_conf( CONF, operations={ diff --git a/adjutant/api/v1/openstack.py b/adjutant/api/v1/openstack.py index 0da478c..9619c31 100644 --- a/adjutant/api/v1/openstack.py +++ b/adjutant/api/v1/openstack.py @@ -467,9 +467,9 @@ class UpdateProjectQuotas(BaseDelegateAPI): quota_manager = QuotaManager(self.project_id) for region in regions: if self.check_region_exists(region): - region_quotas.append( - quota_manager.get_region_quota_data(region, include_usage) - ) + quota = quota_manager.get_region_quota_data(region, include_usage) + if quota: + region_quotas.append(quota) else: return Response({"ERROR": ["Region: %s is not valid" % region]}, 400) @@ -489,15 +489,67 @@ class UpdateProjectQuotas(BaseDelegateAPI): request.data["project_id"] = request.keystone_user["project_id"] self.project_id = request.keystone_user["project_id"] - regions = request.data.get("regions", None) + # Fetch the currently existing regions. + active_regions = { + region.id: region for region in user_store.IdentityManager().list_regions() + } + # Get the regions for which quota updates should be applied, + # parsing the list to get the unique region names while + # preserving list order. + # If no regions are specified in the request, update all regions. + _target_regions = request.data.get("regions", []) + target_regions = list( + ( + dict.fromkeys(_target_regions) if _target_regions else active_regions + ).keys() + ) + + # Create a mapping to check whether or not quota management + # is enabled on a per-region basis. + quota_enabled_for_region = { + region: bool(services) for region, services in CONF.quota.services.items() + } + + # Filter out regions for which quota management is disabled. + # This checks for region-specific settings first, and if + # there isn't one, checks the settings for '*' (all regions). + regions = [ + region + for region in target_regions + if quota_enabled_for_region.get( + region, + quota_enabled_for_region.get("*", False), + ) + ] + request.data["regions"] = regions + + # Check that any of the specified regions can + # have their quota updated. if not regions: - id_manager = user_store.IdentityManager() - regions = [region.id for region in id_manager.list_regions()] - request.data["regions"] = regions + return Response( + { + "errors": [ + "Unable to update the quotas for the given regions", + ], + }, + status=400, + ) - self.logger.info("(%s) - New UpdateProjectQuotas request." % timezone.now()) + # Check that the specified regions actually exist. + not_regions = [] + for region in regions: + if region not in active_regions: + not_regions.append(region) + if not_regions: + return Response( + {"errors": [f"Invalid regions: {', '.join(not_regions)}"]}, + status=400, + ) + self.logger.info( + "(%s) - New UpdateProjectQuotas request.", + timezone.now(), + ) self.task_manager.create_from_request(self.task_type, request) - return Response({"notes": ["task created"]}, status=202) diff --git a/adjutant/api/v1/tests/test_api_openstack.py b/adjutant/api/v1/tests/test_api_openstack.py index d5a3c65..d16a10f 100644 --- a/adjutant/api/v1/tests/test_api_openstack.py +++ b/adjutant/api/v1/tests/test_api_openstack.py @@ -455,6 +455,334 @@ class QuotaAPITests(AdjutantAPITestCase): instance = CONF.quota.sizes.get(size)["trove"]["instances"] self.assertEqual(trove_quota["instances"], instance) + def test_quota_show(self): + """Check fetching the current quota sizes for available regions.""" + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {} + + response = self.client.get( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + { + res["region"]: res["current_quota_size"] + for res in response.data["regions"] + }, + {"RegionOne": "small", "RegionTwo": "small"}, + ) + + def test_quota_show_explicit_single(self): + """Check the quota size for a single region explicitly.""" + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"regions": "RegionOne"} + + response = self.client.get( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + { + res["region"]: res["current_quota_size"] + for res in response.data["regions"] + }, + {"RegionOne": "small"}, + ) + + def test_quota_show_explicit_multiple(self): + """Check the quota size for multiple regions explicitly.""" + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"regions": "RegionOne,RegionTwo"} + + response = self.client.get( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + { + res["region"]: res["current_quota_size"] + for res in response.data["regions"] + }, + {"RegionOne": "small", "RegionTwo": "small"}, + ) + + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + { + "operation": "override", + "value": { + "RegionTwo": [], + "*": ["cinder", "neutron", "nova"], + }, + }, + ], + }, + ) + def test_quota_show_region_disabled(self): + """Check that if a request for showing the quota size of a region + for which quota management is disabled, an OK response is returned + with no regions listed. + """ + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"regions": "RegionTwo"} + + response = self.client.get( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["regions"], []) + + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + { + "operation": "override", + "value": { + "RegionTwo": [], + "*": ["cinder", "neutron", "nova"], + }, + }, + ], + }, + ) + def test_quota_show_one_region_disabled(self): + """Check that if a request for showing quota sizes for multiple + regions are made, and one of those regions have quota management + disabled, that only quotas for enabled regions are returned. + """ + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"regions": "RegionOne,RegionTwo"} + + response = self.client.get( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + { + res["region"]: res["current_quota_size"] + for res in response.data["regions"] + }, + {"RegionOne": "small"}, + ) + + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + {"operation": "override", "value": {}}, + ], + }, + ) + def test_quota_show_disabled(self): + """Check that no quota sizes for regions are returned if + quota management is disabled entirely. + """ + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {} + + response = self.client.get( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["regions"], []) + + def test_quota_show_invalid_region(self): + """Check that if a request for showing the quota size of an + invalid region is made, an error response is returned. + """ + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"regions": "RegionThree"} + + response = self.client.get( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_update_quota_no_history(self): """Update the quota size of a project with no history""" @@ -780,6 +1108,165 @@ class QuotaAPITests(AdjutantAPITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["regions"][0]["current_quota_size"], "small") + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + { + "operation": "override", + "value": { + "RegionOne": [], + "*": ["cinder", "neutron", "nova"], + }, + }, + ], + }, + ) + def test_update_quota_disabled_region(self): + """Check that if a quota update request is made for a disabled region, + the request is denied and the quota is not changed. + """ + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"size": "medium", "regions": ["RegionOne"]} + + response = self.client.post( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.check_quota_cache("RegionOne", project.id, "small") + + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + { + "operation": "override", + "value": { + "RegionTwo": [], + "*": ["cinder", "neutron", "nova"], + }, + }, + ], + }, + ) + def test_update_quota_one_region_disabled(self): + """Check that if a quota update request is made for multiple regions + and one of them has quota management disabled, only the enabled + regions have their quota updated. + """ + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"size": "medium", "regions": ["RegionOne", "RegionTwo"]} + + response = self.client.post( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + + self.check_quota_cache("RegionOne", project.id, "medium") + self.check_quota_cache("RegionTwo", project.id, "small") + + @conf_utils.modify_conf( + CONF, + operations={ + "adjutant.quota.services": [ + {"operation": "override", "value": {}}, + ], + }, + ) + def test_update_quota_disabled(self): + """Check that quota update requests return error responses and + updates are not performed if quota management is disabled entirely. + """ + + project = fake_clients.FakeProject( + name="test_project", + id="test_project_id", + ) + + user = fake_clients.FakeUser( + name="test@example.com", password="123", email="test@example.com" + ) + + setup_identity_cache(projects=[project], users=[user]) + + admin_headers = { + "project_name": "test_project", + "project_id": project.id, + "roles": "project_admin,member,project_mod", + "username": "test@example.com", + "user_id": "user_id", + "authenticated": True, + } + + url = "/v1/openstack/quotas/" + + data = {"size": "medium", "regions": ["RegionOne", "RegionTwo"]} + + response = self.client.post( + url, + data, + headers=admin_headers, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + self.check_quota_cache("RegionOne", project.id, "small") + self.check_quota_cache("RegionTwo", project.id, "small") + @conf_utils.modify_conf( CONF, operations={ diff --git a/adjutant/common/quota.py b/adjutant/common/quota.py index 2234d25..65f8b24 100644 --- a/adjutant/common/quota.py +++ b/adjutant/common/quota.py @@ -203,17 +203,15 @@ class QuotaManager(object): # TODO(amelia): Try to find out which endpoints are available and get # the non enabled ones out of the list - self.default_helpers = dict(self._quota_updaters) + self.default_helpers = {} self.helpers = {} quota_services = dict(CONF.quota.services) - all_regions = quota_services.pop("*", None) - if all_regions: - self.default_helpers = {} - for service in all_regions: - if service in self._quota_updaters: - self.default_helpers[service] = self._quota_updaters[service] + all_regions = quota_services.pop("*", []) + for service in all_regions: + if service in self._quota_updaters: + self.default_helpers[service] = self._quota_updaters[service] for region, services in quota_services.items(): self.helpers[region] = {} @@ -314,6 +312,12 @@ class QuotaManager(object): return quota_list[:list_position] def get_region_quota_data(self, region_id, include_usage=True): + # NOTE(callumdickinson): If the region has no services + # for which quotas should be managed, return None so the caller + # can handle this case properly. + if not self.helpers.get(region_id, self.default_helpers): + return None + current_quota = self.get_current_region_quota(region_id) current_quota_size = self.get_quota_size(current_quota) change_options = self.get_quota_change_options(current_quota_size) @@ -340,13 +344,18 @@ class QuotaManager(object): return current_usage def set_region_quota(self, region_id, quota_dict): + region_helpers = self.helpers.get(region_id, self.default_helpers) + if not region_helpers: + return [ + ( + "WARNING: Quota management disabled in region " + f"{region_id}, skipping." + ), + ] notes = [] for service_name, values in quota_dict.items(): - updater_class = self.helpers.get(region_id, self.default_helpers).get( - service_name - ) + updater_class = region_helpers.get(service_name) if not updater_class: - notes.append("No quota updater found for %s. Ignoring" % service_name) continue service_helper = updater_class(region_id, self.project_id) diff --git a/releasenotes/notes/disable-quota-management-feddbaab2c304758.yaml b/releasenotes/notes/disable-quota-management-feddbaab2c304758.yaml new file mode 100644 index 0000000..fcc001d --- /dev/null +++ b/releasenotes/notes/disable-quota-management-feddbaab2c304758.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Quota management can now be disabled for specific regions by setting + ``quota.services.`` to ``[]`` in the configuration. + This allows additional flexibility in what services are deployed to + which region. + - | + Quota management can now be disabled entirely in Adjutant by setting + ``quota.services`` to ``{}`` in the configuration, if quota management + is not required in deployments.