Beispiel #1
0
def make_update(action, query):
    hostname = query.hostname.encode('ascii')
    D = dns.name.from_text(domain)
    H = dns.name.from_text(query.hostname)

    if H.is_subdomain(D):
        R = H.relativize(D)
    else:
        return "400 NOTAUTH %s\n" % H.to_text()

    keyring, algo = read_session_key("/etc/bind/keys/webapp.key")
    update = dns.update.Update(D, keyring=keyring, keyalgorithm=algo)
    if action == 'update':
       update.present(R, 'a')
       update.replace(R, 300, 'a', query.ip.encode('ascii'))
    elif action == 'delete':
       update.present(R, 'a')
       update.delete(R, 'a')
    elif action == 'add':
       update.absent(R, 'a')
       update.add(R, 300, 'a', query.ip.encode('ascii'))
    response = dns.query.tcp(update, '127.0.0.1')

    if response.rcode() == 0:
        return "NOERROR %s\n" % H.to_text()
    else:
        return "%s %s\n" % (dns.rcode.to_text(response.rcode()), H.to_text())
    def record_exists(self):
        update = dns.update.Update(self.zone,
                                   keyring=self.keyring,
                                   keyalgorithm=self.algorithm)
        try:
            update.present(self.record, self.type)
        except dns.rdatatype.UnknownRdatatype as ee:
            self.module.fail_json(msg='Record error: {}'.format(str(ee)))

        response = self.__do_update(update)
        if dns.message.Message.rcode(response) == 0:
            if self.state == 'absent':
                return True
            try:
                update.present(self.record, self.type, self.value)
            except AttributeError:
                self.module.fail_json(msg='value needed when state=present')
            except dns.exception.SyntaxError:
                self.module.fail_json(msg='Invalid/malformed value')
            response = self.__do_update(update)
            if dns.message.Message.rcode(response) == 0:
                return True
            else:
                return 2
        else:
            return False
Beispiel #3
0
    def record_exists(self):
        update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm)
        try:
            update.present(self.module.params['record'], self.module.params['type'])
        except dns.rdatatype.UnknownRdatatype as e:
            self.module.fail_json(msg='Record error: {0}'.format(to_native(e)))

        response = self.__do_update(update)
        self.dns_rc = dns.message.Message.rcode(response)
        if self.dns_rc == 0:
            if self.module.params['state'] == 'absent':
                return 1
            for entry in self.module.params['value']:
                try:
                    update.present(self.module.params['record'], self.module.params['type'], entry)
                except AttributeError:
                    self.module.fail_json(msg='value needed when state=present')
                except dns.exception.SyntaxError:
                    self.module.fail_json(msg='Invalid/malformed value')
            response = self.__do_update(update)
            self.dns_rc = dns.message.Message.rcode(response)
            if self.dns_rc == 0:
                return 1
            else:
                return 2
        else:
            return 0
Beispiel #4
0
    def record_exists(self):
        update = dns.update.Update(self.zone,
                                   keyring=self.keyring,
                                   keyalgorithm=self.algorithm)
        try:
            update.present(self.module.params['record'],
                           self.module.params['type'])
        except dns.rdatatype.UnknownRdatatype as e:
            self.module.fail_json(msg='Record error: {0}'.format(to_native(e)))

        response = self.__do_update(update)
        self.dns_rc = dns.message.Message.rcode(response)
        if self.dns_rc == 0:
            if self.module.params['state'] == 'absent':
                return 1
            for entry in self.value:
                try:
                    update.present(self.module.params['record'],
                                   self.module.params['type'], entry)
                except AttributeError:
                    self.module.fail_json(
                        msg='value needed when state=present')
                except dns.exception.SyntaxError:
                    self.module.fail_json(msg='Invalid/malformed value')
            response = self.__do_update(update)
            self.dns_rc = dns.message.Message.rcode(response)
            if self.dns_rc == 0:
                if self.ttl_changed():
                    return 2
                else:
                    return 1
            else:
                return 2
        else:
            return 0
