designate/designate/objects/recordset.py

238 lines
8.2 KiB
Python

# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from copy import deepcopy
from designate import exceptions
from designate import utils
from designate.objects import base
from designate.objects.validation_error import ValidationError
from designate.objects.validation_error import ValidationErrorList
LOG = logging.getLogger(__name__)
class RecordSet(base.DictObjectMixin, base.PersistentObjectMixin,
base.DesignateObject):
@property
def action(self):
# Return action as UPDATE if present. CREATE and DELETE are returned
# if they are the only ones.
action = 'NONE'
actions = {'CREATE': 0, 'DELETE': 0, 'UPDATE': 0, 'NONE': 0}
for record in self.records:
actions[record.action] += 1
if actions['CREATE'] != 0 and actions['UPDATE'] == 0 and \
actions['DELETE'] == 0 and actions['NONE'] == 0:
action = 'CREATE'
elif actions['DELETE'] != 0 and actions['UPDATE'] == 0 and \
actions['CREATE'] == 0 and actions['NONE'] == 0:
action = 'DELETE'
elif actions['UPDATE'] != 0 or actions['CREATE'] != 0 or \
actions['DELETE'] != 0:
action = 'UPDATE'
return action
@property
def managed(self):
managed = False
for record in self.records:
if record.managed:
return True
return managed
@property
def status(self):
# Return the worst status in order of ERROR, PENDING, ACTIVE
status = 'ACTIVE'
for record in self.records:
if (record.status == 'ERROR') or \
(record.status == 'PENDING' and status != 'ERROR') or \
(status != 'PENDING'):
status = record.status
return status
FIELDS = {
'shard': {
'schema': {
'type': 'integer',
'minimum': 0,
'maximum': 4095
}
},
'tenant_id': {
'schema': {
'type': 'string',
},
'read_only': True
},
'domain_id': {
'schema': {
'type': 'string',
'description': 'Zone identifier',
'format': 'uuid'
},
},
'name': {
'schema': {
'type': 'string',
'description': 'Zone name',
'format': 'domainname',
'maxLength': 255,
},
'immutable': True,
'required': True
},
'type': {
'schema': {
'type': 'string',
'description': 'RecordSet type (TODO: Make types extensible)',
'enum': ['A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS',
'PTR', 'SSHFP', 'SOA']
},
'required': True,
'immutable': True
},
'ttl': {
'schema': {
'type': ['integer', 'null'],
'description': 'Default time to live',
'minimum': 0,
'maximum': 2147483647
},
},
'description': {
'schema': {
'type': ['string', 'null'],
'maxLength': 160
},
},
'records': {
'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):
errors = ValidationErrorList()
# Get the right classes (e.g. A for Recordsets with type: 'A')
try:
record_list_cls = self.obj_cls_from_name('%sList' % self.type)
record_cls = self.obj_cls_from_name(self.type)
except KeyError as e:
e = ValidationError()
e.path = ['recordset', 'type']
e.validator = 'value'
e.validator_value = [self.type]
e.message = ("'%(type)s' is not a supported Record type"
% {'type': self.type})
# Add it to the list for later
errors.append(e)
raise exceptions.InvalidObject(
"Provided object does not match "
"schema", errors=errors, object=self)
# Get any rules that the record type imposes on the record
changes = record_cls.get_recordset_schema_changes()
old_fields = {}
if changes:
LOG.debug("Record %s is overriding the RecordSet schema with: %s" %
(record_cls.obj_name(), changes))
old_fields = deepcopy(self.FIELDS)
self.FIELDS = utils.deep_dict_merge(self.FIELDS, changes)
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:
if len(error.path) > 1 and isinstance(error.path[1], int):
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)
finally:
if old_fields:
self.FIELDS = old_fields
# Send in the traditional Record objects to central / storage
self.records = old_records
class RecordSetList(base.ListObjectMixin, base.DesignateObject,
base.PagedListObjectMixin):
LIST_ITEM_TYPE = RecordSet