Merge "Add Admin API and add extensions"

This commit is contained in:
Jenkins 2015-03-19 15:00:07 +00:00 committed by Gerrit Code Review
commit 3ea4b30971
25 changed files with 354 additions and 30 deletions

View File

@ -55,7 +55,8 @@ DESIGNATE_SERVICE_PORT_MDNS=${DESIGNATE_SERVICE_PORT_MDNS:-5354}
DESIGNATE_TEST_NSREC=${DESIGNATE_TEST_NSREC:-ns1.devstack.org.} DESIGNATE_TEST_NSREC=${DESIGNATE_TEST_NSREC:-ns1.devstack.org.}
DESIGNATE_ENABLED_EXTENSIONS_V1=${DESIGNATE_ENABLED_EXTENSIONS_V1:-"quotas"} DESIGNATE_ENABLED_EXTENSIONS_V1=${DESIGNATE_ENABLED_EXTENSIONS_V1:-"quotas"}
DESIGNATE_ENABLED_EXTENSIONS_V2=${DESIGNATE_ENABLED_EXTENSIONS_V2:-"quotas"} DESIGNATE_ENABLED_EXTENSIONS_V2=${DESIGNATE_ENABLED_EXTENSIONS_V2:-""}
DESIGNATE_ENABLED_EXTENSIONS_ADMIN=${DESIGNATE_ENABLED_EXTENSIONS_ADMIN:-"quotas"}
# Tell Tempest this project is present # Tell Tempest this project is present
TEMPEST_SERVICES+=,designate TEMPEST_SERVICES+=,designate
@ -113,6 +114,7 @@ function configure_designate {
iniset $DESIGNATE_CONF pool_manager_cache:sqlalchemy connection `database_connection_url designate_pool_manager` iniset $DESIGNATE_CONF pool_manager_cache:sqlalchemy connection `database_connection_url designate_pool_manager`
iniset $DESIGNATE_CONF service:api enabled_extensions_v1 $DESIGNATE_ENABLED_EXTENSIONS_V1 iniset $DESIGNATE_CONF service:api enabled_extensions_v1 $DESIGNATE_ENABLED_EXTENSIONS_V1
iniset $DESIGNATE_CONF service:api enabled_extensions_v2 $DESIGNATE_ENABLED_EXTENSIONS_V2 iniset $DESIGNATE_CONF service:api enabled_extensions_v2 $DESIGNATE_ENABLED_EXTENSIONS_V2
iniset $DESIGNATE_CONF service:api enabled_extensions_admin $DESIGNATE_ENABLED_EXTENSIONS_ADMIN
sudo cp $DESIGNATE_DIR/etc/designate/rootwrap.conf.sample $DESIGNATE_ROOTWRAP_CONF sudo cp $DESIGNATE_DIR/etc/designate/rootwrap.conf.sample $DESIGNATE_ROOTWRAP_CONF
iniset $DESIGNATE_ROOTWRAP_CONF DEFAULT filters_path $DESIGNATE_DIR/etc/designate/rootwrap.d root-helper iniset $DESIGNATE_ROOTWRAP_CONF DEFAULT filters_path $DESIGNATE_DIR/etc/designate/rootwrap.d root-helper
@ -146,6 +148,7 @@ function configure_designate {
iniset $DESIGNATE_CONF service:api api_base_uri $DESIGNATE_SERVICE_PROTOCOL://$DESIGNATE_SERVICE_HOST:$DESIGNATE_SERVICE_PORT/ iniset $DESIGNATE_CONF service:api api_base_uri $DESIGNATE_SERVICE_PROTOCOL://$DESIGNATE_SERVICE_HOST:$DESIGNATE_SERVICE_PORT/
iniset $DESIGNATE_CONF service:api enable_api_v1 True iniset $DESIGNATE_CONF service:api enable_api_v1 True
iniset $DESIGNATE_CONF service:api enable_api_v2 True iniset $DESIGNATE_CONF service:api enable_api_v2 True
iniset $DESIGNATE_CONF service:api enable_api_admin True
if is_service_enabled tls-proxy; then if is_service_enabled tls-proxy; then
# Set the service port for a proxy to take the original # Set the service port for a proxy to take the original
iniset $DESIGNATE_CONF service:api api_port $DESIGNATE_SERVICE_PORT_INT iniset $DESIGNATE_CONF service:api api_port $DESIGNATE_SERVICE_PORT_INT

View File

@ -35,4 +35,5 @@ cfg.CONF.register_opts([
'keystone'), 'keystone'),
cfg.BoolOpt('enable-api-v1', default=True), cfg.BoolOpt('enable-api-v1', default=True),
cfg.BoolOpt('enable-api-v2', default=False), cfg.BoolOpt('enable-api-v2', default=False),
cfg.BoolOpt('enable-api-admin', default=False),
], group='service:api') ], group='service:api')

