netbox-plugin-dns 1.2.7b2__py3-none-any.whl → 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of netbox-plugin-dns might be problematic. Click here for more details.

Files changed (48) hide show
  1. netbox_dns/__init__.py +56 -29
  2. netbox_dns/api/field_serializers.py +25 -0
  3. netbox_dns/api/nested_serializers.py +19 -1
  4. netbox_dns/api/serializers_/dnssec_key_template.py +13 -0
  5. netbox_dns/api/serializers_/dnssec_policy.py +31 -0
  6. netbox_dns/api/serializers_/record.py +2 -0
  7. netbox_dns/api/serializers_/record_template.py +2 -0
  8. netbox_dns/api/serializers_/zone.py +10 -1
  9. netbox_dns/choices/dnssec_key_template.py +4 -4
  10. netbox_dns/choices/dnssec_policy.py +2 -2
  11. netbox_dns/choices/record.py +66 -19
  12. netbox_dns/choices/utilities.py +4 -26
  13. netbox_dns/choices/zone.py +96 -1
  14. netbox_dns/fields/choice_array.py +13 -0
  15. netbox_dns/fields/timeperiod.py +15 -13
  16. netbox_dns/filtersets/dnssec_policy.py +47 -1
  17. netbox_dns/filtersets/zone.py +7 -2
  18. netbox_dns/filtersets/zone_template.py +2 -2
  19. netbox_dns/forms/dnssec_key_template.py +2 -1
  20. netbox_dns/forms/dnssec_policy.py +82 -33
  21. netbox_dns/forms/nameserver.py +2 -0
  22. netbox_dns/forms/record_template.py +1 -0
  23. netbox_dns/forms/zone.py +78 -15
  24. netbox_dns/forms/zone_template.py +9 -0
  25. netbox_dns/graphql/types.py +1 -0
  26. netbox_dns/locale/de/LC_MESSAGES/django.mo +0 -0
  27. netbox_dns/locale/fr/LC_MESSAGES/django.mo +0 -0
  28. netbox_dns/migrations/0018_zone_domain_status_zone_expiration_date.py +23 -0
  29. netbox_dns/migrations/0019_dnssecpolicy_parental_agents.py +25 -0
  30. netbox_dns/models/dnssec_policy.py +14 -3
  31. netbox_dns/models/record.py +4 -1
  32. netbox_dns/models/zone.py +65 -4
  33. netbox_dns/models/zone_template.py +1 -1
  34. netbox_dns/tables/zone.py +6 -1
  35. netbox_dns/template_content.py +2 -1
  36. netbox_dns/templates/netbox_dns/dnssecpolicy.html +10 -0
  37. netbox_dns/templates/netbox_dns/zone/registration.html +19 -0
  38. netbox_dns/urls.py +7 -0
  39. netbox_dns/utilities/conversions.py +13 -0
  40. netbox_dns/validators/dns_value.py +3 -0
  41. netbox_dns/validators/dnssec.py +10 -8
  42. netbox_dns/views/dnssec_policy.py +3 -1
  43. netbox_dns/views/zone.py +11 -1
  44. {netbox_plugin_dns-1.2.7b2.dist-info → netbox_plugin_dns-1.2.8.dist-info}/METADATA +4 -3
  45. {netbox_plugin_dns-1.2.7b2.dist-info → netbox_plugin_dns-1.2.8.dist-info}/RECORD +48 -45
  46. {netbox_plugin_dns-1.2.7b2.dist-info → netbox_plugin_dns-1.2.8.dist-info}/WHEEL +1 -1
  47. {netbox_plugin_dns-1.2.7b2.dist-info → netbox_plugin_dns-1.2.8.dist-info/licenses}/LICENSE +0 -0
  48. {netbox_plugin_dns-1.2.7b2.dist-info → netbox_plugin_dns-1.2.8.dist-info}/top_level.txt +0 -0
netbox_dns/forms/zone.py CHANGED
@@ -23,7 +23,7 @@ from utilities.forms.fields import (
23
23
  DynamicModelChoiceField,
24
24
  )
25
25
  from utilities.release import load_release_data
26
- from utilities.forms.widgets import BulkEditNullBooleanSelect
26
+ from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker
27
27
  from utilities.forms.rendering import FieldSet
28
28
  from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
29
29
  from tenancy.models import Tenant, TenantGroup
