# 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. import random import socket import threading import time import dns.exception import dns.message import dns.opcode import dns.query import dns.rdatatype import dns.zone import eventlet from oslo_log import log as logging from oslo_serialization import base64 import designate.conf from designate import context from designate import exceptions from designate import objects CONF = designate.conf.CONF LOG = logging.getLogger(__name__) class TsigKeyring(dict): """Implements the DNSPython KeyRing API, backed by the Designate DB""" def __init__(self, storage): super().__init__() self.storage = storage def __getitem__(self, key): return self.get(key) def get(self, key, default=None): try: name = key.to_text(True) criterion = {'name': name} tsigkey = self.storage.find_tsigkey( context.get_current(), criterion ) return base64.decode_as_bytes(tsigkey.secret) except exceptions.TsigKeyNotFound: return default class ZoneLock: """A Lock across all zones that enforces a rate limit on NOTIFYs""" def __init__(self, delay): self.lock = threading.Lock() self.data = {} self.delay = delay def acquire(self, zone): with self.lock: # If no one holds the lock for the zone, grant it if zone not in self.data: self.data[zone] = time.monotonic() return True # Otherwise, get the time that it was locked locktime = self.data[zone] now = time.monotonic() period = now - locktime # If it has been locked for longer than the allowed period # give the lock to the new requester if period > self.delay: self.data[zone] = now return True LOG.debug( 'Lock for %(zone)s can\'t be released for %(period)s seconds', { 'zone': zone, 'period': str(self.delay - period) } ) # Don't grant the lock for the zone return False def release(self, zone): # Release the lock with self.lock: try: self.data.pop(zone) except KeyError: pass def from_dnspython_zone(dnspython_zone): # dnspython never builds a zone with more than one SOA, even if we give # it a zonefile that contains more than one soa = dnspython_zone.get_rdataset(dnspython_zone.origin, 'SOA') if soa is None: raise exceptions.BadRequest('An SOA record is required') if soa.ttl == 0: soa.ttl = CONF['service:central'].min_ttl email = soa[0].rname.to_text(omit_final_dot=True) email = email.replace('.', '@', 1) name = dnspython_zone.origin.to_text() values = { 'name': name, 'email': email, 'ttl': soa.ttl, 'serial': soa[0].serial, 'retry': soa[0].retry, 'expire': soa[0].expire } zone = objects.Zone(**values) rrsets = dnspyrecords_to_recordsetlist(dnspython_zone.nodes) zone.recordsets = rrsets return zone def dnspyrecords_to_recordsetlist(dnspython_records): rrsets = objects.RecordSetList() for rname in dnspython_records.keys(): for rdataset in dnspython_records[rname]: rrset = dnspythonrecord_to_recordset(rname, rdataset) if rrset is None: continue rrsets.append(rrset) return rrsets def dnspythonrecord_to_recordset(rname, rdataset): record_type = dns.rdatatype.to_text(rdataset.rdtype) name = rname.to_text() # Create the other recordsets values = { 'name': name, 'type': record_type } if rdataset.ttl != 0: values['ttl'] = rdataset.ttl rrset = objects.RecordSet(**values) rrset.records = objects.RecordList() for rdata in rdataset: rr = objects.Record(data=rdata.to_text()) rrset.records.append(rr) return rrset def xfr_timeout(): return CONF['service:worker'].xfr_timeout def do_axfr(zone_name, servers, source=None): """ Requests an AXFR for a given zone name and process the response :returns: Zone instance from dnspython """ random.shuffle(servers) xfr = None for srv in servers: for address in get_ip_addresses(srv['host']): to = eventlet.Timeout(xfr_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) except OSError: 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_dns_message(zone_name, rdatatype, opcode): """ Create a dns message using dnspython """ dns_message = dns.message.make_query(zone_name, rdatatype) dns_message.set_opcode(opcode) return dns_message def notify(zone_name, host, port=53, timeout=10): """ Create a NOTIFY message and send it """ dns_message = prepare_dns_message( zone_name, rdatatype=dns.rdatatype.SOA, opcode=dns.opcode.NOTIFY ) return send_dns_message(dns_message, host, port=port, timeout=timeout) def soa_query(zone_name, host, port=53, timeout=10): """ Create a SOA Query message and send it """ dns_message = prepare_dns_message( zone_name, rdatatype=dns.rdatatype.SOA, opcode=dns.opcode.QUERY ) return send_dns_message(dns_message, host, port=port, timeout=timeout) def use_all_tcp(): return CONF['service:worker'].all_tcp 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 use_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 = soa_query(zone_name, host, 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))