Enable Record Data Validation in v2 API

This enables the validation of data against schemas defined in the
individual record objects

* This is a permanent interface, implemented in a temp fashion *

* Current we convert all the Record() objects to objects of the
  right type, and then validate. If validation is successful we restore
  the generic objects and send them to central.

* We override the RecordSet validate() command, and add extra logic
  This allows for the v2 API to function, but recursive validation from
  a Domain object will not work, until another solution is found.

* The way schemas are build up has also changed
** Each schema is built on .validate()
** There is no embedded "obj://<object_name>" references anymore
** The entire schema is added as one blob

Closes-Bug: #1338256
Implements: blueprint validation-cleanup
APIImpact

Change-Id: I8d1d614a9a9c0c1d3faeb0f98778231278f37bc4
This commit is contained in:
Graham Hayes 2015-03-30 19:31:29 +01:00
parent 5eb5cac9ea
commit 707bc639eb
14 changed files with 216 additions and 44 deletions

View File

@ -29,17 +29,6 @@ from designate.objects.pool import Pool, PoolList # noqa
from designate.objects.pool_attribute import PoolAttribute, PoolAttributeList # noqa
from designate.objects.pool_ns_record import PoolNsRecord, PoolNsRecordList # noqa
from designate.objects.quota import Quota, QuotaList # noqa
from designate.objects.rrdata_a import RRData_A # noqa
from designate.objects.rrdata_aaaa import RRData_AAAA # noqa
from designate.objects.rrdata_cname import RRData_CNAME # noqa
from designate.objects.rrdata_mx import RRData_MX # noqa
from designate.objects.rrdata_ns import RRData_NS # noqa
from designate.objects.rrdata_ptr import RRData_PTR # noqa
from designate.objects.rrdata_soa import RRData_SOA # noqa
from designate.objects.rrdata_spf import RRData_SPF # noqa
from designate.objects.rrdata_srv import RRData_SRV # noqa
from designate.objects.rrdata_sshfp import RRData_SSHFP # noqa
from designate.objects.rrdata_txt import RRData_TXT # noqa
from designate.objects.record import Record, RecordList # noqa
from designate.objects.recordset import RecordSet, RecordSetList # noqa
from designate.objects.server import Server, ServerList # noqa
@ -50,3 +39,17 @@ from designate.objects.validation_error import ValidationError # noqa
from designate.objects.validation_error import ValidationErrorList # noqa
from designate.objects.zone_transfer_request import ZoneTransferRequest, ZoneTransferRequestList # noqa
from designate.objects.zone_transfer_accept import ZoneTransferAccept, ZoneTransferAcceptList # noqa
# Record Types
from designate.objects.rrdata_a import A, AList # noqa
from designate.objects.rrdata_aaaa import AAAA, AAAAList # noqa
from designate.objects.rrdata_cname import CNAME, CNAMEList # noqa
from designate.objects.rrdata_mx import MX, MXList # noqa
from designate.objects.rrdata_ns import NS, NSList # noqa
from designate.objects.rrdata_ptr import PTR, PTRList # noqa
from designate.objects.rrdata_soa import SOA, SOAList # noqa
from designate.objects.rrdata_spf import SPF, SPFList # noqa
from designate.objects.rrdata_srv import SRV, SRVList # noqa
from designate.objects.rrdata_sshfp import SSHFP, SSHFPList # noqa
from designate.objects.rrdata_txt import TXT, TXTList # noqa

View File