View File

@ -0,0 +1,52 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# 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.deploy
from oslo.config import cfg
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
OPTS = [
cfg.ListOpt('enabled-extensions-admin', default=[],
help='Enabled Admin API Extensions'),
]
cfg.CONF.register_opts(OPTS, group='service:api')
def factory(global_config, **local_conf):
if not cfg.CONF['service:api'].enable_api_admin:
def disabled_app(environ, start_response):
status = '404 Not Found'
start_response(status, [])
return []
return disabled_app
conf = {
'app': {
'root': 'designate.api.admin.controllers.root.RootController',
'modules': ['designate.api.admin'],
'errors': {
404: '/errors/not_found',
405: '/errors/method_not_allowed',
'__force_dict__': True
}
}
}
app = pecan.deploy.deploy(conf)
return app

View File

@ -0,0 +1,46 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Kiall Mac Innes <kiall@hp.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
import pecan.deploy
from oslo.config import cfg
from oslo_log import log as logging
from designate.api.v2 import patches
LOG = logging.getLogger(__name__)
cfg.CONF.register_opts([
cfg.BoolOpt('pecan_debug', default=False,
help='Pecan HTML Debug Interface'),
], group='service:api')
def setup_app(pecan_config):
config = dict(pecan_config)
config['app']['debug'] = cfg.CONF['service:api'].pecan_debug
pecan.configuration.set_config(config, overwrite=True)
app = pecan.make_app(
pecan_config.app.root,
debug=getattr(pecan_config.app, 'debug', False),
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
request_cls=patches.Request
)
return app

View File

@ -18,7 +18,7 @@ import pecan
from oslo_log import log as logging from oslo_log import log as logging
from designate.api.v2.controllers import rest from designate.api.v2.controllers import rest
from designate.api.v2.views.extensions import reports as reports_view from designate.api.admin.views.extensions import reports as reports_view
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -18,7 +18,7 @@ from oslo_log import log as logging
from designate import schema from designate import schema
from designate.api.v2.controllers import rest from designate.api.v2.controllers import rest
from designate.api.v2.views.extensions import quotas as quotas_view from designate.api.admin.views.extensions import quotas as quotas_view
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -17,8 +17,8 @@
from oslo_log import log as logging from oslo_log import log as logging
from designate.api.v2.controllers import rest from designate.api.v2.controllers import rest
from designate.api.v2.controllers.extensions import counts from designate.api.admin.controllers.extensions import counts
from designate.api.v2.controllers.extensions import tenants from designate.api.admin.controllers.extensions import tenants
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -18,7 +18,7 @@ import pecan
from oslo_log import log as logging from oslo_log import log as logging
from designate.api.v2.controllers import rest from designate.api.v2.controllers import rest
from designate.api.v2.views.extensions import reports as reports_view from designate.api.admin.views.extensions import reports as reports_view
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -0,0 +1,46 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# 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 oslo_log import log as logging
from stevedore import named
from designate.api.v2.controllers import errors
LOG = logging.getLogger(__name__)
class RootController(object):
"""
This is /admin/ Controller. Pecan will find all controllers via the object
properties attached to this.
"""
def __init__(self):
enabled_ext = cfg.CONF['service:api'].enabled_extensions_admin
if len(enabled_ext) > 0:
self._mgr = named.NamedExtensionManager(
namespace='designate.api.admin.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)
errors = errors.ErrorsController()

View File

@ -25,6 +25,7 @@ def factory(global_config, **local_conf):
base = cfg.CONF['service:api'].api_base_uri.rstrip('/') base = cfg.CONF['service:api'].api_base_uri.rstrip('/')
def _version(version, status): def _version(version, status):
if version.isdigit():
versions.append({ versions.append({
'id': 'v%s' % version, 'id': 'v%s' % version,
'status': status, 'status': status,
@ -33,6 +34,15 @@ def factory(global_config, **local_conf):
'rel': 'self' 'rel': 'self'
}] }]
}) })
else:
versions.append({
'id': '%s' % version,
'status': status,
'links': [{
'href': base + '/' + version,
'rel': 'self'
}]
})
if cfg.CONF['service:api'].enable_api_v1: if cfg.CONF['service:api'].enable_api_v1:
_version('1', 'CURRENT') _version('1', 'CURRENT')

View File

