From fd8a5a0d9d6f4d0e0dfcec1da9b8a8f1cd4ecd47 Mon Sep 17 00:00:00 2001 From: Sven Luethi Date: Mon, 13 Jul 2026 13:28:27 +0200 Subject: [PATCH] modified: netbox_dns_bridge/catalog_zone_manager.py modified: netbox_dns_bridge/management/commands/dns-transfer-endpoint.py modified: netbox_dns_bridge/request_handler.py modified: netbox_dns_bridge/signals/netbox_dns_zones.py Range of bug fixes and improvements --- netbox_dns_bridge/catalog_zone_manager.py | 93 +++++++------------ .../commands/dns-transfer-endpoint.py | 2 - netbox_dns_bridge/notify_handler.py | 26 ++---- netbox_dns_bridge/request_handler.py | 4 + netbox_dns_bridge/signals/netbox_dns_zones.py | 7 +- 5 files changed, 50 insertions(+), 82 deletions(-) diff --git a/netbox_dns_bridge/catalog_zone_manager.py b/netbox_dns_bridge/catalog_zone_manager.py index 5e5da3e..1a9d305 100644 --- a/netbox_dns_bridge/catalog_zone_manager.py +++ b/netbox_dns_bridge/catalog_zone_manager.py @@ -7,35 +7,40 @@ from netbox_dns.models import Zone from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier from uuid import uuid4 from base64 import b32encode -from django.db import transaction, close_old_connections, OperationalError +from django.db import ( + transaction, + close_old_connections, + OperationalError, + RelatedObjectDoesNotExist +) LOGGER = get_logger(__name__) -def init() -> None: - _init_soa_serial() - _create_missing_member_identifiers() - - def increment_soa_serial() -> int: with transaction.atomic(): try: - soa_serial = IntegerKeyValueSetting.objects.select_for_update().get( - key="catalog-zone-soa-serial" + soa_serial_obj = ( + IntegerKeyValueSetting.objects.select_for_update().get_or_create( + key="catalog-zone-soa-serial", + defaults={"value": 1}, + ) ) - new_soa_serial = soa_serial.value + 1 + new_soa_serial = soa_serial_obj.value + 1 if new_soa_serial > 0xFFFFFFFF: LOGGER.warning( - f"Catalog serial {soa_serial.value} reached max — wrapping back to 1" + f"Catalog serial {soa_serial_obj.value} reached max — wrapping back to 1" ) new_soa_serial = 1 - soa_serial.value = new_soa_serial - soa_serial.save() - soa_serial.refresh_from_db() - LOGGER.debug(f"Catalog zone SOA serial number is now {soa_serial.value}") + soa_serial_obj.value = new_soa_serial + soa_serial_obj.save() + soa_serial_obj.refresh_from_db() + LOGGER.debug( + f"Catalog zone SOA serial number is now {soa_serial_obj.value}" + ) - return soa_serial.value + return soa_serial_obj.value except OperationalError as e: LOGGER.error( f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}" @@ -64,7 +69,14 @@ def create_zone(name, view_name) -> dns.zone.Zone: qname = dns.name.from_text(nb_zone.name, dns.name.root) # Create PTR record - p_name = nb_zone.catz_identifier.name + try: + catz_identifier = nb_zone.catz_identifier + except RelatedObjectDoesNotExist: + catz_identifier = nb_zone.catz_identifier.create( + name=_generate_member_identifier() + ) + + p_name = catz_identifier.name ptr_name = dns.name.from_text(p_name, ptr_base) if not ptr_name.is_subdomain(origin): @@ -113,20 +125,6 @@ def create_zone(name, view_name) -> dns.zone.Zone: return None -def _init_soa_serial() -> None: - try: - IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial") - LOGGER.info("Catalog zone SOA serial number loaded from database") - except IntegerKeyValueSetting.DoesNotExist: - IntegerKeyValueSetting.objects.create(key="catalog-zone-soa-serial", value=1) - LOGGER.debug( - "Catalog zone SOA serial number was not set in the database. Set to 1" - ) - except OperationalError as e: - LOGGER.error(f"ERROR: Failed to initialize the Catalog Zone's SOA serial: {e}") - close_old_connections() - - def _generate_member_identifier() -> str: return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8") @@ -142,40 +140,11 @@ def update_member_identifier(zone: Zone) -> None: close_old_connections() -def _create_missing_member_identifiers() -> None: - try: - existing_zone_ids = CatalogZoneMemberIdentifier.objects.values_list( - "zone_id", flat=True - ) - - missing_zones = Zone.objects.exclude(id__in=existing_zone_ids) - - new_objects = [ - CatalogZoneMemberIdentifier( - zone=zone, - name=_generate_member_identifier(), - ) - for zone in missing_zones - ] - - for identifier in new_objects: - LOGGER.debug( - f"Zone {identifier.zone} missing catz member identifier. Creating..." - ) - - CatalogZoneMemberIdentifier.objects.bulk_create( - new_objects, - ignore_conflicts=False, - ) - - except OperationalError as e: - LOGGER.error(f"ERROR: Could not create catz member identifier: {e}") - close_old_connections() - - def _create_soa_rdataset() -> dns.rdataset: try: - serial = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial").value + serial = IntegerKeyValueSetting.objects.get_or_create( + key="catalog-zone-soa-serial", defaults={"value": 1} + ).value # SOA Record components ttl = 0 diff --git a/netbox_dns_bridge/management/commands/dns-transfer-endpoint.py b/netbox_dns_bridge/management/commands/dns-transfer-endpoint.py index e6a1c4e..2576d47 100644 --- a/netbox_dns_bridge/management/commands/dns-transfer-endpoint.py +++ b/netbox_dns_bridge/management/commands/dns-transfer-endpoint.py @@ -14,7 +14,6 @@ from django.core.management.base import BaseCommand from django.conf import settings from django.db import close_old_connections, OperationalError from netbox_dns.models import View -from netbox_dns_bridge import catalog_zone_manager as catzm from netbox_dns_bridge.request_handler import UDPRequestHandler, TCPRequestHandler from netbox_dns_bridge.dns_server import UDPDNSServer, TCPDNSServer from netbox_dns_bridge.logger import get_logger @@ -80,7 +79,6 @@ class Command(BaseCommand): # Load parameters port = options["port"] address = options["address"] - catzm.init() self.tsig_keys = SETTINGS.get("tsig_keys", None) if not self.tsig_keys: diff --git a/netbox_dns_bridge/notify_handler.py b/netbox_dns_bridge/notify_handler.py index ce9b6b6..c8bda1f 100644 --- a/netbox_dns_bridge/notify_handler.py +++ b/netbox_dns_bridge/notify_handler.py @@ -52,12 +52,14 @@ class SendDNSNotify(JobRunner): view_name = kwargs["view_name"] try: - catz_serial = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial").value + catz_serial = IntegerKeyValueSetting.objects.get( + key="catalog-zone-soa-serial" + ).value zones = [ (kwargs["zone_name"], kwargs["soa_serial"]), - ("catz", catz_serial), - (f"{view_name}.catz", catz_serial) + ("catz", catz_serial), + (f"{view_name}.catz", catz_serial), ] view = View.objects.get(name=view_name) @@ -72,7 +74,7 @@ class SendDNSNotify(JobRunner): for zone_name, soa_serial in zones: zone_name = dns.name.from_text(zone_name, dns.name.root).to_text() msg = dns.message.make_query(zone_name, dns.rdatatype.SOA) - msg.flags |= dns.flags.AA # = 0 + msg.flags |= dns.flags.AA # = 0 msg.set_opcode(dns.opcode.NOTIFY) soa_rdata = dns.rdata.from_text( @@ -80,9 +82,7 @@ class SendDNSNotify(JobRunner): dns.rdatatype.SOA, f"invalid. invalid. {soa_serial} 60 10 1209600 0", ) - soa_rrset = dns.rrset.from_rdata( - zone_name, 0, soa_rdata - ) + soa_rrset = dns.rrset.from_rdata(zone_name, 0, soa_rdata) msg.authority.append(soa_rrset) msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name) @@ -114,13 +114,9 @@ class SendDNSNotify(JobRunner): def schedule(zone: Zone): try: - soa_record = Record.objects.filter( - zone=zone, type="SOA", active=True - ).first() + soa_record = Record.objects.filter(zone=zone, type="SOA", active=True).first() if soa_record is None: - LOGGER.error( - f"Zone {zone.name} has no active SOA record — skipping NOTIFY" - ) + LOGGER.error(f"Zone {zone.name} has no active SOA record — skipping NOTIFY") return soa_serial = int(soa_record.value.split()[2]) @@ -130,9 +126,7 @@ def schedule(zone: Zone): close_old_connections() return except (ValueError, IndexError) as e: - LOGGER.error( - f"Failed to parse SOA serial for zone {zone.name}: {e}" - ) + LOGGER.error(f"Failed to parse SOA serial for zone {zone.name}: {e}") return SendDNSNotify.enqueue( diff --git a/netbox_dns_bridge/request_handler.py b/netbox_dns_bridge/request_handler.py index e6d111e..a8f32d5 100644 --- a/netbox_dns_bridge/request_handler.py +++ b/netbox_dns_bridge/request_handler.py @@ -215,6 +215,10 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler): else: rrsets.append(rrset) + if soa_rrset is None: + LOGGER.error(f"Zone {dname} has no SOA — aborting") + return + rrsets.insert(0, soa_rrset) # Opening SOA rrsets.append(soa_rrset) # Closing SOA diff --git a/netbox_dns_bridge/signals/netbox_dns_zones.py b/netbox_dns_bridge/signals/netbox_dns_zones.py index 0056e8d..38712d1 100644 --- a/netbox_dns_bridge/signals/netbox_dns_zones.py +++ b/netbox_dns_bridge/signals/netbox_dns_zones.py @@ -27,13 +27,15 @@ def zone_post_save(sender, instance, created, **kwargs): zone = instance # Quit if this is not the first signal in this session for this zone. - request = current_request.get() - if request is not None: + try: + request = current_request.get() handled_zones = getattr(request, "_handled_zones", set()) if zone.pk in handled_zones: return handled_zones.add(zone.pk) request._handled_zones = handled_zones + except LookupError: + pass # On commit, increment soa serial and if notify is enabled, schedule bg job. def _on_commit(): @@ -53,6 +55,7 @@ def zone_post_save(sender, instance, created, **kwargs): if old_name and zone.name != old_name: catzm.update_member_identifier(zone) + @receiver(post_delete, sender=Zone) def zone_post_delete(sender, instance, **kwargs): catzm.increment_soa_serial()