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 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
@@ -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:
+8 -14
View File
@@ -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)
(f"{view_name}.catz", catz_serial),
]
view = View.objects.get(name=view_name)
@@ -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(
+4
View File
@@ -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
@@ -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.
try:
request = current_request.get()
if request is not None:
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()