Merge "Add DomainMaster Object to designate objects"

This commit is contained in:
Jenkins 2015-08-22 16:21:58 +00:00 committed by Gerrit Code Review
commit ca8f9474a0
14 changed files with 371 additions and 87 deletions

View File

@ -40,6 +40,18 @@ class RelationNotLoaded(Base):
error_code = 500
error_type = 'relation_not_loaded'
def __init__(self, *args, **kwargs):
self.relation = kwargs.pop('relation', None)
super(RelationNotLoaded, self).__init__(*args, **kwargs)
self.error_message = "%(relation)s is not loaded on %(object)s" % \
{"relation": self.relation, "object": self.object.obj_name()}
def __str__(self):
return self.error_message
class AdapterNotFound(Base):
error_code = 500
@ -318,6 +330,10 @@ class DomainNotFound(NotFound):
error_type = 'domain_not_found'
class DomainMasterNotFound(NotFound):
error_type = 'domain_master_not_found'
class DomainAttributeNotFound(NotFound):
error_type = 'domain_attribute_not_found'

View File

@ -31,7 +31,7 @@ class XFRMixin(object):
"""
def domain_sync(self, context, domain, servers=None):
servers = servers or domain.masters
servers = dnsutils.expand_servers(servers)
servers = servers.to_list()
timeout = cfg.CONF["service:mdns"].xfr_timeout
try:

View File

@ -21,6 +21,7 @@ from designate.objects.base import PagedListObjectMixin # noqa
from designate.objects.blacklist import Blacklist, BlacklistList # noqa
from designate.objects.domain import Domain, DomainList # noqa
from designate.objects.domain_attribute import DomainAttribute, DomainAttributeList # noqa
from designate.objects.domain_master import DomainMaster, DomainMasterList # noqa
from designate.objects.floating_ip import FloatingIP, FloatingIPList # noqa
from designate.objects.pool_manager_status import PoolManagerStatus, PoolManagerStatusList # noqa
from designate.objects.pool import Pool, PoolList # noqa

View File

@ -16,6 +16,7 @@ from designate.objects.adapters.base import DesignateAdapter # noqa
# API v2
from designate.objects.adapters.api_v2.blacklist import BlacklistAPIv2Adapter, BlacklistListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.domain import DomainAPIv2Adapter, DomainListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.domain_master import DomainMasterAPIv2Adapter, DomainMasterListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.floating_ip import FloatingIPAPIv2Adapter, FloatingIPListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.record import RecordAPIv2Adapter, RecordListAPIv2Adapter # noqa
from designate.objects.adapters.api_v2.recordset import RecordSetAPIv2Adapter, RecordSetListAPIv2Adapter # noqa

View File

@ -15,7 +15,6 @@ from oslo_log import log as logging
from designate.objects.adapters.api_v2 import base
from designate import objects
from designate import exceptions
LOG = logging.getLogger(__name__)
@ -64,26 +63,16 @@ class DomainAPIv2Adapter(base.APIv2Adapter):
@classmethod
def _parse_object(cls, values, object, *args, **kwargs):
# TODO(Graham): Remove this when
# https://bugs.launchpad.net/designate/+bug/1432842 is fixed
if 'masters' in values:
if isinstance(values['masters'], list):
object.set_masters(values.get('masters'))
del values['masters']
else:
errors = objects.ValidationErrorList()
e = objects.ValidationError()
e.path = ['masters']
e.validator = 'type'
e.validator_value = ["list"]
e.message = ("'%(data)s' is not a valid list of masters"
% {'data': values['masters']})
# Add it to the list for later
errors.append(e)
raise exceptions.InvalidObject(
"Provided object does not match "
"schema", errors=errors, object=cls.ADAPTER_OBJECT())
object.masters = objects.adapters.DesignateAdapter.parse(
cls.ADAPTER_FORMAT,
values['masters'],
objects.DomainMasterList(),
*args, **kwargs)
del values['masters']
return super(DomainAPIv2Adapter, cls)._parse_object(
values, object, *args, **kwargs)

View File

@ -0,0 +1,92 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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 oslo_log import log as logging
from designate.objects.adapters.api_v2 import base
from designate import objects
from designate import utils
LOG = logging.getLogger(__name__)
class DomainMasterAPIv2Adapter(base.APIv2Adapter):
ADAPTER_OBJECT = objects.DomainMaster
MODIFICATIONS = {
'fields': {
'value': {
'read_only': False
}
},
'options': {
'links': False,
'resource_name': 'domain_master',
'collection_name': 'domain_masters',
}
}
@classmethod
def _render_object(cls, object, *arg, **kwargs):
if object.port is 53:
return object.host
else:
return "%(host)s:%(port)d" % object.to_dict()
@classmethod
def _parse_object(cls, value, object, *args, **kwargs):
object.host, object.port = utils.split_host_port(value)
return object
class DomainMasterListAPIv2Adapter(base.APIv2Adapter):
ADAPTER_OBJECT = objects.DomainMasterList
MODIFICATIONS = {
'options': {
'links': False,
'resource_name': 'domain_master',
'collection_name': 'domain_masters',
}
}
@classmethod
def _render_list(cls, list_object, *args, **kwargs):
r_list = []
for object in list_object:
r_list.append(cls.get_object_adapter(
cls.ADAPTER_FORMAT,
object).render(cls.ADAPTER_FORMAT, object, *args, **kwargs))
return r_list
@classmethod
def _parse_list(cls, values, output_object, *args, **kwargs):
for value in values:
# Add the object to the list
output_object.append(
# Get the right Adapter
cls.get_object_adapter(
cls.ADAPTER_FORMAT,
# This gets the internal type of the list, and parses it
# We need to do `get_object_adapter` as we need a new
# instance of the Adapter
output_object.LIST_ITEM_TYPE()).parse(
value, output_object.LIST_ITEM_TYPE()))
# Return the filled list
return output_object

View File

@ -154,7 +154,7 @@ class DesignateObject(object):
def _obj_check_relation(self, name):
if name in self.FIELDS and self.FIELDS[name].get('relation', False):
if not self.obj_attr_is_set(name):
raise exceptions.RelationNotLoaded
raise exceptions.RelationNotLoaded(object=self, relation=name)
@classmethod
def obj_cls_from_name(cls, name):
@ -310,9 +310,21 @@ class DesignateObject(object):
ValidationErrorList = self.obj_cls_from_name('ValidationErrorList')
ValidationError = self.obj_cls_from_name('ValidationError')
values = self.to_dict()
errors = ValidationErrorList()
try:
values = self.to_dict()
except exceptions.RelationNotLoaded as e:
e = ValidationError()
e.path = ['type']
e.validator = 'required'
e.validator_value = [e.relation]
e.message = "'%s' is a required property" % e.relation
errors.append(e)
raise exceptions.InvalidObject(
"Provided object does not match "
"schema", errors=errors, object=self)
LOG.debug("Validating '%(name)s' object with values: %(values)r", {
'name': self.obj_name(),
'values': values,

View File

@ -17,8 +17,6 @@ from designate import exceptions
from designate.objects import base
from designate.objects.validation_error import ValidationError
from designate.objects.validation_error import ValidationErrorList
from designate.objects.domain_attribute import DomainAttribute
from designate.objects.domain_attribute import DomainAttributeList
class Domain(base.DictObjectMixin, base.SoftDeleteObjectMixin,
@ -145,6 +143,10 @@ class Domain(base.DictObjectMixin, base.SoftDeleteObjectMixin,
'relation': True,
'relation_cls': 'DomainAttributeList'
},
'masters': {
'relation': True,
'relation_cls': 'DomainMasterList'
},
'type': {
'schema': {
'type': 'string',
@ -165,47 +167,43 @@ class Domain(base.DictObjectMixin, base.SoftDeleteObjectMixin,
'id', 'type', 'name', 'pool_id', 'serial', 'action', 'status'
]
@property
def masters(self):
if self.obj_attr_is_set('attributes'):
return [i.value for i in self.attributes if i.key == 'master']
else:
return None
# TODO(ekarlso): Make this a property sette rpr Kiall's comments later.
def set_masters(self, masters):
attributes = DomainAttributeList()
for m in masters:
obj = DomainAttribute(key='master', value=m)
attributes.append(obj)
self.attributes = attributes
def get_master_by_ip(self, host):
"""
Utility to get the master by it's ip for this domain.
"""
for srv in self.masters:
srv_host, _ = utils.split_host_port(srv)
srv_host, _ = utils.split_host_port(srv.to_data())
if host == srv_host:
return srv
return False
def validate(self):
if self.type == 'SECONDARY' and self.masters is None:
try:
if self.type == 'SECONDARY' and self.masters is None:
errors = ValidationErrorList()
e = ValidationError()
e.path = ['type']
e.validator = 'required'
e.validator_value = ['masters']
e.message = "'masters' is a required property"
errors.append(e)
raise exceptions.InvalidObject(
"Provided object does not match "
"schema", errors=errors, object=self)
super(Domain, self).validate()
except exceptions.RelationNotLoaded as ex:
errors = ValidationErrorList()
e = ValidationError()
e.path = ['type']
e.validator = 'required'
e.validator_value = ['masters']
e.message = "'masters' is a required property"
e.validator_value = [ex.relation]
e.message = "'%s' is a required property" % ex.relation
errors.append(e)
raise exceptions.InvalidObject(
"Provided object does not match "
"schema", errors=errors, object=self)
super(Domain, self).validate()
class DomainList(base.ListObjectMixin, base.DesignateObject,
base.PagedListObjectMixin):

View File

@ -0,0 +1,57 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Endre Karlson <endre.karlson@hp.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from designate.objects import base
from designate import utils
class DomainMaster(base.DictObjectMixin, base.PersistentObjectMixin,
base.DesignateObject):
FIELDS = {
'domain_id': {},
'host': {
'schema': {
'type': 'string',
'format': 'ip-or-host',
'required': True,
},
},
'port': {
'schema': {
'type': 'integer',
'minimum': 1,
'maximum': 65535,
'required': True,
},
}
}
def to_data(self):
return "%(host)s:%(port)d" % self.to_dict()
@classmethod
def from_data(cls, data):
host, port = utils.split_host_port(data)
return cls.from_dict({"host": host, "port": port})
class DomainMasterList(base.ListObjectMixin, base.DesignateObject):
LIST_ITEM_TYPE = DomainMaster
def to_data(self):
rlist = []
for item in self.objects:
rlist.append(item.to_data())
return rlist

View File

@ -102,6 +102,20 @@ def is_hostname(instance):
return True
@draft3_format_checker.checks("ip-or-host")
@draft4_format_checker.checks("ip-or-host")
def is_ip_or_host(instance):
if not isinstance(instance, compat.str_types):
return True
if not re.match(RE_DOMAINNAME, instance)\
and not is_ipv4(instance)\
and not is_ipv6(instance):
return False
return True
@draft3_format_checker.checks("domain-name")
@draft4_format_checker.checks("domainname")
def is_domainname(instance):

View File

@ -232,9 +232,17 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
def _load_relations(domain):
if domain.type == 'SECONDARY':
domain.attributes = self._find_domain_attributes(
domain.masters = self._find_domain_masters(
context, {'domain_id': domain.id})
domain.obj_reset_changes(['attributes'])
else:
# This avoids an extra DB call per primary zone. This will
# always have 0 results for a PRIMARY zone.
domain.masters = objects.DomainMasterList()
domain.attributes = self._find_domain_masters(
context, {'domain_id': domain.id})
domain.obj_reset_changes(['masters', 'attributes'])
if one:
_load_relations(domains)
@ -252,7 +260,7 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
# Don't handle recordsets for now
domain = self._create(
tables.domains, domain, exceptions.DuplicateDomain,
['attributes', 'recordsets'],
['attributes', 'recordsets', 'masters'],
extra_values=extra_values)
if domain.obj_attr_is_set('attributes'):
@ -260,7 +268,12 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
self.create_domain_attribute(context, domain.id, attrib)
else:
domain.attributes = objects.DomainAttributeList()
domain.obj_reset_changes('attributes')
if domain.obj_attr_is_set('masters'):
for master in domain.masters:
self.create_domain_master(context, domain.id, master)
else:
domain.masters = objects.DomainMasterList()
domain.obj_reset_changes(['masters', 'attributes'])
return domain
@ -288,7 +301,7 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
updated_domain = self._update(
context, tables.domains, domain, exceptions.DuplicateDomain,
exceptions.DomainNotFound,
['attributes', 'recordsets'])
['attributes', 'recordsets', 'masters'])
if domain.obj_attr_is_set('attributes'):
# Gather the Attribute ID's we have
@ -330,6 +343,46 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
attr.domain_id = domain.id
self.create_domain_attribute(context, domain.id, attr)
if domain.obj_attr_is_set('masters'):
# Gather the Attribute ID's we have
have = set([r.id for r in self._find_domain_masters(
context, {'domain_id': domain.id})])
# Prep some lists of changes
keep = set([])
create = []
update = []
# Determine what to change
for i in domain.masters:
keep.add(i.id)
try:
i.obj_get_original_value('id')
except KeyError:
create.append(i)
else:
update.append(i)
# NOTE: Since we're dealing with mutable objects, the return value
# of create/update/delete attribute is not needed.
# The original item will be mutated in place on the input
# "domain.attributes" list.
# Delete Attributes
for i_id in have - keep:
attr = self._find_domain_masters(
context, {'id': i_id}, one=True)
self.delete_domain_master(context, attr.id)
# Update Attributes
for i in update:
self.update_domain_master(context, i)
# Create Attributes
for attr in create:
attr.domain_id = domain.id
self.create_domain_master(context, domain.id, attr)
if domain.obj_attr_is_set('recordsets'):
existing = self.find_recordsets(context, {'domain_id': domain.id})
@ -431,6 +484,71 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
return deleted_domain_attribute
# Domain master methods
def _find_domain_masters(self, context, criterion, one=False,
marker=None, limit=None, sort_key=None,
sort_dir=None):
criterion['key'] = 'master'
attribs = self._find(context, tables.domain_attributes,
objects.DomainAttribute,
objects.DomainAttributeList,
exceptions.DomainMasterNotFound,
criterion, one,
marker, limit, sort_key, sort_dir)
masters = objects.DomainMasterList()
for attrib in attribs:
masters.append(objects.DomainMaster().from_data(attrib.value))
return masters
def create_domain_master(self, context, domain_id, domain_master):
domain_attribute = objects.DomainAttribute()
domain_attribute.domain_id = domain_id
domain_attribute.key = 'master'
domain_attribute.value = domain_master.to_data()
return self._create(tables.domain_attributes, domain_attribute,
exceptions.DuplicateDomainAttribute)
def get_domain_masters(self, context, domain_attribute_id):
return self._find_domain_masters(
context, {'id': domain_attribute_id}, one=True)
def find_domain_masters(self, context, criterion=None, marker=None,
limit=None, sort_key=None, sort_dir=None):
return self._find_domain_masters(context, criterion, marker=marker,
limit=limit, sort_key=sort_key,
sort_dir=sort_dir)
def find_domain_master(self, context, criterion):
return self._find_domain_master(context, criterion, one=True)
def update_domain_master(self, context, domain_master):
domain_attribute = objects.DomainAttribute()
domain_attribute.domain_id = domain_master.domain_id
domain_attribute.key = 'master'
domain_attribute.value = domain_master.to_data()
return self._update(context, tables.domain_attributes,
domain_attribute,
exceptions.DuplicateDomainAttribute,
exceptions.DomainAttributeNotFound)
def delete_domain_master(self, context, domain_master_id):
domain_attribute = self._find_domain_attributes(
context, {'id': domain_master_id}, one=True)
deleted_domain_attribute = self._delete(
context, tables.domain_attributes, domain_attribute,
exceptions.DomainAttributeNotFound)
return deleted_domain_attribute
# RecordSet Methods
def _find_recordsets(self, context, criterion, one=False, marker=None,
limit=None, sort_key=None, sort_dir=None):

View File

@ -518,7 +518,6 @@ class ApiV2ZonesTest(ApiV2TestCase):
# Create a zone
fixture = self.get_domain_fixture('SECONDARY', 0)
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
fixture['attributes'] = [{"key": "master", "value": "10.0.0.10"}]
# Create a zone
zone = self.create_domain(**fixture)

View File

@ -121,13 +121,16 @@ class MdnsRequestHandlerTest(MdnsTestCase):
self.assertEqual(expected_response, binascii.b2a_hex(response))
def _get_secondary_domain(self, values=None, attributes=None):
def _get_secondary_domain(self, values=None, attributes=None,
masters=None):
attributes = attributes or []
masters = masters or [{"host": "10.0.0.1", "port": 53}]
fixture = self.get_domain_fixture("SECONDARY", values=values)
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
domain = objects.Domain(**fixture)
domain.attributes = objects.DomainAttributeList()
domain.attributes = objects.DomainAttributeList().from_list(attributes)
domain.masters = objects.DomainMasterList().from_list(masters)
return domain
def _get_soa_answer(self, serial):
@ -145,8 +148,6 @@ class MdnsRequestHandlerTest(MdnsTestCase):
master = "10.0.0.1"
domain = self._get_secondary_domain({"serial": 123})
domain.attributes.append(objects.DomainAttribute(
**{"key": "master", "value": master}))
# expected response is an error code NOERROR. The other fields are
# id 50048
@ -176,7 +177,8 @@ class MdnsRequestHandlerTest(MdnsTestCase):
response = next(self.handler(request)).to_wire()
self.mock_tg.add_thread.assert_called_with(
self.handler.domain_sync, self.context, domain, [master])
self.handler.domain_sync, self.context, domain,
[domain.masters[0]])
self.assertEqual(expected_response, binascii.b2a_hex(response))
@mock.patch.object(dns.resolver.Resolver, 'query')
@ -186,8 +188,6 @@ class MdnsRequestHandlerTest(MdnsTestCase):
master = "10.0.0.1"
domain = self._get_secondary_domain({"serial": 123})
domain.attributes.append(objects.DomainAttribute(
**{"key": "master", "value": master}))
# expected response is an error code NOERROR. The other fields are
# id 50048
@ -226,10 +226,8 @@ class MdnsRequestHandlerTest(MdnsTestCase):
# Have a domain with different master then the one where the notify
# comes from causing it to be "ignored" as in not transferred and
# logged
master = "10.0.0.1"
domain = self._get_secondary_domain({"serial": 123})
domain.attributes.append(objects.DomainAttribute(
**{"key": "master", "value": master}))
# expected response is an error code REFUSED. The other fields are
# id 50048

View File

@ -18,7 +18,6 @@ import unittest
from oslo_log import log as logging
from testtools import ExpectedException as raises # with raises(...): ...
import mock
import oslotest.base
from designate import exceptions
@ -42,47 +41,37 @@ class DomainTest(oslotest.base.BaseTestCase):
def test_masters_none(self):
domain = objects.Domain()
self.assertEqual(domain.masters, None)
with raises(exceptions.RelationNotLoaded):
self.assertEqual(domain.masters, None)
def test_masters(self):
domain = objects.Domain(
attributes=objects.DomainAttributeList.from_list([
objects.DomainAttribute(key='master', value='1.0.0.0')
masters=objects.DomainMasterList.from_list([
{'host': '1.0.0.0', 'port': 53}
])
)
self.assertEqual(domain.masters, ['1.0.0.0'])
self.assertEqual(
domain.masters.to_list(), [{'host': '1.0.0.0', 'port': 53}])
def test_masters_2(self):
domain = objects.Domain(
attributes=objects.DomainAttributeList.from_list([
objects.DomainAttribute(key='master', value='1.0.0.0'),
objects.DomainAttribute(key='master', value='2.0.0.0')
masters=objects.DomainMasterList.from_list([
{'host': '1.0.0.0'},
{'host': '2.0.0.0'}
])
)
self.assertEqual(len(domain.masters), 2)
def test_set_masters_none(self):
domain = create_test_domain()
domain.set_masters(('1.0.0.0', '2.0.0.0'))
self.assertEqual(len(domain.attributes), 2)
def test_get_master_by_ip(self):
domain = objects.Domain(
attributes=objects.DomainAttributeList.from_list([
objects.DomainAttribute(key='master', value='1.0.0.0'),
objects.DomainAttribute(key='master', value='2.0.0.0')
masters=objects.DomainMasterList.from_list([
{'host': '1.0.0.0', 'port': 53},
{'host': '2.0.0.0', 'port': 53}
])
)
m = domain.get_master_by_ip('2.0.0.0').to_data()
def mock_split(v):
assert ':' not in v
return v, ''
with mock.patch('designate.objects.domain.utils.split_host_port',
side_effect=mock_split):
m = domain.get_master_by_ip('2.0.0.0')
self.assertEqual(m, '2.0.0.0')
self.assertEqual(m, '2.0.0.0:53')
@unittest.expectedFailure # bug: domain.masters is not iterable
def test_get_master_by_ip_none(self):