Beispiel #5
0
 def delete_a(self):
     try:
         update = dns.update.Update(self.get_domain_fqdn(), keyring=self.__keyring)
         update.present(self.__hostname, 'TXT', self.__txt_value)
         update.delete(self.__hostname, 'A')
         return dns.query.tcp(update, self.__server_ip).rcode()
     except Exception, error:
         raise Exception("Error deleting DNS A Entry: %s" % (error))
Beispiel #6
0
 def update_a(self):
     try:
         update = dns.update.Update(self.get_domain_fqdn(), keyring=self.__keyring)
         # Só realiza o update se tiver uma entrada TXT e o valor dela bater com o calculado pelo servidor
         update.present(self.__hostname, 'TXT', self.__txt_value)
         update.replace(self.__hostname, self.__ttl, 'A', self.__ip)
         return dns.query.tcp(update, self.__server_ip).rcode()
     except Exception, error:
         raise Exception("Error updating DNS A Entry: %s" % (error))
Beispiel #7
0
 def delete_rev(self):
     try:            
         name = self.validate_rev_name()
         if not name:
             return None
         
         update = dns.update.Update(self.get_rev_domain_fqdn(), keyring=self.__keyring)
         update.present(name, 'PTR', self.get_fqdn())
         update.delete(name, 'PTR')
         return dns.query.tcp(update, self.__server_ip).rcode()
     except Exception, error:
         raise Exception("Error deleting DNS PTR Entry: %s" % (error))
Beispiel #8
0
def _prepare_dns_updates(add_rrsets, delete_rrsets, my_zones):
    """Prepare a set of DNS updates for the specified rrset additions and deletions.
    
    One update will be created for each zone mentioned in the rrsets.  
    Constrints will be added to the DNS update message:
    
      * when deleting the record, ensure that it existed
      * when adding a record, ensure that it did not exist
      * when modifying (deleting and readding) a record, ensure that it existed
    
    Returns a dict mapping zone names to dnspython Update objects.
    """
    
    updates = {}
    
    for rrset in delete_rrsets:
        zone = _get_zone(rrset.name, my_zones)
        
        # Create a new update for this zone if necessary
        if zone not in updates:
            updates[zone] = dns.update.Update(zone, keyring=_create_keyring(zone))
        
        update = updates[zone]
        
        # Require the record exist before deleting it.
        update.present(rrset.name, *rrset.items)
        
        # Delete the record.
        update.delete(rrset.name, rrset)
        
    for rrset in add_rrsets:
        zone = _get_zone(rrset.name, my_zones)
        
        # Create a new update for this zone if necessary
        if zone not in updates:
            updates[zone] = dns.update.Update(zone, keyring=_create_keyring(zone))
        
        update = updates[zone]
        
        # For additions only, require that the record not exist before adding 
        # it.  We're processing each modification as a delete/add pair, so 
        # it will exist before the update (and we ensure this above).
        
        if rrset.name not in [delete.name for delete in delete_rrsets]:
            update.absent(rrset.name, rrset.rdtype)
        
        update.add(rrset.name, rrset)
    
    return updates
Beispiel #9
0
    def record_exists(self):
        update = dns.update.Update(self.zone, keyring=self.keyring)
        update.present(self.record, self.type)

        try:
            response = dns.query.tcp(update, self.server, timeout=10)
            if dns.message.Message.rcode(response) == 0:
                update.present(self.record, self.type, self.value)
                response = dns.query.tcp(update, self.server, timeout=10)
                if dns.message.Message.rcode(response) == 0:
                    return True
                else:
                    return 2
            else:
                return False
        except:
            self.module.fail_json(msg='Connection to DNS server failed')
    def record_exists(self):
        update = dns.update.Update(self.zone, keyring=self.keyring)
        update.present(self.record, self.type)

        try:
            response = dns.query.tcp(update, self.server, timeout=10)
            if dns.message.Message.rcode(response) == 0:
                update.present(self.record, self.type, self.value)
                response = dns.query.tcp(update, self.server, timeout=10)
                if dns.message.Message.rcode(response) == 0:
                    return True
                else:
                    return 2
            else:
                return False
        except:
            self.module.fail_json(msg="Connection to DNS server failed")
