Switch to plain .json schemas and improve Schema handling.

Additionaly - All hostnames now REQUIRE a trailing "."

Change-Id: I1ee0b6e8911e1b6207aace619dd19c014363bfd7
This commit is contained in:
Kiall Mac Innes 2012-12-02 13:07:25 +00:00
parent 52c7dd4fb3
commit 88882e4373
19 changed files with 641 additions and 319 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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):

View File

@ -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

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

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

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

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

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

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

View File

@ -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 %}

View File

@ -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')

View File

@ -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):

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

View File

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

View File

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

View File

@ -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