@ -34,7 +34,7 @@ class Schema(object):
self.validator = validators.Draft3Validator( self.validator = validators.Draft3Validator(
self.raw_schema, resolver=self.resolver, self.raw_schema, resolver=self.resolver,
format_checker=format.draft3_format_checker) format_checker=format.draft3_format_checker)
elif version == 'v2': elif version in ['v2', 'admin']:
self.validator = validators.Draft4Validator( self.validator = validators.Draft4Validator(
self.raw_schema, resolver=self.resolver, self.raw_schema, resolver=self.resolver,
format_checker=format.draft4_format_checker) format_checker=format.draft4_format_checker)

View File

@ -0,0 +1,150 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 itertools
from oslo_log import log as logging
from webtest import TestApp
from designate.api import admin as admin_api
from designate.api import middleware
from designate.tests.test_api import ApiTestCase
LOG = logging.getLogger(__name__)
INVALID_ID = [
'2fdadfb1-cf96-4259-ac6b-bb7b6d2ff98g',
'2fdadfb1cf964259ac6bbb7b6d2ff9GG',
'12345'
]
class AdminApiTestCase(ApiTestCase):
def setUp(self):
super(AdminApiTestCase, self).setUp()
# Ensure the v2 API is enabled
self.config(enable_api_admin=True, group='service:api')
# Create the application
self.app = admin_api.factory({})
# Inject the NormalizeURIMiddleware middleware
self.app = middleware.NormalizeURIMiddleware(self.app)
# Inject the FaultWrapper middleware
self.app = middleware.FaultWrapperMiddleware(self.app)
# Inject the TestContext middleware
self.app = middleware.TestContextMiddleware(
self.app, self.admin_context.tenant,
self.admin_context.tenant)
# Obtain a test client
self.client = TestApp(self.app)
def tearDown(self):
self.app = None
self.client = None
super(AdminApiTestCase, self).tearDown()
def _assert_invalid_uuid(self, method, url_format, *args, **kw):
"""
Test that UUIDs used in the URL is valid.
"""
count = url_format.count('%s')
for i in itertools.product(INVALID_ID, repeat=count):
self._assert_exception('invalid_uuid', 400, method, url_format % i)
def _assert_exception(self, expected_type, expected_status, obj,
*args, **kwargs):
"""
Checks the response that a api call with a exception contains the
wanted data.
"""
kwargs.setdefault('status', expected_status)
response = obj(*args, **kwargs) if not hasattr(obj, 'json') else obj
self.assertEqual(expected_status, response.json['code'])
self.assertEqual(expected_type, response.json['type'])
def _assert_invalid_paging(self, data, url, key):
"""
Test that certain circumstances is invalid for paging in a given url.
"""
self._assert_paging(data, url, key=key,
limit='invalid_limit',
expected_type='invalid_limit',
expected_status=400)
self._assert_paging(data, url, key=key,
sort_dir='invalid_sort_dir',
expected_type='invalid_sort_dir',
expected_status=400)
self._assert_paging(data, url, key=key,
sort_key='invalid_sort_key',
expected_type='invalid_sort_key',
expected_status=400)
self._assert_paging(data, url, key=key,
marker='invalid_marker',
expected_type='invalid_marker',
expected_status=400)
def _assert_paging(self, data, url, key=None, limit=5, sort_dir='asc',
sort_key='created_at', marker=None,
expected_type=None, expected_status=200):
def _page(marker=None):
params = {'limit': limit,
'sort_dir': sort_dir,
'sort_key': sort_key}
if marker is not None:
params['marker'] = marker
r = self.client.get(url, params, status=expected_status)
if expected_status != 200:
if expected_type:
self._assert_exception(expected_type, expected_status, r)
return r
else:
return r.json[key] if key in r.json else r.json
response = _page(marker=marker)
if expected_status != 200:
if expected_type:
self._assert_exception(expected_type, expected_status,
response)
return response
x = 0
length = len(data)
for i in xrange(0, length):
assert data[i]['id'] == response[x]['id']
x += 1
# Don't bother getting a new page if we're at the last item
if x == len(response) and i != length - 1:
x = 0
response = _page(response[-1:][0]['id'])
_page(marker=response[-1:][0]['id'])

View File

@ -16,16 +16,16 @@
# under the License. # under the License.
from oslo.config import cfg from oslo.config import cfg
from designate.tests.test_api.test_v2 import ApiV2TestCase from designate.tests.test_api.test_admin import AdminApiTestCase
cfg.CONF.import_opt('enabled_extensions_v2', 'designate.api.v2', cfg.CONF.import_opt('enabled_extensions_admin', 'designate.api.admin',
group='service:api') group='service:api')
class ApiV2QuotasTest(ApiV2TestCase): class AdminApiQuotasTest(AdminApiTestCase):
def setUp(self): def setUp(self):
self.config(enabled_extensions_v2=['quotas'], group='service:api') self.config(enabled_extensions_admin=['quotas'], group='service:api')
super(ApiV2QuotasTest, self).setUp() super(AdminApiQuotasTest, self).setUp()
def test_get_quotas(self): def test_get_quotas(self):
self.policy({'get_quotas': '@'}) self.policy({'get_quotas': '@'})