Beispiel #11
0
def generate_update_from_diff(zonename, added, removed, oldsoa, keyring,
                              keyalgo, force_conflicts):

    update = dns.update.Update(zonename, keyring=keyring, keyalgorithm=keyalgo)

    if (not force_conflicts):
        # Require the old SOA to still be present
        # (Essentially requires that the zone hasn't changed while editing)

        update.present(oldsoa[0], oldsoa[2])

    for (name, ttl, rdata) in removed:
        update.delete(name, rdata)

    for (name, ttl, rdata) in added:
        update.add(name, ttl, rdata)

    return update
Beispiel #12
0
def generate_update_from_diff(zonename, original_zone, updated_zone,
                              keyring, keyalgo, force_conflicts):
    update = dns.update.Update(zonename, keyring = keyring,
                               keyalgorithm = keyalgo)

    if (not force_conflicts):
        # Require the old SOA to still be present
        # (Essentially requires that the zone hasn't changed while editing)
        oldsoa = get_single_record(original_zone.iterate_rdatas(), 
                                   dns.rdatatype.SOA)
        update.present(oldsoa[0], oldsoa[2])

    added, removed = get_zone_diff(original_zone, updated_zone)

    for (name, ttl, rdata) in removed:
        update.delete(name, rdata)

    for (name, ttl, rdata) in added:
        update.add(name, ttl, rdata)

    return [update, len(added), len(removed)]
Beispiel #13
0
 def test_to_wire1(self): # type: () -> None
     update = dns.update.Update('example')
     update.id = 1
     update.present('foo')
     update.present('foo', 'a')
     update.present('bar', 'a', '10.0.0.5')
     update.absent('blaz2')
     update.absent('blaz2', 'a')
     update.replace('foo', 300, 'a', '10.0.0.1', '10.0.0.2')
     update.add('bar', 300, 'a', '10.0.0.3')
     update.delete('bar', 'a', '10.0.0.4')
     update.delete('blaz', 'a')
     update.delete('blaz2')
     self.assertTrue(update.to_wire() == goodwire)
Beispiel #14
0
 def test_to_wire3(self):
     update = dns.update.Update('example')
     update.id = 1
     update.present('foo')
     update.present('foo', 'a')
     update.present('bar', 'a', '10.0.0.5')
     update.absent('blaz2')
     update.absent('blaz2', 'a')
     update.replace('foo', 300, 'a', '10.0.0.1', '10.0.0.2')
     update.add('bar', dns.rdataset.from_text(1, 1, 300, '10.0.0.3'))
     update.delete('bar', 'a', '10.0.0.4')
     update.delete('blaz', 'a')
     update.delete('blaz2')
     self.failUnless(update.to_wire() == goodwire)
Beispiel #15
0
 def test_to_wire3(self):  # type: () -> None
     update = dns.update.Update('example')
     update.id = 1
     update.present('foo')
     update.present('foo', 'a')
     update.present('bar', 'a', '10.0.0.5')
     update.absent('blaz2')
     update.absent('blaz2', 'a')
     update.replace('foo', 300, 'a', '10.0.0.1', '10.0.0.2')
     update.add('bar', dns.rdataset.from_text(1, 1, 300, '10.0.0.3'))
     update.delete('bar', 'a', '10.0.0.4')
     update.delete('blaz', 'a')
     update.delete('blaz2')
     self.assertEqual(update.to_wire(), goodwire)
Beispiel #16
0
 def test_to_wire1(self): # type: () -> None
     update = dns.update.Update('example')
     update.id = 1
     update.present('foo')
     update.present('foo', 'a')
     update.present('bar', 'a', '10.0.0.5')
     update.absent('blaz2')
     update.absent('blaz2', 'a')
     update.replace('foo', 300, 'a', '10.0.0.1', '10.0.0.2')
     update.add('bar', 300, 'a', '10.0.0.3')
     update.delete('bar', 'a', '10.0.0.4')
     update.delete('blaz', 'a')
     update.delete('blaz2')
     self.failUnless(update.to_wire() == goodwire)
