diff --git a/netbox_dns/api/serializers_/record.py b/netbox_dns/api/serializers_/record.py index 6e79e872..c0174c72 100644 --- a/netbox_dns/api/serializers_/record.py +++ b/netbox_dns/api/serializers_/record.py @@ -41,6 +41,8 @@ class Meta: "tenant", "ipam_ip_address", "absolute_value", + "expiration_date", + "expired", ) brief_fields = ( @@ -57,6 +59,7 @@ class Meta: "description", "managed", "active", + "expired", ) url = serializers.HyperlinkedIdentityField( @@ -102,3 +105,7 @@ class Meta: required=False, allow_null=True, ) + expired = serializers.SerializerMethodField() + + def get_expired(self, instance): + return instance.is_expired diff --git a/netbox_dns/filtersets/record.py b/netbox_dns/filtersets/record.py index 70da74ba..c3f82d10 100755 --- a/netbox_dns/filtersets/record.py +++ b/netbox_dns/filtersets/record.py @@ -1,6 +1,7 @@ import django_filters import netaddr from django.db.models import Q +from django.utils.timezone import datetime from ipam.models import IPAddress from netbox.filtersets import PrimaryModelFilterSet @@ -73,6 +74,10 @@ class Meta: method="filter_ip_address", ) active = django_filters.BooleanFilter() + expiration_date = django_filters.DateFromToRangeFilter() + expired = django_filters.BooleanFilter( + method="filter_expired", + ) def filter_ip_address(self, queryset, name, value): if not value: @@ -88,6 +93,12 @@ def filter_ip_address(self, queryset, name, value): except (netaddr.AddrFormatError, ValueError): return queryset.none() + def filter_expired(self, queryset, name, value): + if value: + return queryset.filter(expiration_date__lt=datetime.now()) + + return queryset.exclude(expiration_date__lt=datetime.now()) + def search(self, queryset, name, value): if not value.strip(): return queryset diff --git a/netbox_dns/forms/record.py b/netbox_dns/forms/record.py index 1e06ec36..54b9cec9 100755 --- a/netbox_dns/forms/record.py +++ b/netbox_dns/forms/record.py @@ -22,7 +22,7 @@ TagFilterField, ) from utilities.forms.rendering import FieldSet -from utilities.forms.widgets import BulkEditNullBooleanSelect +from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker __all__ = ( "RecordForm", @@ -46,6 +46,7 @@ class Meta: "value", "status", "ttl", + "expiration_date", "disable_ptr", "tenant_group", "tenant", @@ -57,6 +58,10 @@ class Meta: "ttl": _("TTL"), } + widgets = { + "expiration_date": DatePicker, + } + fieldsets = ( FieldSet( "name", @@ -68,6 +73,7 @@ class Meta: "status", "ttl", "disable_ptr", + "expiration_date", name=_("Record"), ), FieldSet( @@ -141,6 +147,8 @@ class RecordFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): "ttl", "disable_ptr", "active", + "expiration_date_before", + "expiration_date_after", name=_("Attributes"), ), FieldSet( @@ -176,6 +184,16 @@ class RecordFilterForm(TenancyFilterForm, PrimaryModelFilterSetForm): widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES), label=_("Disable PTR"), ) + expiration_date_before = forms.DateField( + required=False, + label=_("Expiration Date Before"), + widget=DatePicker, + ) + expiration_date_after = forms.DateField( + required=False, + label=_("Expiration Date After"), + widget=DatePicker, + ) status = forms.MultipleChoiceField( choices=RecordStatusChoices, required=False, @@ -221,6 +239,7 @@ class Meta: "value", "ttl", "disable_ptr", + "expiration_date", "tenant", "tags", ) @@ -302,6 +321,7 @@ class RecordBulkEditForm(PrimaryModelBulkEditForm): "status", "ttl", "disable_ptr", + "expiration_date", name=_("Attributes"), ), FieldSet( @@ -315,6 +335,7 @@ class RecordBulkEditForm(PrimaryModelBulkEditForm): "description", "ttl", "tenant", + "expiration_date", ) zone = DynamicModelChoiceField( @@ -355,3 +376,8 @@ class RecordBulkEditForm(PrimaryModelBulkEditForm): required=False, label=_("Tenant"), ) + expiration_date = forms.DateField( + required=False, + label=_("Expiration Date"), + widget=DatePicker, + ) diff --git a/netbox_dns/graphql/filters/record.py b/netbox_dns/graphql/filters/record.py index 5c6ce862..5564289f 100644 --- a/netbox_dns/graphql/filters/record.py +++ b/netbox_dns/graphql/filters/record.py @@ -1,9 +1,10 @@ +from datetime import date from typing import TYPE_CHECKING, Annotated import strawberry import strawberry_django from strawberry.scalars import ID -from strawberry_django import FilterLookup +from strawberry_django import DateFilterLookup, FilterLookup try: from strawberry_django import StrFilterLookup @@ -58,6 +59,7 @@ class NetBoxDNSRecordFilter( | None ) = strawberry_django.filter_field() value: StrFilterLookup[str] | None = strawberry_django.filter_field() + expiration_date: DateFilterLookup[date] | None = strawberry_django.filter_field() disable_ptr: FilterLookup[bool] | None = strawberry_django.filter_field() status: ( Annotated[ diff --git a/netbox_dns/migrations/0032_record_expiration_date.py b/netbox_dns/migrations/0032_record_expiration_date.py new file mode 100644 index 00000000..ebe5f073 --- /dev/null +++ b/netbox_dns/migrations/0032_record_expiration_date.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.5 on 2026-06-07 10:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("netbox_dns", "0031_record_netbox_dns_record_name_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="record", + name="expiration_date", + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/netbox_dns/models/record.py b/netbox_dns/models/record.py index 0140f13b..9fe21871 100644 --- a/netbox_dns/models/record.py +++ b/netbox_dns/models/record.py @@ -1,4 +1,5 @@ import ipaddress +from datetime import date import dns import netaddr @@ -144,6 +145,7 @@ class Meta: "disable_ptr", "description", "tenant", + "expiration_date", ) def __init__(self, *args, **kwargs): @@ -258,6 +260,11 @@ def __str__(self): null=True, blank=True, ) + expiration_date = models.DateField( + verbose_name=_("Expiration Date"), + blank=True, + null=True, + ) @property def cleanup_ptr_record(self): @@ -307,6 +314,13 @@ def is_active(self): and self.zone.status in ZONE_ACTIVE_STATUS_LIST ) + @property + def is_expired(self): + if self.expiration_date is None: + return False + + return self.expiration_date < date.today() + @property def is_address_record(self): return self.type in (RecordTypeChoices.A, RecordTypeChoices.AAAA) diff --git a/netbox_dns/tables/record.py b/netbox_dns/tables/record.py index ea067043..dc4d3e4a 100644 --- a/netbox_dns/tables/record.py +++ b/netbox_dns/tables/record.py @@ -73,6 +73,7 @@ class Meta(PrimaryModelTable.Meta): fields = ( "status", "description", + "expiration_date", ) default_columns = ( diff --git a/netbox_dns/templates/netbox_dns/record.html b/netbox_dns/templates/netbox_dns/record.html index 055e0765..944cfa67 100644 --- a/netbox_dns/templates/netbox_dns/record.html +++ b/netbox_dns/templates/netbox_dns/record.html @@ -39,11 +39,11 @@ {% block content %}
| {% checkmark object.disable_ptr %} | |
| {% trans "Expiration Date" %} | +{{ object.expiration_date|isodate }} | +
|---|---|
| {% trans "Warning" %} | +{{ expiration_warning }} | +
| {% trans "PTR Record" %} | @@ -149,52 +161,52 @@