API Version 2 - Initial Framework

Change-Id: Id99f9b7191c8ae949311b95e60426c55d0faed55
This commit is contained in:
Kiall Mac Innes 2013-07-29 12:27:45 +01:00 committed by Kiall Mac Innes
parent 4859390823
commit 5697f2e01d
30 changed files with 831 additions and 4 deletions

View File

@ -33,6 +33,8 @@ cfg.CONF.register_opts([
cfg.StrOpt('auth_strategy', default='noauth',
help='The strategy to use for auth. Supports noauth or '
'keystone'),
cfg.BoolOpt('enable-api-v1', default=True),
cfg.BoolOpt('enable-api-v2', default=False),
], group='service:api')

View File

@ -59,6 +59,14 @@ class DesignateRequest(flask.Request, wrappers.AcceptMixin,
def factory(global_config, **local_conf):
if not cfg.CONF['service:api'].enable_api_v1:
def disabled_app(environ, start_response):
status = '404 Not Found'
start_response(status, [])
return []
return disabled_app
app = flask.Flask('designate.api.v1')
app.request_class = DesignateRequest
app.config.update(

View File

@ -0,0 +1,41 @@
# 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.deploy
from oslo.config import cfg
from designate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def factory(global_config, **local_conf):
if not cfg.CONF['service:api'].enable_api_v2:
def disabled_app(environ, start_response):
status = '404 Not Found'
start_response(status, [])
return []
return disabled_app
conf = {
'app': {
'root': 'designate.api.v2.controllers.root.RootController',
'modules': ['designate.api.v2']
}
}
app = pecan.deploy.deploy(conf)
return app

38
designate/api/v2/app.py Normal file
View File

@ -0,0 +1,38 @@
# 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 designate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def setup_app(pecan_config):
config = dict(pecan_config)
if cfg.CONF.debug:
config['app']['debug'] = True
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),
)
return app

View File

View File

@ -0,0 +1,26 @@
# 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.
from designate.openstack.common import log as logging
from designate.api.v2.controllers import schemas
LOG = logging.getLogger(__name__)
class RootController(object):
schemas = schemas.SchemasController()
# zones = zones.ZonesController()
# limits = limits.LimitsController()
# pools = pools.PoolsController()

View File

@ -0,0 +1,38 @@
# 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 os
import pecan
from designate import exceptions
from designate import utils
from designate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class SchemasController(object):
@pecan.expose(template='json:', content_type='application/schema+json')
def _default(self, *remainder):
if len(remainder) == 0:
pecan.abort(404)
try:
schema_name = os.path.join(*remainder)
schema_json = utils.load_schema('v2', schema_name)
except exceptions.ResourceNotFound:
pecan.abort(404)
return schema_json

View File

@ -14,18 +14,30 @@
# License for the specific language governing permissions and limitations
# under the License.
import flask
from oslo.config import cfg
def factory(global_config, **local_conf):
app = flask.Flask('designate.api.versions')
versions = []
if cfg.CONF['service:api'].enable_api_v1:
versions.append({
"id": "v1",
"status": "CURRENT"
})
if cfg.CONF['service:api'].enable_api_v2:
versions.append({
"id": "v2",
"status": "EXPERIMENTAL"
})
@app.route('/', methods=['GET'])
def version_list():
return flask.jsonify({
"versions": [{
"id": "v1",
"status": "CURRENT"
}]
"versions": versions
})
return app

View File

@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["address"],
"properties": {
"address": {
"type": "string",
"format": "ipv4"
}
}
}

View File

@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["address"],
"properties": {
"address": {
"type": "string",
"format": "ipv6"
}
}
}

View File

@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["cname"],
"properties": {
"cname": {
"type": "string",
"format": "hostname"
}
}
}

View File

@ -0,0 +1,17 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["preference", "exchange"],
"properties": {
"preference": {
"type": "integer"
},
"exchange": {
"type": "string",
"format": "hostname"
}
}
}

View File

