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
This commit is contained in:
Sven Luethi committed 2026-07-13 13:41:29 +02:00
1 parent b72fa9b664
commit fd8a5a0d9d
5 files changed
+47 -79

No files matched your search

+31 -62
View File
@@ -7,35 +7,40 @@ from netbox_dns.models import Zone
from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier
from uuid import uuid4 from uuid import uuid4
from base64 import b32encode 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__) LOGGER = get_logger(__name__)
def init() -> None:
_init_soa_serial()
_create_missing_member_identifiers()
def increment_soa_serial() -> int: def increment_soa_serial() -> int:
with transaction.atomic(): with transaction.atomic():
try: try:
soa_serial = IntegerKeyValueSetting.objects.select_for_update().get( soa_serial_obj = (
key="catalog-zone-soa-serial" 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: if new_soa_serial > 0xFFFFFFFF:
LOGGER.warning( 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 new_soa_serial = 1
soa_serial.value = new_soa_serial soa_serial_obj.value = new_soa_serial
soa_serial.save() soa_serial_obj.save()
soa_serial.refresh_from_db() soa_serial_obj.refresh_from_db()
LOGGER.debug(f"Catalog zone SOA serial number is now {soa_serial.value}") 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: except OperationalError as e:
LOGGER.error( LOGGER.error(
f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}" 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) qname = dns.name.from_text(nb_zone.name, dns.name.root)
# Create PTR record # 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) ptr_name = dns.name.from_text(p_name, ptr_base)
if not ptr_name.is_subdomain(origin): if not ptr_name.is_subdomain(origin):
@@ -113,20 +125,6 @@ def create_zone(name, view_name) -> dns.zone.Zone:
return None 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: def _generate_member_identifier() -> str:
return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8") return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8")
@@ -142,40 +140,11 @@ def update_member_identifier(zone: Zone) -> None:
close_old_connections() 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: def _create_soa_rdataset() -> dns.rdataset:
try: 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 # SOA Record components
ttl = 0 ttl = 0
@@ -14,7 +14,6 @@ from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
from django.db import close_old_connections, OperationalError from django.db import close_old_connections, OperationalError
from netbox_dns.models import View 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.request_handler import UDPRequestHandler, TCPRequestHandler
from netbox_dns_bridge.dns_server import UDPDNSServer, TCPDNSServer from netbox_dns_bridge.dns_server import UDPDNSServer, TCPDNSServer
from netbox_dns_bridge.logger import get_logger from netbox_dns_bridge.logger import get_logger
@@ -80,7 +79,6 @@ class Command(BaseCommand):
# Load parameters # Load parameters
port = options["port"] port = options["port"]
address = options["address"] address = options["address"]
catzm.init()
self.tsig_keys = SETTINGS.get("tsig_keys", None) self.tsig_keys = SETTINGS.get("tsig_keys", None)
if not self.tsig_keys: if not self.tsig_keys:
+8 -14
View File
@@ -52,12 +52,14 @@ class SendDNSNotify(JobRunner):
view_name = kwargs["view_name"] view_name = kwargs["view_name"]
try: try:
catz_serial = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial").value catz_serial = IntegerKeyValueSetting.objects.get(
key="catalog-zone-soa-serial"
).value
zones = [ zones = [
(kwargs["zone_name"], kwargs["soa_serial"]), (kwargs["zone_name"], kwargs["soa_serial"]),
("catz", catz_serial), ("catz", catz_serial),
(f"{view_name}.catz", catz_serial) (f"{view_name}.catz", catz_serial),
] ]
view = View.objects.get(name=view_name) view = View.objects.get(name=view_name)
@@ -80,9 +82,7 @@ class SendDNSNotify(JobRunner):
dns.rdatatype.SOA, dns.rdatatype.SOA,
f"invalid. invalid. {soa_serial} 60 10 1209600 0", f"invalid. invalid. {soa_serial} 60 10 1209600 0",
) )
soa_rrset = dns.rrset.from_rdata( soa_rrset = dns.rrset.from_rdata(zone_name, 0, soa_rdata)
zone_name, 0, soa_rdata
)
msg.authority.append(soa_rrset) msg.authority.append(soa_rrset)
msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name) msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name)
@@ -114,13 +114,9 @@ class SendDNSNotify(JobRunner):
def schedule(zone: Zone): def schedule(zone: Zone):
try: try:
soa_record = Record.objects.filter( soa_record = Record.objects.filter(zone=zone, type="SOA", active=True).first()
zone=zone, type="SOA", active=True
).first()
if soa_record is None: if soa_record is None:
LOGGER.error( LOGGER.error(f"Zone {zone.name} has no active SOA record — skipping NOTIFY")
f"Zone {zone.name} has no active SOA record — skipping NOTIFY"
)
return return
soa_serial = int(soa_record.value.split()[2]) soa_serial = int(soa_record.value.split()[2])
@@ -130,9 +126,7 @@ def schedule(zone: Zone):
close_old_connections() close_old_connections()
return return
except (ValueError, IndexError) as e: except (ValueError, IndexError) as e:
LOGGER.error( LOGGER.error(f"Failed to parse SOA serial for zone {zone.name}: {e}")
f"Failed to parse SOA serial for zone {zone.name}: {e}"
)
return return
SendDNSNotify.enqueue( SendDNSNotify.enqueue(
+4
View File
@@ -215,6 +215,10 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
else: else:
rrsets.append(rrset) 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.insert(0, soa_rrset) # Opening SOA
rrsets.append(soa_rrset) # Closing SOA rrsets.append(soa_rrset) # Closing SOA
@@ -27,13 +27,15 @@ def zone_post_save(sender, instance, created, **kwargs):
zone = instance zone = instance
# Quit if this is not the first signal in this session for this zone. # Quit if this is not the first signal in this session for this zone.
try:
request = current_request.get() request = current_request.get()
if request is not None:
handled_zones = getattr(request, "_handled_zones", set()) handled_zones = getattr(request, "_handled_zones", set())
if zone.pk in handled_zones: if zone.pk in handled_zones:
return return
handled_zones.add(zone.pk) handled_zones.add(zone.pk)
request._handled_zones = handled_zones request._handled_zones = handled_zones
except LookupError:
pass
# On commit, increment soa serial and if notify is enabled, schedule bg job. # On commit, increment soa serial and if notify is enabled, schedule bg job.
def _on_commit(): def _on_commit():
@@ -53,6 +55,7 @@ def zone_post_save(sender, instance, created, **kwargs):
if old_name and zone.name != old_name: if old_name and zone.name != old_name:
catzm.update_member_identifier(zone) catzm.update_member_identifier(zone)
@receiver(post_delete, sender=Zone) @receiver(post_delete, sender=Zone)
def zone_post_delete(sender, instance, **kwargs): def zone_post_delete(sender, instance, **kwargs):
catzm.increment_soa_serial() catzm.increment_soa_serial()