Merge "Allow disabling quota management"

This commit is contained in:
Zuul 2024-07-24 01:57:41 +00:00 committed by Gerrit Code Review
commit b8c1194f90
6 changed files with 858 additions and 22 deletions

View File

@ -432,6 +432,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, {})
@ -479,9 +491,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)
@ -491,6 +511,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])
@ -528,6 +554,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,
]
)

View File

@ -821,6 +821,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
@ -875,6 +991,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={

View File

@ -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)

View File

@ -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={

View File

@ -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)

View File

@ -0,0 +1,11 @@
---
features:
- |
Quota management can now be disabled for specific regions by setting
``quota.services.<region_name>`` 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.