@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["nsdname"],
"properties": {
"nsdname": {
"type": "string",
"format": "hostname"
}
}
}

View File

@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["ptrdname"],
"properties": {
"ptrdname": {
"type": "string",
"format": "hostname"
}
}
}

View File

@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["mname", "rname", "serial", "refresh", "retry", "expire", "minimun"],
"properties": {
"mname": {
"type": "string",
"format": "hostname"
},
"rname": {
"type": "string",
"format": "hostname"
},
"serial": {
"type": "integer",
"min": 0,
"max": 214748364
},
"refresh": {
"type": "integer",
"min": 0,
"max": 214748364
},
"retry": {
"type": "integer",
"min": 0,
"max": 214748364
},
"expire": {
"type": "integer",
"min": 0,
"max": 214748364
},
"minimun": {
"type": "integer",
"min": 0,
"max": 214748364
}
}
}

View File

@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["text"],
"properties": {
"text": {
"type": "string"
}
}
}

View File

@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["priority", "weight", "port", "target"],
"properties": {
"priority": {
"type": "integer",
"min": 0,
"max": 65535
},
"weight": {
"type": "integer",
"min": 0,
"max": 65535
},
"port": {
"type": "integer",
"min": 0,
"max": 65535
},
"target": {
"type": "string",
"format": "hostname"
}
}
}

View File

@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["algorithm", "type", "fingerprint"],
"properties": {
"algorithm": {
"type": "integer",
"min": 1,
"max": 2
},
"type": {
"type": "integer",
"min": 1,
"max": 1
},
"fingerprint": {
"type": "string",
"pattern": "^[0-9A-Fa-f]{40}$"
}
}
}

View File

@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"additionalProperties": false,
"required": ["text"],
"properties": {
"text": {
"type": "string"
}
}
}

View File

@ -0,0 +1,113 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "recordset",
"title": "recordset",
"description": "RecordSet",
"additionalProperties": false,
"required": ["recordset"],
"properties": {
"recordset": {
"type": "object",
"additionalProperties": false,
"required": ["zone_id", "name", "type", "records"],
"properties": {
"id": {
"type": "string",
"description": "RecordSet identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"readOnly": true
},
"zone_id": {
"type": "string",
"description": "Zone identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"immutable": true
},
"name": {
"type": "string",
"description": "RecordSet name",
"format": "hostname",
"maxLength": 255,
"immutable": true
},
"type": {
"type": "string",
"description": "RecordSet type (TODO: Make types extensible)",
"enum": ["A", "MX"]
},
"ttl": {
"type": ["integer", "null"],
"description": "Default time to live",
"min": 0,
"max": 2147483647,
"default": null
},
"records": {
"type": "array",
"description": "Records Array",
"uniqueItems": true,
"minItems": 1
},
"notes": {
"type": ["string", "null"],
"description": "Notes",
"maxLength": 100
},
"version": {
"type": "integer",
"description": "RecordSet version number",
"readOnly": true
},
"created_at": {
"type": "string",
"description": "Date and time of RecordSet creation",
"format": "date-time",
"readOnly": true
},
"updated_at": {
"type": ["string", "null"],
"description": "Date and time of last recordset modification",
"format": "date-time",
"readOnly": true
}
},
"oneOf": [
{"properties": {"type": {"enum": ["A"]}, "records": {"items": {"$ref": "rdata/a#"}}}},
{"properties": {"type": {"enum": ["AAAA"]}, "records": {"items": {"$ref": "rdata/aaaa#"}}}},
{"properties": {"type": {"enum": ["CNAME"]}, "records": {"items": {"$ref": "rdata/cname#"}}}},
{"properties": {"type": {"enum": ["MX"]}, "records": {"items": {"$ref": "rdata/mx#"}}}},
{"properties": {"type": {"enum": ["NS"]}, "records": {"items": {"$ref": "rdata/ns#"}}}},
{"properties": {"type": {"enum": ["PTR"]}, "records": {"items": {"$ref": "rdata/ptr#"}}}},
{"properties": {"type": {"enum": ["SOA"]}, "records": {"items": {"$ref": "rdata/soa#"}}}},
{"properties": {"type": {"enum": ["SPF"]}, "records": {"items": {"$ref": "rdata/spf#"}}}},
{"properties": {"type": {"enum": ["SRV"]}, "records": {"items": {"$ref": "rdata/srv#"}}}},
{"properties": {"type": {"enum": ["SSHFP"]}, "records": {"items": {"$ref": "rdata/sshfp#"}}}},
{"properties": {"type": {"enum": ["TXT"]}, "records": {"items": {"$ref": "rdata/txt#"}}}}
]
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
}
}
}
},
"links": [{
"rel": "self",
"href": "/zones/{zone_id}/recordsets/{id}"
}, {
"rel": "collection",
"href": "/zones/{zone_id}/recordsets"
}]
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "recordsets",
"title": "recordsets",
"description": "RecordSets",
"additionalProperties": false,
"required": ["recordsets"],
"properties": {
"recordsets": {
"type": "array",
"description": "RecordSets",
"items": {"$ref": "recordset#/properties/recordset"}
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
},
"next": {
"type": "string",
"format": "url"
},
"previous": {
"type": "string",
"format": "url"
}
}
}
}
}