Beispiel #17
0
 def test_to_wire1(self):
     update = dns.update.Update('example')
     update.id = 1
     update.present('foo')
     update.present('foo', 'a')
     update.present('bar', 'a', '10.0.0.5')
     update.absent('blaz2')
     update.absent('blaz2', 'a')
     update.replace('foo', 300, 'a', '10.0.0.1', '10.0.0.2')
     update.add('bar', 300, 'a', '10.0.0.3')
     update.delete('bar', 'a', '10.0.0.4')
     update.delete('blaz', 'a')
     update.delete('blaz2')
     self.failUnless(update.to_wire() == goodwire)
Beispiel #18
0
 def test_to_wire3(self):  # type: () -> None
     update = dns.update.Update("example")
     update.id = 1
     update.present("foo")
     update.present("foo", "a")
     update.present("bar", "a", "10.0.0.5")
     update.absent("blaz2")
     update.absent("blaz2", "a")
     update.replace("foo", 300, "a", "10.0.0.1", "10.0.0.2")
     update.add("bar", dns.rdataset.from_text(1, 1, 300, "10.0.0.3"))
     update.delete("bar", "a", "10.0.0.4")
     update.delete("blaz", "a")
     update.delete("blaz2")
     self.assertEqual(update.to_wire(), goodwire)
Beispiel #19
0
 def test_to_wire2(self): # type: () -> None
     update = dns.update.Update('example')
     update.id = 1
     update.present('foo')
     update.present('foo', 'a')
     update.present('bar', 'a', '10.0.0.5')
     update.absent('blaz2')
     update.absent('blaz2', 'a')
     update.replace('foo', 300, 'a', '10.0.0.1', '10.0.0.2')
     update.add('bar', 300, dns.rdata.from_text(1, 1, '10.0.0.3'))
     update.delete('bar', 'a', '10.0.0.4')
     update.delete('blaz', 'a')
     update.delete('blaz2')
     self.failUnless(update.to_wire() == goodwire)
