Move from hard-coded CatalogZone SOA values (refresh, retry, expire and minimum)

to DB driven to permit user modification:

modified:   netbox_dns_bridge/models.py
  Add 4 cols for values.

modified:   netbox_dns_bridge/migrations/0003_catalogzone_and_per_zone_identifier.py
  Adjust migration. This expects that the last migration the user has is 0002 (<=v1.5.8)

modified:   netbox_dns_bridge/jobs/notify.py
modified:   netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py
  Update to use new db values for SOA creation.

Change UI to permit editing for SOA values and disable member identifier editing.
modified:   netbox_dns_bridge/forms.py
modified:   netbox_dns_bridge/navigation.py
new file:   netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_edit.html
new file:   netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_list.html
new file:   netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_members.html
deleted:    netbox_dns_bridge/templates/netbox_dns_bridge/catz_overview.html
modified:   netbox_dns_bridge/tables.py
modified:   netbox_dns_bridge/urls.py
modified:   netbox_dns_bridge/views.py
modified:   netbox_dns_bridge/filtersets.py
  GUI components are entirely AI generated. Plumbing also with AI assist but code verified.
This commit is contained in:
sven authored and Sven Luthi committed 2026-09-02 12:05:21 +02:00
1 parent 69781e2b08
commit a3f53767e4
14 files changed
+209 -63

No files matched your search