View File

@ -0,0 +1,122 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "zone",
"title": "zone",
"description": "Zone",
"additionalProperties": false,
"required": ["zone"],
"properties": {
"zone": {
"type": "object",
"additionalProperties": false,
"required": ["name", "email"],
"properties": {
"id": {
"type": "string",
"description": "Zone identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"readOnly": true
},
"pool_id": {
"type": "string",
"description": "Pool identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"immutable": true
},
"project_id": {
"type": "string",
"description": "Project identifier",
"maxLength": 36,
"immutable": true
},
"name": {
"type": "string",
"description": "Zone name",
"format": "domain-name",
"maxLength": 255,
"immutable": true
},
"email": {
"type": "string",
"description": "Hostmaster email address",
"format": "email",
"maxLength": 255
},
"ttl": {
"type": "integer",
"description": "Default time to live",
"min": 0,
"max": 2147483647
},
"status": {
"type": "string",
"description": "Zone Status",
"enum": ["ACTIVE", "PENDING", "DELETING", "ERROR"],
"readOnly": true
},
"serial": {
"type": "integer",
"description": "Zone serial number",
"min": 1,
"max": 4294967295,
"readOnly": true
},
"notes": {
"type": ["string", "null"],
"description": "Notes",
"maxLength": 100
},
"version": {
"type": "integer",
"description": "Zone version number",
"readOnly": true
},
"created_at": {
"type": "string",
"description": "Date and time of Zone creation",
"format": "date-time",
"readOnly": true
},
"updated_at": {
"type": ["string", "null"],
"description": "Date and time of last zone modification",
"format": "date-time",
"readOnly": true
}
}
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
}
}
}
},
"links": [{
"rel": "self",
"href": "/zones/{id}"
}, {
"rel": "recordsets",
"href": "/zones/{id}/recordsets"
}, {
"rel": "nameservers",
"href": "/zones/{id}/nameservers"
}, {
"rel": "pool",
"href": "/pools/{pool_id}/nameservers"
}, {
"rel": "collection",
"href": "/zones"
}]
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "zones",
"title": "zones",
"description": "Zones",
"additionalProperties": false,
"required": ["zones"],
"properties": {
"zones": {
"type": "array",
"description": "Zones",
"items": {"$ref": "zone#/properties/zone"}
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
},
"next": {
"type": "string",
"format": "url"
},
"previous": {
"type": "string",
"format": "url"
}
}
}
}
}

View File

@ -31,6 +31,9 @@ class Schema(object):
if version == 'v1':
self.validator = validators.Draft3Validator(
self.raw_schema, resolver=self.resolver)
elif version == 'v2':
self.validator = validators.Draft4Validator(
self.raw_schema, resolver=self.resolver)
else:
raise Exception('Unknown API version: %s' % version)