View File

@ -16,16 +16,16 @@
# under the License. # under the License.
from oslo.config import cfg from oslo.config import cfg
from designate.tests.test_api.test_v2 import ApiV2TestCase from designate.tests.test_api.test_admin import AdminApiTestCase
cfg.CONF.import_opt('enabled_extensions_v2', 'designate.api.v2', cfg.CONF.import_opt('enabled_extensions_admin', 'designate.api.admin',
group='service:api') group='service:api')
class ApiV2ReportsTest(ApiV2TestCase): class AdminApiReportsTest(AdminApiTestCase):
def setUp(self): def setUp(self):
self.config(enabled_extensions_v2=['reports'], group='service:api') self.config(enabled_extensions_admin=['reports'], group='service:api')
super(ApiV2ReportsTest, self).setUp() super(AdminApiReportsTest, self).setUp()
def test_get_counts(self): def test_get_counts(self):
self.policy({'count_tenants': '@'}) self.policy({'count_tenants': '@'})

View File

@ -3,6 +3,7 @@ use = egg:Paste#urlmap
/: osapi_dns_app_versions /: osapi_dns_app_versions
/v1: osapi_dns_v1 /v1: osapi_dns_v1
/v2: osapi_dns_v2 /v2: osapi_dns_v2
/admin: osapi_dns_admin
[app:osapi_dns_app_versions] [app:osapi_dns_app_versions]
paste.app_factory = designate.api.versions:factory paste.app_factory = designate.api.versions:factory
@ -23,6 +24,14 @@ keystone = request_id faultwrapper validation_API_v2 authtoken keystonecontext m
[app:osapi_dns_app_v2] [app:osapi_dns_app_v2]
paste.app_factory = designate.api.v2:factory paste.app_factory = designate.api.v2:factory
[composite:osapi_dns_admin]
use = call:designate.api.middleware:auth_pipeline_factory
noauth = request_id faultwrapper noauthcontext maintenance normalizeuri osapi_dns_app_admin
keystone = request_id faultwrapper authtoken keystonecontext maintenance normalizeuri osapi_dns_app_admin
[app:osapi_dns_app_admin]
paste.app_factory = designate.api.admin:factory
[filter:request_id] [filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory paste.filter_factory = oslo_middleware:RequestId.factory

View File

@ -84,6 +84,13 @@ debug = False
# Enable Version 2 API (experimental) # Enable Version 2 API (experimental)
#enable_api_v2 = False #enable_api_v2 = False
# Enable Admin API (experimental)
#enable_api_admin = False
# Enabled Admin API extensions
# Can be one or more of : reports, quotas, counts, tenants
#enabled_extensions_admin =
# Show the pecan HTML based debug interface (v2 only) # Show the pecan HTML based debug interface (v2 only)
# This is only useful for development, and WILL break python-designateclient # This is only useful for development, and WILL break python-designateclient
# if an error occurs # if an error occurs
@ -94,7 +101,7 @@ debug = False
#enabled_extensions_v1 = #enabled_extensions_v1 =
# Enabled API Version 2 extensions # Enabled API Version 2 extensions
# Can be one or more of : reports, quotas # Can be one or more of :
#enabled_extensions_v2 = #enabled_extensions_v2 =
#----------------------- #-----------------------

View File

@ -24,7 +24,7 @@ class QuotasClient(object):
@classmethod @classmethod
def quotas_uri(cls, tenant_id): def quotas_uri(cls, tenant_id):
return "/v2/quotas/" + tenant_id return "/admin/quotas/" + tenant_id
@classmethod @classmethod
def deserialize(cls, resp, body, model_type): def deserialize(cls, resp, body, model_type):

View File

@ -53,9 +53,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 = designate.api.admin.extensions =
reports = designate.api.v2.controllers.extensions.reports:ReportsController reports = designate.api.admin.controllers.extensions.reports:ReportsController
quotas = designate.api.v2.controllers.extensions.quotas:QuotasController quotas = designate.api.admin.controllers.extensions.quotas:QuotasController
designate.storage = designate.storage =
sqlalchemy = designate.storage.impl_sqlalchemy:SQLAlchemyStorage sqlalchemy = designate.storage.impl_sqlalchemy:SQLAlchemyStorage