Compare commits

...
3 Commits
Author SHA1 Message Date
sven 3fa3cd6a01 Well tested core components (DNS flow, signal handling, background jobs).
Finally able to bump to version v1.7.0
2026-09-22 23:54:36 +02:00
sven c15bdfca05 Refactor: move shared NOTIFY helpers into a NotifyJobRunner base class
- New NotifyJobRunner base class (extends netbox.jobs.JobRunner) holds
  the plugin SETTINGS and the helpers shared by both NOTIFY jobs:
  load_tsig_key(), send_notify() and get_soa_serial() (promoted from
  the former module-level _-prefixed functions).
- SendClientNotify and SendNSNotify extend the base and access the
  helpers via self. Behavior, job names and kwargs are unchanged.
- The nameserver-only helpers moved into SendNSNotify as
  notify_ns_enabled() (classmethod, reads cls.SETTINGS) and
  resolve_zone_nameserver_ips() (instance method).
- No changes to any callers: schedule_client_notify(),
  schedule_ns_notify() and the catalog zone NOTIFY scheduling keep
  their names and call sites.
2026-09-22 23:15:09 +02:00
sven fe24dbdd98 Add configurable catalog zone naming
- New setting 'catalog_zone_name' (default: 'catz') to change the name of
  the per-view catalog zone. Full DNS names (multiple labels) are
  supported; names are handled as dns.name.Name objects, making the
  comparison case-insensitive and tolerant of trailing dots.
- New setting 'catalog_zone_name_includes_view' (default: False). When
  enabled, the catalog zone name is prefixed with the view name:
  '<view>.<catalog_zone_name>'.
- The transfer endpoint now serves exactly the one configured catalog
  zone name per view instead of accepting both 'catz' and
  '<view>.catz'.
- The catalog zone NOTIFY now targets only the configured name instead
  of notifying for both variants. Scheduling moved to
  catalog_zone_manager, which owns all catalog zone naming.
2026-09-22 22:22:26 +02:00
5 changed files with 233 additions and 189 deletions

No files matched your search