Beispiel #20
0
    def update_zone(self, zone_name, zi, db_soa_serial=None, 
            candidate_soa_serial=None,
            force_soa_serial_update=False, wrap_serial_next_time=False,
            date_stamp=None, nsec3_seed=False, clear_dnskey=False,
            clear_nsec3=False):
        """
        Use dnspython to update a Zone in the DNS server

        Use wrap_serial_next_time to 'fix' SOA serial numbers grossly not 
        in the operations format YYYYMMDDnn. date is a datetime object in 
        localtime.
        """
        # Read in via AXFR zone for comparison purposes
        try:
            zone, dnskey_flag, nsec3param_flag = self.read_zone(zone_name)
            update_info = {'dnskey_flag': dnskey_flag, 
                            'nsec3param_flag': nsec3param_flag}
        except NoSuchZoneOnServerError as exc:
            msg = str(exc)
            #return (RCODE_FATAL, msg, None, None)
            # Send RESET as server not configured yet.
            return (RCODE_RESET, msg, None, None)

        except (dns.query.UnexpectedSource, dns.query.BadResponse) as exc:
            msg = ("Zone '%s', - server %s not operating correctly." 
                        % (zone_name, server.server_name))
            return (RCODE_FATAL, msg, None, None)
        
        except (IOError, OSError) as exc:
            if exc.errno in (errno.EACCES, errno.EPERM, errno.ECONNREFUSED, 
                    errno.ENETUNREACH, errno.ETIMEDOUT):
                msg = ("Zone '%s' - server %s:%s not available - %s"
                        % (zone_name, self.server, self.port, exc.strerror))
                return (RCODE_ERROR, msg, None, None)
            msg = ("Zone '%s' - server %s:%s, fatal error %s."
                   % (zone_name, self.server, self.port, exc.strerror))
            return (RCODE_FATAL, msg, None, None)


        # Get current SOA record for zone to include as prerequiste in update
        # Makes update transaction idempotent
        current_soa_rr = zone.find_rdataset(zone.origin, RRTYPE_SOA).items[0]
        
        update_soa_serial_flag = False
        curr_serial_no = self.get_serial_no(zone)
        # In case of a DR failover, our DB can have a more recent serial number
        # than in name server
        try:
            new_serial_no = new_soa_serial_no(curr_serial_no, zone_name, 
                    db_soa_serial=db_soa_serial,
                    candidate=candidate_soa_serial,
                    wrap_serial_next_time=wrap_serial_next_time,
                    date_stamp=date_stamp)
        except SOASerialError as exc:
            msg = str(exc)
            if (not sys.stdin.isatty()):
                log_critical(msg)
            return (RCODE_FATAL, msg, None, None)
        if wrap_serial_next_time or force_soa_serial_update:
            # Apply serial number to SOA record.
            zi.update_soa_serial(new_serial_no)
        else:
            # An increment should only be performed after difference
            update_soa_serial_flag = True

        # Compare server_zone with zi.rrs
        # Find additions and deletions
        del_rrs = [rr for rr in zone.iterate_rdatas()
                    if rr not in zi.iterate_dnspython_rrs()]
        add_rrs = [rr for rr in zi.iterate_dnspython_rrs()
                    if rr not in zone.iterate_rdatas()]
        # Check if DNSSEC settings need to be changed 
        do_clear_nsec3 = clear_nsec3 and nsec3param_flag
        do_clear_dnskey = clear_dnskey and dnskey_flag
        do_nsec3_seed = nsec3_seed and not nsec3param_flag

        if (not del_rrs and not add_rrs and not do_clear_nsec3 
                and not do_clear_dnskey and not do_nsec3_seed):
            msg = ("Domain '%s' not updated as no change detected" 
                        % (zone_name))
            return (RCODE_NOCHANGE, msg, curr_serial_no, update_info)
      
        # Incremental update of SOA serial number
        soa_rdtype = dns.rdatatype.from_text(RRTYPE_SOA)
        if update_soa_serial_flag:
            # Apply serial number to SOA record.
            zi.update_soa_serial(new_serial_no)
            # recalculate add_rrs - got to be done or else updates will be
            # missed
            add_rrs = [rr for rr in zi.iterate_dnspython_rrs()
                        if rr not in zone.iterate_rdatas()]
       
        # Groom updates for DynDNS update perculiarities

        # SOA can never be deleted RFC 2136 Section 3.4.2.3 and 3.4.2.4
        # so skip this.
        del_rrs = [rr for rr in del_rrs 
                if (rr[2].rdtype != soa_rdtype)]

        # Can never delete the last NS on the root of a zone,
        # so pre add all '@' NS records (RFC 2136 Sec
        # 3.4.2.4)
        tl_label = dns.name.from_text('@', origin=dns.name.empty)
        ns_rdtype = dns.rdatatype.from_text(RRTYPE_NS)
        pre_add_rrs = [rr for rr in add_rrs 
                        if (rr[0] == tl_label and rr[2].rdtype == ns_rdtype)]
        tl_ns_rdata = [rr[2] for rr in pre_add_rrs]
        add_rrs = [rr for rr in add_rrs if rr not in pre_add_rrs]
        # Remove '@' NS delete from del_rrs if record in pre_add_rrs 
        # ie, we are just doing a TTL update!
        del_rrs = [rr for rr in del_rrs 
                    if (not(rr[0] == tl_label and rr[2] in tl_ns_rdata))]

        # CNAMEs can only be added to vacant nodes, or totally replace 
        # RRSET on a node RFC 2136 Section 3.4.2.2
        # Choose to enforce this at zi API level.

        # DNSSEC processing - prepare NSEC3PARM rdata 
        if do_nsec3_seed:
            rn = random.getrandbits(int(settings['nsec3_salt_bit_length']))
            hash_alg = settings['nsec3_hash_algorithm']
            flags = settings['nsec3_flags']
            iterations = settings['nsec3_iterations']
            nsec3param_rdata = ("%s %s %s %016x" 
                                % (hash_alg, flags, iterations, rn))
            # Test rn as random can produce garbage sometimes...
            rdata_list = nsec3param_rdata.split()
            try:
                # This is the piece of code where dnspython blows up...
                stuff = bytes.fromhex(rdata_list[-1])
            except Exception:
                msg = ("Failed to seed NSEC3 salt - SM reset required")
                return (RCODE_RESET, msg, None, update_info)

        # Prepare dnspython tsigkeyring
        keyring = dns.tsigkeyring.from_text({
            self.key_name : self.tsig_key['secret'] })
        if (self.tsig_key['algorithm'] == 'hmac-md5'):
            key_algorithm = dns.tsig.HMAC_MD5
        else:
            key_algorithm = dns.name.from_text(self.tsig_key['algorithm'])
        
        # Create update 
        # We have to use absolute FQDNs on LHS  and RHS to make sure updates
        # to NS etc happen
        # While doing this also handle wee things for DNSSEC processing
        origin = dns.name.from_text(zone_name)
        update = dns.update.Update(origin, keyring=keyring,
                    keyname = self.key_name, keyalgorithm=key_algorithm)
        update.present(origin, current_soa_rr)
        for rr in pre_add_rrs:
            update.add(rr[0], rr[1], rr[2])
        for rr in del_rrs:
            update.delete(rr[0], rr[2])
        # Add DNSSEC clearance stuff to end of delete section of update
        if do_clear_nsec3:
            update.delete(origin, RRTYPE_NSEC3PARAM)
        if do_clear_dnskey:
            update.delete(origin, RRTYPE_DNSKEY)
        for rr in add_rrs:
            update.add(rr[0], rr[1], rr[2])
        # NSEC3PARAM seeding
        if do_nsec3_seed:
            update.add(origin, '0', RRTYPE_NSEC3PARAM, nsec3param_rdata)

        # Do dee TING!
        response = dns.query.tcp(update, self.server, port=self.port)

        # Process reply
        rcode = response.rcode()
        rcode_text = dns.rcode.to_text(response.rcode())
        success_rcodes = (dns.rcode.NOERROR)
        if (rcode in self.success_rcodes):
            msg = ("Update '%s' to domain '%s' succeeded" 
                            % (new_serial_no, zone_name))
            return (RCODE_OK, msg, new_serial_no, update_info)
        elif (rcode in self.retry_rcodes):
            msg = ("Update '%s' to domain '%s' failed: %s - will retry"
                        % (new_serial_no, zone_name, rcode_text))
            return (RCODE_ERROR, msg, None, update_info)
        elif (rcode in self.reset_rcodes):
            msg = ("Update '%s' to domain '%s' failed: %s - SM reset required"
                        % (new_serial_no, zone_name, rcode_text))
            return (RCODE_RESET, msg, None, update_info)
        elif (rcode in self.fatal_rcodes):
            msg = ("Update '%s' to domain '%s' permanently failed: %s"
                        % (new_serial_no, zone_name, rcode_text))
            return (RCODE_FATAL, msg, None, update_info)
        else:
            msg = ("Update '%s' to domain '%s' permanently failed: '%s'"
                    " - unknown response"
                        % (new_serial_no, zone_name, response.rcode()))
            return (RCODE_FATAL, msg, None, update_info)
