From 619b4753cdbbbc9764c86e2d3f9780f48bcb4dd0 Mon Sep 17 00:00:00 2001 From: Graham Hayes Date: Tue, 28 Jun 2016 16:11:39 +0100 Subject: [PATCH] Add v2/quotas This adds the quotas api from /admin to /v2 with some changes. All users can GET /v2/quotas/ Users with "All-Projects" permission can view other projects (by setting X-Auth-All-Projects:True in the HTTP Headers) Users with "All-Projects" and "set-quotas" can set other projects quotas Moved the API rendering to Designate Object based rendering Change-Id: I7a0b828824ad6f274d922748f5f9a68157cd939a Depends-On: I06180a7402fc45940d4b312666cf2dfd33af1305 --- api-ref/source/dns-api-v2-quota.inc | 196 ++++++++++++++++++ api-ref/source/index.rst | 1 + api-ref/source/parameters.yaml | 7 + .../samples/quotas/get-quotas-response.json | 7 + .../samples/quotas/set-quotas-request.json | 3 + .../samples/quotas/set-quotas-response.json | 7 + .../admin/controllers/extensions/quotas.py | 3 + designate/api/v2/controllers/quotas.py | 78 +++++++ designate/api/v2/controllers/root.py | 2 + designate/central/service.py | 6 +- designate/objects/adapters/api_v2/quota.py | 41 ++-- designate/objects/quota.py | 24 ++- devstack/plugin.sh | 2 + functionaltests/api/v2/base.py | 5 +- .../api/v2/clients/quotas_client.py | 6 +- .../notes/v2-api-quotas-dd7e189cddcf7b96.yaml | 15 ++ 16 files changed, 385 insertions(+), 18 deletions(-) create mode 100644 api-ref/source/dns-api-v2-quota.inc create mode 100644 api-ref/source/samples/quotas/get-quotas-response.json create mode 100644 api-ref/source/samples/quotas/set-quotas-request.json create mode 100644 api-ref/source/samples/quotas/set-quotas-response.json create mode 100644 designate/api/v2/controllers/quotas.py create mode 100644 releasenotes/notes/v2-api-quotas-dd7e189cddcf7b96.yaml diff --git a/api-ref/source/dns-api-v2-quota.inc b/api-ref/source/dns-api-v2-quota.inc new file mode 100644 index 000000000..5bcc79131 --- /dev/null +++ b/api-ref/source/dns-api-v2-quota.inc @@ -0,0 +1,196 @@ +====== +Quotas +====== + +Quota operations. + + +View Quotas +=========== + +.. rest_method:: GET /v2/quotas/{project_id} + +View a projects quotas + +This returns a key:value set of quotas on the system. + +.. note:: + + If a user is viewing another projects quotas, they will need to set + ``x-auth-all-projects`` to ``True`` + + + They will need a role with the ``All-Projects`` permission to do this. + + +Normal response codes: 200 + +Error response codes: 409,405,404,403,401,400,503 + + +Request +------- + +.. rest_parameters:: parameters.yaml + + - x-auth-token: x-auth-token + - x-auth-all-projects: x-auth-all-projects + - x-auth-sudo-project-id: x-auth-sudo-project-id + - project_id: path_project_id + + +Response Parameters +------------------- + +.. rest_parameters:: parameters.yaml + + - x-openstack-request-id: x-openstack-request-id + + +Response Example +---------------- + +.. literalinclude:: samples/quotas/get-quotas-response.json + :language: javascript + + +View Current Project's Quotas +============================= + +.. rest_method:: GET /v2/quotas/ + +View the quotas for the current project + +This returns a key:value set of quotas on the system. + +Normal response codes: 200 + +Error response codes: 409,405,404,403,401,400,503 + + +Request +------- + +.. rest_parameters:: parameters.yaml + + - x-auth-token: x-auth-token + - x-auth-all-projects: x-auth-all-projects + - x-auth-sudo-project-id: x-auth-sudo-project-id + - project_id: path_project_id + + +Response Parameters +------------------- + +.. rest_parameters:: parameters.yaml + + - x-openstack-request-id: x-openstack-request-id + + +Response Example +---------------- + +.. literalinclude:: samples/quotas/get-quotas-response.json + :language: javascript + + +Set Quotas +========== + +.. rest_method:: PATCH /v2/quotas/{project_id} + +Set a projects quotas + +The request should be a key:value set of quotas to be set + +This returns a key:value set of quotas on the system. + +.. note:: + + If a user is updating another projects quotas, they will need to set + ``x-auth-all-projects`` to ``True`` + + + They will need a role with the "All-Projects" and "set-quotas" + permission to do this. + + +Normal response codes: 200 + +Error response codes: 409,405,404,403,401,400,503 + + +Request Example +--------------- + +.. literalinclude:: samples/quotas/set-quotas-request.json + :language: javascript + +Request +------- + +.. rest_parameters:: parameters.yaml + + - x-auth-token: x-auth-token + - x-auth-all-projects: x-auth-all-projects + - x-auth-sudo-project-id: x-auth-sudo-project-id + - project_id: path_project_id + + +Response Parameters +------------------- + +.. rest_parameters:: parameters.yaml + + - x-openstack-request-id: x-openstack-request-id + + +Response Example +---------------- + +.. literalinclude:: samples/quotas/set-quotas-response.json + :language: javascript + + + + + +Reset Quotas +============ + +.. rest_method:: DELETE /v2/quotas/{project_id} + +Reset all quotas for a project to default + +.. note:: + + If a user is resetting another projects quotas, they will need to set + ``x-auth-all-projects`` to ``True`` + + They will need a role with the ``All-Projects`` and "set-quotas" + permission to do this. + + +Normal response codes: 204 + +Error response codes: 409,405,404,403,401,400,503 + + +Request +------- + +.. rest_parameters:: parameters.yaml + + - x-auth-token: x-auth-token + - x-auth-all-projects: x-auth-all-projects + - x-auth-sudo-project-id: x-auth-sudo-project-id + - project_id: path_project_id + + +Response Parameters +------------------- + +.. rest_parameters:: parameters.yaml + + - x-openstack-request-id: x-openstack-request-id + diff --git a/api-ref/source/index.rst b/api-ref/source/index.rst index 9b4f9c4bb..31c4658ca 100644 --- a/api-ref/source/index.rst +++ b/api-ref/source/index.rst @@ -19,3 +19,4 @@ .. include:: dns-api-v2-tld.inc .. include:: dns-api-v2-tsigkey.inc .. include:: dns-api-v2-blacklist.inc +.. include:: dns-api-v2-quota.inc diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 4c14e9497..bb13e281a 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -64,6 +64,13 @@ path_pool_id: required: true type: uuid +path_project_id: + description: | + ID for the project + in: path + required: true + type: uuid + path_recordset_id: description: | ID for the recordset diff --git a/api-ref/source/samples/quotas/get-quotas-response.json b/api-ref/source/samples/quotas/get-quotas-response.json new file mode 100644 index 000000000..db3696231 --- /dev/null +++ b/api-ref/source/samples/quotas/get-quotas-response.json @@ -0,0 +1,7 @@ +{ + "api_export_size": 1000, + "recordset_records": 20, + "zone_records": 500, + "zone_recordsets": 500, + "zones": 100 +} diff --git a/api-ref/source/samples/quotas/set-quotas-request.json b/api-ref/source/samples/quotas/set-quotas-request.json new file mode 100644 index 000000000..5eb9ddc1d --- /dev/null +++ b/api-ref/source/samples/quotas/set-quotas-request.json @@ -0,0 +1,3 @@ +{ + "zones": 500 +} diff --git a/api-ref/source/samples/quotas/set-quotas-response.json b/api-ref/source/samples/quotas/set-quotas-response.json new file mode 100644 index 000000000..db84b8f61 --- /dev/null +++ b/api-ref/source/samples/quotas/set-quotas-response.json @@ -0,0 +1,7 @@ +{ + "api_export_size": 1000, + "recordset_records": 20, + "zone_records": 500, + "zone_recordsets": 500, + "zones": 500 +} diff --git a/designate/api/admin/controllers/extensions/quotas.py b/designate/api/admin/controllers/extensions/quotas.py index eb5c1e343..8985f4427 100644 --- a/designate/api/admin/controllers/extensions/quotas.py +++ b/designate/api/admin/controllers/extensions/quotas.py @@ -35,6 +35,7 @@ class QuotasController(rest.RestController): def get_one(self, tenant_id): request = pecan.request context = pecan.request.environ['context'] + context.all_tenants = True quotas = self.central_api.get_quotas(context, tenant_id) @@ -46,6 +47,7 @@ class QuotasController(rest.RestController): request = pecan.request response = pecan.response context = request.environ['context'] + context.all_tenants = True body = request.body_dict # Validate the request conforms to the schema @@ -69,6 +71,7 @@ class QuotasController(rest.RestController): request = pecan.request response = pecan.response context = request.environ['context'] + context.all_tenants = True self.central_api.reset_quotas(context, tenant_id) diff --git a/designate/api/v2/controllers/quotas.py b/designate/api/v2/controllers/quotas.py new file mode 100644 index 000000000..837274eef --- /dev/null +++ b/designate/api/v2/controllers/quotas.py @@ -0,0 +1,78 @@ +# COPYRIGHT 2014 Rackspace +# +# Author: Tim Simmons +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import pecan +from oslo_log import log as logging + +from designate.api.v2.controllers import rest +from designate.objects.adapters import DesignateAdapter +from designate.objects import QuotaList + +LOG = logging.getLogger(__name__) + + +class QuotasController(rest.RestController): + + @pecan.expose(template='json:', content_type='application/json') + def get_all(self): + context = pecan.request.environ['context'] + + quotas = self.central_api.get_quotas(context, context.tenant) + + quotas = QuotaList.from_dict(quotas) + + return DesignateAdapter.render('API_v2', quotas) + + @pecan.expose(template='json:', content_type='application/json') + def get_one(self, tenant_id): + context = pecan.request.environ['context'] + + quotas = self.central_api.get_quotas(context, tenant_id) + + quotas = QuotaList.from_dict(quotas) + + return DesignateAdapter.render('API_v2', quotas) + + @pecan.expose(template='json:', content_type='application/json') + def patch_one(self, tenant_id): + """Modify a Quota""" + request = pecan.request + context = request.environ['context'] + body = request.body_dict + + quotas = DesignateAdapter.parse('API_v2', body, QuotaList()) + + for quota in quotas: + self.central_api.set_quota(context, tenant_id, quota.resource, + quota.hard_limit) + + quotas = self.central_api.get_quotas(context, tenant_id) + + quotas = QuotaList.from_dict(quotas) + + return DesignateAdapter.render('API_v2', quotas) + + @pecan.expose(template=None, content_type='application/json') + def delete_one(self, tenant_id): + """Reset to the Default Quotas""" + request = pecan.request + response = pecan.response + context = request.environ['context'] + + self.central_api.reset_quotas(context, tenant_id) + + response.status_int = 204 + + return '' diff --git a/designate/api/v2/controllers/root.py b/designate/api/v2/controllers/root.py index 8d05cd979..2417a1af2 100644 --- a/designate/api/v2/controllers/root.py +++ b/designate/api/v2/controllers/root.py @@ -27,6 +27,7 @@ from designate.api.v2.controllers import service_status from designate.api.v2.controllers import zones from designate.api.v2.controllers import tsigkeys from designate.api.v2.controllers import recordsets +from designate.api.v2.controllers import quotas LOG = logging.getLogger(__name__) @@ -62,3 +63,4 @@ class RootController(object): service_statuses = service_status.ServiceStatusController() tsigkeys = tsigkeys.TsigKeysController() recordsets = recordsets.RecordSetsViewController() + quotas = quotas.QuotasController() diff --git a/designate/central/service.py b/designate/central/service.py index 23639ee80..3186f8865 100644 --- a/designate/central/service.py +++ b/designate/central/service.py @@ -643,8 +643,8 @@ class Service(service.RPCService, service.Service): target = {'tenant_id': tenant_id} policy.check('get_quotas', context, target) - # This allows admins to get quota information correctly for all tenants - context.all_tenants = True + if tenant_id != context.tenant and not context.all_tenants: + raise exceptions.Forbidden() return self.quota.get_quotas(context, tenant_id) @@ -663,6 +663,8 @@ class Service(service.RPCService, service.Service): } policy.check('set_quota', context, target) + if tenant_id != context.tenant and not context.all_tenants: + raise exceptions.Forbidden() return self.quota.set_quota(context, tenant_id, resource, hard_limit) diff --git a/designate/objects/adapters/api_v2/quota.py b/designate/objects/adapters/api_v2/quota.py index 953cf8475..865765cee 100644 --- a/designate/objects/adapters/api_v2/quota.py +++ b/designate/objects/adapters/api_v2/quota.py @@ -24,22 +24,12 @@ class QuotaAPIv2Adapter(base.APIv2Adapter): MODIFICATIONS = { 'fields': { - 'zones': { - 'rename': 'domains', + 'resource': { 'read_only': False }, - 'zone_records': { - 'rename': 'domain_records', + 'hard_limit': { 'read_only': False }, - 'zone_recordsets': { - 'rename': 'domain_recordsets', - 'read_only': False - }, - 'recordset_records': { - 'read_only': False - }, - }, 'options': { 'links': True, @@ -60,3 +50,30 @@ class QuotaListAPIv2Adapter(base.APIv2Adapter): 'collection_name': 'quotas', } } + + @classmethod + def _render_list(cls, list_object, *args, **kwargs): + + r_list = {} + + for object in list_object: + r_list[object.resource] = object.hard_limit + + return r_list + + @classmethod + def _parse_list(cls, values, output_object, *args, **kwargs): + + for key, value in values.items(): + # Add the object to the list + output_object.append( + cls.ADAPTER_OBJECT.LIST_ITEM_TYPE.from_dict( + { + 'resource': key, + 'hard_limit': value, + } + ) + ) + + # Return the filled list + return output_object diff --git a/designate/objects/quota.py b/designate/objects/quota.py index e8c3a85b4..6b6e6a302 100644 --- a/designate/objects/quota.py +++ b/designate/objects/quota.py @@ -24,9 +24,31 @@ class Quota(base.DictObjectMixin, base.PersistentObjectMixin, } STRING_KEYS = [ - 'id', 'resource', 'tenant_id', 'hard_limit' + 'resource', 'tenant_id', 'hard_limit' ] class QuotaList(base.ListObjectMixin, base.DesignateObject): LIST_ITEM_TYPE = Quota + + @classmethod + def from_dict(cls, _dict): + + instance = cls() + + for field, value in _dict.items(): + item = cls.LIST_ITEM_TYPE() + item.resource = field + item.hard_limit = value + instance.append(item) + + return instance + + def to_dict(self): + + _dict = {} + + for quota in self.objects: + _dict[quota.resource] = quota.hard_limit + + return _dict diff --git a/devstack/plugin.sh b/devstack/plugin.sh index ca3922c49..9f84bdbc4 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -161,6 +161,8 @@ function configure_designate_tempest() { iniset $TEMPEST_CONFIG dns_feature_enabled api_v2 $DESIGNATE_ENABLE_API_V2 iniset $TEMPEST_CONFIG dns_feature_enabled api_admin $DESIGNATE_ENABLE_API_ADMIN iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_root_recordsets True + iniset $TEMPEST_CONFIG dns_feature_enabled api_v2_quotas True + iniset $TEMPEST_CONFIG dns_feature_enabled bug_1573141_fixed True # Tell tempest where are nameservers are. nameservers=$DESIGNATE_SERVICE_HOST:$DESIGNATE_SERVICE_PORT_DNS diff --git a/functionaltests/api/v2/base.py b/functionaltests/api/v2/base.py index 5d0d3ecb1..a6460a240 100644 --- a/functionaltests/api/v2/base.py +++ b/functionaltests/api/v2/base.py @@ -36,7 +36,10 @@ class DesignateV2Test(BaseDesignateTest): 'zones': 9999999, 'recordset_records': 9999999, 'zone_records': 9999999, - 'zone_recordsets': 9999999}})) + 'zone_recordsets': 9999999 + } + }) + ) def ensure_tld_exists(self, tld='com'): if cfg.CONF.testconfig.no_admin_setup: diff --git a/functionaltests/api/v2/clients/quotas_client.py b/functionaltests/api/v2/clients/quotas_client.py index 02889b673..a7536b7b3 100644 --- a/functionaltests/api/v2/clients/quotas_client.py +++ b/functionaltests/api/v2/clients/quotas_client.py @@ -32,8 +32,10 @@ class QuotasClient(ClientMixin): return self.deserialize(resp, body, QuotasModel) def patch_quotas(self, tenant_id, quotas_model, **kwargs): - resp, body = self.client.patch(self.quotas_uri(tenant_id), - body=quotas_model.to_json(), **kwargs) + resp, body = self.client.patch( + self.quotas_uri(tenant_id), + body=quotas_model.to_json(), + **kwargs) return self.deserialize(resp, body, QuotasModel) def delete_quotas(self, tenant_id, **kwargs): diff --git a/releasenotes/notes/v2-api-quotas-dd7e189cddcf7b96.yaml b/releasenotes/notes/v2-api-quotas-dd7e189cddcf7b96.yaml new file mode 100644 index 000000000..6b1b99501 --- /dev/null +++ b/releasenotes/notes/v2-api-quotas-dd7e189cddcf7b96.yaml @@ -0,0 +1,15 @@ +--- +features: + - This adds the quotas api from /admin to /v2 with some changes. + + All users can GET /v2/quotas/ + + Users with "All-Projects" permission can view other projects + (by setting X-Auth-All-Projects:True in the HTTP Headers) + + Users with "All-Projects" and "set-quotas" can set other + projects quotas + + Moved the API rendering to Designate Object based rendering +fixes: + - V1 API Users can now query v1/quotas/ for quotas