View File

@ -0,0 +1,123 @@
# Copyright 2012 Managed I.T.
#
# 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.
from designate.openstack.common import log as logging
from designate import exceptions
from designate import schema
from designate.tests import TestCase
LOG = logging.getLogger(__name__)
class SchemasV2Test(TestCase):
__test__ = True
def test_recordset(self):
validator = schema.Schema('v2', 'recordset')
# Pass Expected
validator.validate({
'recordset': {
'id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'zone_id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'name': 'example.com.',
'type': 'A',
'records': [
{'address': "127.0.0.1"},
{'address': "127.0.0.2"},
]
}
})
# Pass Expected
validator.validate({
'recordset': {
'id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'zone_id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'name': 'example.com.',
'type': 'MX',
'records': [
{'preference': 10, 'exchange': 'mail.example.com.'},
]
}
})
with self.assertRaises(exceptions.InvalidObject):
# Fail Expected - Empty Records Array
validator.validate({
'recordset': {
'id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'zone_id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'name': 'example.com.',
'type': 'A',
'records': []
}
})
with self.assertRaises(exceptions.InvalidObject):
# Fail Expected - No Records
validator.validate({
'recordset': {
'id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'zone_id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'name': 'example.com.',
'type': 'A'
}
})
with self.assertRaises(exceptions.InvalidObject):
# Fail Expected - MX records in an A RRset
validator.validate({
'recordset': {
'id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'zone_id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'name': 'example.com.',
'type': 'A',
'records': [
{'address': "127.0.0.1"},
{'preference': 10, 'exchange': 'mail.example.com.'},
]
}
})
with self.assertRaises(exceptions.InvalidObject):
# Fail Expected - A records in an MX RRset
validator.validate({
'recordset': {
'id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'zone_id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'name': 'example.com.',
'type': 'MX',
'records': [
{'preference': 10, 'exchange': 'mail.example.com.'},
{'address': "127.0.0.1"},
]
}
})
with self.assertRaises(exceptions.InvalidObject):
# Fail Expected - AAAA records in an A RRset
validator.validate({
'recordset': {
'id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'zone_id': 'b22d09e0-efa3-11e2-b778-0800200c9a66',
'name': 'example.com.',
'type': 'A',
'records': [
{'address': "127.0.0.1"},
{'address': "::1"},
]
}
})

View File

@ -2,6 +2,7 @@
use = egg:Paste#urlmap
/: osapi_dns_app_versions
/v1: osapi_dns_v1
/v2: osapi_dns_v2
[app:osapi_dns_app_versions]
paste.app_factory = designate.api.versions:factory
@ -17,6 +18,14 @@ paste.app_factory = designate.api.v1:factory
[filter:faultwrapper_v1]
paste.filter_factory = designate.api.v1:FaultWrapperMiddleware.factory
[composite:osapi_dns_v2]
use = call:designate.api.middleware:auth_pipeline_factory
noauth = noauthcontext maintenance osapi_dns_app_v2
keystone = authtoken keystonecontext maintenance osapi_dns_app_v2
[app:osapi_dns_app_v2]
paste.app_factory = designate.api.v2:factory
[filter:maintenance]
paste.filter_factory = designate.api.middleware:MaintenanceMiddleware.factory

View File

@ -57,6 +57,12 @@ root_helper = sudo
# Authentication strategy to use - can be either "noauth" or "keystone"
#auth_strategy = noauth
# Enable Version 1 API
#enable_api_v1 = True
# Enable Version 2 API (experimental)
#enable_api_v2 = False
# Enabled API Version 1 extensions
#enabled_extensions_v1 = diagnostics, quotas, reports, sync

View File

@ -10,6 +10,7 @@ oslo.config>=1.1.0
Paste
PasteDeploy>=1.5.0
pbr>=0.5.21,<1.0
pecan>=0.2.0
python-keystoneclient>=0.3.0
Routes>=1.12.3
SQLAlchemy>=0.7.8,<=0.7.99