@ -84,20 +84,18 @@ def _schema_ref_resolver(uri):
return obj.obj_get_schema()
def make_class_validator(cls):
def make_class_validator(obj):
schema = {
'$schema': 'http://json-schema.org/draft-04/hyper-schema',
'title': cls.obj_name(),
'description': 'Designate %s Object' % cls.obj_name(),
'title': obj.obj_name(),
'description': 'Designate %s Object' % obj.obj_name(),
}
if issubclass(cls, ListObjectMixin):
if isinstance(obj, ListObjectMixin):
schema['type'] = 'array',
schema['items'] = {
'$ref': 'obj://%s#/' % cls.LIST_ITEM_TYPE.obj_name()
}
schema['items'] = make_class_validator(obj.LIST_ITEM_TYPE)
else:
schema['type'] = 'object'
@ -105,11 +103,11 @@ def make_class_validator(cls):
schema['required'] = []
schema['properties'] = {}
for name, properties in cls.FIELDS.items():
for name, properties in obj.FIELDS.items():
if properties.get('relation', False):
schema['properties'][name] = {
'$ref': 'obj://%s#/' % properties.get('relation_cls')
}
if obj.obj_attr_is_set(name):
schema['properties'][name] = \
make_class_validator(getattr(obj, name))
else:
schema['properties'][name] = properties.get('schema', {})
@ -119,9 +117,11 @@ def make_class_validator(cls):
resolver = jsonschema.RefResolver.from_schema(
schema, handlers={'obj': _schema_ref_resolver})
cls._obj_validator = validators.Draft4Validator(
obj._obj_validator = validators.Draft4Validator(
schema, resolver=resolver, format_checker=format.draft4_format_checker)
return schema
class DesignateObjectMetaclass(type):
def __init__(cls, names, bases, dict_):
@ -132,7 +132,6 @@ class DesignateObjectMetaclass(type):
return
make_class_properties(cls)
make_class_validator(cls)
# Add a reference to the finished class into the _obj_classes
# dictionary, allowing us to lookup classes by their name later - this
@ -192,11 +191,10 @@ class DesignateObject(object):
for field, value in _dict.items():
if (field in instance.FIELDS and
instance.FIELDS[field].get('relation', False)):
relation_cls_name = instance.FIELDS[field]['relation_cls']
# We're dealing with a relation, we'll want to create the
# correct object type and recurse
relation_cls = cls.obj_cls_from_name(
instance.FIELDS[field]['relation_cls'])
relation_cls = cls.obj_cls_from_name(relation_cls_name)
if isinstance(value, list):
setattr(instance, field, relation_cls.from_list(value))
@ -282,9 +280,15 @@ class DesignateObject(object):
@property
def is_valid(self):
"""Returns True if the Object is valid."""
make_class_validator(self)
return self._obj_validator.is_valid(self.to_dict())
def validate(self):
make_class_validator(self)
# NOTE(kiall): We make use of the Object registry here in order to
# avoid an impossible circular import.
ValidationErrorList = self.obj_cls_from_name('ValidationErrorList')

View File

@ -12,11 +12,17 @@
# 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 copy import deepcopy
from designate import exceptions
from designate.objects import base
from designate.objects.validation_error import ValidationError
from designate.objects.validation_error import ValidationErrorList
class RecordSet(base.DictObjectMixin, base.PersistentObjectMixin,
base.DesignateObject):
@property
def action(self):
# Return action as UPDATE if present. CREATE and DELETE are returned
@ -100,8 +106,84 @@ class RecordSet(base.DictObjectMixin, base.PersistentObjectMixin,
'relation': True,
'relation_cls': 'RecordList'
},
# TODO(graham): implement the polymorphic class relations
# 'records': {
# 'polymorphic': 'type',
# 'relation': True,
# 'relation_cls': lambda type_: '%sList' % type_
# },
}
def validate(self):
# Get the right classes (e.g. A for Recordsets with type: 'A')
record_list_cls = self.obj_cls_from_name('%sList' % self.type)
record_cls = self.obj_cls_from_name(self.type)
errors = ValidationErrorList()
error_indexes = []
# Copy these for safekeeping
old_records = deepcopy(self.records)
# Blank the records for this object with the right list type
self.records = record_list_cls()
i = 0
for record in old_records:
record_obj = record_cls()
try:
record_obj._from_string(record.data)
# The _from_string() method will throw a ValueError if there is not
# enough data blobs
except ValueError as e:
# Something broke in the _from_string() method
# Fake a correct looking ValidationError() object
e = ValidationError()
e.path = ['records', i]
e.validator = 'format'
e.validator_value = [self.type]
e.message = ("'%(data)s' is not a '%(type)s' Record"
% {'data': record.data, 'type': self.type})
# Add it to the list for later
errors.append(e)
error_indexes.append(i)
else:
# Seems to have loaded right - add it to be validated by
# JSONSchema
self.records.append(record_obj)
i += 1
try:
# Run the actual validate code
super(RecordSet, self).validate()
except exceptions.InvalidObject as e:
# Something is wrong according to JSONSchema - append our errors
increment = 0
# This code below is to make sure we have the index for the record
# list correct. JSONSchema may be missing some of the objects due
# to validation above, so this re - inserts them, and makes sure
# the index is right
for error in e.errors:
error.path[1] += increment
while error.path[1] in error_indexes:
increment += 1
error.path[1] += 1
# Add the list from above
e.errors.extend(errors)
# Raise the exception
raise e
else:
# If JSONSchema passes, but we found parsing errors,
# raise an exception
if len(errors) > 0:
raise exceptions.InvalidObject(
"Provided object does not match "
"schema", errors=errors, object=self)
# Send in the traditional Record objects to central / storage
self.records = old_records
class RecordSetList(base.ListObjectMixin, base.DesignateObject,
base.PagedListObjectMixin):

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_A(Record):
class A(Record):
"""
A Resource Record Type
Defined in: RFC1035
@ -39,3 +40,8 @@ class RRData_A(Record):
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 1
class AList(RecordList):
LIST_ITEM_TYPE = A

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_AAAA(Record):
class AAAA(Record):
"""
AAAA Resource Record Type
Defined in: RFC3596
@ -39,3 +40,8 @@ class RRData_AAAA(Record):
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 28
class AAAAList(RecordList):
LIST_ITEM_TYPE = AAAA

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_CNAME(Record):
class CNAME(Record):
"""
CNAME Resource Record Type
Defined in: RFC1035
@ -40,3 +41,8 @@ class RRData_CNAME(Record):
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 5
class CNAMEList(RecordList):
LIST_ITEM_TYPE = CNAME

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_MX(Record):
class MX(Record):
"""
MX Resource Record Type
Defined in: RFC1035
@ -43,8 +44,16 @@ class RRData_MX(Record):
return '%(priority)s %(exchange)s' % self
def _from_string(self, value):
self.priority, self.exchange = value.split(' ')
priority, exchange = value.split(' ')
self.priority = int(priority)
self.exchange = exchange
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 15
class MXList(RecordList):
LIST_ITEM_TYPE = MX

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_NS(Record):
class NS(Record):
"""
NS Resource Record Type
Defined in: RFC1035
@ -40,3 +41,8 @@ class RRData_NS(Record):
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 2
class NSList(RecordList):
LIST_ITEM_TYPE = NS

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_PTR(Record):
class PTR(Record):
"""
PTR Resource Record Type
Defined in: RFC1035
@ -40,3 +41,8 @@ class RRData_PTR(Record):
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 12
class PTRList(RecordList):
LIST_ITEM_TYPE = PTR

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_SOA(Record):
class SOA(Record):
"""
SOA Resource Record Type
Defined in: RFC1035
@ -83,10 +84,21 @@ class RRData_SOA(Record):
return ("%(mname)s %(rname)s %(serial)s %(refresh)s %(retry)s "
"%(expire)s %(minimum)s" % self)
def _from_string(self, value):
self.mname, self.rname, self.serial, self.refresh, self.retry, \
self.expire, self.minimum = value.split(' ')
def _from_string(self, v):
mname, rname, serial, refresh, retry, expire, minimum = v.split(' ')
self.mname = mname
self.rname = rname
self.serial = int(serial)
self.refresh = int(refresh)
self.retry = int(retry)
self.expire = int(expire)
self.minimum = int(minimum)
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 6
class SOAList(RecordList):
LIST_ITEM_TYPE = SOA

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_SPF(Record):
class SPF(Record):
"""
SPF Resource Record Type
Defined in: RFC4408
@ -38,3 +39,8 @@ class RRData_SPF(Record):
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 99
class SPFList(RecordList):
LIST_ITEM_TYPE = SPF

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_SRV(Record):
class SRV(Record):
"""
SRV Resource Record Type
Defined in: RFC2782
@ -59,8 +60,17 @@ class RRData_SRV(Record):
return "%(priority)s %(weight)s %(target)s %(port)s" % self
def _from_string(self, value):
self.priortiy, self.weight, self.port, self.target = value.split(' ')
priortiy, weight, port, target = value.split(' ')
self.priortiy = int(priortiy)
self.weight = int(weight)
self.port = int(port)
self.target = target
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 33
class SRVList(RecordList):
LIST_ITEM_TYPE = SRV

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_SSHFP(Record):
class SSHFP(Record):
"""
SSHFP Resource Record Type
Defined in: RFC4255
@ -50,8 +51,17 @@ class RRData_SSHFP(Record):
return "%(algorithm)s %(fp_type)s %(fingerprint)s" % self
def _from_string(self, value):
self.algorithm, self.fp_type, self.fingerprint = value.split(' ')
algorithm, fp_type, fingerprint = value.split(' ')
self.algorithm = int(algorithm)
self.fp_type = int(fp_type)
self.fingerprint = fingerprint
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 44
class SSHFPList(RecordList):
LIST_ITEM_TYPE = SSHFP

View File

@ -13,9 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects.record import Record
from designate.objects.record import RecordList
class RRData_TXT(Record):
class TXT(Record):
"""
TXT Resource Record Type
Defined in: RFC1035
@ -38,3 +39,8 @@ class RRData_TXT(Record):
# The record type is defined in the RFC. This will be used when the record
# is sent by mini-dns.
RECORD_TYPE = 16
class TXTList(RecordList):
LIST_ITEM_TYPE = TXT