@@ -38,7 +38,7 @@ from netbox_dns.models import (
38
38
  ZoneTemplate,
39
39
  DNSSECPolicy,
40
40
  )
41
- from netbox_dns.choices import ZoneStatusChoices
41
+ from netbox_dns.choices import ZoneStatusChoices, ZoneEPPStatusChoices
42
42
  from netbox_dns.utilities import name_to_unicode, network_to_reverse
43
43
  from netbox_dns.fields import RFC2317NetworkFormField, TimePeriodField
44
44
  from netbox_dns.validators import validate_ipv4, validate_prefix, validate_rfc2317
@@ -59,10 +59,21 @@ class RollbackTransaction(Exception):
59
59
 
60
60
 
61
61
  class ZoneTemplateUpdateMixin:
62
+ def _check_soa_mname(self):
63
+ if (
64
+ self.cleaned_data.get("soa_mname") is None
65
+ and "soa_mname" in self.fields.keys()
66
+ ):
67
+ self.add_error(
68
+ "soa_mname",
69
+ _("soa_mname not set and no template or default value defined"),
70
+ )
71
+
62
72
  def clean(self, *args, **kwargs):
63
73
  super().clean(*args, **kwargs)
64
74
 
65
75
  if (template := self.cleaned_data.get("template")) is None:
76
+ self._check_soa_mname()
66
77
  return
67
78
 
68
79
  if not self.cleaned_data.get("nameservers") and template.nameservers.all():
@@ -77,11 +88,7 @@ class ZoneTemplateUpdateMixin:
77
88
  ) not in (None, ""):
78
89
  self.cleaned_data[field] = getattr(template, field)
79
90
 
80
- if self.cleaned_data.get("soa_mname") is None:
81
- self.add_error(
82
- "soa_mname",
83
- _("soa_mname not set and no template or default value defined"),
84
- )
91
+ self._check_soa_mname()
85
92
 
86
93
  if self.errors:
87
94
  return
@@ -285,6 +292,8 @@ class ZoneForm(ZoneTemplateUpdateMixin, TenancyForm, NetBoxModelForm):
285
292
  FieldSet(
286
293
  "registrar",
287
294
  "registry_domain_id",
295
+ "expiration_date",
296
+ "domain_status",
288
297
  "registrant",
289
298
  "admin_c",
290
299
  "tech_c",
@@ -379,6 +388,8 @@ class ZoneForm(ZoneTemplateUpdateMixin, TenancyForm, NetBoxModelForm):
379
388
  "inline_signing",
380
389
  "registrar",
381
390
  "registry_domain_id",
391
+ "expiration_date",
392
+ "domain_status",
382
393
  "registrant",
383
394
  "admin_c",
384
395
  "tech_c",
@@ -387,8 +398,8 @@ class ZoneForm(ZoneTemplateUpdateMixin, TenancyForm, NetBoxModelForm):
387
398
  "tenant",
388
399
  "tags",
389
400
  )
390
- help_texts = {
391
- "soa_mname": _("Primary nameserver for the zone"),
401
+ widgets = {
402
+ "expiration_date": DatePicker,
392
403
  }
393
404
 
394
405
 
@@ -412,7 +423,7 @@ class ZoneFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
412
423
  name=_("SOA"),
413
424
  ),
414
425
  FieldSet(
415
- "dnssec_policy",
426
+ "dnssec_policy_id",
416
427
  "inline_signing",
417
428
  name=_("DNSSEC"),
418
429
  ),
