From ea4b054ac6d1449846a510c67e6e05f87c61e331 Mon Sep 17 00:00:00 2001 From: Graham Hayes Date: Tue, 21 Jul 2015 15:05:29 +0100 Subject: [PATCH] Add DomainMaster Object to designate objects Change-Id: I60dbe23be0d6f2ea6b6f3f9d7bd26ce2e2f7686f --- designate/exceptions.py | 16 +++ designate/mdns/xfr.py | 2 +- designate/objects/__init__.py | 1 + designate/objects/adapters/__init__.py | 1 + designate/objects/adapters/api_v2/domain.py | 27 ++-- .../objects/adapters/api_v2/domain_master.py | 92 +++++++++++++ designate/objects/base.py | 16 ++- designate/objects/domain.py | 46 +++---- designate/objects/domain_master.py | 57 ++++++++ designate/schema/format.py | 14 ++ designate/storage/impl_sqlalchemy/__init__.py | 128 +++++++++++++++++- .../tests/test_api/test_v2/test_zones.py | 1 - designate/tests/test_mdns/test_handler.py | 18 ++- .../tests/unit/test_objects/test_domain.py | 39 ++---- 14 files changed, 371 insertions(+), 87 deletions(-) create mode 100644 designate/objects/adapters/api_v2/domain_master.py create mode 100644 designate/objects/domain_master.py diff --git a/designate/exceptions.py b/designate/exceptions.py index 98ad1c42..abd07f59 100644 --- a/designate/exceptions.py +++ b/designate/exceptions.py @@ -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' diff --git a/designate/mdns/xfr.py b/designate/mdns/xfr.py index d0445b0f..26bc81cc 100644 --- a/designate/mdns/xfr.py +++ b/designate/mdns/xfr.py @@ -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: diff --git a/designate/objects/__init__.py b/designate/objects/__init__.py index 98f9c123..c55273b0 100644 --- a/designate/objects/__init__.py +++ b/designate/objects/__init__.py @@ -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 diff --git a/designate/objects/adapters/__init__.py b/designate/objects/adapters/__init__.py index c019b977..9994543c 100644 --- a/designate/objects/adapters/__init__.py +++ b/designate/objects/adapters/__init__.py @@ -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 diff --git a/designate/objects/adapters/api_v2/domain.py b/designate/objects/adapters/api_v2/domain.py index 5158ff13..2fafceee 100644 --- a/designate/objects/adapters/api_v2/domain.py +++ b/designate/objects/adapters/api_v2/domain.py @@ -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) diff --git a/designate/objects/adapters/api_v2/domain_master.py b/designate/objects/adapters/api_v2/domain_master.py new file mode 100644 index 00000000..4ef725c1 --- /dev/null +++ b/designate/objects/adapters/api_v2/domain_master.py @@ -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 diff --git a/designate/objects/base.py b/designate/objects/base.py index cb94d296..af0f8f51 100644 --- a/designate/objects/base.py +++ b/designate/objects/base.py @@ -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, diff --git a/designate/objects/domain.py b/designate/objects/domain.py index d813b645..017b05ec 100644 --- a/designate/objects/domain.py +++ b/designate/objects/domain.py @@ -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): diff --git a/designate/objects/domain_master.py b/designate/objects/domain_master.py new file mode 100644 index 00000000..348ee49f --- /dev/null +++ b/designate/objects/domain_master.py @@ -0,0 +1,57 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# Author: Endre Karlson +# +# 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 diff --git a/designate/schema/format.py b/designate/schema/format.py index 7dcddc17..49691aa1 100644 --- a/designate/schema/format.py +++ b/designate/schema/format.py @@ -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): diff --git a/designate/storage/impl_sqlalchemy/__init__.py b/designate/storage/impl_sqlalchemy/__init__.py index ff56323b..9b15d1a4 100644 --- a/designate/storage/impl_sqlalchemy/__init__.py +++ b/designate/storage/impl_sqlalchemy/__init__.py @@ -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): diff --git a/designate/tests/test_api/test_v2/test_zones.py b/designate/tests/test_api/test_v2/test_zones.py index 65ac8eb8..17335652 100644 --- a/designate/tests/test_api/test_v2/test_zones.py +++ b/designate/tests/test_api/test_v2/test_zones.py @@ -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) diff --git a/designate/tests/test_mdns/test_handler.py b/designate/tests/test_mdns/test_handler.py index 0817b3e3..860d0512 100644 --- a/designate/tests/test_mdns/test_handler.py +++ b/designate/tests/test_mdns/test_handler.py @@ -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 diff --git a/designate/tests/unit/test_objects/test_domain.py b/designate/tests/unit/test_objects/test_domain.py index 0d84021d..723201e9 100644 --- a/designate/tests/unit/test_objects/test_domain.py +++ b/designate/tests/unit/test_objects/test_domain.py @@ -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):