Switch to plain .json schemas and improve Schema handling.
Additionaly - All hostnames now REQUIRE a trailing "." Change-Id: I1ee0b6e8911e1b6207aace619dd19c014363bfd7
This commit is contained in:
parent
52c7dd4fb3
commit
88882e4373
@ -15,32 +15,25 @@
|
||||
# under the License.
|
||||
import flask
|
||||
from moniker.openstack.common import log as logging
|
||||
from moniker.openstack.common import jsonutils as json
|
||||
from moniker import exceptions
|
||||
from moniker.api.v1.schemas import domain_schema, domains_schema
|
||||
from moniker import schema
|
||||
from moniker.central import api as central_api
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
blueprint = flask.Blueprint('domains', __name__)
|
||||
|
||||
|
||||
def _append_domain_links(values, domain_id):
|
||||
values['self'] = flask.url_for('.get_domain', domain_id=domain_id)
|
||||
values['records'] = flask.url_for('records.get_records',
|
||||
domain_id=domain_id)
|
||||
values['schema'] = flask.url_for('.get_domain_schema')
|
||||
|
||||
return values
|
||||
domain_schema = schema.Schema('v1', 'domain')
|
||||
domains_schema = schema.Schema('v1', 'domains')
|
||||
|
||||
|
||||
@blueprint.route('/schemas/domain', methods=['GET'])
|
||||
def get_domain_schema():
|
||||
return flask.jsonify(domain_schema.raw())
|
||||
return flask.jsonify(domain_schema.raw)
|
||||
|
||||
|
||||
@blueprint.route('/schemas/domains', methods=['GET'])
|
||||
def get_domains_schema():
|
||||
return flask.jsonify(domains_schema.raw())
|
||||
return flask.jsonify(domains_schema.raw)
|
||||
|
||||
|
||||
@blueprint.route('/domains', methods=['POST'])
|
||||
@ -55,12 +48,11 @@ def create_domain():
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
except exceptions.InvalidObject, e:
|
||||
return flask.Response(status=400, response=str(e))
|
||||
response_body = json.dumps({'errors': e.errors})
|
||||
return flask.Response(status=400, response=response_body)
|
||||
except exceptions.DuplicateDomain:
|
||||
return flask.Response(status=409)
|
||||
else:
|
||||
domain = _append_domain_links(domain, domain['id'])
|
||||
|
||||
domain = domain_schema.filter(domain)
|
||||
|
||||
response = flask.jsonify(domain)
|
||||
@ -79,9 +71,9 @@ def get_domains():
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
|
||||
domains = domains_schema.filter(domains)
|
||||
domains = domains_schema.filter({'domains': domains})
|
||||
|
||||
return flask.jsonify(domains=domains)
|
||||
return flask.jsonify(domains)
|
||||
|
||||
|
||||
@blueprint.route('/domains/<domain_id>', methods=['GET'])
|
||||
@ -95,8 +87,6 @@ def get_domain(domain_id):
|
||||
except exceptions.DomainNotFound:
|
||||
return flask.Response(status=404)
|
||||
else:
|
||||
domain = _append_domain_links(domain, domain['id'])
|
||||
|
||||
domain = domain_schema.filter(domain)
|
||||
|
||||
return flask.jsonify(domain)
|
||||
@ -113,14 +103,13 @@ def update_domain(domain_id):
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
except exceptions.InvalidObject, e:
|
||||
return flask.Response(status=400, response=str(e))
|
||||
response_body = json.dumps({'errors': e.errors})
|
||||
return flask.Response(status=400, response=response_body)
|
||||
except exceptions.DomainNotFound:
|
||||
return flask.Response(status=404)
|
||||
except exceptions.DuplicateDomain:
|
||||
return flask.Response(status=409)
|
||||
else:
|
||||
domain = _append_domain_links(domain, domain['id'])
|
||||
|
||||
domain = domain_schema.filter(domain)
|
||||
|
||||
return flask.jsonify(domain)
|
||||
|
@ -15,32 +15,25 @@
|
||||
# under the License.
|
||||
import flask
|
||||
from moniker.openstack.common import log as logging
|
||||
from moniker.openstack.common import jsonutils as json
|
||||
from moniker import exceptions
|
||||
from moniker.api.v1.schemas import record_schema, records_schema
|
||||
from moniker import schema
|
||||
from moniker.central import api as central_api
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
blueprint = flask.Blueprint('records', __name__)
|
||||
|
||||
|
||||
def _append_record_links(values, domain_id, record_id):
|
||||
values['self'] = flask.url_for('.get_record', domain_id=domain_id,
|
||||
record_id=record_id)
|
||||
values['domain'] = flask.url_for('domains.get_domain', domain_id=domain_id)
|
||||
values['schema'] = flask.url_for('.get_record_schema')
|
||||
|
||||
return values
|
||||
record_schema = schema.Schema('v1', 'record')
|
||||
records_schema = schema.Schema('v1', 'records')
|
||||
|
||||
|
||||
@blueprint.route('/schemas/record', methods=['GET'])
|
||||
def get_record_schema():
|
||||
return flask.jsonify(record_schema.raw())
|
||||
return flask.jsonify(record_schema.raw)
|
||||
|
||||
|
||||
@blueprint.route('/schemas/records', methods=['GET'])
|
||||
def get_records_schema():
|
||||
return flask.jsonify(records_schema.raw())
|
||||
return flask.jsonify(records_schema.raw)
|
||||
|
||||
|
||||
@blueprint.route('/domains/<domain_id>/records', methods=['POST'])
|
||||
@ -54,12 +47,11 @@ def create_record(domain_id):
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
except exceptions.InvalidObject, e:
|
||||
return flask.Response(status=400, response=str(e))
|
||||
response_body = json.dumps({'errors': e.errors})
|
||||
return flask.Response(status=400, response=response_body)
|
||||
except exceptions.DuplicateRecord:
|
||||
return flask.Response(status=409)
|
||||
else:
|
||||
record = _append_record_links(record, record['domain_id'],
|
||||
record['id'])
|
||||
record = record_schema.filter(record)
|
||||
|
||||
response = flask.jsonify(record)
|
||||
@ -79,7 +71,9 @@ def get_records(domain_id):
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
|
||||
return flask.jsonify(records=records)
|
||||
records = records_schema.filter({'records': records})
|
||||
|
||||
return flask.jsonify(records)
|
||||
|
||||
|
||||
@blueprint.route('/domains/<domain_id>/records/<record_id>', methods=['GET'])
|
||||
@ -93,8 +87,6 @@ def get_record(domain_id, record_id):
|
||||
except exceptions.RecordNotFound:
|
||||
return flask.Response(status=404)
|
||||
else:
|
||||
record = _append_record_links(record, record['domain_id'],
|
||||
record['id'])
|
||||
record = record_schema.filter(record)
|
||||
|
||||
return flask.jsonify(record)
|
||||
@ -112,14 +104,13 @@ def update_record(domain_id, record_id):
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
except exceptions.InvalidObject, e:
|
||||
return flask.Response(status=400, response=str(e))
|
||||
response_body = json.dumps({'errors': e.errors})
|
||||
return flask.Response(status=400, response=response_body)
|
||||
except exceptions.RecordNotFound:
|
||||
return flask.Response(status=404)
|
||||
except exceptions.DuplicateRecord:
|
||||
return flask.Response(status=409)
|
||||
else:
|
||||
record = _append_record_links(record, record['domain_id'],
|
||||
record['id'])
|
||||
record = record_schema.filter(record)
|
||||
|
||||
return flask.jsonify(record)
|
||||
|
@ -1,154 +0,0 @@
|
||||
from moniker.schema import Schema, CollectionSchema
|
||||
|
||||
SERVER_PROPERTIES = {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'Server 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}$'),
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Server DNS name',
|
||||
'maxLength': 255,
|
||||
'pattern': '^.+[^\\.]$', # TODO: Figure out the correct regex
|
||||
'required': True,
|
||||
},
|
||||
'ipv4': {
|
||||
'type': 'string',
|
||||
'description': 'IPv4 address of server',
|
||||
'format': 'ip-address',
|
||||
'required': True,
|
||||
},
|
||||
'ipv6': {
|
||||
'type': 'string',
|
||||
'description': 'IPv6 address of server',
|
||||
'format': 'ipv6',
|
||||
},
|
||||
'created_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of server creation',
|
||||
'format': 'date-time',
|
||||
},
|
||||
'updated_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of last server update',
|
||||
'format': 'date-time',
|
||||
},
|
||||
'self': {'type': 'string'},
|
||||
'schema': {'type': 'string'},
|
||||
}
|
||||
|
||||
SERVER_LINKS = [
|
||||
{'rel': 'self', 'href': '{self}'},
|
||||
{'rel': 'describedby', 'href': '{schema}'},
|
||||
]
|
||||
|
||||
DOMAIN_PROPERTIES = {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'Domain 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}$'),
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'Domain name',
|
||||
'maxLength': 255,
|
||||
'pattern': '^.+[^\\.]$', # TODO: Figure out the correct regex
|
||||
'required': True,
|
||||
},
|
||||
'email': {
|
||||
'type': 'string',
|
||||
'description': 'Hostmaster email address',
|
||||
'maxLength': 255,
|
||||
'required': True,
|
||||
},
|
||||
'ttl': {
|
||||
'type': 'integer',
|
||||
'description': 'Time to live',
|
||||
},
|
||||
'serial': {
|
||||
'type': 'integer',
|
||||
'description': 'Serial Number',
|
||||
},
|
||||
'created_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of image registration',
|
||||
'format': 'date-time',
|
||||
},
|
||||
'updated_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of image registration',
|
||||
'format': 'date-time',
|
||||
},
|
||||
'self': {'type': 'string'},
|
||||
'records': {'type': 'string'},
|
||||
'schema': {'type': 'string'},
|
||||
}
|
||||
|
||||
DOMAIN_LINKS = [
|
||||
{'rel': 'self', 'href': '{self}'},
|
||||
{'rel': 'records', 'href': '{records}', 'method': 'GET'},
|
||||
{'rel': 'describedby', 'href': '{schema}', 'method': 'GET'},
|
||||
]
|
||||
|
||||
RECORD_PROPERTIES = {
|
||||
'id': {
|
||||
'type': 'string',
|
||||
'description': 'Record 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}$'),
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'DNS Record Name',
|
||||
'maxLength': 255,
|
||||
'pattern': '^.+[^\\.]$', # TODO: Figure out the correct regex
|
||||
'required': True,
|
||||
},
|
||||
'type': {
|
||||
'type': 'string',
|
||||
'description': 'DNS Record Type',
|
||||
'enum': ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS'],
|
||||
},
|
||||
'data': {
|
||||
'type': 'string',
|
||||
'description': 'DNS Record Value',
|
||||
'maxLength': 255,
|
||||
'required': True,
|
||||
},
|
||||
'ttl': {
|
||||
'type': 'integer',
|
||||
'description': 'Time to live.',
|
||||
'min': 60, # TODO: This should be a config option
|
||||
},
|
||||
'created_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of image registration',
|
||||
'format': 'date-time',
|
||||
},
|
||||
'updated_at': {
|
||||
'type': 'string',
|
||||
'description': 'Date and time of image registration',
|
||||
'format': 'date-time',
|
||||
},
|
||||
'self': {'type': 'string'},
|
||||
'domain': {'type': 'string'},
|
||||
'schema': {'type': 'string'},
|
||||
}
|
||||
|
||||
RECORD_LINKS = [
|
||||
{'rel': 'self', 'href': '{self}'},
|
||||
{'rel': 'domain', 'href': '{domain}'},
|
||||
{'rel': 'describedby', 'href': '{schema}'},
|
||||
]
|
||||
|
||||
server_schema = Schema('server', SERVER_PROPERTIES, SERVER_LINKS)
|
||||
servers_schema = CollectionSchema('servers', server_schema)
|
||||
|
||||
domain_schema = Schema('domain', DOMAIN_PROPERTIES, DOMAIN_LINKS)
|
||||
domains_schema = CollectionSchema('domains', domain_schema)
|
||||
|
||||
record_schema = Schema('record', RECORD_PROPERTIES, RECORD_LINKS)
|
||||
records_schema = CollectionSchema('records', record_schema)
|
@ -15,30 +15,25 @@
|
||||
# under the License.
|
||||
import flask
|
||||
from moniker.openstack.common import log as logging
|
||||
from moniker.openstack.common import jsonutils as json
|
||||
from moniker import exceptions
|
||||
from moniker.api.v1.schemas import server_schema, servers_schema
|
||||
from moniker import schema
|
||||
from moniker.central import api as central_api
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
blueprint = flask.Blueprint('servers', __name__)
|
||||
|
||||
|
||||
def _append_server_links(values, server_id):
|
||||
values['self'] = flask.url_for('.get_server', server_id=server_id)
|
||||
values['schema'] = flask.url_for('.get_server_schema')
|
||||
|
||||
return values
|
||||
server_schema = schema.Schema('v1', 'server')
|
||||
servers_schema = schema.Schema('v1', 'servers')
|
||||
|
||||
|
||||
@blueprint.route('/schemas/server', methods=['GET'])
|
||||
def get_server_schema():
|
||||
return flask.jsonify(server_schema.raw())
|
||||
return flask.jsonify(server_schema.raw)
|
||||
|
||||
|
||||
@blueprint.route('/schemas/servers', methods=['GET'])
|
||||
def get_servers_schema():
|
||||
return flask.jsonify(servers_schema.raw())
|
||||
return flask.jsonify(servers_schema.raw)
|
||||
|
||||
|
||||
@blueprint.route('/servers', methods=['POST'])
|
||||
@ -52,12 +47,11 @@ def create_server():
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
except exceptions.InvalidObject, e:
|
||||
return flask.Response(status=400, response=str(e))
|
||||
response_body = json.dumps({'errors': e.errors})
|
||||
return flask.Response(status=400, response=response_body)
|
||||
except exceptions.DuplicateServer:
|
||||
return flask.Response(status=409)
|
||||
else:
|
||||
server = _append_server_links(server, server['id'])
|
||||
|
||||
server = server_schema.filter(server)
|
||||
|
||||
response = flask.jsonify(server)
|
||||
@ -76,9 +70,9 @@ def get_servers():
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
|
||||
servers = servers_schema.filter(servers)
|
||||
servers = servers_schema.filter({'servers': servers})
|
||||
|
||||
return flask.jsonify(servers=servers)
|
||||
return flask.jsonify(servers)
|
||||
|
||||
|
||||
@blueprint.route('/servers/<server_id>', methods=['GET'])
|
||||
@ -92,8 +86,6 @@ def get_server(server_id):
|
||||
except exceptions.ServerNotFound:
|
||||
return flask.Response(status=404)
|
||||
else:
|
||||
server = _append_server_links(server, server['id'])
|
||||
|
||||
server = server_schema.filter(server)
|
||||
|
||||
return flask.jsonify(server)
|
||||
@ -110,14 +102,13 @@ def update_server(server_id):
|
||||
except exceptions.Forbidden:
|
||||
return flask.Response(status=401)
|
||||
except exceptions.InvalidObject, e:
|
||||
return flask.Response(status=400, response=str(e))
|
||||
response_body = json.dumps({'errors': e.errors})
|
||||
return flask.Response(status=400, response=response_body)
|
||||
except exceptions.ServerNotFound:
|
||||
return flask.Response(status=404)
|
||||
except exceptions.DuplicateServer:
|
||||
return flask.Response(status=409)
|
||||
else:
|
||||
server = _append_server_links(server, server['id'])
|
||||
|
||||
server = server_schema.filter(server)
|
||||
|
||||
return flask.jsonify(server)
|
||||
|
@ -32,7 +32,9 @@ class NoServersConfigured(ConfigurationError):
|
||||
|
||||
|
||||
class InvalidObject(Base):
|
||||
pass
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.errors = kwargs.pop('errors', None)
|
||||
super(InvalidObject, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class Forbidden(Base):
|
||||
|
@ -33,7 +33,7 @@ class InitCommand(Command):
|
||||
"Init database"
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
utils.read_config('moniker-central')
|
||||
utils.read_config('moniker-central', [])
|
||||
|
||||
url = cfg.CONF.database_connection
|
||||
|
||||
@ -53,7 +53,7 @@ class SyncCommand(Command):
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
# TODO: Support specifying version
|
||||
utils.read_config('moniker-central')
|
||||
utils.read_config('moniker-central', [])
|
||||
|
||||
url = cfg.CONF.database_connection
|
||||
|
||||
|
62
moniker/resources/schemas/v1/domain.json
Normal file
62
moniker/resources/schemas/v1/domain.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"id": "/schemas/domain",
|
||||
|
||||
"$schema": "http://json-schema.org/draft-03/hyper-schema",
|
||||
|
||||
"title": "domain",
|
||||
"description": "Domain",
|
||||
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Domain 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
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Domain name",
|
||||
"format": "host-name",
|
||||
"maxLength": 255,
|
||||
"required": true
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Hostmaster email address",
|
||||
"format": "email",
|
||||
"maxLength": 255,
|
||||
"required": true
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live"
|
||||
},
|
||||
"serial": {
|
||||
"type": "integer",
|
||||
"description": "Serial Number",
|
||||
"readonly": true
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "Date and time of image registration",
|
||||
"format": "date-time",
|
||||
"readonly": true
|
||||
},
|
||||
"updated_at": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Date and time of image registration",
|
||||
"format": "date-time",
|
||||
"readonly": true
|
||||
}
|
||||
},
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "/domains/{id}"
|
||||
}, {
|
||||
"rel": "records",
|
||||
"href": "/domains/{id}/records"
|
||||
}, {
|
||||
"rel": "collection",
|
||||
"href": "/domains"
|
||||
}]
|
||||
}
|
16
moniker/resources/schemas/v1/domains.json
Normal file
16
moniker/resources/schemas/v1/domains.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "/schemas/domains",
|
||||
|
||||
"$schema": "http://json-schema.org/draft-03/hyper-schema",
|
||||
|
||||
"title": "domains",
|
||||
"description": "Domains",
|
||||
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": "array",
|
||||
"description": "Domains",
|
||||
"items": {"$ref": "/schemas/domain"}
|
||||
}
|
||||
}
|
||||
}
|
186
moniker/resources/schemas/v1/record.json
Normal file
186
moniker/resources/schemas/v1/record.json
Normal file
@ -0,0 +1,186 @@
|
||||
{
|
||||
"id": "/schemas/record",
|
||||
|
||||
"$schema": "http://json-schema.org/draft-03/hyper-schema",
|
||||
|
||||
"title": "record",
|
||||
"description": "Record",
|
||||
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Record 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
|
||||
},
|
||||
"domain_id": {
|
||||
"type": "string",
|
||||
"description": "Domain 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
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "DNS Record Name",
|
||||
"format": "host-name",
|
||||
"maxLength": 255,
|
||||
"required": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "DNS Record Type",
|
||||
"enum": ["A", "AAAA", "CNAME", "MX", "SRV", "TXT", "SPF", "NS"],
|
||||
"required": true
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "DNS Record Value",
|
||||
"maxLength": 255,
|
||||
"required": true
|
||||
},
|
||||
"priority": {
|
||||
"type": ["integer", "null"],
|
||||
"description": "DNS Record Priority",
|
||||
"min": 0,
|
||||
"max": 99999
|
||||
},
|
||||
"ttl": {
|
||||
"type": ["integer", "null"],
|
||||
"description": "Time to live",
|
||||
"min": 60
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "Date and time of image registration",
|
||||
"format": "date-time",
|
||||
"readonly": true
|
||||
},
|
||||
"updated_at": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Date and time of image registration",
|
||||
"format": "date-time",
|
||||
"readonly": true
|
||||
}
|
||||
},
|
||||
"oneOf": [{
|
||||
"description": "An A Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["A"]
|
||||
},
|
||||
"data": {
|
||||
"format": "ip-address",
|
||||
"required": true
|
||||
},
|
||||
"priority": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "An AAAA Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["AAAA"]
|
||||
},
|
||||
"data": {
|
||||
"format": "ipv6",
|
||||
"required": true
|
||||
},
|
||||
"priority": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "A CNAME Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["CNAME"]
|
||||
},
|
||||
"data": {
|
||||
"format": "host-name",
|
||||
"required": true
|
||||
},
|
||||
"priority": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "A MX Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["MX"]
|
||||
},
|
||||
"data": {
|
||||
"format": "host-name",
|
||||
"required": true
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "A SRV Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["SRV"]
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "A TXT Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["TXT"]
|
||||
},
|
||||
"priority": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "A SPF Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["SPF"]
|
||||
},
|
||||
"priority": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "A NS Record",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["NS"]
|
||||
},
|
||||
"data": {
|
||||
"format": "host-name",
|
||||
"required": true
|
||||
},
|
||||
"priority": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "/domains/{domain_id}/records/{id}"
|
||||
}, {
|
||||
"rel": "domain",
|
||||
"href": "/domains/{domain_id}"
|
||||
}, {
|
||||
"rel": "collection",
|
||||
"href": "/domains/{domain_id}/records"
|
||||
}]
|
||||
}
|
16
moniker/resources/schemas/v1/records.json
Normal file
16
moniker/resources/schemas/v1/records.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "/schemas/records",
|
||||
|
||||
"$schema": "http://json-schema.org/draft-03/hyper-schema",
|
||||
|
||||
"title": "records",
|
||||
"description": "Records",
|
||||
|
||||
"properties": {
|
||||
"records": {
|
||||
"type": "array",
|
||||
"description": "Records",
|
||||
"items": {"$ref": "/schemas/record"}
|
||||
}
|
||||
}
|
||||
}
|
68
moniker/resources/schemas/v1/server.json
Normal file
68
moniker/resources/schemas/v1/server.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"id": "/schemas/server",
|
||||
|
||||
"$schema": "http://json-schema.org/draft-03/hyper-schema",
|
||||
|
||||
"title": "server",
|
||||
"description": "Server",
|
||||
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Server 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
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Server DNS name",
|
||||
"format": "host-name",
|
||||
"maxLength": 255,
|
||||
"required": true
|
||||
},
|
||||
"ipv4": {
|
||||
"type": "string",
|
||||
"description": "IPv4 address of server",
|
||||
"format": "ip-address"
|
||||
},
|
||||
"ipv6": {
|
||||
"type": "string",
|
||||
"description": "IPv6 address of server",
|
||||
"format": "ipv6"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"description": "Date and time of server creation",
|
||||
"format": "date-time",
|
||||
"readonly": true
|
||||
},
|
||||
"updated_at": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Date and time of last server update",
|
||||
"format": "date-time",
|
||||
"readonly": true
|
||||
}
|
||||
},
|
||||
"anyOf": [{
|
||||
"description": "IPv4",
|
||||
"properties": {
|
||||
"ipv4": {
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"description": "IPv6",
|
||||
"properties": {
|
||||
"ipv6": {
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}],
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "/servers/{id}"
|
||||
}, {
|
||||
"rel": "collection",
|
||||
"href": "/servers"
|
||||
}]
|
||||
}
|
16
moniker/resources/schemas/v1/servers.json
Normal file
16
moniker/resources/schemas/v1/servers.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "/schemas/servers",
|
||||
|
||||
"$schema": "http://json-schema.org/draft-03/hyper-schema",
|
||||
|
||||
"title": "servers",
|
||||
"description": "Servers",
|
||||
|
||||
"properties": {
|
||||
"servers": {
|
||||
"type": "array",
|
||||
"description": "Servers",
|
||||
"items": {"$ref": "/schemas/server"}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
$TTL {{ domain.ttl }}
|
||||
|
||||
{{ domain.name }}. IN SOA {{ servers[0].name }}. {{ domain.email | replace("@", ".") }}. (
|
||||
{{ domain.name }} IN SOA {{ servers[0].name }} {{ domain.email | replace("@", ".") }}. (
|
||||
{{ domain.serial }} ; serial
|
||||
{{ domain.refresh }} ; refresh
|
||||
{{ domain.retry }} ; retry
|
||||
@ -14,9 +14,9 @@ $TTL {{ domain.ttl }}
|
||||
|
||||
{% for record in records %}
|
||||
{% if record.type in ('NS', 'MX', 'CNAME', 'SRV') -%}
|
||||
{{record.name}}. {{record.ttl or ''}} IN {{record.type}} {{record.priority or ''}} {{record.data}}.
|
||||
{{record.name}} {{record.ttl or ''}} IN {{record.type}} {{record.priority or ''}} {{record.data}}.
|
||||
{%- else -%}
|
||||
{{record.name}}. {{record.ttl or ''}} IN {{record.type}} {{record.priority or ''}} {{record.data}}
|
||||
{{record.name}} {{record.ttl or ''}} IN {{record.type}} {{record.priority or ''}} {{record.data}}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
|
||||
|
@ -1,114 +1,214 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
# Copyright 2012 Managed I.T.
|
||||
#
|
||||
# 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
|
||||
# Author: Kiall Mac Innes <kiall@managedit.ie>
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# NOTE(kiall): Copied from Glance
|
||||
# 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 re
|
||||
import jsonschema
|
||||
from moniker.openstack.common.gettextutils import _
|
||||
import ipaddr
|
||||
import iso8601
|
||||
from moniker.openstack.common import log as logging
|
||||
from moniker import exceptions
|
||||
from moniker import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# TODO: We shouldn't hard code this list..
|
||||
resolver = jsonschema.RefResolver(store={
|
||||
'/schemas/domain': utils.load_schema('v1', 'domain'),
|
||||
'/schemas/domains': utils.load_schema('v1', 'domains'),
|
||||
'/schemas/record': utils.load_schema('v1', 'record'),
|
||||
'/schemas/records': utils.load_schema('v1', 'records'),
|
||||
'/schemas/server': utils.load_schema('v1', 'server'),
|
||||
'/schemas/servers': utils.load_schema('v1', 'servers'),
|
||||
})
|
||||
|
||||
|
||||
class SchemaValidator(jsonschema.Draft3Validator):
|
||||
def validate_format(self, format, instance, schema):
|
||||
if format == "date-time":
|
||||
# ISO 8601 format
|
||||
if self.is_type(instance, "string"):
|
||||
try:
|
||||
iso8601.parse_date(instance)
|
||||
except:
|
||||
msg = "%s is not an ISO 8601 date" % (instance)
|
||||
yield jsonschema.ValidationError(msg)
|
||||
elif format == "date":
|
||||
# YYYY-MM-DD
|
||||
if self.is_type(instance, "string"):
|
||||
# TODO: I'm sure there is a more accurate regex than this..
|
||||
pattern = ('^[0-9]{4}-(((0[13578]|(10|12))-'
|
||||
'(0[1-9]|[1-2][0-9]|3[0-1]))|'
|
||||
'(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-'
|
||||
'(0[1-9]|[1-2][0-9]|30)))$')
|
||||
|
||||
if not re.match(pattern, instance):
|
||||
msg = "%s is not a date" % (instance)
|
||||
yield jsonschema.ValidationError(msg)
|
||||
elif format == "time":
|
||||
# hh:mm:ss
|
||||
if self.is_type(instance, "string"):
|
||||
# TODO: I'm sure there is a more accurate regex than this..
|
||||
pattern = "^(?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)$"
|
||||
if not re.match(pattern, instance):
|
||||
msg = "%s is not a time" % (instance)
|
||||
yield jsonschema.ValidationError(msg)
|
||||
pass
|
||||
elif format == "email":
|
||||
# A valid email address
|
||||
pass
|
||||
elif format == "ip-address":
|
||||
# IPv4 Address
|
||||
if self.is_type(instance, "string"):
|
||||
try:
|
||||
ipaddr.IPv4Address(instance)
|
||||
except ipaddr.AddressValueError:
|
||||
msg = "%s is not an IPv4 address" % (instance)
|
||||
yield jsonschema.ValidationError(msg)
|
||||
elif format == "ipv6":
|
||||
# IPv6 Address
|
||||
if self.is_type(instance, "string"):
|
||||
try:
|
||||
ipaddr.IPv6Address(instance)
|
||||
except ipaddr.AddressValueError:
|
||||
msg = "%s is not an IPv6 address" % (instance)
|
||||
yield jsonschema.ValidationError(msg)
|
||||
elif format == "host-name":
|
||||
# A valid hostname
|
||||
if self.is_type(instance, "string"):
|
||||
# TODO: I'm sure there is a more accurate regex than this..
|
||||
pattern = ('^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*'
|
||||
'([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])\.$')
|
||||
|
||||
if not re.match(pattern, instance):
|
||||
msg = "%s is not a host name" % (instance)
|
||||
yield jsonschema.ValidationError(msg)
|
||||
|
||||
def validate_anyOf(self, schemas, instance, schema):
|
||||
for s in schemas:
|
||||
if self.is_valid(instance, s):
|
||||
return
|
||||
else:
|
||||
yield jsonschema.ValidationError(
|
||||
"%r is not valid for any of listed schemas %r" %
|
||||
(instance, schemas)
|
||||
)
|
||||
|
||||
def validate_allOf(self, schemas, instance, schema):
|
||||
for s in schemas:
|
||||
if not self.is_valid(instance, s):
|
||||
yield jsonschema.ValidationError(
|
||||
"%r is not valid against %r" % (instance, s)
|
||||
)
|
||||
|
||||
def validate_oneOf(self, schemas, instance, schema):
|
||||
match = False
|
||||
for s in schemas:
|
||||
if self.is_valid(instance, s):
|
||||
if match:
|
||||
yield jsonschema.ValidationError(
|
||||
"%r matches more than one schema in %r" %
|
||||
(instance, schemas)
|
||||
)
|
||||
match = True
|
||||
if not match:
|
||||
yield jsonschema.ValidationError(
|
||||
"%r is not valid for any of listed schemas %r" %
|
||||
(instance, schemas)
|
||||
)
|
||||
|
||||
|
||||
class Schema(object):
|
||||
def __init__(self, name, properties=None, links=None):
|
||||
self.name = name
|
||||
if properties is None:
|
||||
properties = {}
|
||||
self.properties = properties
|
||||
self.links = links
|
||||
def __init__(self, version, name):
|
||||
self.raw_schema = utils.load_schema(version, name)
|
||||
self.validator = SchemaValidator(self.raw_schema, resolver=resolver)
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return self.validator.schema
|
||||
|
||||
@property
|
||||
def properties(self):
|
||||
return self.schema['properties']
|
||||
|
||||
@property
|
||||
def resolver(self):
|
||||
return self.validator.resolver
|
||||
|
||||
@property
|
||||
def links(self):
|
||||
return self.schema['links']
|
||||
|
||||
@property
|
||||
def raw(self):
|
||||
return self.raw_schema
|
||||
|
||||
def validate(self, obj):
|
||||
try:
|
||||
jsonschema.validate(obj, self.raw())
|
||||
except jsonschema.ValidationError as e:
|
||||
raise exceptions.InvalidObject("Provided object does not match "
|
||||
"schema '%s': %s"
|
||||
% (self.name, str(e)))
|
||||
errors = []
|
||||
|
||||
for error in self.validator.iter_errors(obj):
|
||||
errors.append({
|
||||
'path': ".".join(reversed(error.path)),
|
||||
'message': error.message,
|
||||
'validator': error.validator
|
||||
})
|
||||
|
||||
if len(errors) > 0:
|
||||
raise exceptions.InvalidObject("Provided object does not match "
|
||||
"schema", errors=errors)
|
||||
|
||||
def filter(self, instance, properties=None):
|
||||
if not properties:
|
||||
properties = self.properties
|
||||
|
||||
def filter(self, obj):
|
||||
filtered = {}
|
||||
for key, value in obj.iteritems():
|
||||
if self._filter_func(self.properties, key) and value is not None:
|
||||
filtered[key] = value
|
||||
|
||||
for name, subschema in properties.items():
|
||||
if 'type' in subschema and subschema['type'] == 'array':
|
||||
subinstance = instance.get(name, None)
|
||||
filtered[name] = self._filter_array(subinstance, subschema)
|
||||
elif 'type' in subschema and subschema['type'] == 'object':
|
||||
subinstance = instance.get(name, None)
|
||||
properties = subschema['properties']
|
||||
filtered[name] = self.filter(subinstance, properties)
|
||||
else:
|
||||
filtered[name] = instance.get(name, None)
|
||||
|
||||
return filtered
|
||||
|
||||
@staticmethod
|
||||
def _filter_func(properties, key):
|
||||
return key in properties
|
||||
def _filter_array(self, instance, schema):
|
||||
if 'items' in schema and isinstance(schema['items'], list):
|
||||
# TODO: We currently don't make use of this..
|
||||
raise NotImplementedError()
|
||||
|
||||
def merge_properties(self, properties):
|
||||
# Ensure custom props aren't attempting to override base props
|
||||
original_keys = set(self.properties.keys())
|
||||
new_keys = set(properties.keys())
|
||||
intersecting_keys = original_keys.intersection(new_keys)
|
||||
conflicting_keys = [k for k in intersecting_keys
|
||||
if self.properties[k] != properties[k]]
|
||||
if len(conflicting_keys) > 0:
|
||||
props = ', '.join(conflicting_keys)
|
||||
reason = _("custom properties (%(props)s) conflict "
|
||||
"with base properties")
|
||||
raise exceptions.SchemaLoadError(reason=reason % {'props': props})
|
||||
elif 'items' in schema:
|
||||
schema = schema['items']
|
||||
|
||||
self.properties.update(properties)
|
||||
if '$ref' in schema:
|
||||
schema = self.resolver.resolve(self.schema, schema['$ref'])
|
||||
|
||||
def raw(self):
|
||||
raw = {
|
||||
'name': self.name,
|
||||
'properties': self.properties,
|
||||
'additionalProperties': False,
|
||||
}
|
||||
if self.links:
|
||||
raw['links'] = self.links
|
||||
return raw
|
||||
properties = schema['properties']
|
||||
|
||||
return [self.filter(i, properties) for i in instance]
|
||||
|
||||
class PermissiveSchema(Schema):
|
||||
@staticmethod
|
||||
def _filter_func(properties, key):
|
||||
return True
|
||||
elif 'properties' in schema:
|
||||
schema = schema['properties']
|
||||
|
||||
def raw(self):
|
||||
raw = super(PermissiveSchema, self).raw()
|
||||
raw['additionalProperties'] = {'type': 'string'}
|
||||
return raw
|
||||
if '$ref' in schema:
|
||||
schema = self.resolver.resolve(self.schema, schema['$ref'])
|
||||
|
||||
return [self.filter(i, schema) for i in instance]
|
||||
|
||||
class CollectionSchema(object):
|
||||
def __init__(self, name, item_schema):
|
||||
self.name = name
|
||||
self.item_schema = item_schema
|
||||
|
||||
def filter(self, obj):
|
||||
if not obj:
|
||||
return []
|
||||
|
||||
return [self.item_schema.filter(o) for o in obj]
|
||||
|
||||
def raw(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'properties': {
|
||||
self.name: {
|
||||
'type': 'array',
|
||||
'items': self.item_schema.raw(),
|
||||
},
|
||||
'first': {'type': 'string'},
|
||||
'next': {'type': 'string'},
|
||||
'schema': {'type': 'string'},
|
||||
},
|
||||
'links': [
|
||||
{'rel': 'first', 'href': '{first}'},
|
||||
{'rel': 'next', 'href': '{next}'},
|
||||
{'rel': 'describedby', 'href': '{schema}'},
|
||||
],
|
||||
}
|
||||
else:
|
||||
raise NotImplementedError('Can\'t filter unknown array type')
|
||||
|
@ -17,7 +17,6 @@ from sqlalchemy.types import TypeDecorator, CHAR, VARCHAR
|
||||
from sqlalchemy.dialects.postgresql import UUID as pgUUID
|
||||
from sqlalchemy.dialects.postgresql import INET as pgINET
|
||||
import uuid
|
||||
import ipaddr
|
||||
|
||||
|
||||
class UUID(TypeDecorator):
|
||||
|
24
moniker/tests/test_schema.py
Normal file
24
moniker/tests/test_schema.py
Normal file
@ -0,0 +1,24 @@
|
||||
# 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 moniker.tests import TestCase
|
||||
from moniker import schema
|
||||
|
||||
|
||||
class TestSchema(TestCase):
|
||||
def test_constructor(self):
|
||||
domain = schema.Schema('v1', 'domain')
|
||||
|
||||
self.assertIsInstance(domain, schema.Schema)
|
@ -35,6 +35,15 @@ class TestUtils(TestCase):
|
||||
with self.assertRaises(exceptions.ResourceNotFound):
|
||||
utils.resource_string(name)
|
||||
|
||||
def test_load_schema(self):
|
||||
schema = utils.load_schema('v1', 'domain')
|
||||
|
||||
self.assertIsInstance(schema, dict)
|
||||
|
||||
def test_load_schema_missing(self):
|
||||
with self.assertRaises(exceptions.ResourceNotFound):
|
||||
utils.load_schema('v1', 'missing')
|
||||
|
||||
def test_load_template(self):
|
||||
name = 'bind9-config.jinja2'
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
# under the License.
|
||||
import os
|
||||
import pkg_resources
|
||||
import json
|
||||
from jinja2 import Template
|
||||
from moniker.openstack.common import log as logging
|
||||
from moniker.openstack.common import cfg
|
||||
@ -79,6 +80,12 @@ def resource_string(*args):
|
||||
return pkg_resources.resource_string('moniker', resource_path)
|
||||
|
||||
|
||||
def load_schema(version, name):
|
||||
schema_string = resource_string('schemas', version, '%s.json' % name)
|
||||
|
||||
return json.loads(schema_string)
|
||||
|
||||
|
||||
def load_template(template_name):
|
||||
template_string = resource_string('templates', template_name)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
Flask>=0.8
|
||||
eventlet
|
||||
jsonschema>=0.6
|
||||
https://github.com/Julian/jsonschema/archive/870869b4e6b210fac66823bd2c1ca103dc9eb9d9.tar.gz#egg=jsonschema
|
||||
ipaddr
|
||||
Paste
|
||||
PasteDeploy
|
||||
|
Loading…
Reference in New Issue
Block a user