+18
View File
@@ -5,6 +5,8 @@ import netbox_dns.models
from netbox.filtersets import NetBoxModelFilterSet
from netbox_dns.choices import ZoneStatusChoices
from .models import CatalogZone
class ZoneFilterSet(NetBoxModelFilterSet):
view = django_filters.ModelMultipleChoiceFilter(
@@ -27,3 +29,19 @@ class ZoneFilterSet(NetBoxModelFilterSet):
Q(name__icontains=value)
| Q(catz_identifier__name__icontains=value)
)
class CatalogZoneFilterSet(NetBoxModelFilterSet):
view = django_filters.ModelMultipleChoiceFilter(
field_name="view",
queryset=netbox_dns.models.View.objects.all(),
)
class Meta:
model = CatalogZone
fields = ("id", "view")
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(Q(view__name__icontains=value))
+40 -2
View File
@@ -2,12 +2,40 @@ from django import forms
from django.utils.translation import gettext_lazy as _
import netbox_dns.models
from netbox.forms import NetBoxModelFilterSetForm
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm
from netbox_dns.choices import ZoneStatusChoices
from utilities.forms.fields import DynamicModelMultipleChoiceField
from .models import CatalogZone
class CatalogZoneFilterForm(NetBoxModelFilterSetForm):
class CatalogZoneForm(NetBoxModelForm):
soa_serial = forms.IntegerField(label=_("SOA Serial"))
soa_refresh = forms.IntegerField(label=_("Refresh"))
soa_retry = forms.IntegerField(label=_("Retry"))
soa_expire = forms.IntegerField(label=_("Expire"))
soa_minimum = forms.IntegerField(label=_("Minimum"))
view = forms.ModelChoiceField(
queryset=netbox_dns.models.View.objects.all(),
disabled=True,
required=False,
label=_("View"),
)
class Meta:
model = CatalogZone
fields = (
"view",
"soa_serial",
"soa_refresh",
"soa_retry",
"soa_expire",
"soa_minimum",
)
class CatalogZoneMemberFilterForm(NetBoxModelFilterSetForm):
model = netbox_dns.models.Zone
q = forms.CharField(required=False, label=_("Search"))
@@ -19,3 +47,13 @@ class CatalogZoneFilterForm(NetBoxModelFilterSetForm):
choices=ZoneStatusChoices,
required=False,
)
class CatalogZoneFilterForm(NetBoxModelFilterSetForm):
model = CatalogZone
q = forms.CharField(required=False, label=_("Search"))
view = DynamicModelMultipleChoiceField(
queryset=netbox_dns.models.View.objects.all(),
required=False,
)
+26 -2
View File
@@ -51,6 +51,10 @@ def _send_notify(
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()
@@ -61,7 +65,7 @@ def _send_notify(
soa_rdata = dns.rdata.from_text(
dns.rdataclass.IN,
dns.rdatatype.SOA,
f"invalid. invalid. {soa_serial} 60 10 1209600 0",
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)
@@ -95,6 +99,10 @@ class SendClientNotify(JobRunner):
view_name = kwargs["view_name"]
zone_name = kwargs["zone_name"]
soa_serial = kwargs["soa_serial"]
soa_refresh = kwargs.get("soa_refresh", 60)
soa_retry = kwargs.get("soa_retry", 10)
soa_expire = kwargs.get("soa_expire", 1209600)
soa_minimum = kwargs.get("soa_minimum", 0)
try:
view = View.objects.get(name=view_name)
@@ -111,6 +119,8 @@ class SendClientNotify(JobRunner):
_send_notify(
zone_name, soa_serial, clients, tsig_key,
view_name, notify_over_tcp, port, log_prefix="NOTIFY",
soa_refresh=soa_refresh, soa_retry=soa_retry,
soa_expire=soa_expire, soa_minimum=soa_minimum,
)
except View.DoesNotExist as e:
@@ -278,17 +288,31 @@ def schedule_catalog_zone_notify(view: View):
return
try:
serial = view.catalog_zone.soa_serial
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,
)
@@ -174,7 +174,9 @@ def _create_soa_rdataset(view: NBView) -> dns.rdataset:
rdata = dns.rdata.from_text(
rclass,
rtype,
f"invalid. invalid. {serial} 60 10 1209600 0",
f"invalid. invalid. {serial} {serial_obj.soa_refresh} "
f"{serial_obj.soa_retry} {serial_obj.soa_expire} "
f"{serial_obj.soa_minimum}",
)
# Create Rdataset and add the RDATA to it
@@ -88,6 +88,10 @@ class Migration(migrations.Migration):
),
),
("soa_serial", models.IntegerField(default=1)),
("soa_refresh", models.IntegerField(default=60)),
("soa_retry", models.IntegerField(default=10)),
("soa_expire", models.IntegerField(default=1209600)),
("soa_minimum", models.IntegerField(default=0)),
(
"view",
models.OneToOneField(
+8
View File
@@ -1,4 +1,5 @@
from django.db import models
from django.urls import reverse
# import netbox.models
import netbox_dns.models
@@ -13,6 +14,10 @@ class CatalogZone(models.Model):
related_name="catalog_zone",
)
soa_serial = models.IntegerField(default=1)
soa_refresh = models.IntegerField(default=60)
soa_retry = models.IntegerField(default=10)
soa_expire = models.IntegerField(default=1209600)
soa_minimum = models.IntegerField(default=0)
class Meta:
ordering = ("view__name",)
@@ -20,6 +25,9 @@ class CatalogZone(models.Model):
def __str__(self):
return f"{self.view.name}: {self.soa_serial}"
def get_absolute_url(self):
return reverse("plugins:netbox_dns_bridge:catalogzone_edit", args=[self.pk])
class CatalogZoneMemberIdentifier(models.Model):
_netbox_private = True
+13 -3
View File
@@ -1,13 +1,20 @@
from django.utils.translation import gettext_lazy as _
from netbox.plugins import PluginMenu, PluginMenuItem
information_menu_item = PluginMenuItem(
link="plugins:netbox_dns_bridge:catz",
catalog_zones_menu_item = PluginMenuItem(
link="plugins:netbox_dns_bridge:catalog_zones",
link_text=_("Catalog Zones"),
permissions=[],
staff_only=True,
)
catalog_zone_members_menu_item = PluginMenuItem(
link="plugins:netbox_dns_bridge:catalog_zone_members",
link_text=_("Catalog Zone Members"),
permissions=[],
staff_only=True,
)
notify_menu_item = PluginMenuItem(
link="plugins:netbox_dns_bridge:notify",
link_text=_("Notify"),
@@ -18,7 +25,10 @@ notify_menu_item = PluginMenuItem(
menu = PluginMenu(
label=_("DNS Bridge"),
groups=(
(_("Information"), (information_menu_item, notify_menu_item)),
(
_("Information"),
(catalog_zones_menu_item, catalog_zone_members_menu_item, notify_menu_item),
),
),
icon_class="mdi mdi-bridge",
)
+35 -1
View File
@@ -4,8 +4,10 @@ from django.utils.translation import gettext_lazy as _
import netbox_dns.models
from netbox.tables import NetBoxTable
from .models import CatalogZone
class CatalogZoneTable(NetBoxTable):
class CatalogZoneMemberTable(NetBoxTable):
name = tables.Column(linkify=True)
view = tables.Column(
accessor="view__name",
@@ -22,3 +24,35 @@ class CatalogZoneTable(NetBoxTable):
model = netbox_dns.models.Zone
fields = ("name", "view", "status", "catz_identifier")
default_columns = ("name", "view", "status", "catz_identifier")
class CatalogZoneTable(NetBoxTable):
view = tables.Column(
accessor="view__name",
verbose_name=_("View"),
linkify=True,
)
soa_serial = tables.Column(verbose_name=_("SOA Serial"))
soa_refresh = tables.Column(verbose_name=_("Refresh"))
soa_retry = tables.Column(verbose_name=_("Retry"))
soa_expire = tables.Column(verbose_name=_("Expire"))
soa_minimum = tables.Column(verbose_name=_("Minimum"))
class Meta(NetBoxTable.Meta):
model = CatalogZone
fields = (
"view",
"soa_serial",
"soa_refresh",
"soa_retry",
"soa_expire",
"soa_minimum",
)
default_columns = (
"view",
"soa_serial",
"soa_refresh",
"soa_retry",
"soa_expire",
"soa_minimum",
)
@@ -0,0 +1,4 @@
{% extends 'generic/object_edit.html' %}
{% load i18n %}
{% block title %}{% trans "Edit Catalog Zone" %}{% endblock %}
@@ -0,0 +1,4 @@
{% extends 'generic/object_list.html' %}
{% load i18n %}
{% block title %}{% trans "Catalog Zones" %}{% endblock %}
@@ -0,0 +1,4 @@
{% extends 'generic/object_list.html' %}
{% load i18n %}
{% block title %}{% trans "Catalog Zone Members" %}{% endblock %}
@@ -1,37 +0,0 @@
{% extends 'generic/object_list.html' %}
{% load i18n %}
{% block title %}{% trans "Catalog Zone Info" %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-md-12">
<div class="card">
<h5 class="card-header">{% trans "Catalog Zone SOA Serial Numbers" %}</h5>
<div class="card-body">
{% if catalog_zones %}
<table class="table table-sm">
<thead>
<tr>
<th>{% trans "View" %}</th>
<th>{% trans "SOA Serial" %}</th>
</tr>
</thead>
<tbody>
{% for cz in catalog_zones %}
<tr>
<td>{{ cz.view.name }}</td>
<td>{{ cz.soa_serial }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<span class="text-muted">{% trans "No catalog zones initialized." %}</span>
{% endif %}
</div>
</div>
</div>
</div>
{{ block.super }}
{% endblock %}
+17 -2
View File
@@ -1,8 +1,23 @@
from django.urls import path
from .views import CatzOverviewView, NotifyOverviewView
from .views import (
CatalogZoneEditView,
CatalogZoneListView,
CatalogZoneMembersView,
NotifyOverviewView,
)
urlpatterns = [
path("catz/", CatzOverviewView.as_view(), name="catz"),
path("catalog-zones/", CatalogZoneListView.as_view(), name="catalog_zones"),
path(
"catalog-zones/<int:pk>/edit/",
CatalogZoneEditView.as_view(),
name="catalogzone_edit",
),
path(
"catalog-zone-members/",
CatalogZoneMembersView.as_view(),
name="catalog_zone_members",
),
path("notify/", NotifyOverviewView.as_view(), name="notify"),
]
+33 -15
View File
@@ -4,34 +4,52 @@ from django.utils.decorators import method_decorator
from django.views import View
import netbox_dns.models
from netbox.views.generic import ObjectListView
from netbox.views.generic import ObjectEditView, ObjectListView
from .filtersets import ZoneFilterSet
from .forms import CatalogZoneFilterForm
from .filtersets import CatalogZoneFilterSet, ZoneFilterSet
from .forms import (
CatalogZoneFilterForm,
CatalogZoneForm,
CatalogZoneMemberFilterForm,
)
from .models import CatalogZone, SeenTransferClients
from .tables import CatalogZoneTable
from .tables import CatalogZoneMemberTable, CatalogZoneTable
@method_decorator(
user_passes_test(lambda u: u.is_superuser), name="dispatch"
)
class CatzOverviewView(ObjectListView):
class CatalogZoneListView(ObjectListView):
queryset = CatalogZone.objects.select_related("view").order_by("view__name")
table = CatalogZoneTable
filterset = CatalogZoneFilterSet
filterset_form = CatalogZoneFilterForm
template_name = "netbox_dns_bridge/catalog_zone_list.html"
actions = ("edit", "export")
@method_decorator(
user_passes_test(lambda u: u.is_superuser), name="dispatch"
)
class CatalogZoneEditView(ObjectEditView):
queryset = CatalogZone.objects.select_related("view")
form = CatalogZoneForm
template_name = "netbox_dns_bridge/catalog_zone_edit.html"
@method_decorator(
user_passes_test(lambda u: u.is_superuser), name="dispatch"
)
class CatalogZoneMembersView(ObjectListView):
queryset = netbox_dns.models.Zone.objects.select_related(
"view", "catz_identifier"
).order_by("view__name", "name")
table = CatalogZoneTable
table = CatalogZoneMemberTable
filterset = ZoneFilterSet
filterset_form = CatalogZoneFilterForm
template_name = "netbox_dns_bridge/catz_overview.html"
filterset_form = CatalogZoneMemberFilterForm
template_name = "netbox_dns_bridge/catalog_zone_members.html"
actions = ()
def get_extra_context(self, request):
context = super().get_extra_context(request)
context["catalog_zones"] = CatalogZone.objects.select_related(
"view"
).order_by("view__name")
return context
@method_decorator(
user_passes_test(lambda u: u.is_superuser), name="dispatch"