diff options
23 files changed, 521 insertions, 381 deletions
@@ -155,7 +155,6 @@ voting: false - designate-bind9-scoped-tokens - designate-pdns4 - - designate-grenade-pdns4 - designate-ipv6-only-pdns4 - designate-ipv6-only-bind9 @@ -165,7 +164,6 @@ - designate-bind9 - designate-bind9-scoped-tokens - designate-pdns4 - - designate-grenade-pdns4 - designate-ipv6-only-pdns4 - designate-ipv6-only-bind9 diff --git a/designate/backend/agent.py b/designate/backend/agent.py index 72f6ce97..67f8c80c 100644 --- a/designate/backend/agent.py +++ b/designate/backend/agent.py @@ -24,25 +24,23 @@ Configured in the [service:pool_manager] section """ -import eventlet import dns -import dns.rdataclass -import dns.rdatatype import dns.exception import dns.flags -import dns.rcode import dns.message import dns.opcode +import dns.rcode +import dns.rdataclass +import dns.rdatatype from oslo_config import cfg from oslo_log import log as logging from designate.backend import base +from designate.backend import private_codes +from designate.conf.agent import DEFAULT_AGENT_PORT +from designate import dnsutils from designate import exceptions from designate.mdns import rpcapi as mdns_api -from designate.utils import DEFAULT_AGENT_PORT -import designate.backend.private_codes as pcodes - -dns_query = eventlet.import_patched('dns.query') LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -72,9 +70,9 @@ class AgentPoolBackend(base.Backend): response, retry = self._make_and_send_dns_message( zone.name, self.timeout, - pcodes.CC, - pcodes.CREATE, - pcodes.CLASSCC, + private_codes.CC, + private_codes.CREATE, + private_codes.CLASSCC, self.host, self.port ) @@ -100,9 +98,9 @@ class AgentPoolBackend(base.Backend): response, retry = self._make_and_send_dns_message( zone.name, self.timeout, - pcodes.CC, - pcodes.DELETE, - pcodes.CLASSCC, + private_codes.CC, + private_codes.DELETE, + private_codes.CLASSCC, self.host, self.port ) @@ -134,7 +132,7 @@ class AgentPoolBackend(base.Backend): 'port': dest_port, 'timeout': timeout, 'retry': retry}) response = None - elif isinstance(response, dns_query.BadResponse): + elif isinstance(response, dns.query.BadResponse): LOG.warning("Got BadResponse while trying to send '%(msg)s' for " "'%(zone)s' to '%(server)s:%(port)d'. " "Timeout='%(timeout)d' seconds. Retry='%(retry)d'", @@ -173,14 +171,10 @@ class AgentPoolBackend(base.Backend): def _send_dns_message(self, dns_message, dest_ip, dest_port, timeout): try: - if not CONF['service:mdns'].all_tcp: - response = dns_query.udp( - dns_message, dest_ip, port=dest_port, timeout=timeout) - else: - response = dns_query.tcp( - dns_message, dest_ip, port=dest_port, timeout=timeout) - return response + return dnsutils.send_dns_message( + dns_message, dest_ip, port=dest_port, timeout=timeout + ) except dns.exception.Timeout as timeout: return timeout - except dns_query.BadResponse as badResponse: + except dns.query.BadResponse as badResponse: return badResponse diff --git a/designate/backend/impl_bind9.py b/designate/backend/impl_bind9.py index ab8d06b0..b026e277 100644 --- a/designate/backend/impl_bind9.py +++ b/designate/backend/impl_bind9.py @@ -109,6 +109,28 @@ class Bind9Backend(base.Backend): context, zone, self._host, self._port, self.timeout, self.retry_interval, self.max_retries, self.delay) + def get_zone(self, context, zone): + """Returns True if zone exists and False if not""" + LOG.debug('Get Zone') + + view = 'in %s' % self._view if self._view else '' + + rndc_op = [ + 'showzone', + '%s %s' % (zone['name'].rstrip('.'), view), + ] + try: + self._execute_rndc(rndc_op) + except exceptions.Backend as e: + if "not found" in str(e): + LOG.debug('Zone %s not found on the backend', zone['name']) + return False + else: + LOG.warning('RNDC call failure: %s', e) + raise e + + return True + def delete_zone(self, context, zone): """Delete a new Zone by executin rndc Do not raise exceptions if the zone does not exist. @@ -136,14 +158,21 @@ class Bind9Backend(base.Backend): """ Update a DNS zone. - This will execute a rndc modzone as the zone + This will execute a rndc modzone if the zone already exists but masters might need to be refreshed. + Or, will create the zone if it does not exist. :param context: Security context information. :param zone: the DNS zone. """ LOG.debug('Update Zone') + if not self.get_zone(context, zone): + # If zone does not exist yet, create it + self.create_zone(context, zone) + # Newly created zone won't require an update + return + masters = [] for master in self.masters: host = master['host'] diff --git a/designate/central/service.py b/designate/central/service.py index b9f920aa..cd551a94 100644 --- a/designate/central/service.py +++ b/designate/central/service.py @@ -562,51 +562,55 @@ class Service(service.RPCService): objects.Record(data=r, managed=True) for r in ns_records]) values = { 'name': zone['name'], - 'type': "NS", + 'type': 'NS', 'records': recordlist } ns, zone = self._create_recordset_in_storage( context, zone, objects.RecordSet(**values), - increment_serial=False) + increment_serial=False + ) return ns def _add_ns(self, context, zone, ns_record): # Get NS recordset # If the zone doesn't have an NS recordset yet, create one - recordsets = self.find_recordsets( - context, criterion={'zone_id': zone['id'], 'type': "NS"} - ) - - managed = [] - for rs in recordsets: - if rs.managed: - managed.append(rs) - - if len(managed) == 0: + try: + recordset = self.find_recordset( + context, + criterion={ + 'zone_id': zone['id'], + 'name': zone['name'], + 'type': 'NS' + } + ) + except exceptions.RecordSetNotFound: self._create_ns(context, zone, [ns_record]) return - elif len(managed) != 1: - raise exceptions.RecordSetNotFound("No valid recordset found") - - ns_recordset = managed[0] # Add new record to recordset based on the new nameserver - ns_recordset.records.append( - objects.Record(data=ns_record, managed=True)) + recordset.records.append( + objects.Record(data=ns_record, managed=True) + ) - self._update_recordset_in_storage(context, zone, ns_recordset, + self._update_recordset_in_storage(context, zone, recordset, set_delayed_notify=True) def _delete_ns(self, context, zone, ns_record): - ns_recordset = self.find_recordset( - context, criterion={'zone_id': zone['id'], 'type': "NS"}) + recordset = self.find_recordset( + context, + criterion={ + 'zone_id': zone['id'], + 'name': zone['name'], + 'type': 'NS' + } + ) - for record in copy.deepcopy(ns_recordset.records): + for record in list(recordset.records): if record.data == ns_record: - ns_recordset.records.remove(record) + recordset.records.remove(record) - self._update_recordset_in_storage(context, zone, ns_recordset, + self._update_recordset_in_storage(context, zone, recordset, set_delayed_notify=True) # Quota Enforcement Methods @@ -2541,46 +2545,49 @@ class Service(service.RPCService): @notification('dns.pool.update') @transaction def update_pool(self, context, pool): - policy.check('update_pool', context) # If there is a nameserver, then additional steps need to be done # Since these are treated as mutable objects, we're only going to # be comparing the nameserver.value which is the FQDN - if pool.obj_attr_is_set('ns_records'): - elevated_context = context.elevated(all_tenants=True) + elevated_context = context.elevated(all_tenants=True) - # TODO(kiall): ListObjects should be able to give you their - # original set of values. - original_pool_ns_records = self._get_pool_ns_records(context, - pool.id) - # Find the current NS hostnames - existing_ns = set([n.hostname for n in original_pool_ns_records]) + # TODO(kiall): ListObjects should be able to give you their + # original set of values. + original_pool_ns_records = self._get_pool_ns_records( + context, pool.id + ) - # Find the desired NS hostnames - request_ns = set([n.hostname for n in pool.ns_records]) + updated_pool = self.storage.update_pool(context, pool) - # Get the NS's to be created and deleted, ignoring the ones that - # are in both sets, as those haven't changed. - # TODO(kiall): Factor in priority - create_ns = request_ns.difference(existing_ns) - delete_ns = existing_ns.difference(request_ns) + if not pool.obj_attr_is_set('ns_records'): + return updated_pool - updated_pool = self.storage.update_pool(context, pool) + # Find the current NS hostnames + existing_ns = set([n.hostname for n in original_pool_ns_records]) + + # Find the desired NS hostnames + request_ns = set([n.hostname for n in pool.ns_records]) + + # Get the NS's to be created and deleted, ignoring the ones that + # are in both sets, as those haven't changed. + # TODO(kiall): Factor in priority + create_ns = request_ns.difference(existing_ns) + delete_ns = existing_ns.difference(request_ns) # After the update, handle new ns_records - for ns in create_ns: + for ns_record in create_ns: # Create new NS recordsets for every zone zones = self.find_zones( context=elevated_context, criterion={'pool_id': pool.id, 'action': '!DELETE'}) - for z in zones: - self._add_ns(elevated_context, z, ns) + for zone in zones: + self._add_ns(elevated_context, zone, ns_record) # Then handle the ns_records to delete - for ns in delete_ns: + for ns_record in delete_ns: # Cannot delete the last nameserver, so verify that first. - if len(pool.ns_records) == 0: + if not pool.ns_records: raise exceptions.LastServerDeleteNotAllowed( "Not allowed to delete last of servers" ) @@ -2588,9 +2595,10 @@ class Service(service.RPCService): # Delete the NS record for every zone zones = self.find_zones( context=elevated_context, - criterion={'pool_id': pool.id}) - for z in zones: - self._delete_ns(elevated_context, z, ns) + criterion={'pool_id': pool.id} + ) + for zone in zones: + self._delete_ns(elevated_context, zone, ns_record) return updated_pool diff --git a/designate/dnsutils.py b/designate/dnsutils.py index 6833d542..a3b4d2f9 100644 --- a/designate/dnsutils.py +++ b/designate/dnsutils.py @@ -21,9 +21,10 @@ from threading import Lock import six import dns import dns.exception +import dns.query +import dns.rdatatype import dns.zone import eventlet -from dns import rdatatype from oslo_serialization import base64 from oslo_log import log as logging @@ -313,7 +314,7 @@ def dnspyrecords_to_recordsetlist(dnspython_records): def dnspythonrecord_to_recordset(rname, rdataset): - record_type = rdatatype.to_text(rdataset.rdtype) + record_type = dns.rdatatype.to_text(rdataset.rdtype) name = rname.to_text() if six.PY3 and isinstance(name, bytes): @@ -347,39 +348,122 @@ def do_axfr(zone_name, servers, timeout=None, source=None): timeout = timeout or CONF["service:mdns"].xfr_timeout xfr = None - for srv in servers: - to = eventlet.Timeout(timeout) - log_info = {'name': zone_name, 'host': srv} - try: - LOG.info("Doing AXFR for %(name)s from %(host)s", log_info) - - xfr = dns.query.xfr(srv['host'], zone_name, relativize=False, - timeout=1, port=srv['port'], source=source) - raw_zone = dns.zone.from_xfr(xfr, relativize=False) - break - except eventlet.Timeout as t: - if t == to: - LOG.error("AXFR timed out for %(name)s from %(host)s", - log_info) - continue - except dns.exception.FormError: - LOG.error("Zone %(name)s is not present on %(host)s." - "Trying next server.", log_info) - except socket.error: - LOG.error("Connection error when doing AXFR for %(name)s from " - "%(host)s", log_info) - except Exception: - LOG.exception("Problem doing AXFR %(name)s from %(host)s. " + for address in get_ip_addresses(srv['host']): + to = eventlet.Timeout(timeout) + log_info = {'name': zone_name, 'host': srv, 'address': address} + try: + LOG.info( + 'Doing AXFR for %(name)s from %(host)s %(address)s', + log_info + ) + xfr = dns.query.xfr( + address, zone_name, relativize=False, timeout=1, + port=srv['port'], source=source + ) + raw_zone = dns.zone.from_xfr(xfr, relativize=False) + LOG.debug("AXFR Successful for %s", raw_zone.origin.to_text()) + return raw_zone + except eventlet.Timeout as t: + if t == to: + LOG.error("AXFR timed out for %(name)s from %(host)s", + log_info) + continue + except dns.exception.FormError: + LOG.error("Zone %(name)s is not present on %(host)s." "Trying next server.", log_info) - finally: - to.cancel() - continue - else: - raise exceptions.XFRFailure( - "XFR failed for %(name)s. No servers in %(servers)s was reached." % - {"name": zone_name, "servers": servers}) + except socket.error: + LOG.error("Connection error when doing AXFR for %(name)s from " + "%(host)s", log_info) + except Exception: + LOG.exception("Problem doing AXFR %(name)s from %(host)s. " + "Trying next server.", log_info) + finally: + to.cancel() + + raise exceptions.XFRFailure( + "XFR failed for %(name)s. No servers in %(servers)s was reached." % + {"name": zone_name, "servers": servers} + ) + + +def prepare_msg(zone_name, rdatatype=dns.rdatatype.SOA, + dns_opcode=dns.opcode.QUERY): + """ + Do the needful to set up a dns packet with dnspython + """ + dns_message = dns.message.make_query(zone_name, rdatatype) + dns_message.set_opcode(dns_opcode) + + return dns_message + + +def dig(zone_name, host, rdatatype, port=53): + """ + Set up and send a regular dns query, datatype configurable + """ + query = prepare_msg(zone_name, rdatatype=rdatatype) + + return send_dns_message(query, host, port=port) + + +def notify(zone_name, host, port=53): + """ + Set up a notify packet and send it + """ + msg = prepare_msg(zone_name, dns_opcode=dns.opcode.NOTIFY) + + return send_dns_message(msg, host, port=port) - LOG.debug("AXFR Successful for %s", raw_zone.origin.to_text()) - return raw_zone +def send_dns_message(dns_message, host, port=53, timeout=10): + """ + Send the dns message and return the response + + :return: dns.Message of the response to the dns query + """ + ip_address = get_ip_address(host) + # This can raise some exceptions, but we'll catch them elsewhere + if not CONF['service:mdns'].all_tcp: + return dns.query.udp( + dns_message, ip_address, port=port, timeout=timeout) + return dns.query.tcp( + dns_message, ip_address, port=port, timeout=timeout) + + +def get_serial(zone_name, host, port=53): + """ + Possibly raises dns.exception.Timeout or dns.query.BadResponse. + Possibly returns 0 if, e.g., the answer section is empty. + """ + resp = dig(zone_name, host, dns.rdatatype.SOA, port=port) + if not resp.answer: + return 0 + rdataset = resp.answer[0].to_rdataset() + if not rdataset: + return 0 + return rdataset[0].serial + + +def get_ip_address(ip_address_or_hostname): + """ + Provide an ip or hostname and return a valid ip4 or ipv6 address. + + :return: ip address + """ + addresses = get_ip_addresses(ip_address_or_hostname) + if not addresses: + return None + return addresses[0] + + +def get_ip_addresses(ip_address_or_hostname): + """ + Provide an ip or hostname and return all valid ip4 or ipv6 addresses. + + :return: ip addresses + """ + addresses = [] + for res in socket.getaddrinfo(ip_address_or_hostname, 0): + addresses.append(res[4][0]) + return list(set(addresses)) diff --git a/designate/mdns/notify.py b/designate/mdns/notify.py index 0d66970d..0e464960 100644 --- a/designate/mdns/notify.py +++ b/designate/mdns/notify.py @@ -28,6 +28,7 @@ import dns.opcode from oslo_config import cfg from oslo_log import log as logging +from designate import dnsutils from designate.mdns import base from designate.metrics import metrics @@ -186,8 +187,9 @@ class NotifyEndpoint(base.BaseEndpoint): 'zone': zone.name, 'server': host, 'port': port}) try: - response = self._send_dns_message(dns_message, host, port, - timeout) + response = dnsutils.send_dns_message( + dns_message, host, port, timeout=timeout + ) except socket.error as e: if e.errno != socket.errno.EAGAIN: @@ -285,21 +287,3 @@ class NotifyEndpoint(base.BaseEndpoint): dns_message.flags |= dns.flags.RD return dns_message - - def _send_dns_message(self, dns_message, host, port, timeout): - """ - Send DNS Message over TCP or UDP, return response. - - :param dns_message: The dns message that needs to be sent. - :param host: The destination ip of dns_message. - :param port: The destination port of dns_message. - :param timeout: The timeout in seconds to wait for a response. - :return: response - """ - send = dns_query.tcp if CONF['service:mdns'].all_tcp else dns_query.udp - return send( - dns_message, - socket.gethostbyname(host), - port=port, - timeout=timeout - ) diff --git a/designate/objects/fields.py b/designate/objects/fields.py index 19a52c0d..416f0d4d 100644 --- a/designate/objects/fields.py +++ b/designate/objects/fields.py @@ -100,7 +100,7 @@ class StringFields(ovoo_fields.StringField): RE_NAPTR_SERVICE = r'^([A-Za-z]([A-Za-z0-9]*)(\+[A-Za-z]([A-Za-z0-9]{0,31}))*)?' # noqa RE_NAPTR_REGEXP = r'^([^0-9i\\])(.*)\1((.+)|(\\[1-9]))\1(i?)' RE_KVP = r'^\s[A-Za-z0-9]+=[A-Za-z0-9]+' - RE_URL_MAIL = r'^mailto:[A-Za-z0-9_\-]+@.*' + RE_URL_MAIL = r'^mailto:[A-Za-z0-9_\-]+(\+[A-Za-z0-9_\-]+)?@.*' RE_URL_HTTP = r'^http(s)?://.*/' def __init__(self, nullable=False, read_only=False, @@ -204,9 +204,9 @@ class DomainField(StringFields): if len(host) > 63: raise ValueError("Host %s is too long" % host) if not value.endswith('.'): - raise ValueError("Domain %s is not end with a dot" % value) + raise ValueError("Domain %s does not end with a dot" % value) if not re.match(self.RE_ZONENAME, value): - raise ValueError("Domain %s is not match" % value) + raise ValueError("Domain %s is invalid" % value) return value @@ -220,7 +220,7 @@ class EmailField(StringFields): raise ValueError("%s is not an email" % value) email = value.replace('@', '.') if not re.match(self.RE_ZONENAME, "%s." % email): - raise ValueError("Email %s is not match" % value) + raise ValueError("Email %s is invalid" % value) return value @@ -237,9 +237,9 @@ class HostField(StringFields): if len(host) > 63: raise ValueError("Host %s is too long" % host) if value.endswith('.') is False: - raise ValueError("Host name %s is not end with a dot" % value) + raise ValueError("Host name %s does not end with a dot" % value) if not re.match(self.RE_HOSTNAME, value): - raise ValueError("Host name %s is not match" % value) + raise ValueError("Host name %s is invalid" % value) return value @@ -256,7 +256,7 @@ class SRVField(StringFields): if len(host) > 63: raise ValueError("Host %s is too long" % host) if value.endswith('.') is False: - raise ValueError("Host name %s is not end with a dot" % value) + raise ValueError("Host name %s does not end with a dot" % value) if not re.match(self.RE_SRV_HOST_NAME, value): raise ValueError("Host name %s is not a SRV record" % value) return value @@ -291,7 +291,7 @@ class TldField(StringFields): def coerce(self, obj, attr, value): value = super(TldField, self).coerce(obj, attr, value) if not re.match(self.RE_TLDNAME, value): - raise ValueError("%s is not an TLD" % value) + raise ValueError("%s is not a TLD" % value) return value @@ -319,7 +319,7 @@ class NaptrServiceField(StringFields): raise ValueError("NAPTR record service field cannot be longer than" " 255 characters" % value) if not re.match(self.RE_NAPTR_SERVICE, "%s" % value): - raise ValueError("%s NAPTR record service does not match" % value) + raise ValueError("%s NAPTR record service is invalid" % value) return value @@ -334,7 +334,7 @@ class NaptrRegexpField(StringFields): " 255 characters" % value) if value: if not re.match(self.RE_NAPTR_REGEXP, "%s" % value): - raise ValueError("%s is not a NAPTR record regexp" % value) + raise ValueError("%s NAPTR record is invalid" % value) return value @@ -356,10 +356,11 @@ class CaaPropertyField(StringFields): raise ValueError("Host %s is too long" % host) idn_with_dot = idn + '.' if not re.match(self.RE_ZONENAME, idn_with_dot): - raise ValueError("Domain %s does not match" % idn) + raise ValueError("Domain %s is invalid" % idn) for entry in entries: if not re.match(self.RE_KVP, entry): - raise ValueError("%s is not valid key-value pair" % entry) + raise ValueError("%s is not a valid key-value pair" % + entry) elif tag == 'iodef': if re.match(self.RE_URL_MAIL, val): parts = val.split('@') @@ -370,7 +371,7 @@ class CaaPropertyField(StringFields): raise ValueError("Host %s is too long" % host) idn_with_dot = idn + '.' if not re.match(self.RE_ZONENAME, idn_with_dot): - raise ValueError("Domain %s does not match" % idn) + raise ValueError("Domain %s is invalid" % idn) elif re.match(self.RE_URL_HTTP, val): parts = val.split('/') idn = parts[2] @@ -380,9 +381,9 @@ class CaaPropertyField(StringFields): raise ValueError("Host %s is too long" % host) idn_with_dot = idn + '.' if not re.match(self.RE_ZONENAME, idn_with_dot): - raise ValueError("Domain %s does not match" % idn) + raise ValueError("Domain %s is invalid" % idn) else: - raise ValueError("%s is not valid URL" % val) + raise ValueError("%s is not a valid URL" % val) else: raise ValueError("Property tag %s must be 'issue', 'issuewild'" " or 'iodef'" % value) diff --git a/designate/storage/impl_sqlalchemy/__init__.py b/designate/storage/impl_sqlalchemy/__init__.py index fe46a88b..3a8234e6 100644 --- a/designate/storage/impl_sqlalchemy/__init__.py +++ b/designate/storage/impl_sqlalchemy/__init__.py @@ -1492,6 +1492,8 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage): # all_tenants was not used, we don't know what records to return, # so return an empty list. if not context.project_id: + if one: + return objects.ZoneTransferRequest() return objects.ZoneTransferRequestList() query = query.where(or_( diff --git a/designate/tests/unit/backend/test_agent.py b/designate/tests/unit/backend/test_agent.py index d1e585bf..408544d8 100644 --- a/designate/tests/unit/backend/test_agent.py +++ b/designate/tests/unit/backend/test_agent.py @@ -14,11 +14,13 @@ from unittest import mock import dns +import dns.query import dns.rdataclass import dns.rdatatype import designate.backend.agent as agent import designate.backend.private_codes as pcodes +from designate import dnsutils from designate import exceptions from designate import objects from designate import tests @@ -130,7 +132,7 @@ class AgentBackendTestCase(tests.TestCase): def test_make_and_send_dns_message_bad_response(self): self.backend._make_dns_message = mock.Mock(return_value='') self.backend._send_dns_message = mock.Mock( - return_value=agent.dns_query.BadResponse()) + return_value=dns.query.BadResponse()) out = self.backend._make_and_send_dns_message('h', 123, 1, 2, 3, 4, 5) @@ -176,50 +178,16 @@ class AgentBackendTestCase(tests.TestCase): self.assertEqual((response, 0), out) - @mock.patch.object(agent.dns_query, 'tcp') - @mock.patch.object(agent.dns_query, 'udp') - def test_send_dns_message(self, mock_udp, mock_tcp): + @mock.patch.object(dnsutils, 'get_ip_address') + @mock.patch.object(dns.query, 'tcp') + @mock.patch.object(dns.query, 'udp') + def test_send_dns_message(self, mock_udp, mock_tcp, mock_get_ip_address): mock_udp.return_value = 'mock udp resp' + mock_get_ip_address.return_value = '10.0.1.39' - out = self.backend._send_dns_message('msg', 'host', 123, 1) + out = self.backend._send_dns_message('msg', '10.0.1.39', 123, 1) - self.assertFalse(agent.dns_query.tcp.called) - agent.dns_query.udp.assert_called_with('msg', 'host', port=123, - timeout=1) + self.assertFalse(mock_tcp.called) + mock_udp.assert_called_with('msg', '10.0.1.39', port=123, + timeout=1) self.assertEqual('mock udp resp', out) - - @mock.patch.object(agent.dns_query, 'tcp') - @mock.patch.object(agent.dns_query, 'udp') - def test_send_dns_message_timeout(self, mock_udp, mock_tcp): - mock_udp.side_effect = dns.exception.Timeout - - out = self.backend._send_dns_message('msg', 'host', 123, 1) - - agent.dns_query.udp.assert_called_with('msg', 'host', port=123, - timeout=1) - self.assertIsInstance(out, dns.exception.Timeout) - - @mock.patch.object(agent.dns_query, 'tcp') - @mock.patch.object(agent.dns_query, 'udp') - def test_send_dns_message_bad_response(self, mock_udp, mock_tcp): - mock_udp.side_effect = agent.dns_query.BadResponse - - out = self.backend._send_dns_message('msg', 'host', 123, 1) - - agent.dns_query.udp.assert_called_with('msg', 'host', port=123, - timeout=1) - self.assertIsInstance(out, agent.dns_query.BadResponse) - - @mock.patch.object(agent.dns_query, 'tcp') - @mock.patch.object(agent.dns_query, 'udp') - def test_send_dns_message_tcp(self, mock_udp, mock_tcp): - self.CONF.set_override('all_tcp', True, 'service:mdns') - - mock_tcp.return_value = 'mock tcp resp' - - out = self.backend._send_dns_message('msg', 'host', 123, 1) - - self.assertFalse(agent.dns_query.udp.called) - agent.dns_query.tcp.assert_called_with('msg', 'host', port=123, - timeout=1) - self.assertEqual('mock tcp resp', out) diff --git a/designate/tests/unit/backend/test_bind9.py b/designate/tests/unit/backend/test_bind9.py index c1189e84..272569ae 100644 --- a/designate/tests/unit/backend/test_bind9.py +++ b/designate/tests/unit/backend/test_bind9.py @@ -80,6 +80,15 @@ class Bind9BackendTestCase(designate.tests.TestCase): ) @mock.patch.object(impl_bind9.Bind9Backend, '_execute_rndc') + def test_get_zone(self, mock_execute): + with fixtures.random_seed(0): + self.backend.get_zone(self.admin_context, self.zone) + + mock_execute.assert_called_with( + ['showzone', 'example.com '] + ) + + @mock.patch.object(impl_bind9.Bind9Backend, '_execute_rndc') def test_create_zone_with_view(self, mock_execute): self.target['options'].append( {'key': 'view', 'value': 'guest'}, diff --git a/designate/tests/unit/mdns/test_notify.py b/designate/tests/unit/mdns/test_notify.py index 68b47467..45c82338 100644 --- a/designate/tests/unit/mdns/test_notify.py +++ b/designate/tests/unit/mdns/test_notify.py @@ -20,6 +20,7 @@ import dns import dns.rdataclass import dns.rdatatype +from designate import dnsutils import designate.mdns.notify as notify import designate.tests from designate.tests.unit import RoObject @@ -130,12 +131,11 @@ class MdnsNotifyTest(designate.tests.TestCase): self.assertEqual(('ERROR', 310, 0), out) @mock.patch('time.sleep') - def test_make_and_send_dns_message_timeout(self, mock_sleep): + @mock.patch.object(dnsutils, 'send_dns_message') + def test_make_and_send_dns_message_timeout(self, mock_send_dns_message, + mock_sleep): zone = RoObject(name='zn') - self.notify._make_dns_message = mock.Mock(return_value='') - self.notify._send_dns_message = mock.Mock( - side_effect=dns.exception.Timeout - ) + mock_send_dns_message.side_effect = dns.exception.Timeout out = self.notify._make_and_send_dns_message( zone, 'host', 123, 1, 2, 3 @@ -143,12 +143,12 @@ class MdnsNotifyTest(designate.tests.TestCase): self.assertEqual((None, 3), out) - def test_make_and_send_dns_message_bad_response(self): + @mock.patch.object(dnsutils, 'send_dns_message') + def test_make_and_send_dns_message_bad_response(self, + mock_send_dns_message): zone = RoObject(name='zn') self.notify._make_dns_message = mock.Mock(return_value='') - self.notify._send_dns_message = mock.Mock( - side_effect=notify.dns_query.BadResponse - ) + mock_send_dns_message.side_effect = notify.dns_query.BadResponse out = self.notify._make_and_send_dns_message( zone, 'host', 123, 1, 2, 3 @@ -157,15 +157,14 @@ class MdnsNotifyTest(designate.tests.TestCase): self.assertEqual((None, 1), out) @mock.patch('time.sleep') - def test_make_and_send_dns_message_eagain(self, mock_sleep): + @mock.patch.object(dnsutils, 'send_dns_message') + def test_make_and_send_dns_message_eagain(self, mock_send_dns_message, + mock_sleep): # bug #1558096 zone = RoObject(name='zn') - self.notify._make_dns_message = mock.Mock(return_value='') socket_error = socket.error() socket_error.errno = socket.errno.EAGAIN - self.notify._send_dns_message = mock.Mock( - side_effect=socket_error - ) + mock_send_dns_message.side_effect = socket_error out = self.notify._make_and_send_dns_message( zone, 'host', 123, 1, 2, 3 @@ -173,15 +172,15 @@ class MdnsNotifyTest(designate.tests.TestCase): self.assertEqual((None, 3), out) - def test_make_and_send_dns_message_econnrefused(self): + @mock.patch.object(dnsutils, 'send_dns_message') + def test_make_and_send_dns_message_econnrefused(self, + mock_send_dns_message): # bug #1558096 zone = RoObject(name='zn') - self.notify._make_dns_message = mock.Mock(return_value='') socket_error = socket.error() socket_error.errno = socket.errno.ECONNREFUSED # socket errors other than EAGAIN should raise - self.notify._send_dns_message = mock.Mock( - side_effect=socket_error) + mock_send_dns_message.side_effect = socket_error self.assertRaises( socket.error, @@ -189,11 +188,11 @@ class MdnsNotifyTest(designate.tests.TestCase): zone, 'host', 123, 1, 2, 3 ) - def test_make_and_send_dns_message_nxdomain(self): + @mock.patch.object(dnsutils, 'send_dns_message') + def test_make_and_send_dns_message_nxdomain(self, mock_send_dns_message): zone = RoObject(name='zn') - self.notify._make_dns_message = mock.Mock(return_value='') response = RoObject(rcode=mock.Mock(return_value=dns.rcode.NXDOMAIN)) - self.notify._send_dns_message = mock.Mock(return_value=response) + mock_send_dns_message.return_value = response out = self.notify._make_and_send_dns_message( zone, 'host', 123, 1, 2, 3 @@ -201,17 +200,17 @@ class MdnsNotifyTest(designate.tests.TestCase): self.assertEqual((response, 1), out) - def test_make_and_send_dns_message_missing_AA_flags(self): + @mock.patch.object(dnsutils, 'send_dns_message') + def test_make_and_send_dns_message_missing_AA_flags(self, + mock_send_dns_message): zone = RoObject(name='zn') - self.notify._make_dns_message = mock.Mock(return_value='') - response = RoObject( rcode=mock.Mock(return_value=dns.rcode.NOERROR), # rcode is NOERROR but (flags & dns.flags.AA) gives 0 flags=0, answer=['answer'], ) - self.notify._send_dns_message = mock.Mock(return_value=response) + mock_send_dns_message.return_value = response out = self.notify._make_and_send_dns_message( zone, 'host', 123, 1, 2, 3 @@ -219,9 +218,10 @@ class MdnsNotifyTest(designate.tests.TestCase): self.assertEqual((None, 1), out) - def test_make_and_send_dns_message_error_flags(self): + @mock.patch.object(dnsutils, 'send_dns_message') + def test_make_and_send_dns_message_error_flags(self, + mock_send_dns_message): zone = RoObject(name='zn') - self.notify._make_dns_message = mock.Mock(return_value='') response = RoObject( rcode=mock.Mock(return_value=dns.rcode.NOERROR), # rcode is NOERROR but flags are not NOERROR @@ -229,7 +229,7 @@ class MdnsNotifyTest(designate.tests.TestCase): ednsflags=321, answer=['answer'], ) - self.notify._send_dns_message = mock.Mock(return_value=response) + mock_send_dns_message.return_value = response out = self.notify._make_and_send_dns_message( zone, 'host', 123, 1, 2, 3 @@ -266,23 +266,3 @@ class MdnsNotifyTest(designate.tests.TestCase): ';AUTHORITY', ';ADDITIONAL', ], txt) - - @mock.patch.object(notify.dns_query, 'udp') - def test_send_udp_dns_message(self, mock_udp): - self.CONF.set_override('all_tcp', False, 'service:mdns') - - self.notify._send_dns_message('msg', '192.0.2.1', 1234, 1) - - mock_udp.assert_called_with( - 'msg', '192.0.2.1', port=1234, timeout=1 - ) - - @mock.patch.object(notify.dns_query, 'tcp') - def test_send_tcp_dns_message(self, mock_tcp): - self.CONF.set_override('all_tcp', True, 'service:mdns') - - self.notify._send_dns_message('msg', '192.0.2.1', 1234, 1) - - mock_tcp.assert_called_with( - 'msg', '192.0.2.1', port=1234, timeout=1 - ) diff --git a/designate/tests/unit/objects/test_caa_object.py b/designate/tests/unit/objects/test_caa_object.py index 877ffb25..6045a98f 100644 --- a/designate/tests/unit/objects/test_caa_object.py +++ b/designate/tests/unit/objects/test_caa_object.py @@ -50,6 +50,13 @@ class CAARecordTest(oslotest.base.BaseTestCase): self.assertEqual(0, caa_record.flags) self.assertEqual('iodef mailto:security@example.net', caa_record.prpt) + caa_record = objects.CAA() + caa_record._from_string('0 iodef mailto:security+caa@example.net') + + self.assertEqual(0, caa_record.flags) + self.assertEqual('iodef mailto:security+caa@example.net', + caa_record.prpt) + def test_parse_caa_invalid(self): caa_record = objects.CAA() self.assertRaisesRegex( @@ -72,7 +79,7 @@ class CAARecordTest(oslotest.base.BaseTestCase): caa_record = objects.CAA() self.assertRaisesRegex( ValueError, - 'Domain abc. does not match', + 'Domain abc. is invalid', caa_record._from_string, '0 issue abc.' ) @@ -80,7 +87,7 @@ class CAARecordTest(oslotest.base.BaseTestCase): caa_record = objects.CAA() self.assertRaisesRegex( ValueError, - 'def is not valid key-value pair', + 'def is not a valid key-value pair', caa_record._from_string, '0 issue abc;def' ) @@ -98,7 +105,7 @@ class CAARecordTest(oslotest.base.BaseTestCase): caa_record = objects.CAA() self.assertRaisesRegex( ValueError, - 'Domain example.net. does not match', + 'Domain example.net. is invalid', caa_record._from_string, '0 iodef mailto:me@example.net.' ) @@ -116,7 +123,7 @@ class CAARecordTest(oslotest.base.BaseTestCase): caa_record = objects.CAA() self.assertRaisesRegex( ValueError, - 'Domain example.net. does not match', + 'Domain example.net. is invalid', caa_record._from_string, '0 iodef https://example.net./' ) @@ -124,6 +131,6 @@ class CAARecordTest(oslotest.base.BaseTestCase): caa_record = objects.CAA() self.assertRaisesRegex( ValueError, - 'https:// is not valid URL', + 'https:// is not a valid URL', caa_record._from_string, '0 iodef https://' ) diff --git a/designate/tests/unit/test_central/test_basic.py b/designate/tests/unit/test_central/test_basic.py index e75922e3..d3bb6b41 100644 --- a/designate/tests/unit/test_central/test_basic.py +++ b/designate/tests/unit/test_central/test_basic.py @@ -789,13 +789,13 @@ class CentralZoneTestCase(CentralBasic): def test_add_ns_creation(self): self.service._create_ns = mock.Mock() - self.service.find_recordsets = mock.Mock( - return_value=[] + self.service.find_recordset = mock.Mock( + side_effect=exceptions.RecordSetNotFound() ) self.service._add_ns( self.context, - RoObject(id=CentralZoneTestCase.zone__id), + RoObject(name='foo', id=CentralZoneTestCase.zone__id), RoObject(name='bar') ) ctx, zone, records = self.service._create_ns.call_args[0] @@ -804,16 +804,15 @@ class CentralZoneTestCase(CentralBasic): def test_add_ns(self): self.service._update_recordset_in_storage = mock.Mock() - recordsets = [ - RoObject(records=objects.RecordList.from_list([]), managed=True) - ] - self.service.find_recordsets = mock.Mock( - return_value=recordsets + self.service.find_recordset = mock.Mock( + return_value=RoObject( + records=objects.RecordList.from_list([]), managed=True + ) ) self.service._add_ns( self.context, - RoObject(id=CentralZoneTestCase.zone__id), + RoObject(name='foo', id=CentralZoneTestCase.zone__id), RoObject(name='bar') ) ctx, zone, rset = \ @@ -822,29 +821,6 @@ class CentralZoneTestCase(CentralBasic): self.assertTrue(rset.records[0].managed) self.assertEqual('bar', rset.records[0].data.name) - def test_add_ns_with_other_ns_rs(self): - self.service._update_recordset_in_storage = mock.Mock() - - recordsets = [ - RoObject(records=objects.RecordList.from_list([]), managed=True), - RoObject(records=objects.RecordList.from_list([]), managed=False) - ] - - self.service.find_recordsets = mock.Mock( - return_value=recordsets - ) - - self.service._add_ns( - self.context, - RoObject(id=CentralZoneTestCase.zone__id), - RoObject(name='bar') - ) - ctx, zone, rset = \ - self.service._update_recordset_in_storage.call_args[0] - self.assertEqual(1, len(rset.records)) - self.assertTrue(rset.records[0].managed) - self.assertEqual('bar', rset.records[0].data.name) - def test_create_zone_no_servers(self): self.service._enforce_zone_quota = mock.Mock() self.service._is_valid_zone_name = mock.Mock() diff --git a/designate/tests/unit/test_dnsutils.py b/designate/tests/unit/test_dnsutils.py index 8345b8ef..eac016fc 100644 --- a/designate/tests/unit/test_dnsutils.py +++ b/designate/tests/unit/test_dnsutils.py @@ -23,6 +23,7 @@ import dns.rcode import dns.rdatatype import dns.zone import eventlet +from oslo_config import cfg import oslotest.base from dns import zone as dnszone @@ -31,6 +32,8 @@ from designate import dnsutils from designate import exceptions from designate import objects +CONF = cfg.CONF + SAMPLES = { ("cname.example.com.", "CNAME"): { "ttl": 10800, @@ -320,3 +323,19 @@ class TestDoAfxr(oslotest.base.BaseTestCase): self.assertTrue(mock_xfr.called) self.assertTrue(mock_from_xfr.called) + + @mock.patch.object(dns.query, 'udp') + def test_send_udp_dns_message(self, mock_udp): + CONF.set_override('all_tcp', False, 'service:mdns') + dnsutils.send_dns_message('msg', '192.0.2.1', 1234, 1) + mock_udp.assert_called_with( + 'msg', '192.0.2.1', port=1234, timeout=1 + ) + + @mock.patch.object(dns.query, 'tcp') + def test_send_tcp_dns_message(self, mock_tcp): + CONF.set_override('all_tcp', True, 'service:mdns') + dnsutils.send_dns_message('msg', '192.0.2.1', 1234, 1) + mock_tcp.assert_called_with( + 'msg', '192.0.2.1', port=1234, timeout=1 + ) diff --git a/designate/tests/unit/workers/test_base_task.py b/designate/tests/unit/workers/test_base_task.py index 9b37b52e..f419f4f4 100644 --- a/designate/tests/unit/workers/test_base_task.py +++ b/designate/tests/unit/workers/test_base_task.py @@ -14,17 +14,105 @@ # License for the specific language governing permissions and limitations # under the License.mport threading import oslotest.base +from unittest import mock +from designate import exceptions +from designate import objects from designate.worker.tasks import base class TestTask(oslotest.base.BaseTestCase): def setUp(self): super(TestTask, self).setUp() + self.context = mock.Mock() self.task = base.Task(None) + self.storage = self.task._storage = mock.Mock() def test_constructor(self): self.assertTrue(self.task) def test_call(self): self.assertRaises(NotImplementedError, self.task) + + def test_current_action_is_valid(self): + self.storage.get_zone = mock.Mock( + return_value=objects.Zone(action='UPDATE') + ) + self.assertTrue( + self.task.is_current_action_valid( + self.context, 'UPDATE', objects.Zone(action='UPDATE')) + ) + + self.storage.get_zone = mock.Mock( + return_value=objects.Zone(action='CREATE') + ) + self.assertTrue( + self.task.is_current_action_valid( + self.context, 'CREATE', objects.Zone(action='CREATE')) + ) + + self.storage.get_zone = mock.Mock( + return_value=objects.Zone(action='UPDATE') + ) + self.assertTrue( + self.task.is_current_action_valid( + self.context, 'CREATE', objects.Zone(action='CREATE')) + ) + + self.storage.get_zone = mock.Mock( + return_value=objects.Zone(action='DELETE') + ) + self.assertTrue( + self.task.is_current_action_valid( + self.context, 'DELETE', objects.Zone(action='DELETE')) + ) + + def test_current_action_delete_always_valid(self): + self.assertTrue( + self.task.is_current_action_valid( + self.context, 'DELETE', None) + ) + + def test_current_action_bad_storage_always_valid(self): + self.storage.get_zone = mock.Mock( + side_effect=exceptions.DesignateException() + ) + self.assertTrue( + self.task.is_current_action_valid( + self.context, 'CREATE', objects.Zone(action='CREATE')) + ) + + def test_current_action_is_not_valid_none(self): + self.storage.get_zone = mock.Mock( + return_value=objects.Zone(action='NONE') + ) + self.assertFalse( + self.task.is_current_action_valid( + self.context, 'UPDATE', objects.Zone(action='UPDATE')) + ) + + def test_current_action_is_not_valid_deleted(self): + self.storage.get_zone = mock.Mock( + return_value=objects.Zone(action='DELETE') + ) + self.assertFalse( + self.task.is_current_action_valid( + self.context, 'UPDATE', objects.Zone(action='UPDATE')) + ) + + def test_current_action_is_not_found(self): + self.storage.get_zone = mock.Mock( + side_effect=exceptions.ZoneNotFound() + ) + self.assertTrue( + self.task.is_current_action_valid( + self.context, 'CREATE', objects.Zone(action='CREATE')) + ) + + self.storage.get_zone = mock.Mock( + side_effect=exceptions.ZoneNotFound() + ) + self.assertFalse( + self.task.is_current_action_valid( + self.context, 'UPDATE', objects.Zone(action='UPDATE')) + ) diff --git a/designate/tests/unit/workers/test_zone_tasks.py b/designate/tests/unit/workers/test_zone_tasks.py index 25b9c41a..47eb7d27 100644 --- a/designate/tests/unit/workers/test_zone_tasks.py +++ b/designate/tests/unit/workers/test_zone_tasks.py @@ -20,11 +20,11 @@ import oslotest.base from oslo_config import cfg from oslo_config import fixture as cfg_fixture +from designate import dnsutils from designate import exceptions from designate import objects from designate.tests.unit import utils from designate.worker import processing -from designate.worker import utils as wutils from designate.worker.tasks import zone CONF = cfg.CONF @@ -167,7 +167,7 @@ class TestZoneActionOnTarget(oslotest.base.BaseTestCase): self.context = mock.Mock() self.executor = mock.Mock() - @mock.patch.object(wutils, 'notify') + @mock.patch.object(dnsutils, 'notify') def test_call_create(self, mock_notify): self.zone = objects.Zone(name='example.org.', action='CREATE') self.actor = zone.ZoneActionOnTarget( @@ -185,7 +185,7 @@ class TestZoneActionOnTarget(oslotest.base.BaseTestCase): port=53 ) - @mock.patch.object(wutils, 'notify') + @mock.patch.object(dnsutils, 'notify') def test_call_update(self, mock_notify): self.zone = objects.Zone(name='example.org.', action='UPDATE') self.actor = zone.ZoneActionOnTarget( @@ -203,7 +203,7 @@ class TestZoneActionOnTarget(oslotest.base.BaseTestCase): port=53 ) - @mock.patch.object(wutils, 'notify') + @mock.patch.object(dnsutils, 'notify') def test_call_delete(self, mock_notify): self.zone = objects.Zone(name='example.org.', action='DELETE') self.actor = zone.ZoneActionOnTarget( @@ -217,7 +217,7 @@ class TestZoneActionOnTarget(oslotest.base.BaseTestCase): mock_notify.assert_not_called() - @mock.patch.object(wutils, 'notify') + @mock.patch.object(dnsutils, 'notify') @mock.patch('time.sleep', mock.Mock()) def test_call_exception_raised(self, mock_notify): self.backend.create_zone.side_effect = exceptions.BadRequest() @@ -250,7 +250,7 @@ class TestSendNotify(oslotest.base.BaseTestCase): self.executor = mock.Mock() - @mock.patch.object(wutils, 'notify') + @mock.patch.object(dnsutils, 'notify') def test_call_notify(self, mock_notify): self.zone = objects.Zone(name='example.org.') self.actor = zone.SendNotify( @@ -267,7 +267,7 @@ class TestSendNotify(oslotest.base.BaseTestCase): port=53 ) - @mock.patch.object(wutils, 'notify') + @mock.patch.object(dnsutils, 'notify') def test_call_notify_timeout(self, mock_notify): mock_notify.side_effect = dns.exception.Timeout() self.zone = objects.Zone(name='example.org.') @@ -282,7 +282,7 @@ class TestSendNotify(oslotest.base.BaseTestCase): self.actor ) - @mock.patch.object(wutils, 'notify') + @mock.patch.object(dnsutils, 'notify') def test_call_dont_notify(self, mock_notify): CONF.set_override('notify', False, 'service:worker') @@ -668,11 +668,11 @@ class TestPollForZone(oslotest.base.BaseTestCase): self.task._max_retries = 3 self.task._retry_interval = 2 - @mock.patch.object(zone.wutils, 'get_serial', mock.Mock(return_value=10)) + @mock.patch.object(dnsutils, 'get_serial', mock.Mock(return_value=10)) def test_get_serial(self): self.assertEqual(10, self.task._get_serial()) - zone.wutils.get_serial.assert_called_with( + dnsutils.get_serial.assert_called_with( 'example.org.', 'ns.example.org', port=53 diff --git a/designate/worker/README.md b/designate/worker/README.md index 9c5d9920..47da1934 100644 --- a/designate/worker/README.md +++ b/designate/worker/README.md @@ -29,7 +29,7 @@ class SendNotify(base.Task): port = int(self.target.options.get('port')) try: - wutils.notify(self.zone.name, host, port=port) + dnsutils.notify(self.zone.name, host, port=port) return True except Exception: return False diff --git a/designate/worker/tasks/base.py b/designate/worker/tasks/base.py index b6959391..5c3c8294 100644 --- a/designate/worker/tasks/base.py +++ b/designate/worker/tasks/base.py @@ -18,6 +18,7 @@ from oslo_config import cfg from oslo_log import log as logging from designate.central import rpcapi as central_rpcapi +from designate import exceptions from designate import quota from designate import storage from designate import utils @@ -139,5 +140,52 @@ class Task(TaskConfig): self._worker_api = worker_rpcapi.WorkerAPI.get_instance() return self._worker_api + def is_current_action_valid(self, context, action, zone): + """Is our current action still valid?""" + + # We always allow for DELETE operations. + if action == 'DELETE': + return True + + try: + zone = self.storage.get_zone(context, zone.id) + + # If the zone is either in a DELETE or NONE state, + # we don't need to continue with the current action. + if zone.action in ['DELETE', 'NONE']: + LOG.info( + 'Failed to %(action)s zone_name=%(zone_name)s ' + 'zone_id=%(zone_id)s action state has changed ' + 'to %(current_action)s, not retrying action', + { + 'action': action, + 'zone_name': zone.name, + 'zone_id': zone.id, + 'current_action': zone.action, + } + ) + return False + except exceptions.ZoneNotFound: + if action != 'CREATE': + LOG.info( + 'Failed to %(action)s zone_name=%(zone_name)s ' + 'zone_id=%(zone_id)s Error=ZoneNotFound', + { + 'action': action, + 'zone_name': zone.name, + 'zone_id': zone.id, + } + ) + return False + except Exception as e: + LOG.warning( + 'Error trying to get zone action. Error=%(error)s', + { + 'error': str(e), + } + ) + + return True + def __call__(self): raise NotImplementedError diff --git a/designate/worker/tasks/zone.py b/designate/worker/tasks/zone.py index 3189de27..6b18b693 100644 --- a/designate/worker/tasks/zone.py +++ b/designate/worker/tasks/zone.py @@ -20,10 +20,10 @@ import dns from oslo_config import cfg from oslo_log import log as logging -from designate.worker import utils as wutils -from designate.worker.tasks import base +from designate import dnsutils from designate import exceptions from designate import utils +from designate.worker.tasks import base LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -88,17 +88,21 @@ class ZoneActionOnTarget(base.Task): self.action, self.zone.name, self.target) return True except Exception as e: - LOG.info('Failed to %(action)s zone %(zone)s on ' - 'target %(target)s on attempt %(attempt)d, ' - 'Error: %(error)s.', - { - 'action': self.action, - 'zone': self.zone.name, - 'target': self.target.id, - 'attempt': retry + 1, - 'error': str(e) - }) - time.sleep(self.retry_interval) + LOG.info( + 'Failed to %(action)s zone_name=%(zone_name)s ' + 'zone_id=%(zone_id)s on target=%(target)s on ' + 'attempt=%(attempt)d Error=%(error)s', + { + 'action': self.action, + 'zone_name': self.zone.name, + 'zone_id': self.zone.id, + 'target': self.target, + 'attempt': retry + 1, + 'error': str(e), + } + ) + + time.sleep(self.retry_interval) return False @@ -124,7 +128,7 @@ class SendNotify(base.Task): port = int(self.target.options.get('port')) try: - wutils.notify(self.zone.name, host, port=port) + dnsutils.notify(self.zone.name, host, port=port) LOG.debug('Sent NOTIFY to %(host)s:%(port)s for zone %(zone)s', { 'host': host, @@ -311,7 +315,7 @@ class PollForZone(base.Task): self.ns = ns def _get_serial(self): - return wutils.get_serial( + return dnsutils.get_serial( self.zone.name, self.ns.host, port=self.ns.port @@ -404,6 +408,10 @@ class ZonePoller(base.Task, ThresholdMixin): {'zone': self.zone.name, 'n': retry + 1}) time.sleep(retry_interval) + if not self.is_current_action_valid(self.context, self.zone.action, + self.zone): + break + return query_result def _on_failure(self, error_status): diff --git a/designate/worker/utils.py b/designate/worker/utils.py deleted file mode 100644 index f82d5432..00000000 --- a/designate/worker/utils.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2016 Rackspace Inc. -# -# Author: Tim Simmons <tim.simmons@rackspace> -# -# 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.mport threading -import dns -import dns.exception -import dns.query -from oslo_config import cfg -from oslo_log import log as logging - -LOG = logging.getLogger(__name__) -CONF = cfg.CONF - - -def prepare_msg(zone_name, rdatatype=dns.rdatatype.SOA, notify=False): - """ - Do the needful to set up a dns packet with dnspython - """ - dns_message = dns.message.make_query(zone_name, rdatatype) - if notify: - dns_message.set_opcode(dns.opcode.NOTIFY) - else: - dns_message.set_opcode(dns.opcode.QUERY) - return dns_message - - -def dig(zone_name, host, rdatatype, port=53): - """ - Set up and send a regular dns query, datatype configurable - """ - query = prepare_msg(zone_name, rdatatype=rdatatype) - - return send_dns_msg(query, host, port=port) - - -def notify(zone_name, host, port=53): - """ - Set up a notify packet and send it - """ - msg = prepare_msg(zone_name, notify=True) - - return send_dns_msg(msg, host, port=port) - - -def send_dns_msg(dns_message, host, port=53): - """ - Send the dns message and return the response - - :return: dns.Message of the response to the dns query - """ - # This can raise some exceptions, but we'll catch them elsewhere - if not CONF['service:mdns'].all_tcp: - return dns.query.udp( - dns_message, host, port=port, timeout=10) - else: - return dns.query.tcp( - dns_message, host, port=port, timeout=10) - - -def get_serial(zone_name, host, port=53): - """ - Possibly raises dns.exception.Timeout or dns.query.BadResponse. - Possibly returns 0 if, e.g., the answer section is empty. - """ - resp = dig(zone_name, host, dns.rdatatype.SOA, port=port) - if not resp.answer: - return 0 - rdataset = resp.answer[0].to_rdataset() - if not rdataset: - return 0 - return rdataset[0].serial diff --git a/releasenotes/notes/Fix-update-zone-create-zone-ada1fd81de479492.yaml b/releasenotes/notes/Fix-update-zone-create-zone-ada1fd81de479492.yaml new file mode 100644 index 00000000..600ec937 --- /dev/null +++ b/releasenotes/notes/Fix-update-zone-create-zone-ada1fd81de479492.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixed an issue where new BIND9 pool instances may fail on zone update. diff --git a/releasenotes/notes/bug-1958533-allow-caa-mail-subaddr-d02cdc46bbb118ad.yaml b/releasenotes/notes/bug-1958533-allow-caa-mail-subaddr-d02cdc46bbb118ad.yaml new file mode 100644 index 00000000..694d0d2f --- /dev/null +++ b/releasenotes/notes/bug-1958533-allow-caa-mail-subaddr-d02cdc46bbb118ad.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + CAA records now allow the use of `+` prefixed subadresses like + `security+caa@example.net` within mail urls. + (https://www.rfc-editor.org/rfc/rfc5233.html#section-1) + + See `bug 1958533`_ for more information. + + .. _bug 1958533: https://bugs.launchpad.net/designate/+bug/1958533 diff --git a/releasenotes/notes/fix-zone-transfer-request-scoped-token-fc9d3be407e1a50a.yaml b/releasenotes/notes/fix-zone-transfer-request-scoped-token-fc9d3be407e1a50a.yaml new file mode 100644 index 00000000..77d1fa86 --- /dev/null +++ b/releasenotes/notes/fix-zone-transfer-request-scoped-token-fc9d3be407e1a50a.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug where deleting a zone transfer request may fail when using + a system scoped token. |