Add Extensions to the v2 API
*Provide a framework for adding extensions to the v2 API. *Port the Quotas extension *Add v2 extension unit tests *Fix small bug with noauth middleware and quota retrieval blueprint v2-extensions Closes-Bug: #1333892 Change-Id: I1dc395e7266a17c8c860e00ee470e14e94e82769
This commit is contained in:
parent
1d3ea84717
commit
e4b19ad5b9
@ -23,7 +23,7 @@
|
||||
"*.class",
|
||||
"*.psd",
|
||||
"*.db",
|
||||
".vagrant",
|
||||
".vagrant"
|
||||
],
|
||||
"folder_exclude_patterns":
|
||||
[
|
||||
|
@ -22,6 +22,12 @@ from designate.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
OPTS = [
|
||||
cfg.ListOpt('enabled-extensions-v2', default=[],
|
||||
help='Enabled API Extensions'),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(OPTS, group='service:api')
|
||||
|
||||
def factory(global_config, **local_conf):
|
||||
if not cfg.CONF['service:api'].enable_api_v2:
|
||||
|
0
designate/api/v2/controllers/extensions/__init__.py
Normal file
0
designate/api/v2/controllers/extensions/__init__.py
Normal file
77
designate/api/v2/controllers/extensions/quotas.py
Normal file
77
designate/api/v2/controllers/extensions/quotas.py
Normal file
@ -0,0 +1,77 @@
|
||||
# COPYRIGHT 2014 Rackspace
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.com>
|
||||
#
|
||||
# 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 designate.openstack.common import log as logging
|
||||
from designate import schema
|
||||
from designate.api.v2.controllers import rest
|
||||
from designate.api.v2.views.extensions import quotas as quotas_view
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuotasController(rest.RestController):
|
||||
_view = quotas_view.QuotasView()
|
||||
_resource_schema = schema.Schema('v2', 'quota')
|
||||
|
||||
@staticmethod
|
||||
def get_path():
|
||||
return '.quotas'
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
def get_one(self, tenant_id):
|
||||
request = pecan.request
|
||||
context = pecan.request.environ['context']
|
||||
|
||||
quotas = self.central_api.get_quotas(context, tenant_id)
|
||||
|
||||
return self._view.show(context, request, quotas)
|
||||
|
||||
@pecan.expose(template='json:', content_type='application/json')
|
||||
def patch_one(self, tenant_id):
|
||||
""" Modify a Quota """
|
||||
request = pecan.request
|
||||
response = pecan.response
|
||||
context = request.environ['context']
|
||||
body = request.body_dict
|
||||
|
||||
# Validate the request conforms to the schema
|
||||
self._resource_schema.validate(body)
|
||||
|
||||
values = self._view.load(context, request, body)
|
||||
|
||||
for resource, hard_limit in values.iteritems():
|
||||
self.central_api.set_quota(context, tenant_id, resource,
|
||||
hard_limit)
|
||||
|
||||
response.status_int = 200
|
||||
|
||||
quotas = self.central_api.get_quotas(context, tenant_id)
|
||||
|
||||
return self._view.show(context, request, 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 ''
|
@ -13,6 +13,9 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from oslo.config import cfg
|
||||
from stevedore import named
|
||||
|
||||
from designate import exceptions
|
||||
from designate.openstack.common import log as logging
|
||||
from designate.api.v2.controllers import limits
|
||||
@ -32,6 +35,22 @@ class RootController(object):
|
||||
This is /v2/ Controller. Pecan will find all controllers via the object
|
||||
properties attached to this.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
enabled_ext = cfg.CONF['service:api'].enabled_extensions_v2
|
||||
if len(enabled_ext) > 0:
|
||||
self._mgr = named.NamedExtensionManager(
|
||||
namespace='designate.api.v2.extensions',
|
||||
names=enabled_ext,
|
||||
invoke_on_load=True)
|
||||
for ext in self._mgr:
|
||||
controller = self
|
||||
path = ext.obj.get_path()
|
||||
for p in path.split('.')[:-1]:
|
||||
if p != '':
|
||||
controller = getattr(controller, p)
|
||||
setattr(controller, path.split('.')[-1], ext.obj)
|
||||
|
||||
limits = limits.LimitsController()
|
||||
schemas = schemas.SchemasController()
|
||||
reverse = reverse.ReverseController()
|
||||
|
0
designate/api/v2/views/extensions/__init__.py
Normal file
0
designate/api/v2/views/extensions/__init__.py
Normal file
55
designate/api/v2/views/extensions/quotas.py
Normal file
55
designate/api/v2/views/extensions/quotas.py
Normal file
@ -0,0 +1,55 @@
|
||||
# COPYRIGHT 2014 Rackspace
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.com>
|
||||
#
|
||||
# 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.
|
||||
from designate.api.v2.views import base as base_view
|
||||
from designate.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class QuotasView(base_view.BaseView):
|
||||
""" Model a Quota API response as a python dictionary """
|
||||
|
||||
_resource_name = 'quota'
|
||||
_collection_name = 'quotas'
|
||||
|
||||
def show_basic(self, context, request, quota):
|
||||
""" Basic view of a quota """
|
||||
return {
|
||||
"zones": quota['domains'],
|
||||
"zone_records": quota['domain_records'],
|
||||
"zone_recordsets": quota['domain_recordsets'],
|
||||
"recordset_records": quota['recordset_records']
|
||||
}
|
||||
|
||||
def load(self, context, request, body):
|
||||
""" Extract a "central" compatible dict from an API call """
|
||||
valid_keys = ('domain_records', 'domain_recordsets', 'domains',
|
||||
'recordset_records')
|
||||
|
||||
quota = body["quota"]
|
||||
|
||||
old_keys = {
|
||||
'zones': 'domains',
|
||||
'zone_records': 'domain_records',
|
||||
'zone_recordsets': 'domain_recordsets',
|
||||
'recordset_records': 'recordset_records'
|
||||
}
|
||||
|
||||
for key in quota:
|
||||
quota[old_keys[key]] = quota.pop(key)
|
||||
|
||||
return self._load(context, request, body, valid_keys)
|
@ -290,6 +290,9 @@ class Service(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
|
||||
|
||||
return self.quota.get_quotas(context, tenant_id)
|
||||
|
||||
def get_quota(self, context, tenant_id, resource):
|
||||
|
47
designate/resources/schemas/v2/quota.json
Normal file
47
designate/resources/schemas/v2/quota.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/hyper-schema",
|
||||
"id": "quota",
|
||||
"title": "quota",
|
||||
"description": "quota",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"quota"
|
||||
],
|
||||
"properties": {
|
||||
"quota": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [],
|
||||
"properties": {
|
||||
"zones": {
|
||||
"type": "integer",
|
||||
"description": "Number of zones allowed",
|
||||
"min": 0,
|
||||
"max": 2147483647,
|
||||
"default": 10
|
||||
},
|
||||
"zone_recordsets": {
|
||||
"type": "integer",
|
||||
"description": "Number of zone recordsets allowed",
|
||||
"min": 0,
|
||||
"max": 2147483647,
|
||||
"default": 500
|
||||
},
|
||||
"zone_records": {
|
||||
"type": "integer",
|
||||
"description": "Number of zone records allowed",
|
||||
"min": 0,
|
||||
"max": 2147483647,
|
||||
"default": 500
|
||||
},
|
||||
"recordset_records": {
|
||||
"type": "integer",
|
||||
"description": "Number of recordset records allowed",
|
||||
"min": 0,
|
||||
"max": 2147483647,
|
||||
"default": 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
121
designate/tests/test_api/test_v2/extensions/test_quotas.py
Normal file
121
designate/tests/test_api/test_v2/extensions/test_quotas.py
Normal file
@ -0,0 +1,121 @@
|
||||
# coding=utf-8
|
||||
# COPYRIGHT 2014 Rackspace
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.com>
|
||||
#
|
||||
# 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.
|
||||
from oslo.config import cfg
|
||||
|
||||
from designate.tests.test_api.test_v2 import ApiV2TestCase
|
||||
|
||||
cfg.CONF.import_opt('enabled_extensions_v2', 'designate.api.v2',
|
||||
group='service:api')
|
||||
|
||||
|
||||
class ApiV2QuotasTest(ApiV2TestCase):
|
||||
def setUp(self):
|
||||
self.config(enabled_extensions_v2=['quotas'], group='service:api')
|
||||
super(ApiV2QuotasTest, self).setUp()
|
||||
|
||||
def test_get_quotas(self):
|
||||
self.policy({'get_quotas': '@'})
|
||||
context = self.get_admin_context()
|
||||
|
||||
response = self.client.get('/quotas/%s' % context.tenant,
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
|
||||
self.assertIn('quota', response.json)
|
||||
self.assertIn('zones', response.json['quota'])
|
||||
self.assertIn('zone_records', response.json['quota'])
|
||||
self.assertIn('zone_recordsets', response.json['quota'])
|
||||
self.assertIn('recordset_records', response.json['quota'])
|
||||
|
||||
max_zones = response.json['quota']['zones']
|
||||
max_zone_records = response.json['quota']['zone_records']
|
||||
self.assertEqual(cfg.CONF.quota_domains, max_zones)
|
||||
self.assertEqual(cfg.CONF.quota_domain_records, max_zone_records)
|
||||
|
||||
def test_patch_quotas(self):
|
||||
self.policy({'set_quotas': '@'})
|
||||
context = self.get_context(tenant='a', is_admin=True)
|
||||
|
||||
response = self.client.get('/quotas/%s' % 'a',
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
|
||||
self.assertIn('quota', response.json)
|
||||
self.assertIn('zones', response.json['quota'])
|
||||
current_count = response.json['quota']['zones']
|
||||
|
||||
body = {'quota': {"zones": 1337}}
|
||||
response = self.client.patch_json('/quotas/%s' % 'a', body,
|
||||
status=200,
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
response = self.client.get('/quotas/%s' % 'a',
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
|
||||
new_count = response.json['quota']['zones']
|
||||
|
||||
self.assertNotEqual(current_count, new_count)
|
||||
|
||||
def test_reset_quotas(self):
|
||||
self.policy({'reset_quotas': '@'})
|
||||
context = self.get_context(tenant='a', is_admin=True)
|
||||
|
||||
response = self.client.get('/quotas/%s' % 'a',
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
|
||||
self.assertIn('quota', response.json)
|
||||
self.assertIn('zones', response.json['quota'])
|
||||
current_count = response.json['quota']['zones']
|
||||
|
||||
body = {'quota': {"zones": 1337}}
|
||||
response = self.client.patch_json('/quotas/%s' % 'a', body,
|
||||
status=200,
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
response = self.client.get('/quotas/%s' % 'a',
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
|
||||
new_count = response.json['quota']['zones']
|
||||
|
||||
self.assertNotEqual(current_count, new_count)
|
||||
|
||||
response = self.client.delete('/quotas/%s' % 'a',
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant}, status=204)
|
||||
response = self.client.get('/quotas/%s' % 'a',
|
||||
headers={'X-Test-Tenant-Id':
|
||||
context.tenant})
|
||||
|
||||
newest_count = response.json['quota']['zones']
|
||||
self.assertNotEqual(new_count, newest_count)
|
||||
self.assertEqual(current_count, newest_count)
|
@ -90,6 +90,9 @@ debug = False
|
||||
# Enabled API Version 1 extensions
|
||||
#enabled_extensions_v1 = diagnostics, quotas, reports, sync, touch
|
||||
|
||||
# Enabled API Version 2 extensions
|
||||
#enabled_extensions_v2 = quotas
|
||||
|
||||
#-----------------------
|
||||
# Keystone Middleware
|
||||
#-----------------------
|
||||
|
@ -52,6 +52,9 @@ designate.api.v1.extensions =
|
||||
reports = designate.api.v1.extensions.reports:blueprint
|
||||
touch = designate.api.v1.extensions.touch:blueprint
|
||||
|
||||
designate.api.v2.extensions =
|
||||
quotas = designate.api.v2.controllers.extensions.quotas:QuotasController
|
||||
|
||||
designate.storage =
|
||||
sqlalchemy = designate.storage.impl_sqlalchemy:SQLAlchemyStorage
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user