Beispiel #21
0
    def update_zone(self,
                    zone_name,
                    zi,
                    db_soa_serial=None,
                    candidate_soa_serial=None,
                    force_soa_serial_update=False,
                    wrap_serial_next_time=False,
                    date_stamp=None,
                    nsec3_seed=False,
                    clear_dnskey=False,
                    clear_nsec3=False):
        """
        Use dnspython to update a Zone in the DNS server

        Use wrap_serial_next_time to 'fix' SOA serial numbers grossly not 
        in the operations format YYYYMMDDnn. date is a datetime object in 
        localtime.
        """
        # Read in via AXFR zone for comparison purposes
        try:
            zone, dnskey_flag, nsec3param_flag = self.read_zone(zone_name)
            update_info = {
                'dnskey_flag': dnskey_flag,
                'nsec3param_flag': nsec3param_flag
            }
        except NoSuchZoneOnServerError as exc:
            msg = str(exc)
            #return (RCODE_FATAL, msg, None, None)
            # Send RESET as server not configured yet.
            return (RCODE_RESET, msg, None, None)

        except (dns.query.UnexpectedSource, dns.query.BadResponse) as exc:
            msg = ("Zone '%s', - server %s not operating correctly." %
                   (zone_name, server.server_name))
            return (RCODE_FATAL, msg, None, None)

        except (IOError, OSError) as exc:
            if exc.errno in (errno.EACCES, errno.EPERM, errno.ECONNREFUSED,
                             errno.ENETUNREACH, errno.ETIMEDOUT):
                msg = ("Zone '%s' - server %s:%s not available - %s" %
                       (zone_name, self.server, self.port, exc.strerror))
                return (RCODE_ERROR, msg, None, None)
            msg = ("Zone '%s' - server %s:%s, fatal error %s." %
                   (zone_name, self.server, self.port, exc.strerror))
            return (RCODE_FATAL, msg, None, None)

        # Get current SOA record for zone to include as prerequiste in update
        # Makes update transaction idempotent
        current_soa_rr = zone.find_rdataset(zone.origin, RRTYPE_SOA).items[0]

        update_soa_serial_flag = False
        curr_serial_no = self.get_serial_no(zone)
        # In case of a DR failover, our DB can have a more recent serial number
        # than in name server
        try:
            new_serial_no = new_soa_serial_no(
                curr_serial_no,
                zone_name,
                db_soa_serial=db_soa_serial,
                candidate=candidate_soa_serial,
                wrap_serial_next_time=wrap_serial_next_time,
                date_stamp=date_stamp)
        except SOASerialError as exc:
            msg = str(exc)
            if (not sys.stdin.isatty()):
                log_critical(msg)
            return (RCODE_FATAL, msg, None, None)
        if wrap_serial_next_time or force_soa_serial_update:
            # Apply serial number to SOA record.
            zi.update_soa_serial(new_serial_no)
        else:
            # An increment should only be performed after difference
            update_soa_serial_flag = True

        # Compare server_zone with zi.rrs
        # Find additions and deletions
        del_rrs = [
            rr for rr in zone.iterate_rdatas()
            if rr not in zi.iterate_dnspython_rrs()
        ]
        add_rrs = [
            rr for rr in zi.iterate_dnspython_rrs()
            if rr not in zone.iterate_rdatas()
        ]
        # Check if DNSSEC settings need to be changed
        do_clear_nsec3 = clear_nsec3 and nsec3param_flag
        do_clear_dnskey = clear_dnskey and dnskey_flag
        do_nsec3_seed = nsec3_seed and not nsec3param_flag

        if (not del_rrs and not add_rrs and not do_clear_nsec3
                and not do_clear_dnskey and not do_nsec3_seed):
            msg = ("Domain '%s' not updated as no change detected" %
                   (zone_name))
            return (RCODE_NOCHANGE, msg, curr_serial_no, update_info)

        # Incremental update of SOA serial number
        soa_rdtype = dns.rdatatype.from_text(RRTYPE_SOA)
        if update_soa_serial_flag:
            # Apply serial number to SOA record.
            zi.update_soa_serial(new_serial_no)
            # recalculate add_rrs - got to be done or else updates will be
            # missed
            add_rrs = [
                rr for rr in zi.iterate_dnspython_rrs()
                if rr not in zone.iterate_rdatas()
            ]

        # Groom updates for DynDNS update perculiarities

        # SOA can never be deleted RFC 2136 Section 3.4.2.3 and 3.4.2.4
        # so skip this.
        del_rrs = [rr for rr in del_rrs if (rr[2].rdtype != soa_rdtype)]

        # Can never delete the last NS on the root of a zone,
        # so pre add all '@' NS records (RFC 2136 Sec
        # 3.4.2.4)
        tl_label = dns.name.from_text('@', origin=dns.name.empty)
        ns_rdtype = dns.rdatatype.from_text(RRTYPE_NS)
        pre_add_rrs = [
            rr for rr in add_rrs
            if (rr[0] == tl_label and rr[2].rdtype == ns_rdtype)
        ]
        tl_ns_rdata = [rr[2] for rr in pre_add_rrs]
        add_rrs = [rr for rr in add_rrs if rr not in pre_add_rrs]
        # Remove '@' NS delete from del_rrs if record in pre_add_rrs
        # ie, we are just doing a TTL update!
        del_rrs = [
            rr for rr in del_rrs
            if (not (rr[0] == tl_label and rr[2] in tl_ns_rdata))
        ]

        # CNAMEs can only be added to vacant nodes, or totally replace
        # RRSET on a node RFC 2136 Section 3.4.2.2
        # Choose to enforce this at zi API level.

        # DNSSEC processing - prepare NSEC3PARM rdata
        if do_nsec3_seed:
            rn = random.getrandbits(int(settings['nsec3_salt_bit_length']))
            hash_alg = settings['nsec3_hash_algorithm']
            flags = settings['nsec3_flags']
            iterations = settings['nsec3_iterations']
            nsec3param_rdata = ("%s %s %s %016x" %
                                (hash_alg, flags, iterations, rn))
            # Test rn as random can produce garbage sometimes...
            rdata_list = nsec3param_rdata.split()
            try:
                # This is the piece of code where dnspython blows up...
                stuff = bytes.fromhex(rdata_list[-1])
            except Exception:
                msg = ("Failed to seed NSEC3 salt - SM reset required")
                return (RCODE_RESET, msg, None, update_info)

        # Prepare dnspython tsigkeyring
        keyring = dns.tsigkeyring.from_text(
            {self.key_name: self.tsig_key['secret']})
        if (self.tsig_key['algorithm'] == 'hmac-md5'):
            key_algorithm = dns.tsig.HMAC_MD5
        else:
            key_algorithm = dns.name.from_text(self.tsig_key['algorithm'])

        # Create update
        # We have to use absolute FQDNs on LHS  and RHS to make sure updates
        # to NS etc happen
        # While doing this also handle wee things for DNSSEC processing
        origin = dns.name.from_text(zone_name)
        update = dns.update.Update(origin,
                                   keyring=keyring,
                                   keyname=self.key_name,
                                   keyalgorithm=key_algorithm)
        update.present(origin, current_soa_rr)
        for rr in pre_add_rrs:
            update.add(rr[0], rr[1], rr[2])
        for rr in del_rrs:
            update.delete(rr[0], rr[2])
        # Add DNSSEC clearance stuff to end of delete section of update
        if do_clear_nsec3:
            update.delete(origin, RRTYPE_NSEC3PARAM)
        if do_clear_dnskey:
            update.delete(origin, RRTYPE_DNSKEY)
        for rr in add_rrs:
            update.add(rr[0], rr[1], rr[2])
        # NSEC3PARAM seeding
        if do_nsec3_seed:
            update.add(origin, '0', RRTYPE_NSEC3PARAM, nsec3param_rdata)

        # Do dee TING!
        response = dns.query.tcp(update, self.server, port=self.port)

        # Process reply
        rcode = response.rcode()
        rcode_text = dns.rcode.to_text(response.rcode())
        success_rcodes = (dns.rcode.NOERROR)
        if (rcode in self.success_rcodes):
            msg = ("Update '%s' to domain '%s' succeeded" %
                   (new_serial_no, zone_name))
            return (RCODE_OK, msg, new_serial_no, update_info)
        elif (rcode in self.retry_rcodes):
            msg = ("Update '%s' to domain '%s' failed: %s - will retry" %
                   (new_serial_no, zone_name, rcode_text))
            return (RCODE_ERROR, msg, None, update_info)
        elif (rcode in self.reset_rcodes):
            msg = (
                "Update '%s' to domain '%s' failed: %s - SM reset required" %
                (new_serial_no, zone_name, rcode_text))
            return (RCODE_RESET, msg, None, update_info)
        elif (rcode in self.fatal_rcodes):
            msg = ("Update '%s' to domain '%s' permanently failed: %s" %
                   (new_serial_no, zone_name, rcode_text))
            return (RCODE_FATAL, msg, None, update_info)
        else:
            msg = ("Update '%s' to domain '%s' permanently failed: '%s'"
                   " - unknown response" %
                   (new_serial_no, zone_name, response.rcode()))
            return (RCODE_FATAL, msg, None, update_info)