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:
Tim Simmons 2014-06-16 21:30:43 +00:00
parent 1d3ea84717
commit e4b19ad5b9
13 changed files with 335 additions and 1 deletions

View File

@ -23,7 +23,7 @@
"*.class", "*.class",
"*.psd", "*.psd",
"*.db", "*.db",
".vagrant", ".vagrant"
], ],
"folder_exclude_patterns": "folder_exclude_patterns":
[ [

View File

@ -22,6 +22,12 @@ from designate.openstack.common import log as logging
LOG = logging.getLogger(__name__) 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): def factory(global_config, **local_conf):
if not cfg.CONF['service:api'].enable_api_v2: if not cfg.CONF['service:api'].enable_api_v2:

View 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 ''

View File

@ -13,6 +13,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslo.config import cfg
from stevedore import named
from designate import exceptions from designate import exceptions
from designate.openstack.common import log as logging from designate.openstack.common import log as logging
from designate.api.v2.controllers import limits 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 This is /v2/ Controller. Pecan will find all controllers via the object
properties attached to this. 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() limits = limits.LimitsController()
schemas = schemas.SchemasController() schemas = schemas.SchemasController()
reverse = reverse.ReverseController() reverse = reverse.ReverseController()

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

View File

@ -290,6 +290,9 @@ class Service(service.Service):
target = {'tenant_id': tenant_id} target = {'tenant_id': tenant_id}
policy.check('get_quotas', context, target) 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) return self.quota.get_quotas(context, tenant_id)
def get_quota(self, context, tenant_id, resource): def get_quota(self, context, tenant_id, resource):

View 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
}
}
}
}
}

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

View File

@ -90,6 +90,9 @@ debug = False
# Enabled API Version 1 extensions # Enabled API Version 1 extensions
#enabled_extensions_v1 = diagnostics, quotas, reports, sync, touch #enabled_extensions_v1 = diagnostics, quotas, reports, sync, touch
# Enabled API Version 2 extensions
#enabled_extensions_v2 = quotas
#----------------------- #-----------------------
# Keystone Middleware # Keystone Middleware
#----------------------- #-----------------------

View File

@ -52,6 +52,9 @@ designate.api.v1.extensions =
reports = designate.api.v1.extensions.reports:blueprint reports = designate.api.v1.extensions.reports:blueprint
touch = designate.api.v1.extensions.touch:blueprint touch = designate.api.v1.extensions.touch:blueprint
designate.api.v2.extensions =
quotas = designate.api.v2.controllers.extensions.quotas:QuotasController
designate.storage = designate.storage =
sqlalchemy = designate.storage.impl_sqlalchemy:SQLAlchemyStorage sqlalchemy = designate.storage.impl_sqlalchemy:SQLAlchemyStorage