@@ -425,6 +436,9 @@ class ZoneFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
425
436
  FieldSet(
426
437
  "registrar_id",
427
438
  "registry_domain_id",
439
+ "expiration_date_before",
440
+ "expiration_date_after",
441
+ "domain_status",
428
442
  "registrant_id",
429
443
  "admin_c_id",
430
444
  "tech_c_id",
@@ -451,6 +465,7 @@ class ZoneFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
451
465
  nameserver_id = DynamicModelMultipleChoiceField(
452
466
  queryset=NameServer.objects.all(),
453
467
  required=False,
468
+ null_option=_("None"),
454
469
  label=_("Nameservers"),
455
470
  )
456
471
  active = forms.NullBooleanField(
@@ -488,40 +503,67 @@ class ZoneFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
488
503
  rfc2317_parent_zone_id = DynamicModelMultipleChoiceField(
489
504
  queryset=Zone.objects.all(),
490
505
  required=False,
506
+ null_option=_("None"),
491
507
  label=_("Parent Zone"),
492
508
  )
493
- registrar_id = DynamicModelMultipleChoiceField(
494
- queryset=Registrar.objects.all(),
495
- required=False,
496
- label=_("Registrar"),
497
- )
498
509
  dnssec_policy_id = DynamicModelMultipleChoiceField(
499
510
  queryset=DNSSECPolicy.objects.all(),
500
511
  required=False,
512
+ null_option=_("None"),
501
513
  label=_("DNSSEC Policy"),
502
514
  )
515
+ inline_signing = forms.NullBooleanField(
516
+ required=False,
517
+ widget=forms.Select(choices=BOOLEAN_WITH_BLANK_CHOICES),
518
+ label=_("Use Inline Signing"),
519
+ )
520
+ registrar_id = DynamicModelMultipleChoiceField(
521
+ queryset=Registrar.objects.all(),
522
+ required=False,
523
+ null_option=_("None"),
524
+ label=_("Registrar"),
525
+ )
503
526
  registry_domain_id = forms.CharField(
504
527
  required=False,
505
528
  label=_("Registry Domain ID"),
506
529
  )
530
+ expiration_date_after = forms.DateField(
531
+ required=False,
532
+ label=_("Expiration Date after"),
533
+ widget=DatePicker,
534
+ )
535
+ expiration_date_before = forms.DateField(
536
+ required=False,
537
+ label=_("Expiration Date before"),
538
+ widget=DatePicker,
539
+ )
540
+ domain_status = forms.MultipleChoiceField(
541
+ choices=ZoneEPPStatusChoices,
542
+ required=False,
543
+ label=_("Domain Status"),
544
+ )
507
545
  registrant_id = DynamicModelMultipleChoiceField(
508
546
  queryset=RegistrationContact.objects.all(),
509
547
  required=False,
548
+ null_option=_("None"),
510
549
  label=_("Registrant"),
511
550
  )
512
551
  admin_c_id = DynamicModelMultipleChoiceField(
513
552
  queryset=RegistrationContact.objects.all(),
514
553
  required=False,
554
+ null_option=_("None"),
515
555
  label=_("Administrative Contact"),
516
556
  )
517
557
  tech_c_id = DynamicModelMultipleChoiceField(
518
558
  queryset=RegistrationContact.objects.all(),
519
559
  required=False,
560
+ null_option=_("None"),
520
561
  label=_("Technical Contact"),
521
562
  )
522
563
  billing_c_id = DynamicModelMultipleChoiceField(
523
564
  queryset=RegistrationContact.objects.all(),
524
565
  required=False,
566
+ null_option=_("None"),
525
567
  label=_("Billing Contact"),
526
568
  )
527
569
  tag = TagFilterField(Zone)
@@ -638,6 +680,11 @@ class ZoneImportForm(ZoneTemplateUpdateMixin, NetBoxModelImportForm):
638
680
  required=False,
639
681
  label=_("Registry Domain ID"),
640
682
  )
683
+ domain_status = CSVChoiceField(
684
+ choices=ZoneEPPStatusChoices,
685
+ required=False,
686
+ label=_("Domain Status"),
687
+ )
641
688
  registrant = CSVModelChoiceField(
642
689
  queryset=RegistrationContact.objects.all(),
643
690
  required=False,
@@ -713,6 +760,8 @@ class ZoneImportForm(ZoneTemplateUpdateMixin, NetBoxModelImportForm):
713
760
  "rfc2317_parent_managed",
714
761
  "registrar",
715
762
  "registry_domain_id",
763
+ "expiration_date",
764
+ "domain_status",
716
765
  "registrant",
717
766
  "admin_c",
718
767
  "tech_c",
@@ -846,6 +895,16 @@ class ZoneBulkEditForm(NetBoxModelBulkEditForm):
846
895
  required=False,
847
896
  label=_("Registry Domain ID"),
848
897
  )
898
+ expiration_date = forms.DateField(
899
+ required=False,
900
+ label=_("Expiration Date"),
901
+ widget=DatePicker,
902
+ )
903
+ domain_status = forms.ChoiceField(
904
+ choices=add_blank_choice(ZoneEPPStatusChoices),
905
+ required=False,
906
+ label=_("Domain Status"),
907
+ )
849
908
  registrant = DynamicModelChoiceField(
850
909
  queryset=RegistrationContact.objects.all(),
851
910
  required=False,
@@ -913,6 +972,8 @@ class ZoneBulkEditForm(NetBoxModelBulkEditForm):
913
972
  FieldSet(
914
973
  "registrar",
915
974
  "registry_domain_id",
975
+ "expiration_date",
976
+ "domain_status",
916
977
  "registrant",
917
978
  "admin_c",
918
979
  "tech_c",
@@ -927,6 +988,8 @@ class ZoneBulkEditForm(NetBoxModelBulkEditForm):
927
988
  "nameservers",
928
989
  "rfc2317_prefix",
929
990
  "registrar",
991
+ "expiration_date",
992
+ "domain_status",
930
993
  "registry_domain_id",
931
994
  "registrant",
932
995
  "admin_c",
@@ -127,11 +127,13 @@ class ZoneTemplateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
127
127
  nameserver_id = DynamicModelMultipleChoiceField(
128
128
  queryset=NameServer.objects.all(),
129
129
  required=False,
130
+ null_option=_("None"),
130
131
  label=_("Nameservers"),
131
132
  )
132
133
  soa_mname_id = DynamicModelMultipleChoiceField(
133
134
  queryset=NameServer.objects.all(),
134
135
  required=False,
136
+ null_option=_("None"),
135
137
  label=_("MName"),
136
138
  )
137
139
  soa_rname = forms.CharField(
@@ -141,6 +143,7 @@ class ZoneTemplateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
141
143
  record_template_id = DynamicModelMultipleChoiceField(
142
144
  queryset=RecordTemplate.objects.all(),
143
145
  required=False,
146
+ null_option=_("None"),
144
147
  label=_("Record Templates"),
145
148
  )
146
149
  description = forms.CharField(
@@ -149,31 +152,37 @@ class ZoneTemplateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
149
152
  dnssec_policy_id = DynamicModelMultipleChoiceField(
150
153
  queryset=DNSSECPolicy.objects.all(),
151
154
  required=False,
155
+ null_option=_("None"),
152
156
  label=_("DNSSEC Policy ID"),
153
157
  )
154
158
  registrar_id = DynamicModelMultipleChoiceField(
155
159
  queryset=Registrar.objects.all(),
156
160
  required=False,
161
+ null_option=_("None"),
157
162
  label=_("Registrar"),
158
163
  )
159
164
  registrant_id = DynamicModelMultipleChoiceField(
160
165
  queryset=RegistrationContact.objects.all(),
161
166
  required=False,
167
+ null_option=_("None"),
162
168
  label=_("Registrant"),
163
169
  )
164
170
  admin_c_id = DynamicModelMultipleChoiceField(
165
171
  queryset=RegistrationContact.objects.all(),
166
172
  required=False,
173
+ null_option=_("None"),
167
174
  label=_("Administrative Contact"),
168
175
  )
169
176
  tech_c_id = DynamicModelMultipleChoiceField(
170
177
  queryset=RegistrationContact.objects.all(),
171
178
  required=False,
179
+ null_option=_("None"),
172
180
  label=_("Technical Contact"),
173
181
  )
174
182
  billing_c_id = DynamicModelMultipleChoiceField(
175
183
  queryset=RegistrationContact.objects.all(),
176
184
  required=False,
185
+ null_option=_("None"),
177
186
  label=_("Billing Contact"),
178
187
  )
179
188
  tag = TagFilterField(ZoneTemplate)
@@ -214,6 +214,7 @@ class NetBoxDNSDNSSECPolicyType(NetBoxObjectType):
214
214
  cds_digest_types: List[str]
215
215
  parent_ds_ttl: BigInt | None
216
216
  parent_propagation_delay: BigInt | None
217
+ parental_agents: List[str]
217
218
  use_nsec3: bool
218
219
  nsec3_iterations: BigInt | None
219
220
  nsec3_opt_out: bool
Binary file
Binary file
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.1.7 on 2025-04-03 13:43
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("netbox_dns", "0017_dnssec_policy_zone_zone_template"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="zone",
15
+ name="domain_status",
16
+ field=models.CharField(blank=True, max_length=50, null=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name="zone",
20
+ name="expiration_date",
21
+ field=models.DateField(blank=True, null=True),
22
+ ),
23
+ ]
@@ -0,0 +1,25 @@
1
+ # Generated by Django 5.2 on 2025-04-22 18:05
2
+
3
+ import django.contrib.postgres.fields
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("netbox_dns", "0018_zone_domain_status_zone_expiration_date"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="dnssecpolicy",
16
+ name="parental_agents",
17
+ field=django.contrib.postgres.fields.ArrayField(
18
+ base_field=models.GenericIPAddressField(),
19
+ blank=True,
20
+ default=list,
21
+ null=True,
22
+ size=None,
23
+ ),
24
+ ),
25
+ ]
@@ -1,5 +1,6 @@
1
1
  from django.db import models
2
2
  from django.urls import reverse
3
+ from django.contrib.postgres.fields import ArrayField
3
4
  from django.utils.translation import gettext_lazy as _
4
5
 
5
6
  from netbox.models import NetBoxModel
@@ -117,6 +118,14 @@ class DNSSECPolicy(ContactsMixin, NetBoxModel):
117
118
  blank=True,
118
119
  null=True,
119
120
  )
121
+ parental_agents = ArrayField(
122
+ base_field=models.GenericIPAddressField(
123
+ protocol="both",
124
+ ),
125
+ blank=True,
126
+ null=True,
127
+ default=list,
128
+ )
120
129
 
121
130
  use_nsec3 = models.BooleanField(
122
131
  verbose_name=_("Use NSEC3"),
@@ -181,16 +190,18 @@ class DNSSECPolicy(ContactsMixin, NetBoxModel):
181
190
  return self.publish_safety if self.publish_safety is not None else 3600
182
191
 
183
192
  def get_effective_value(self, attribute):
184
- default_value = get_plugin_config("netbox_dns", f"dnssec_{attribute}", None)
185
-
186
193
  if not hasattr(self, attribute):
187
194
  raise AttributeError(f"DNSSECPolicy does not have attribute {attribute}")
188
195
 
189
196
  if (value := getattr(self, attribute)) is None:
190
- return default_value
197
+ return self.get_fallback_setting(attribute)
191
198
 
192
199
  return value
193
200
 
201
+ @classmethod
202
+ def get_fallback_setting(cls, attribute):
203
+ return get_plugin_config("netbox_dns", f"dnssec_{attribute}")
204
+
194
205
 
195
206
  @register_search
196
207
  class DNSSECPolicyIndex(SearchIndex):
@@ -167,7 +167,7 @@ class Record(ObjectModificationMixin, ContactsMixin, NetBoxModel):
167
167
  default=False,
168
168
  )
169
169
  ptr_record = models.OneToOneField(
170
- verbose_name="PTR Record",
170
+ verbose_name=_("PTR Record"),
171
171
  to="self",
172
172
  on_delete=models.SET_NULL,
173
173
  related_name="address_record",
@@ -649,6 +649,9 @@ class Record(ObjectModificationMixin, ContactsMixin, NetBoxModel):
649
649
 
650
650
  @property
651
651
  def absolute_value(self):
652
+ if self.type in RecordTypeChoices.CUSTOM_TYPES:
653
+ return self.value
654
+
652
655
  zone = dns_name.from_text(self.zone.name)
653
656
  rr = rdata.from_text(RecordClassChoices.IN, self.type, self.value)
654
657
 
netbox_dns/models/zone.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import re
2
2
  from math import ceil
3
- from datetime import datetime
3
+ from datetime import datetime, date
4
4
 
5
5
  from dns import name as dns_name
6
6
  from dns.exception import DNSException
@@ -26,7 +26,12 @@ from netbox.plugins.utils import get_plugin_config
26
26
  from utilities.querysets import RestrictedQuerySet
27
27
  from ipam.models import IPAddress
28
28
 
29
- from netbox_dns.choices import RecordClassChoices, RecordTypeChoices, ZoneStatusChoices
29
+ from netbox_dns.choices import (
30
+ RecordClassChoices,
31
+ RecordTypeChoices,
32
+ ZoneStatusChoices,
33
+ ZoneEPPStatusChoices,
34
+ )
30
35
  from netbox_dns.fields import NetworkField, RFC2317NetworkField
31
36
  from netbox_dns.utilities import (
32
37
  update_dns_records,
@@ -202,6 +207,18 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
202
207
  blank=True,
203
208
  null=True,
204
209
  )
210
+ expiration_date = models.DateField(
211
+ verbose_name=_("Expiration Date"),
212
+ blank=True,
213
+ null=True,
214
+ )
215
+ domain_status = models.CharField(
216
+ verbose_name=_("Domain Status"),
217
+ max_length=50,
218
+ choices=ZoneEPPStatusChoices,
219
+ blank=True,
220
+ null=True,
221
+ )
205
222
  registrant = models.ForeignKey(
206
223
  verbose_name=_("Registrant"),
207
224
  to="RegistrationContact",
@@ -211,7 +228,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
211
228
  null=True,
212
229
  )
213
230
  admin_c = models.ForeignKey(
214
- verbose_name="Administrative Contact",
231
+ verbose_name=_("Administrative Contact"),
215
232
  to="RegistrationContact",
216
233
  on_delete=models.SET_NULL,
217
234
  related_name="admin_c_zones",
@@ -288,6 +305,23 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
288
305
  "tenant",
289
306
  )
290
307
 
308
+ soa_clean_fields = {
309
+ "description",
310
+ "status",
311
+ "dnssec_policy",
312
+ "inline_signing",
313
+ "registrar",
314
+ "registry_domain_id",
315
+ "expiration_date",
316
+ "domain_status",
317
+ "registrant",
318
+ "admin_c",
319
+ "tech_c",
320
+ "billing_c",
321
+ "rfc2317_parent_managed",
322
+ "tenant",
323
+ }
324
+
291
325
  class Meta:
292
326
  verbose_name = _("Zone")
293
327
  verbose_name_plural = _("Zones")
@@ -370,6 +404,9 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
370
404
  def get_status_color(self):
371
405
  return ZoneStatusChoices.colors.get(self.status)
372
406
 
407
+ def get_domain_status_color(self):
408
+ return ZoneEPPStatusChoices.colors.get(self.domain_status)
409
+
373
410
  # TODO: Remove in version 1.3.0 (NetBox #18555)
374
411
  def get_absolute_url(self):
375
412
  return reverse("plugins:netbox_dns:zone", kwargs={"pk": self.pk})
@@ -410,6 +447,8 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
410
447
  self.admin_c,
411
448
  self.tech_c,
412
449
  self.billing_c,
450
+ self.expiration_date,
451
+ self.domain_status,
413
452
  )
414
453
  )
415
454
 
@@ -579,6 +618,26 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
579
618
 
580
619
  return ns_warnings, ns_errors
581
620
 
621
+ def check_expiration(self):
622
+ if self.expiration_date is None:
623
+ return None, None
624
+
625
+ expiration_warning = None
626
+ expiration_error = None
627
+
628
+ expiration_warning_days = get_plugin_config(
629
+ "netbox_dns", "zone_expiration_warning_days"
630
+ )
631
+
632
+ if self.expiration_date < date.today():
633
+ expiration_error = _("The registration for this domain has expired.")
634
+ elif (self.expiration_date - date.today()).days < expiration_warning_days:
635
+ expiration_warning = _(
636
+ f"The registration for his domain will expire less than {expiration_warning_days} days."
637
+ )
638
+
639
+ return expiration_warning, expiration_error
640
+
582
641
  def check_soa_serial_increment(self, old_serial, new_serial):
583
642
  MAX_SOA_SERIAL_INCREMENT = 2**31 - 1
584
643
  SOA_SERIAL_WRAP = 2**32
@@ -870,7 +929,9 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
870
929
 
871
930
  changed_fields = self.changed_fields
872
931
 
873
- if self.soa_serial_auto:
932
+ if self.soa_serial_auto and (
933
+ changed_fields is None or changed_fields - self.soa_clean_fields
934
+ ):
874
935
  self.soa_serial = self.get_auto_serial()
875
936
 
876
937
  super().save(*args, **kwargs)
@@ -139,7 +139,7 @@ class ZoneTemplate(NetBoxModel):
139
139
 
140
140
  class Meta:
141
141
  verbose_name = _("Zone Template")
142
- verbose_name_plural = "Zone Templates"
142
+ verbose_name_plural = _("Zone Templates")
143
143
 
144
144
  ordering = ("name",)
145
145
 
netbox_dns/tables/zone.py CHANGED
@@ -38,7 +38,7 @@ class ZoneTable(TenancyColumnsMixin, NetBoxTable):
38
38
  url_name="plugins:netbox_dns:zone_list",
39
39
  )
40
40
  default_ttl = tables.Column(
41
- verbose_name="Default TTL",
41
+ verbose_name=_("Default TTL"),
42
42
  )
43
43
  dnssec_policy = tables.Column(
44
44
  verbose_name=_("DNSSEC Policy"),
@@ -55,6 +55,9 @@ class ZoneTable(TenancyColumnsMixin, NetBoxTable):
55
55
  verbose_name=_("Registrar"),
56
56
  linkify=True,
57
57
  )
58
+ domain_status = ChoiceFieldColumn(
59
+ verbose_name=_("Domain Status"),
60
+ )
58
61
  registrant = tables.Column(
59
62
  verbose_name=_("Registrant"),
60
63
  linkify=True,
@@ -84,6 +87,8 @@ class ZoneTable(TenancyColumnsMixin, NetBoxTable):
84
87
  "inline_signing",
85
88
  "rfc2317_parent_managed",
86
89
  "registry_domain_id",
90
+ "expiration_date",
91
+ "domain_status",
87
92
  )
88
93
  default_columns = (
89
94
  "name",
@@ -2,6 +2,7 @@ import django_tables2 as tables
2
2
 
3
3
  from django.conf import settings
4
4
  from django.urls import reverse
5
+ from django.utils.translation import gettext_lazy as _
5
6
 
6
7
  from netbox.plugins.utils import get_plugin_config
7
8
  from netbox.plugins import PluginTemplateExtension
@@ -119,7 +120,7 @@ class IPRelatedDNSRecords(PluginTemplateExtension):
119
120
 
120
121
 
121
122
  address_records = tables.ManyToManyColumn(
122
- verbose_name="DNS Address Records",
123
+ verbose_name=_("DNS Address Records"),
123
124
  accessor="netbox_dns_records",
124
125
  linkify_item=True,
125
126
  transform=lambda obj: (
@@ -118,6 +118,16 @@
118
118
  <th scope="row">{% trans "Parent Propagation Delay" %}</th>
119
119
  <td>{{ object.parent_propagation_delay|placeholder }}</td>
120
120
  </tr>
121
+ <tr>
122
+ <th scope="row">{% trans "Parental Agents" %}</th>
123
+ <td>
124
+ <table>
125
+ {% for parental_agent in object.parental_agents %}
126
+ <tr><td>{{ parental_agent }}</td></tr>
127
+ {% endfor %}
128
+ </table>
129
+ </td>
130
+ </tr>
121
131
  </table>
122
132
  </div>
123
133
  <div class="card">
@@ -14,6 +14,25 @@
14
14
  <th scope="row">{% trans "Registry Domain ID" %}</th>
15
15
  <td>{{ object.registry_domain_id|placeholder }}</td>
16
16
  </tr>
17
+ <tr>
18
+ <th scope="row">{% trans "Expiration Date" %}</th>
19
+ <td>{{ object.expiration_date|placeholder }}</td>
20
+ </tr>
21
+ {% if expiration_warning %}
22
+ <tr>
23
+ <th class="text-warning" scope="row">{% trans "Warning" %}</th>
24
+ <td class="text-warning">{{ expiration_warning }}</td>
25
+ </tr>
26
+ {% elif expiration_error %}
27
+ <tr>
28
+ <th class="text-danger" scope="row">{% trans "Error" %}</th>
29
+ <td class="text-danger">{{ expiration_error }}</td>
30
+ </tr>
31
+ {% endif %}
32
+ <tr>
33
+ <th scope="row">{% trans "Domain Status" %}</th>
34
+ <td>{% badge object.get_domain_status_display bg_color=object.get_domain_status_color %}</td>
35
+ </tr>
17
36
  <tr>
18
37
  <th scope="row">{% trans "Registrant" %}</th>
19
38
  <td>{{ object.registrant|linkify|placeholder }}</td>
netbox_dns/urls.py CHANGED
@@ -2,6 +2,13 @@ from django.urls import include, path
2
2
 
3
3
  from utilities.urls import get_model_urls
4
4
 
5
+ # +
6
+ # Import views so the register_model_view is run. This is required for the
7
+ # URLs to be set up properly with get_model_urls().
8
+ # -
9
+ from .views import * # noqa: F401
10
+
11
+
5
12
  app_name = "netbox_dns"
6
13
 
7
14
  urlpatterns = (