+28 -3
View File
@@ -24,9 +24,9 @@ While providing Zone transfers via AXFR, the Server also exposes specialized cat
and other RFC9432 compliant DNS Servers use to automatically discover newly created zones and remove and other RFC9432 compliant DNS Servers use to automatically discover newly created zones and remove
deleted ones. The plugin supports views and basic DNS security via TSIG. deleted ones. The plugin supports views and basic DNS security via TSIG.
The plugin exposes one catalog zone per view. Each catalog zone is made available under the special The plugin exposes one catalog zone per view. The catalog zone name is configurable (see
zone name **"catz"** and addtionally under **"[viewname].catz"** and may be queried through the [Catalog Zone Naming](#catalog-zone-naming) below) and defaults to **"catz"**. It may be queried
built-in DNS server just like any other dns zone. through the built-in DNS server just like any other dns zone.
For proper operation, each view requires an installed TSIG key, and the `dns-transfer-endpoint` must For proper operation, each view requires an installed TSIG key, and the `dns-transfer-endpoint` must
be running as a separate background service using the `manage.py` command. Note that DNSSEC support be running as a separate background service using the `manage.py` command. Note that DNSSEC support
@@ -59,6 +59,31 @@ PLUGINS_CONFIG = {
} }
``` ```
### Catalog Zone Naming
The name of the catalog zone exposed for each view is configurable:
Setting | Default | Description
------- | ------- | -----------------------------------------------------------
`catalog_zone_name` | `catz` | The name of the catalog zone. May be a full DNS name.
`catalog_zone_name_includes_view` | `False` | Prepend the view name: `<view>.<catalog_zone_name>`.
`catalog_zone_name` accepts full DNS names with multiple labels. Case and a trailing dot are
irrelevant — names are handled and compared as `dns.name.Name` objects. With
`catalog_zone_name_includes_view` disabled (the default), the plain `catalog_zone_name` is used for
every view; when enabled, the view name is prepended (e.g. `_default_.catz`).
```
'netbox_dns_bridge': {
'catalog_zone_name': 'catz',
'catalog_zone_name_includes_view': False,
...
}
```
Note that the DNS server consuming the catalog zone must be configured with the same zone name,
e.g. BIND9's `catalog-zones { zone "<catalog_zone_name>" ... }` statement and the corresponding
slave zone declaration must match.
### TSIG Authentication ### TSIG Authentication
Following sets the TSIG key that allows clients to query the transfer endpoint and also the key to Following sets the TSIG key that allows clients to query the transfer endpoint and also the key to
be used to sign NOTIFY messages to clients on zone changes. Each view should have its own unique be used to sign NOTIFY messages to clients on zone changes. Each view should have its own unique
+1 -1
View File
@@ -1,7 +1,7 @@
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
from django.conf import settings from django.conf import settings
__version__ = "1.6.0-rc1" __version__ = "1.7.0"
class DNSBridgeConfig(PluginConfig): class DNSBridgeConfig(PluginConfig):
+145 -177
View File
@@ -12,90 +12,113 @@ from django.db import close_old_connections, OperationalError
from netbox.jobs import JobRunner from netbox.jobs import JobRunner
from netbox_dns.models import View, Zone, Record from netbox_dns.models import View, Zone, Record
from netbox_dns_bridge.utils import get_logger from netbox_dns_bridge.utils import get_logger
from netbox_dns_bridge.models import SeenTransferClient, CatalogZone from netbox_dns_bridge.models import SeenTransferClient
LOGGER = get_logger(__name__) LOGGER = get_logger(__name__)
SETTINGS = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", {})
def _load_tsig_key(view_name: str) -> dns.tsig.Key: class NotifyJobRunner(JobRunner):
tsig_keys = SETTINGS.get("tsig_keys", {}) """Base class for NOTIFY jobs: shared plugin settings and helpers."""
if view_name not in tsig_keys:
raise RuntimeError(f"No TSIG key configured for view '{view_name}'")
key_data = tsig_keys[view_name] SETTINGS = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", {})
raw_key_name = key_data.get("keyname")
secret = key_data.get("secret") def load_tsig_key(self, view_name: str) -> dns.tsig.Key:
algorithm = key_data.get("algorithm") tsig_keys = self.SETTINGS.get("tsig_keys", {})
if not algorithm: if view_name not in tsig_keys:
raise RuntimeError( raise RuntimeError(f"No TSIG key configured for view '{view_name}'")
f"Missing 'algorithm' in TSIG key configuration for view '{view_name}'"
key_data = tsig_keys[view_name]
raw_key_name = key_data.get("keyname")
secret = key_data.get("secret")
algorithm = key_data.get("algorithm")
if not algorithm:
raise RuntimeError(
f"Missing 'algorithm' in TSIG key configuration for view '{view_name}'"
)
if not raw_key_name or not secret:
raise RuntimeError(f"Incomplete TSIG key configuration for view '{view_name}'")
key_name = dns.name.from_text(raw_key_name, origin=None).canonicalize()
if not key_name.is_absolute():
key_name = key_name.concatenate(dns.name.root)
return dns.tsig.Key(name=key_name, secret=secret, algorithm=algorithm)
def send_notify(
self,
zone_name,
soa_serial,
targets,
tsig_key,
view_name,
notify_over_tcp,
port,
log_prefix="NOTIFY",
soa_refresh=60,
soa_retry=10,
soa_expire=1209600,
soa_minimum=0,
):
"""Build a DNS NOTIFY message for *zone_name* and send it to every target IP."""
fqdn = dns.name.from_text(zone_name, dns.name.root).to_text()
msg = dns.message.make_query(fqdn, dns.rdatatype.SOA)
msg.flags |= dns.flags.AA
msg.set_opcode(dns.opcode.NOTIFY)
soa_rdata = dns.rdata.from_text(
dns.rdataclass.IN,
dns.rdatatype.SOA,
f"invalid. invalid. {soa_serial} {soa_refresh} {soa_retry} {soa_expire} {soa_minimum}",
) )
msg.answer.append(dns.rrset.from_rdata(fqdn, 0, soa_rdata))
msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name)
if not raw_key_name or not secret: for target in targets:
raise RuntimeError(f"Incomplete TSIG key configuration for view '{view_name}'") try:
if notify_over_tcp:
LOGGER.debug(f"Sending {log_prefix} over TCP")
dns.query.tcp(msg, target, port=port, timeout=2)
else:
LOGGER.debug(f"Sending {log_prefix} over UDP")
dns.query.udp(msg, target, port=port, timeout=2)
key_name = dns.name.from_text(raw_key_name, origin=None).canonicalize() LOGGER.info(
if not key_name.is_absolute(): f"{log_prefix} sent: {target} {view_name}/{fqdn} {soa_serial}"
key_name = key_name.concatenate(dns.name.root) )
except Exception as e:
LOGGER.error(
f"{log_prefix} failed: {target}:{port}"
f" {view_name}/{fqdn} {soa_serial}: {e}"
)
return dns.tsig.Key(name=key_name, secret=secret, algorithm=algorithm) @staticmethod
def get_soa_serial(zone: Zone):
"""Return the current SOA serial of a zone parsed from its SOA record."""
def _send_notify(
zone_name,
soa_serial,
targets,
tsig_key,
view_name,
notify_over_tcp,
port,
log_prefix="NOTIFY",
soa_refresh=60,
soa_retry=10,
soa_expire=1209600,
soa_minimum=0,
):
"""Build a DNS NOTIFY message for *zone_name* and send it to every target IP."""
fqdn = dns.name.from_text(zone_name, dns.name.root).to_text()
msg = dns.message.make_query(fqdn, dns.rdatatype.SOA)
msg.flags |= dns.flags.AA
msg.set_opcode(dns.opcode.NOTIFY)
soa_rdata = dns.rdata.from_text(
dns.rdataclass.IN,
dns.rdatatype.SOA,
f"invalid. invalid. {soa_serial} {soa_refresh} {soa_retry} {soa_expire} {soa_minimum}",
)
msg.answer.append(dns.rrset.from_rdata(fqdn, 0, soa_rdata))
msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name)
for target in targets:
try: try:
if notify_over_tcp: soa_record = Record.objects.filter(zone=zone, type="SOA", active=True).first()
LOGGER.debug(f"Sending {log_prefix} over TCP") if soa_record is None:
dns.query.tcp(msg, target, port=port, timeout=2) LOGGER.error(f"Zone {zone.name} has no active SOA record — skipping NOTIFY")
else: return None
LOGGER.debug(f"Sending {log_prefix} over UDP")
dns.query.udp(msg, target, port=port, timeout=2)
LOGGER.info( return int(soa_record.value.split()[2])
f"{log_prefix} sent: {target} {view_name}/{fqdn} {soa_serial}"
) except OperationalError as e:
except Exception as e: LOGGER.error(f"DB ERROR while fetching SOA for zone {zone.name}: {e}")
LOGGER.error( close_old_connections()
f"{log_prefix} failed: {target}:{port}" return None
f" {view_name}/{fqdn} {soa_serial}: {e}" except (ValueError, IndexError) as e:
) LOGGER.error(f"Failed to parse SOA serial for zone {zone.name}: {e}")
return None
class SendClientNotify(JobRunner): class SendClientNotify(NotifyJobRunner):
class Meta: class Meta:
name = "Send DNS NOTIFY" name = "Send DNS NOTIFY"
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
notify_over_tcp = SETTINGS.get("notify_over_tcp", False) notify_over_tcp = self.SETTINGS.get("notify_over_tcp", False)
port = SETTINGS.get("notify_client_port", 53) port = self.SETTINGS.get("notify_client_port", 53)
view_name = kwargs["view_name"] view_name = kwargs["view_name"]
zone_name = kwargs["zone_name"] zone_name = kwargs["zone_name"]
soa_serial = kwargs["soa_serial"] soa_serial = kwargs["soa_serial"]
@@ -106,8 +129,8 @@ class SendClientNotify(JobRunner):
try: try:
view = View.objects.get(name=view_name) view = View.objects.get(name=view_name)
tsig_key = _load_tsig_key(view_name) tsig_key = self.load_tsig_key(view_name)
cutoff_hours = SETTINGS.get("notify_client_alive_threshold_hours", 24) cutoff_hours = self.SETTINGS.get("notify_client_alive_threshold_hours", 24)
seen_cutoff = timezone.now() - timezone.timedelta(hours=cutoff_hours) seen_cutoff = timezone.now() - timezone.timedelta(hours=cutoff_hours)
clients = list( clients = list(
@@ -116,7 +139,7 @@ class SendClientNotify(JobRunner):
).values_list("source_ip", flat=True) ).values_list("source_ip", flat=True)
) )
_send_notify( self.send_notify(
zone_name, soa_serial, clients, tsig_key, zone_name, soa_serial, clients, tsig_key,
view_name, notify_over_tcp, port, log_prefix="NOTIFY", view_name, notify_over_tcp, port, log_prefix="NOTIFY",
soa_refresh=soa_refresh, soa_retry=soa_retry, soa_refresh=soa_refresh, soa_retry=soa_retry,
@@ -131,7 +154,7 @@ class SendClientNotify(JobRunner):
close_old_connections() close_old_connections()
class SendNSNotify(JobRunner): class SendNSNotify(NotifyJobRunner):
"""Send a NOTIFY straight to a zone's own nameservers. """Send a NOTIFY straight to a zone's own nameservers.
Unlike ``SendClientNotify`` (which notifies clients that previously queried Unlike ``SendClientNotify`` (which notifies clients that previously queried
@@ -144,8 +167,8 @@ class SendNSNotify(JobRunner):
name = "Send DNS NOTIFY to zone nameservers" name = "Send DNS NOTIFY to zone nameservers"
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
notify_over_tcp = SETTINGS.get("notify_over_tcp", False) notify_over_tcp = self.SETTINGS.get("notify_over_tcp", False)
port = SETTINGS.get("notify_ns_port", 53) port = self.SETTINGS.get("notify_ns_port", 53)
view_name = kwargs["view_name"] view_name = kwargs["view_name"]
zone_name = kwargs["zone_name"] zone_name = kwargs["zone_name"]
soa_serial = kwargs["soa_serial"] soa_serial = kwargs["soa_serial"]
@@ -160,7 +183,7 @@ class SendNSNotify(JobRunner):
close_old_connections() close_old_connections()
return return
ns_targets = _resolve_zone_nameserver_ips(zone) ns_targets = self.resolve_zone_nameserver_ips(zone)
if not ns_targets: if not ns_targets:
LOGGER.warning( LOGGER.warning(
f"NS NOTIFY: no nameserver addresses found in NetBox DNS for " f"NS NOTIFY: no nameserver addresses found in NetBox DNS for "
@@ -169,94 +192,74 @@ class SendNSNotify(JobRunner):
return return
try: try:
tsig_key = _load_tsig_key(view_name) tsig_key = self.load_tsig_key(view_name)
except RuntimeError as e: except RuntimeError as e:
LOGGER.error(f"NS NOTIFY aborted for {view_name}/{zone_name}: {e}") LOGGER.error(f"NS NOTIFY aborted for {view_name}/{zone_name}: {e}")
return return
_send_notify( self.send_notify(
zone_name, soa_serial, ns_targets, tsig_key, zone_name, soa_serial, ns_targets, tsig_key,
view_name, notify_over_tcp, port, log_prefix="NS NOTIFY", view_name, notify_over_tcp, port, log_prefix="NS NOTIFY",
) )
@classmethod
def notify_ns_enabled(cls, zone) -> bool:
"""Return True if a NOTIFY to the zone's nameservers should be sent.
def _get_soa_serial(zone: Zone): Enabled either globally for every zone via ``notify_ns_all_zones`` or per
"""Return the current SOA serial of a zone parsed from its SOA record.""" zone when the boolean custom field named by ``notify_ns_custom_field_name``
try: is set to True on the zone.
soa_record = Record.objects.filter(zone=zone, type="SOA", active=True).first() """
if soa_record is None: if cls.SETTINGS.get("notify_ns_all_zones", False):
LOGGER.error(f"Zone {zone.name} has no active SOA record — skipping NOTIFY") return True
return None
return int(soa_record.value.split()[2]) cf_name = cls.SETTINGS.get("notify_ns_custom_field_name")
if not cf_name:
return False
except OperationalError as e: custom_fields = getattr(zone, "custom_field_data", None) or {}
LOGGER.error(f"DB ERROR while fetching SOA for zone {zone.name}: {e}") return custom_fields.get(cf_name) is True
close_old_connections()
return None
except (ValueError, IndexError) as e:
LOGGER.error(f"Failed to parse SOA serial for zone {zone.name}: {e}")
return None
def resolve_zone_nameserver_ips(self, zone: Zone) -> list:
"""Resolve the zone's nameserver hostnames to IPs using NetBox DNS records.
def _resolve_zone_nameserver_ips(zone: Zone) -> list: For each nameserver assigned to the zone, matching active A/AAAA records
"""Resolve the zone's nameserver hostnames to IPs using NetBox DNS records. within the same view are looked up in NetBox DNS. Returns a de-duplicated,
order-preserving list of IP addresses.
"""
ips = []
try:
nameservers = list(zone.nameservers.all())
view = zone.view
For each nameserver assigned to the zone, matching active A/AAAA records for nameserver in nameservers:
within the same view are looked up in NetBox DNS. Returns a de-duplicated, fqdn = dns.name.from_text(nameserver.name, dns.name.root).to_text()
order-preserving list of IP addresses. # Match both with and without a trailing dot to be resilient against
""" # how the fqdn is stored on the Record model.
ips = [] candidates = {fqdn, fqdn.rstrip(".")}
try:
nameservers = list(zone.nameservers.all())
view = zone.view
for nameserver in nameservers: records = Record.objects.filter(
fqdn = dns.name.from_text(nameserver.name, dns.name.root).to_text() fqdn__in=list(candidates),
# Match both with and without a trailing dot to be resilient against type__in=["A", "AAAA"],
# how the fqdn is stored on the Record model. active=True,
candidates = {fqdn, fqdn.rstrip(".")} zone__view=view,
)
for record in records:
if record.value not in ips:
ips.append(record.value)
records = Record.objects.filter( except OperationalError as e:
fqdn__in=list(candidates), LOGGER.error(f"DB ERROR while resolving nameservers for {zone.name}: {e}")
type__in=["A", "AAAA"], close_old_connections()
active=True,
zone__view=view,
)
for record in records:
if record.value not in ips:
ips.append(record.value)
except OperationalError as e: return ips
LOGGER.error(f"DB ERROR while resolving nameservers for {zone.name}: {e}")
close_old_connections()
return ips
def _notify_ns_enabled(zone) -> bool:
"""Return True if a NOTIFY to the zone's nameservers should be sent.
Enabled either globally for every zone via ``notify_ns_all_zones`` or per
zone when the boolean custom field named by ``notify_ns_custom_field_name``
is set to True on the zone.
"""
if SETTINGS.get("notify_ns_all_zones", False):
return True
cf_name = SETTINGS.get("notify_ns_custom_field_name")
if not cf_name:
return False
custom_fields = getattr(zone, "custom_field_data", None) or {}
return custom_fields.get(cf_name) is True
def schedule_client_notify(zone: Zone): def schedule_client_notify(zone: Zone):
if not SETTINGS.get("notify_clients", False): if not SendClientNotify.SETTINGS.get("notify_clients", False):
return return
soa_serial = _get_soa_serial(zone) soa_serial = SendClientNotify.get_soa_serial(zone)
if soa_serial is None: if soa_serial is None:
return return
@@ -268,10 +271,10 @@ def schedule_client_notify(zone: Zone):
def schedule_ns_notify(zone: Zone): def schedule_ns_notify(zone: Zone):
if not _notify_ns_enabled(zone): if not SendNSNotify.notify_ns_enabled(zone):
return return
soa_serial = _get_soa_serial(zone) soa_serial = SendNSNotify.get_soa_serial(zone)
if soa_serial is None: if soa_serial is None:
return return
@@ -281,38 +284,3 @@ def schedule_ns_notify(zone: Zone):
view_name=zone.view.name, view_name=zone.view.name,
soa_serial=soa_serial, soa_serial=soa_serial,
) )
def schedule_catalog_zone_notify(view: View):
if not SETTINGS.get("notify_clients", False):
return
try:
catalog_zone = view.catalog_zone
except CatalogZone.DoesNotExist:
return
serial = catalog_zone.soa_serial
soa_refresh = catalog_zone.soa_refresh
soa_retry = catalog_zone.soa_retry
soa_expire = catalog_zone.soa_expire
soa_minimum = catalog_zone.soa_minimum
SendClientNotify.enqueue(
zone_name="catz",
soa_serial=serial,
view_name=view.name,
soa_refresh=soa_refresh,
soa_retry=soa_retry,
soa_expire=soa_expire,
soa_minimum=soa_minimum,
)
SendClientNotify.enqueue(
zone_name=f"{view.name}.catz",
soa_serial=serial,
view_name=view.name,
soa_refresh=soa_refresh,
soa_retry=soa_retry,
soa_expire=soa_expire,
soa_minimum=soa_minimum,
)
@@ -8,11 +8,37 @@ from netbox_dns_bridge.models import CatalogZone, CatalogZoneMember
from netbox_dns_bridge.jobs import notify from netbox_dns_bridge.jobs import notify
from uuid import uuid4 from uuid import uuid4
from base64 import b32encode from base64 import b32encode
from django.conf import settings
from django.db import transaction, close_old_connections, OperationalError from django.db import transaction, close_old_connections, OperationalError
LOGGER = get_logger(__name__) LOGGER = get_logger(__name__)
SETTINGS = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", {})
def catalog_zone_name(view_name: str) -> dns.name.Name:
"""Return the fully qualified catalog zone name for a view as a
dns.name.Name.
The name is configured via the ``catalog_zone_name`` setting (fallback:
``catz``). With ``catalog_zone_name_includes_view`` enabled, the view
name is prepended: ``<view>.<catalog_zone_name>``.
"""
base = dns.name.from_text(SETTINGS.get("catalog_zone_name") or "catz")
if SETTINGS.get("catalog_zone_name_includes_view", False):
return dns.name.from_text(view_name, origin=base)
return base
def is_catalog_zone_query(qname: dns.name.Name, view_name: str) -> bool:
"""Check whether a queried name is the catalog zone name of a view.
dns.name.Name equality is case-insensitive, so queries from BIND match
regardless of case.
"""
return qname == catalog_zone_name(view_name)
def increment_soa_serial(view: NBView) -> int: def increment_soa_serial(view: NBView) -> int:
with transaction.atomic(): with transaction.atomic():
@@ -47,13 +73,13 @@ def increment_soa_serial(view: NBView) -> int:
close_old_connections() close_old_connections()
return None return None
notify.schedule_catalog_zone_notify(view) schedule_catalog_zone_notify(view)
return serial return serial
def create_zone(name, view_name) -> dns.zone.Zone: def create_zone(view_name) -> dns.zone.Zone:
# Zone origin # Zone origin: the configured catalog zone name for this view
origin = dns.name.from_text(name, dns.name.root) origin = catalog_zone_name(view_name)
# Create a new empty zone # Create a new empty zone
zone = dns.zone.Zone(origin) zone = dns.zone.Zone(origin)
@@ -117,7 +143,7 @@ def create_zone(name, view_name) -> dns.zone.Zone:
ns_rdataset = _create_ns_rdataset() ns_rdataset = _create_ns_rdataset()
zone_root_node.rdatasets.append(ns_rdataset) zone_root_node.rdatasets.append(ns_rdataset)
# version.catz. record # version.<catalog zone name>. record
version_rdataset = _create_version_rdataset() version_rdataset = _create_version_rdataset()
zone_version_node = zone.find_node( zone_version_node = zone.find_node(
dns.name.from_text("version", origin), create=True dns.name.from_text("version", origin), create=True
@@ -158,6 +184,32 @@ def update_member(zone: NBZone) -> None:
close_old_connections() close_old_connections()
def schedule_catalog_zone_notify(view: NBView) -> None:
"""Schedule a NOTIFY for the catalog zone of a view.
Notifies transfer clients for the single configured catalog zone name
(see ``catalog_zone_name``). The job receives the name as text, since
job kwargs are serialized to JSON.
"""
if not SETTINGS.get("notify_clients", False):
return
try:
catalog_zone = view.catalog_zone
except CatalogZone.DoesNotExist:
return
notify.SendClientNotify.enqueue(
zone_name=catalog_zone_name(view.name).to_text(omit_final_dot=True),
soa_serial=catalog_zone.soa_serial,
view_name=view.name,
soa_refresh=catalog_zone.soa_refresh,
soa_retry=catalog_zone.soa_retry,
soa_expire=catalog_zone.soa_expire,
soa_minimum=catalog_zone.soa_minimum,
)
def _create_soa_rdataset(view: NBView) -> dns.rdataset: def _create_soa_rdataset(view: NBView) -> dns.rdataset:
try: try:
serial_obj, _ = CatalogZone.objects.get_or_create( serial_obj, _ = CatalogZone.objects.get_or_create(
@@ -289,9 +289,8 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
return return
# Check if catalog zone # Check if catalog zone
dname_lower = dname.lower() if catzm.is_catalog_zone_query(qname, nb_view.name):
if dname_lower == "catz" or dname_lower == f"{nb_view.name.lower()}.catz": zone = catzm.create_zone(nb_view.name)
zone = catzm.create_zone(dname_lower, nb_view.name)
else: else:
zone = self._get_zone_from_nb(dname, nb_view.name) zone = self._get_zone_from_nb(dname, nb_view.name)
# When zone was not found, let client know # When zone was not found, let client know