diff --git a/netbox_dns_bridge/filtersets.py b/netbox_dns_bridge/filtersets.py index 87de3a6..fb77cf8 100644 --- a/netbox_dns_bridge/filtersets.py +++ b/netbox_dns_bridge/filtersets.py @@ -3,32 +3,8 @@ from django.db.models import Q 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( - field_name="view", - queryset=netbox_dns.models.View.objects.all(), - ) - status = django_filters.MultipleChoiceFilter( - choices=ZoneStatusChoices, - null_value=None, - ) - - class Meta: - model = netbox_dns.models.Zone - fields = ("id", "name", "view", "status", "catz_identifier") - - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) - | Q(catz_identifier__name__icontains=value) - ) +from .models import CatalogZone, CatalogZoneMember, SeenTransferClient class CatalogZoneFilterSet(NetBoxModelFilterSet): @@ -45,3 +21,39 @@ class CatalogZoneFilterSet(NetBoxModelFilterSet): if not value.strip(): return queryset return queryset.filter(Q(view__name__icontains=value)) + + +class CatalogZoneMemberFilterSet(NetBoxModelFilterSet): + catalog_zone = django_filters.ModelMultipleChoiceFilter( + field_name="catalog_zone", + queryset=CatalogZone.objects.all(), + ) + + class Meta: + model = CatalogZoneMember + fields = ("id", "catalog_zone", "zone") + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | Q(zone__name__icontains=value) + ) + + +class SeenTransferClientFilterSet(NetBoxModelFilterSet): + view = django_filters.ModelMultipleChoiceFilter( + field_name="view", + queryset=netbox_dns.models.View.objects.all(), + ) + + class Meta: + model = SeenTransferClient + fields = ("id", "view", "source_ip") + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(source_ip__icontains=value) | Q(view__name__icontains=value) + ) diff --git a/netbox_dns_bridge/forms.py b/netbox_dns_bridge/forms.py index dd7d784..7225ea3 100644 --- a/netbox_dns_bridge/forms.py +++ b/netbox_dns_bridge/forms.py @@ -3,10 +3,9 @@ from django.utils.translation import gettext_lazy as _ import netbox_dns.models from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm -from netbox_dns.choices import ZoneStatusChoices from utilities.forms.fields import DynamicModelMultipleChoiceField -from .models import CatalogZone +from .models import CatalogZone, CatalogZoneMember, SeenTransferClient class CatalogZoneForm(NetBoxModelForm): @@ -35,20 +34,6 @@ class CatalogZoneForm(NetBoxModelForm): ) -class CatalogZoneMemberFilterForm(NetBoxModelFilterSetForm): - model = netbox_dns.models.Zone - - q = forms.CharField(required=False, label=_("Search")) - view = DynamicModelMultipleChoiceField( - queryset=netbox_dns.models.View.objects.all(), - required=False, - ) - status = forms.MultipleChoiceField( - choices=ZoneStatusChoices, - required=False, - ) - - class CatalogZoneFilterForm(NetBoxModelFilterSetForm): model = CatalogZone @@ -57,3 +42,23 @@ class CatalogZoneFilterForm(NetBoxModelFilterSetForm): queryset=netbox_dns.models.View.objects.all(), required=False, ) + + +class CatalogZoneMemberFilterForm(NetBoxModelFilterSetForm): + model = CatalogZoneMember + + q = forms.CharField(required=False, label=_("Search")) + catalog_zone = forms.ModelMultipleChoiceField( + queryset=CatalogZone.objects.all(), + required=False, + ) + + +class SeenTransferClientFilterForm(NetBoxModelFilterSetForm): + model = SeenTransferClient + + q = forms.CharField(required=False, label=_("Search")) + view = forms.ModelMultipleChoiceField( + queryset=netbox_dns.models.View.objects.all(), + required=False, + ) diff --git a/netbox_dns_bridge/jobs/notify.py b/netbox_dns_bridge/jobs/notify.py index 128805b..1598647 100644 --- a/netbox_dns_bridge/jobs/notify.py +++ b/netbox_dns_bridge/jobs/notify.py @@ -12,7 +12,7 @@ from django.db import close_old_connections, OperationalError from netbox.jobs import JobRunner from netbox_dns.models import View, Zone, Record from netbox_dns_bridge.utils import get_logger -from netbox_dns_bridge.models import SeenTransferClients, CatalogZone +from netbox_dns_bridge.models import SeenTransferClient, CatalogZone LOGGER = get_logger(__name__) SETTINGS = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", {}) @@ -111,7 +111,7 @@ class SendClientNotify(JobRunner): seen_cutoff = timezone.now() - timezone.timedelta(hours=cutoff_hours) clients = list( - SeenTransferClients.objects.filter( + SeenTransferClient.objects.filter( view=view, last_seen__gte=seen_cutoff ).values_list("source_ip", flat=True) ) diff --git a/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py b/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py index 919a753..a565de3 100644 --- a/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py +++ b/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py @@ -4,7 +4,7 @@ import dns.rdatatype import dns.rdataclass from netbox_dns_bridge.utils import get_logger from netbox_dns.models import Zone as NBZone, View as NBView -from netbox_dns_bridge.models import CatalogZone, CatalogZoneMemberIdentifier +from netbox_dns_bridge.models import CatalogZone, CatalogZoneMember from netbox_dns_bridge.jobs import notify from uuid import uuid4 from base64 import b32encode @@ -63,7 +63,7 @@ def create_zone(name, view_name) -> dns.zone.Zone: # get zones from netbox nb_zones = NBZone.objects.filter( view__name=view_name, active=True - ).select_related("catz_identifier") + ).select_related("catalog_zone_member") ptr_base = dns.name.from_text("zones", origin) @@ -73,21 +73,21 @@ def create_zone(name, view_name) -> dns.zone.Zone: # Create PTR record try: - catz_identifier = nb_zone.catz_identifier - except NBZone.catz_identifier.RelatedObjectDoesNotExist: + member = nb_zone.catalog_zone_member + except NBZone.catalog_zone_member.RelatedObjectDoesNotExist: catalog_zone = _get_or_create_catalog_zone(nb_zone.view) - catz_identifier = CatalogZoneMemberIdentifier.objects.create( + member = CatalogZoneMember.objects.create( zone=nb_zone, - name=_generate_member_identifier(), + name=_generate_member_name(), catalog_zone=catalog_zone, ) - p_name = catz_identifier.name + p_name = member.name ptr_name = dns.name.from_text(p_name, ptr_base) if not ptr_name.is_subdomain(origin): raise ValueError( - f"Catalog zone member identifier {ptr_name.to_text()} not a subdomain" + f"Catalog zone member {ptr_name.to_text()} not a subdomain" ) rdata = dns.rdata.from_text( dns.rdataclass.IN, dns.rdatatype.PTR, qname.to_text() @@ -132,7 +132,7 @@ def create_zone(name, view_name) -> dns.zone.Zone: return None -def _generate_member_identifier() -> str: +def _generate_member_name() -> str: return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8") @@ -143,18 +143,18 @@ def _get_or_create_catalog_zone(view: NBView) -> CatalogZone: return catalog_zone -def update_member_identifier(zone: NBZone) -> None: +def update_member(zone: NBZone) -> None: try: catalog_zone = _get_or_create_catalog_zone(zone.view) - CatalogZoneMemberIdentifier.objects.update_or_create( + CatalogZoneMember.objects.update_or_create( zone=zone, defaults={ - "name": _generate_member_identifier(), + "name": _generate_member_name(), "catalog_zone": catalog_zone, }, ) except OperationalError as e: - LOGGER.error(f"ERROR: Failed to update Catalog Zone member identifier: {e}") + LOGGER.error(f"ERROR: Failed to update Catalog Zone member: {e}") close_old_connections() diff --git a/netbox_dns_bridge/management/commands/dns_transfer_endpoint/request_handler.py b/netbox_dns_bridge/management/commands/dns_transfer_endpoint/request_handler.py index d38b0f1..0f725b3 100644 --- a/netbox_dns_bridge/management/commands/dns_transfer_endpoint/request_handler.py +++ b/netbox_dns_bridge/management/commands/dns_transfer_endpoint/request_handler.py @@ -15,7 +15,7 @@ from django.db import close_old_connections, OperationalError from django.conf import settings from netbox_dns.models import Zone, View from . import catalog_zone_manager as catzm -from netbox_dns_bridge.models import SeenTransferClients +from netbox_dns_bridge.models import SeenTransferClient from netbox_dns_bridge.utils import build_dns_zone, get_logger LOGGER = get_logger(__name__) @@ -77,7 +77,7 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler): def _track_seen_client(self, source_ip: str, view: View) -> None: if SETTINGS.get("notify_clients", False): try: - SeenTransferClients.objects.update_or_create( + SeenTransferClient.objects.update_or_create( source_ip=source_ip, view=view, defaults={"last_seen": timezone.now()}, diff --git a/netbox_dns_bridge/migrations/0003_catalogzone_and_per_zone_identifier.py b/netbox_dns_bridge/migrations/0003_catalogzone_and_per_zone_identifier.py deleted file mode 100644 index a8d8ae3..0000000 --- a/netbox_dns_bridge/migrations/0003_catalogzone_and_per_zone_identifier.py +++ /dev/null @@ -1,146 +0,0 @@ -import django.db.models.deletion -from django.db import migrations, models - - -def copy_serials_and_link_identifiers(apps, schema_editor): - """Seed one CatalogZone per view from the old global serial and link identifiers.""" - IntegerKeyValueSetting = apps.get_model( - "netbox_dns_bridge", "IntegerKeyValueSetting" - ) - CatalogZone = apps.get_model("netbox_dns_bridge", "CatalogZone") - CatalogZoneMemberIdentifier = apps.get_model( - "netbox_dns_bridge", "CatalogZoneMemberIdentifier" - ) - - # Preserve the current global serial; default to 1 when never initialized. - seed_serial = 1 - kv = IntegerKeyValueSetting.objects.filter(key="catalog-zone-soa-serial").first() - if kv is not None: - seed_serial = kv.value - - # Create a CatalogZone for every view that currently has at least one - # member identifier, seeding each with the old global serial. - view_ids = set( - CatalogZoneMemberIdentifier.objects.exclude( - zone__view__isnull=True - ).values_list("zone__view_id", flat=True) - ) - - view_id_to_catalog_zone = {} - for view_id in view_ids: - cz, _ = CatalogZone.objects.get_or_create( - view_id=view_id, defaults={"soa_serial": seed_serial} - ) - view_id_to_catalog_zone[view_id] = cz - - # Link each identifier to the CatalogZone matching its zone's view. - for ident in CatalogZoneMemberIdentifier.objects.select_related("zone__view"): - cz = view_id_to_catalog_zone.get(ident.zone.view_id) - if cz is None: - # Defensive: identifier existed without a resolvable view. Create - # the CatalogZone on demand so the NOT NULL constraint can be set. - cz, _ = CatalogZone.objects.get_or_create( - view_id=ident.zone.view_id, defaults={"soa_serial": seed_serial} - ) - view_id_to_catalog_zone[ident.zone.view_id] = cz - ident.catalog_zone = cz - ident.save(update_fields=["catalog_zone"]) - - -def reverse_migration(apps, schema_editor): - """Best-effort reverse: restore the KV row and null the FK.""" - IntegerKeyValueSetting = apps.get_model( - "netbox_dns_bridge", "IntegerKeyValueSetting" - ) - CatalogZone = apps.get_model("netbox_dns_bridge", "CatalogZone") - CatalogZoneMemberIdentifier = apps.get_model( - "netbox_dns_bridge", "CatalogZoneMemberIdentifier" - ) - - # Pick a representative serial (first catalog zone) to write back. - first = CatalogZone.objects.order_by("pk").first() - serial = first.soa_serial if first is not None else 1 - IntegerKeyValueSetting.objects.update_or_create( - key="catalog-zone-soa-serial", defaults={"value": serial} - ) - - CatalogZoneMemberIdentifier.objects.update(catalog_zone=None) - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "netbox_dns", - "0030_dnsseckeytemplate_comments_dnsseckeytemplate_owner_and_more", - ), - ("netbox_dns_bridge", "0002_alter_integerkeyvaluesetting_options_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="CatalogZone", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False - ), - ), - ("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( - on_delete=django.db.models.deletion.CASCADE, - related_name="catalog_zone", - to="netbox_dns.view", - ), - ), - ], - options={ - "ordering": ("view__name",), - }, - ), - migrations.AddField( - model_name="catalogzonememberidentifier", - name="catalog_zone", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="member_identifiers", - to="netbox_dns_bridge.catalogzone", - ), - ), - migrations.AlterField( - model_name="catalogzonememberidentifier", - name="name", - field=models.CharField(max_length=26), - ), - migrations.RunPython( - copy_serials_and_link_identifiers, - reverse_code=reverse_migration, - ), - migrations.AlterField( - model_name="catalogzonememberidentifier", - name="catalog_zone", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="member_identifiers", - to="netbox_dns_bridge.catalogzone", - ), - ), - migrations.AddConstraint( - model_name="catalogzonememberidentifier", - constraint=models.UniqueConstraint( - fields=("name", "catalog_zone"), - name="unique_name_per_catalog_zone", - ), - ), - migrations.DeleteModel( - name="IntegerKeyValueSetting", - ), - ] diff --git a/netbox_dns_bridge/migrations/0003_full_netbox_models.py b/netbox_dns_bridge/migrations/0003_full_netbox_models.py new file mode 100644 index 0000000..62376fa --- /dev/null +++ b/netbox_dns_bridge/migrations/0003_full_netbox_models.py @@ -0,0 +1,233 @@ +import taggit.managers +import utilities.json +import django.db.models.deletion +from django.db import migrations, models + + +def copy_serials_and_link_members(apps, schema_editor): + """Seed one CatalogZone per view from the old global serial and link members.""" + IntegerKeyValueSetting = apps.get_model( + "netbox_dns_bridge", "IntegerKeyValueSetting" + ) + CatalogZone = apps.get_model("netbox_dns_bridge", "CatalogZone") + CatalogZoneMember = apps.get_model("netbox_dns_bridge", "CatalogZoneMember") + + # Preserve the current global serial; default to 1 when never initialized. + seed_serial = 1 + kv = IntegerKeyValueSetting.objects.filter(key="catalog-zone-soa-serial").first() + if kv is not None: + seed_serial = kv.value + + # Create a CatalogZone for every view that currently has at least one + # member, seeding each with the old global serial. + view_ids = set( + CatalogZoneMember.objects.exclude( + zone__view__isnull=True + ).values_list("zone__view_id", flat=True) + ) + + view_id_to_catalog_zone = {} + for view_id in view_ids: + cz, _ = CatalogZone.objects.get_or_create( + view_id=view_id, defaults={"soa_serial": seed_serial} + ) + view_id_to_catalog_zone[view_id] = cz + + # Link each member to the CatalogZone matching its zone's view. + for member in CatalogZoneMember.objects.select_related("zone__view"): + cz = view_id_to_catalog_zone.get(member.zone.view_id) + if cz is None: + # Defensive: member existed without a resolvable view. Create + # the CatalogZone on demand so the NOT NULL constraint can be set. + cz, _ = CatalogZone.objects.get_or_create( + view_id=member.zone.view_id, defaults={"soa_serial": seed_serial} + ) + view_id_to_catalog_zone[member.zone.view_id] = cz + member.catalog_zone = cz + member.save(update_fields=["catalog_zone"]) + + +def reverse_migration(apps, schema_editor): + """Best-effort reverse: restore the KV row and null the FK.""" + IntegerKeyValueSetting = apps.get_model( + "netbox_dns_bridge", "IntegerKeyValueSetting" + ) + CatalogZone = apps.get_model("netbox_dns_bridge", "CatalogZone") + CatalogZoneMember = apps.get_model("netbox_dns_bridge", "CatalogZoneMember") + + # Pick a representative serial (first catalog zone) to write back. + first = CatalogZone.objects.order_by("pk").first() + serial = first.soa_serial if first is not None else 1 + IntegerKeyValueSetting.objects.update_or_create( + key="catalog-zone-soa-serial", defaults={"value": serial} + ) + + CatalogZoneMember.objects.update(catalog_zone=None) + + +class Migration(migrations.Migration): + + dependencies = [ + ("extras", "0140_imageattachment_image_size"), + ( + "netbox_dns", + "0032_record_expiration_date", + ), + ("netbox_dns_bridge", "0002_alter_integerkeyvaluesetting_options_and_more"), + ] + + operations = [ + # --- CatalogZone (full NetBoxModel, born with all fields) --- + migrations.CreateModel( + name="CatalogZone", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False + ), + ), + ("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)), + ("created", models.DateTimeField(auto_now_add=True, null=True)), + ("last_updated", models.DateTimeField(auto_now=True, null=True)), + ( + "custom_field_data", + models.JSONField( + blank=True, + default=dict, + encoder=utilities.json.CustomFieldJSONEncoder, + ), + ), + ( + "tags", + taggit.managers.TaggableManager( + through="extras.TaggedItem", to="extras.Tag" + ), + ), + ( + "view", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="catalog_zone", + to="netbox_dns.view", + ), + ), + ], + options={ + "ordering": ("view__name",), + }, + ), + # --- CatalogZoneMemberIdentifier → CatalogZoneMember (rename + full NetBoxModel) --- + migrations.RenameModel( + old_name="CatalogZoneMemberIdentifier", + new_name="CatalogZoneMember", + ), + migrations.AddField( + model_name="catalogzonemember", + name="created", + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name="catalogzonemember", + name="last_updated", + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name="catalogzonemember", + name="custom_field_data", + field=models.JSONField( + blank=True, + default=dict, + encoder=utilities.json.CustomFieldJSONEncoder, + ), + ), + migrations.AddField( + model_name="catalogzonemember", + name="tags", + field=taggit.managers.TaggableManager( + through="extras.TaggedItem", to="extras.Tag" + ), + ), + migrations.AlterField( + model_name="catalogzonemember", + name="name", + field=models.CharField(max_length=26), + ), + migrations.AlterField( + model_name="catalogzonemember", + name="zone", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="catalog_zone_member", + to="netbox_dns.zone", + ), + ), + migrations.AddField( + model_name="catalogzonemember", + name="catalog_zone", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="members", + to="netbox_dns_bridge.catalogzone", + ), + ), + migrations.RunPython( + copy_serials_and_link_members, + reverse_code=reverse_migration, + ), + migrations.AlterField( + model_name="catalogzonemember", + name="catalog_zone", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="members", + to="netbox_dns_bridge.catalogzone", + ), + ), + migrations.AddConstraint( + model_name="catalogzonemember", + constraint=models.UniqueConstraint( + fields=("name", "catalog_zone"), + name="unique_name_per_catalog_zone", + ), + ), + migrations.DeleteModel( + name="IntegerKeyValueSetting", + ), + # --- SeenTransferClients → SeenTransferClient (rename + full NetBoxModel) --- + migrations.RenameModel( + old_name="SeenTransferClients", + new_name="SeenTransferClient", + ), + migrations.AddField( + model_name="seentransferclient", + name="created", + field=models.DateTimeField(auto_now_add=True, null=True), + ), + migrations.AddField( + model_name="seentransferclient", + name="last_updated", + field=models.DateTimeField(auto_now=True, null=True), + ), + migrations.AddField( + model_name="seentransferclient", + name="custom_field_data", + field=models.JSONField( + blank=True, + default=dict, + encoder=utilities.json.CustomFieldJSONEncoder, + ), + ), + migrations.AddField( + model_name="seentransferclient", + name="tags", + field=taggit.managers.TaggableManager( + through="extras.TaggedItem", to="extras.Tag" + ), + ), + ] diff --git a/netbox_dns_bridge/models.py b/netbox_dns_bridge/models.py index 5eb479e..cd41b4e 100644 --- a/netbox_dns_bridge/models.py +++ b/netbox_dns_bridge/models.py @@ -1,13 +1,11 @@ from django.db import models -from django.urls import reverse # import netbox.models import netbox_dns.models +from netbox.models import NetBoxModel -class CatalogZone(models.Model): - _netbox_private = True - +class CatalogZone(NetBoxModel): view = models.OneToOneField( to=netbox_dns.models.View, on_delete=models.CASCADE, @@ -21,17 +19,14 @@ class CatalogZone(models.Model): class Meta: ordering = ("view__name",) + verbose_name = "Catalog Zone" + verbose_name_plural = "Catalog Zones" 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 +class CatalogZoneMember(NetBoxModel): name = models.CharField( max_length=26, ) @@ -39,17 +34,19 @@ class CatalogZoneMemberIdentifier(models.Model): zone = models.OneToOneField( to=netbox_dns.models.Zone, on_delete=models.CASCADE, - related_name="catz_identifier", + related_name="catalog_zone_member", ) catalog_zone = models.ForeignKey( to=CatalogZone, on_delete=models.CASCADE, - related_name="member_identifiers", + related_name="members", ) class Meta: ordering = ("name",) + verbose_name = "Catalog Zone Member" + verbose_name_plural = "Catalog Zone Members" constraints = [ models.UniqueConstraint( fields=["name", "catalog_zone"], @@ -61,9 +58,7 @@ class CatalogZoneMemberIdentifier(models.Model): return self.name -class SeenTransferClients(models.Model): - _netbox_private = True - +class SeenTransferClient(NetBoxModel): source_ip = models.GenericIPAddressField() last_seen = models.DateTimeField() view = models.ForeignKey( @@ -77,6 +72,8 @@ class SeenTransferClients(models.Model): class Meta: ordering = ("source_ip",) + verbose_name = "Seen Transfer Client" + verbose_name_plural = "Seen Transfer Clients" constraints = [ models.UniqueConstraint( fields=["source_ip", "view"], diff --git a/netbox_dns_bridge/navigation.py b/netbox_dns_bridge/navigation.py index f8b616c..80a0671 100644 --- a/netbox_dns_bridge/navigation.py +++ b/netbox_dns_bridge/navigation.py @@ -2,32 +2,33 @@ from django.utils.translation import gettext_lazy as _ from netbox.plugins import PluginMenu, PluginMenuItem catalog_zones_menu_item = PluginMenuItem( - link="plugins:netbox_dns_bridge:catalog_zones", + link="plugins:netbox_dns_bridge:catalogzone_list", link_text=_("Catalog Zones"), - permissions=[], - staff_only=True, + permissions=["netbox_dns_bridge.view_catalogzone"], ) catalog_zone_members_menu_item = PluginMenuItem( - link="plugins:netbox_dns_bridge:catalog_zone_members", + link="plugins:netbox_dns_bridge:catalogzonemember_list", link_text=_("Catalog Zone Members"), - permissions=[], - staff_only=True, + permissions=["netbox_dns_bridge.view_catalogzonemember"], ) -notify_menu_item = PluginMenuItem( - link="plugins:netbox_dns_bridge:notify", - link_text=_("Notify"), - permissions=[], - staff_only=True, +seen_transfer_clients_menu_item = PluginMenuItem( + link="plugins:netbox_dns_bridge:seentransferclient_list", + link_text=_("Clients"), + permissions=["netbox_dns_bridge.view_seentransferclient"], ) menu = PluginMenu( label=_("DNS Bridge"), groups=( ( - _("Information"), - (catalog_zones_menu_item, catalog_zone_members_menu_item, notify_menu_item), + _("Catalog Zone"), + (catalog_zones_menu_item, catalog_zone_members_menu_item), + ), + ( + _("Notify"), + (seen_transfer_clients_menu_item,), ), ), icon_class="mdi mdi-bridge", diff --git a/netbox_dns_bridge/signals/netbox_dns_zones.py b/netbox_dns_bridge/signals/netbox_dns_zones.py index f335254..b2ff8d9 100644 --- a/netbox_dns_bridge/signals/netbox_dns_zones.py +++ b/netbox_dns_bridge/signals/netbox_dns_zones.py @@ -77,15 +77,15 @@ def zone_post_save(sender, instance, created, **kwargs): transaction.on_commit(_on_commit) if created: - # If zone was created, create catz member identifier record for this zone. - catzm.update_member_identifier(zone) + # If zone was created, create the catalog zone member record for this zone. + catzm.update_member(zone) else: - # Re-create the catz member identifier when the zone name or its view - # changed. A view move leaves the identifier pointing at the wrong + # Re-create the catalog zone member when the zone name or its view + # changed. A view move leaves the member pointing at the wrong # catalog zone, so it must be refreshed. old_name = getattr(zone, "_old_name", None) if (old_name and zone.name != old_name) or view_changed: - catzm.update_member_identifier(zone) + catzm.update_member(zone) @receiver(post_delete, sender=Zone) diff --git a/netbox_dns_bridge/tables.py b/netbox_dns_bridge/tables.py index 61bbb8d..8ee2c36 100644 --- a/netbox_dns_bridge/tables.py +++ b/netbox_dns_bridge/tables.py @@ -1,29 +1,30 @@ import django_tables2 as tables from django.utils.translation import gettext_lazy as _ -import netbox_dns.models from netbox.tables import NetBoxTable +from netbox.tables.columns import ActionsColumn -from .models import CatalogZone +from .models import CatalogZone, CatalogZoneMember, SeenTransferClient class CatalogZoneMemberTable(NetBoxTable): name = tables.Column(linkify=True) + zone = tables.Column( + accessor="zone__name", + verbose_name=_("Zone"), + linkify=True, + ) view = tables.Column( - accessor="view__name", + accessor="catalog_zone__view__name", verbose_name=_("View"), ) - status = tables.Column(verbose_name=_("Status")) - catz_identifier = tables.Column( - accessor="catz_identifier__name", - verbose_name=_("Catalog Zone Identifier"), - default="—", - ) + + actions = ActionsColumn(actions=()) class Meta(NetBoxTable.Meta): - model = netbox_dns.models.Zone - fields = ("name", "view", "status", "catz_identifier") - default_columns = ("name", "view", "status", "catz_identifier") + model = CatalogZoneMember + fields = ("name", "zone", "view") + default_columns = ("name", "zone", "view") class CatalogZoneTable(NetBoxTable): @@ -38,6 +39,8 @@ class CatalogZoneTable(NetBoxTable): soa_expire = tables.Column(verbose_name=_("Expire")) soa_minimum = tables.Column(verbose_name=_("Minimum")) + actions = ActionsColumn(actions=("edit", "changelog")) + class Meta(NetBoxTable.Meta): model = CatalogZone fields = ( @@ -56,3 +59,20 @@ class CatalogZoneTable(NetBoxTable): "soa_expire", "soa_minimum", ) + + +class SeenTransferClientTable(NetBoxTable): + source_ip = tables.Column(linkify=True) + view = tables.Column( + accessor="view__name", + verbose_name=_("View"), + linkify=True, + ) + last_seen = tables.DateTimeColumn(verbose_name=_("Last Seen")) + + actions = ActionsColumn(actions=()) + + class Meta(NetBoxTable.Meta): + model = SeenTransferClient + fields = ("source_ip", "view", "last_seen") + default_columns = ("source_ip", "view", "last_seen") diff --git a/netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_members.html b/netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_members.html deleted file mode 100644 index 1352a41..0000000 --- a/netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_members.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends 'generic/object_list.html' %} -{% load i18n %} - -{% block title %}{% trans "Catalog Zone Members" %}{% endblock %} diff --git a/netbox_dns_bridge/templates/netbox_dns_bridge/catalogzone.html b/netbox_dns_bridge/templates/netbox_dns_bridge/catalogzone.html new file mode 100644 index 0000000..dc11ad4 --- /dev/null +++ b/netbox_dns_bridge/templates/netbox_dns_bridge/catalogzone.html @@ -0,0 +1,50 @@ +{% extends 'generic/object.html' %} +{% load plugins %} +{% load i18n %} + +{% block content %} +
+
+
+
{% trans "Catalog Zone SOA" %}
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "View" %}{{ object.view|linkify }}
{% trans "SOA Serial" %}{{ object.soa_serial }}
{% trans "Refresh" %}{{ object.soa_refresh }}
{% trans "Retry" %}{{ object.soa_retry }}
{% trans "Expire" %}{{ object.soa_expire }}
{% trans "Minimum" %}{{ object.soa_minimum }}
+
+ {% include 'inc/panels/custom_fields.html' %} + {% plugin_left_page object %} +
+
+ {% include 'inc/panels/tags.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox_dns_bridge/templates/netbox_dns_bridge/catalogzonemember.html b/netbox_dns_bridge/templates/netbox_dns_bridge/catalogzonemember.html new file mode 100644 index 0000000..096d280 --- /dev/null +++ b/netbox_dns_bridge/templates/netbox_dns_bridge/catalogzonemember.html @@ -0,0 +1,42 @@ +{% extends 'generic/object.html' %} +{% load plugins %} +{% load i18n %} + +{% block content %} +
+
+
+
{% trans "Catalog Zone Member" %}
+ + + + + + + + + + + + + + + + + +
{% trans "Name" %}{{ object.name }}
{% trans "Zone" %}{{ object.zone|linkify }}
{% trans "Catalog Zone" %}{{ object.catalog_zone|linkify }}
{% trans "View" %}{{ object.catalog_zone.view|linkify }}
+
+ {% include 'inc/panels/custom_fields.html' %} + {% plugin_left_page object %} +
+
+ {% include 'inc/panels/tags.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox_dns_bridge/templates/netbox_dns_bridge/notify_overview.html b/netbox_dns_bridge/templates/netbox_dns_bridge/notify_overview.html deleted file mode 100644 index 78c6a44..0000000 --- a/netbox_dns_bridge/templates/netbox_dns_bridge/notify_overview.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends 'base/layout.html' %} - -{% block title %}DNS Bridge Notify{% endblock %} - -{% block content %} -
-
- -
-
Seen Transfer Clients
-
- - - - - - - - - - {% for c in seen_clients %} - - - - - - {% empty %} - - {% endfor %} - -
Source IPViewLast Seen
{{ c.source_ip }}{{ c.view.name }}{{ c.last_seen|date:"Y-m-d H:i:s" }}
No seen transfer clients.
-
-
- -
-
-{% endblock %} diff --git a/netbox_dns_bridge/templates/netbox_dns_bridge/seentransferclient.html b/netbox_dns_bridge/templates/netbox_dns_bridge/seentransferclient.html new file mode 100644 index 0000000..ffb34d2 --- /dev/null +++ b/netbox_dns_bridge/templates/netbox_dns_bridge/seentransferclient.html @@ -0,0 +1,38 @@ +{% extends 'generic/object.html' %} +{% load plugins %} +{% load i18n %} + +{% block content %} +
+
+
+
{% trans "Seen Transfer Client" %}
+ + + + + + + + + + + + + +
{% trans "Source IP" %}{{ object.source_ip }}
{% trans "View" %}{{ object.view|linkify }}
{% trans "Last Seen" %}{{ object.last_seen|date:"Y-m-d H:i:s" }}
+
+ {% include 'inc/panels/custom_fields.html' %} + {% plugin_left_page object %} +
+
+ {% include 'inc/panels/tags.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} diff --git a/netbox_dns_bridge/urls.py b/netbox_dns_bridge/urls.py index 18d97e2..441a70a 100644 --- a/netbox_dns_bridge/urls.py +++ b/netbox_dns_bridge/urls.py @@ -1,23 +1,47 @@ -from django.urls import path +from django.urls import include, path -from .views import ( +from utilities.urls import get_model_urls + +# Import views so register_model_view decorators run. +from .views import ( # noqa: F401 CatalogZoneEditView, CatalogZoneListView, - CatalogZoneMembersView, - NotifyOverviewView, + CatalogZoneMemberListView, + CatalogZoneMemberView, + CatalogZoneView, + SeenTransferClientListView, + SeenTransferClientView, ) urlpatterns = [ - path("catalog-zones/", CatalogZoneListView.as_view(), name="catalog_zones"), path( - "catalog-zones//edit/", - CatalogZoneEditView.as_view(), - name="catalogzone_edit", + "catalog-zones/", + include( + get_model_urls("netbox_dns_bridge", "catalogzone", detail=False) + ), + ), + path( + "catalog-zones//", + include(get_model_urls("netbox_dns_bridge", "catalogzone")), ), path( "catalog-zone-members/", - CatalogZoneMembersView.as_view(), - name="catalog_zone_members", + include( + get_model_urls("netbox_dns_bridge", "catalogzonemember", detail=False) + ), + ), + path( + "catalog-zone-members//", + include(get_model_urls("netbox_dns_bridge", "catalogzonemember")), + ), + path( + "seen-transfer-clients/", + include( + get_model_urls("netbox_dns_bridge", "seentransferclient", detail=False) + ), + ), + path( + "seen-transfer-clients//", + include(get_model_urls("netbox_dns_bridge", "seentransferclient")), ), - path("notify/", NotifyOverviewView.as_view(), name="notify"), ] diff --git a/netbox_dns_bridge/views.py b/netbox_dns_bridge/views.py index 1b310f8..1463b9d 100644 --- a/netbox_dns_bridge/views.py +++ b/netbox_dns_bridge/views.py @@ -1,67 +1,76 @@ -from django.contrib.auth.decorators import user_passes_test -from django.shortcuts import render -from django.utils.decorators import method_decorator -from django.views import View +from netbox.object_actions import EditObject +from netbox.views import generic +from utilities.views import register_model_view -import netbox_dns.models -from netbox.views.generic import ObjectEditView, ObjectListView - -from .filtersets import CatalogZoneFilterSet, ZoneFilterSet +from .filtersets import ( + CatalogZoneFilterSet, + CatalogZoneMemberFilterSet, + SeenTransferClientFilterSet, +) from .forms import ( CatalogZoneFilterForm, CatalogZoneForm, CatalogZoneMemberFilterForm, + SeenTransferClientFilterForm, +) +from .models import CatalogZone, CatalogZoneMember, SeenTransferClient +from .tables import ( + CatalogZoneMemberTable, + CatalogZoneTable, + SeenTransferClientTable, ) -from .models import CatalogZone, SeenTransferClients -from .tables import CatalogZoneMemberTable, CatalogZoneTable -@method_decorator( - user_passes_test(lambda u: u.is_superuser), name="dispatch" -) -class CatalogZoneListView(ObjectListView): +@register_model_view(CatalogZone, "list", path="", detail=False) +class CatalogZoneListView(generic.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 = CatalogZoneMemberTable - filterset = ZoneFilterSet - filterset_form = CatalogZoneMemberFilterForm - template_name = "netbox_dns_bridge/catalog_zone_members.html" actions = () -@method_decorator( - user_passes_test(lambda u: u.is_superuser), name="dispatch" -) -class NotifyOverviewView(View): - template_name = "netbox_dns_bridge/notify_overview.html" +@register_model_view(CatalogZone) +class CatalogZoneView(generic.ObjectView): + queryset = CatalogZone.objects.select_related("view") + actions = (EditObject,) - def get(self, request): - seen_clients = ( - SeenTransferClients.objects.select_related("view") - .all() - .order_by("source_ip") - ) - context = {"seen_clients": seen_clients} - return render(request, self.template_name, context) + +@register_model_view(CatalogZone, "edit") +class CatalogZoneEditView(generic.ObjectEditView): + queryset = CatalogZone.objects.select_related("view") + form = CatalogZoneForm + + +@register_model_view(CatalogZoneMember, "list", path="", detail=False) +class CatalogZoneMemberListView(generic.ObjectListView): + queryset = CatalogZoneMember.objects.select_related( + "zone", "catalog_zone__view" + ).order_by("catalog_zone__view__name", "name") + table = CatalogZoneMemberTable + filterset = CatalogZoneMemberFilterSet + filterset_form = CatalogZoneMemberFilterForm + actions = () + + +@register_model_view(CatalogZoneMember) +class CatalogZoneMemberView(generic.ObjectView): + queryset = CatalogZoneMember.objects.select_related( + "zone", "catalog_zone__view" + ) + actions = () + + +@register_model_view(SeenTransferClient, "list", path="", detail=False) +class SeenTransferClientListView(generic.ObjectListView): + queryset = SeenTransferClient.objects.select_related("view").order_by("source_ip") + table = SeenTransferClientTable + filterset = SeenTransferClientFilterSet + filterset_form = SeenTransferClientFilterForm + actions = () + + +@register_model_view(SeenTransferClient) +class SeenTransferClientView(generic.ObjectView): + queryset = SeenTransferClient.objects.select_related("view") + actions = ()