netbox-plugin-dns 1.0b1__py3-none-any.whl → 1.0.1__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 (40) hide show
  1. netbox_dns/__init__.py +3 -3
  2. netbox_dns/api/serializers_/view.py +6 -1
  3. netbox_dns/api/serializers_/zone.py +0 -11
  4. netbox_dns/fields/network.py +20 -21
  5. netbox_dns/fields/rfc2317.py +2 -2
  6. netbox_dns/filtersets/view.py +1 -1
  7. netbox_dns/filtersets/zone.py +4 -4
  8. netbox_dns/forms/record.py +30 -2
  9. netbox_dns/forms/view.py +6 -3
  10. netbox_dns/forms/zone.py +71 -102
  11. netbox_dns/graphql/types.py +1 -4
  12. netbox_dns/management/commands/cleanup_rrset_ttl.py +1 -1
  13. netbox_dns/migrations/0001_squashed_netbox_dns_0_22.py +4 -2
  14. netbox_dns/migrations/0003_default_view.py +15 -0
  15. netbox_dns/migrations/0004_create_and_assign_default_view.py +26 -0
  16. netbox_dns/migrations/0005_alter_zone_view_not_null.py +18 -0
  17. netbox_dns/models/nameserver.py +3 -1
  18. netbox_dns/models/record.py +86 -32
  19. netbox_dns/models/view.py +53 -0
  20. netbox_dns/models/zone.py +91 -46
  21. netbox_dns/signals/ipam_coupling.py +1 -2
  22. netbox_dns/tables/view.py +12 -2
  23. netbox_dns/template_content.py +1 -1
  24. netbox_dns/templates/netbox_dns/record.html +1 -1
  25. netbox_dns/templates/netbox_dns/view.html +4 -0
  26. netbox_dns/templates/netbox_dns/zone.html +2 -4
  27. netbox_dns/urls/__init__.py +17 -0
  28. netbox_dns/urls/contact.py +51 -0
  29. netbox_dns/urls/nameserver.py +69 -0
  30. netbox_dns/urls/record.py +41 -0
  31. netbox_dns/urls/registrar.py +63 -0
  32. netbox_dns/urls/view.py +39 -0
  33. netbox_dns/urls/zone.py +57 -0
  34. netbox_dns/validators/dns_name.py +24 -11
  35. netbox_dns/views/record.py +9 -24
  36. {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/METADATA +27 -13
  37. {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/RECORD +39 -30
  38. netbox_dns/urls.py +0 -297
  39. {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/LICENSE +0 -0
  40. {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/WHEEL +0 -0
@@ -55,7 +55,7 @@ class NameServer(NetBoxModel):
55
55
  def get_absolute_url(self):
56
56
  return reverse("plugins:netbox_dns:nameserver", kwargs={"pk": self.pk})
57
57
 
58
- def clean(self):
58
+ def clean(self, *args, **kwargs):
59
59
  try:
60
60
  self.name = normalize_name(self.name)
61
61
  except NameFormatError as exc:
@@ -74,6 +74,8 @@ class NameServer(NetBoxModel):
74
74
  }
75
75
  ) from None
76
76
 
77
+ super().clean(*args, **kwargs)
78
+
77
79
  def save(self, *args, **kwargs):
78
80
  self.full_clean()
79
81
 
@@ -11,11 +11,10 @@ from django.urls import reverse
11
11
 
12
12
  from netbox.models import NetBoxModel
13
13
  from netbox.search import SearchIndex, register_search
14
+ from netbox.plugins.utils import get_plugin_config
14
15
  from utilities.querysets import RestrictedQuerySet
15
16
  from utilities.choices import ChoiceSet
16
17
 
17
- from netbox.plugins.utils import get_plugin_config
18
-
19
18
  from netbox_dns.fields import AddressField
20
19
  from netbox_dns.utilities import (
21
20
  arpa_to_prefix,
@@ -24,6 +23,7 @@ from netbox_dns.utilities import (
24
23
  from netbox_dns.validators import (
25
24
  validate_fqdn,
26
25
  validate_extended_hostname,
26
+ validate_domain_name,
27
27
  )
28
28
 
29
29
  # +
@@ -234,8 +234,8 @@ class Record(NetBoxModel):
234
234
  if self.type != RecordTypeChoices.CNAME:
235
235
  return None
236
236
 
237
- zone = dns_name.from_text(self.zone.name)
238
- value_fqdn = dns_name.from_text(self.value, origin=zone)
237
+ _zone = dns_name.from_text(self.zone.name)
238
+ value_fqdn = dns_name.from_text(self.value, origin=_zone)
239
239
 
240
240
  return value_fqdn.to_text()
241
241
 
@@ -287,12 +287,14 @@ class Record(NetBoxModel):
287
287
  dns_name.from_text(self.ptr_record.zone.rfc2317_parent_zone.name)
288
288
  )
289
289
 
290
+ return None
291
+
290
292
  @property
291
293
  def ptr_zone(self):
292
294
  if self.type == RecordTypeChoices.A:
293
295
  ptr_zone = (
294
296
  zone.Zone.objects.filter(
295
- self.zone.view_filter,
297
+ view=self.zone.view,
296
298
  rfc2317_prefix__net_contains=self.value,
297
299
  )
298
300
  .order_by("rfc2317_prefix__net_mask_length")
@@ -304,7 +306,7 @@ class Record(NetBoxModel):
304
306
 
305
307
  ptr_zone = (
306
308
  zone.Zone.objects.filter(
307
- self.zone.view_filter, arpa_network__net_contains=self.value
309
+ view=self.zone.view, arpa_network__net_contains=self.value
308
310
  )
309
311
  .order_by("arpa_network__net_mask_length")
310
312
  .last()
@@ -319,7 +321,7 @@ class Record(NetBoxModel):
319
321
  ptr_zone is None
320
322
  or self.disable_ptr
321
323
  or not self.is_active
322
- or self.name == "*"
324
+ or self.name.startswith("*")
323
325
  ):
324
326
  if self.ptr_record is not None:
325
327
  with transaction.atomic():
@@ -418,10 +420,8 @@ class Record(NetBoxModel):
418
420
  self.rfc2317_cname_record.save(save_zone_serial=save_zone_serial)
419
421
 
420
422
  return
421
- else:
422
- self.remove_from_rfc2317_cname_record(
423
- save_zone_serial=save_zone_serial
424
- )
423
+
424
+ self.remove_from_rfc2317_cname_record(save_zone_serial=save_zone_serial)
425
425
 
426
426
  rfc2317_cname_record = Record.objects.filter(
427
427
  name=cname_name,
@@ -462,14 +462,14 @@ class Record(NetBoxModel):
462
462
 
463
463
  def validate_name(self):
464
464
  try:
465
- zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
465
+ _zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
466
466
  name = dns_name.from_text(self.name, origin=None)
467
- fqdn = dns_name.from_text(self.name, origin=zone)
467
+ fqdn = dns_name.from_text(self.name, origin=_zone)
468
468
 
469
- zone.to_unicode()
469
+ _zone.to_unicode()
470
470
  name.to_unicode()
471
471
 
472
- self.name = name.relativize(zone).to_text()
472
+ self.name = name.relativize(_zone).to_text()
473
473
  self.fqdn = fqdn.to_text()
474
474
 
475
475
  except dns.exception.DNSException as exc:
@@ -479,7 +479,7 @@ class Record(NetBoxModel):
479
479
  }
480
480
  )
481
481
 
482
- if not fqdn.is_subdomain(zone):
482
+ if not fqdn.is_subdomain(_zone):
483
483
  raise ValidationError(
484
484
  {
485
485
  "name": f"{self.name} is not a name in {self.zone.name}",
@@ -487,7 +487,7 @@ class Record(NetBoxModel):
487
487
  )
488
488
 
489
489
  if self.type not in get_plugin_config(
490
- "netbox_dns", "tolerate_non_rfc1035_types", default=list()
490
+ "netbox_dns", "tolerate_non_rfc1035_types", default=[]
491
491
  ):
492
492
  try:
493
493
  validate_extended_hostname(
@@ -497,7 +497,7 @@ class Record(NetBoxModel):
497
497
  in get_plugin_config(
498
498
  "netbox_dns",
499
499
  "tolerate_leading_underscore_types",
500
- default=list(),
500
+ default=[],
501
501
  )
502
502
  ),
503
503
  )
@@ -509,18 +509,16 @@ class Record(NetBoxModel):
509
509
  ) from None
510
510
 
511
511
  def validate_value(self):
512
- if self.type in (RecordTypeChoices.PTR):
512
+ def _validate_idn(name):
513
513
  try:
514
- validate_fqdn(self.value)
515
- except ValidationError as exc:
514
+ name.to_unicode()
515
+ except dns_name.IDNAException as exc:
516
516
  raise ValidationError(
517
- {
518
- "value": exc,
519
- }
517
+ f"{name.to_text()} is not a valid IDN: {exc}."
520
518
  ) from None
521
519
 
522
520
  try:
523
- rdata.from_text(RecordClassChoices.IN, self.type, self.value)
521
+ rr = rdata.from_text(RecordClassChoices.IN, self.type, self.value)
524
522
  except dns.exception.SyntaxError as exc:
525
523
  raise ValidationError(
526
524
  {
@@ -528,6 +526,59 @@ class Record(NetBoxModel):
528
526
  }
529
527
  ) from None
530
528
 
529
+ try:
530
+ match self.type:
531
+ case RecordTypeChoices.CNAME:
532
+ _validate_idn(rr.target)
533
+ validate_domain_name(
534
+ rr.target.to_text(),
535
+ always_tolerant=True,
536
+ allow_empty_label=True,
537
+ )
538
+
539
+ case (
540
+ RecordTypeChoices.DNAME
541
+ | RecordTypeChoices.NS
542
+ | RecordTypeChoices.HTTPS
543
+ | RecordTypeChoices.SRV
544
+ | RecordTypeChoices.SVCB
545
+ ):
546
+ _validate_idn(rr.target)
547
+ validate_domain_name(rr.target.to_text(), always_tolerant=True)
548
+
549
+ case RecordTypeChoices.PTR | RecordTypeChoices.NSAP_PTR:
550
+ _validate_idn(rr.target)
551
+ validate_fqdn(rr.target.to_text(), always_tolerant=True)
552
+
553
+ case RecordTypeChoices.MX | RecordTypeChoices.RT | RecordTypeChoices.KX:
554
+ _validate_idn(rr.exchange)
555
+ validate_domain_name(rr.exchange.to_text(), always_tolerant=True)
556
+
557
+ case RecordTypeChoices.NSEC:
558
+ _validate_idn(rr.next)
559
+ validate_domain_name(rr.next.to_text(), always_tolerant=True)
560
+
561
+ case RecordTypeChoices.RP:
562
+ _validate_idn(rr.mbox)
563
+ validate_domain_name(rr.mbox.to_text(), always_tolerant=True)
564
+ _validate_idn(rr.txt)
565
+ validate_domain_name(rr.txt.to_text(), always_tolerant=True)
566
+
567
+ case RecordTypeChoices.NAPTR:
568
+ _validate_idn(rr.replacement)
569
+ validate_extended_hostname(
570
+ rr.replacement.to_text(), always_tolerant=True
571
+ )
572
+
573
+ case RecordTypeChoices.PX:
574
+ _validate_idn(rr.map822)
575
+ validate_domain_name(rr.map822.to_text(), always_tolerant=True)
576
+ _validate_idn(rr.mapx400)
577
+ validate_domain_name(rr.mapx400.to_text(), always_tolerant=True)
578
+
579
+ except ValidationError as exc:
580
+ raise ValidationError({"value": exc}) from None
581
+
531
582
  def check_unique_record(self):
532
583
  if not get_plugin_config("netbox_dns", "enforce_unique_records", False):
533
584
  return
@@ -695,6 +746,8 @@ class Record(NetBoxModel):
695
746
  }
696
747
  ) from None
697
748
 
749
+ super().clean(*args, **kwargs)
750
+
698
751
  def save(
699
752
  self,
700
753
  *args,
@@ -732,9 +785,9 @@ class Record(NetBoxModel):
732
785
 
733
786
  super().save(*args, **kwargs)
734
787
 
735
- zone = self.zone
736
- if self.type != RecordTypeChoices.SOA and zone.soa_serial_auto:
737
- zone.update_serial(save_zone_serial=save_zone_serial)
788
+ _zone = self.zone
789
+ if self.type != RecordTypeChoices.SOA and _zone.soa_serial_auto:
790
+ _zone.update_serial(save_zone_serial=save_zone_serial)
738
791
 
739
792
  def delete(self, *args, save_zone_serial=True, **kwargs):
740
793
  if self.rfc2317_cname_record:
@@ -745,16 +798,17 @@ class Record(NetBoxModel):
745
798
 
746
799
  super().delete(*args, **kwargs)
747
800
 
748
- zone = self.zone
749
- if zone.soa_serial_auto:
750
- zone.update_serial(save_zone_serial=save_zone_serial)
801
+ _zone = self.zone
802
+ if _zone.soa_serial_auto:
803
+ _zone.update_serial(save_zone_serial=save_zone_serial)
751
804
 
752
805
 
753
806
  @register_search
754
807
  class RecordIndex(SearchIndex):
755
808
  model = Record
756
809
  fields = (
757
- ("name", 100),
810
+ ("fqdn", 100),
811
+ ("name", 120),
758
812
  ("value", 150),
759
813
  ("zone", 200),
760
814
  ("type", 200),
netbox_dns/models/view.py CHANGED
@@ -1,8 +1,11 @@
1
1
  from django.db import models
2
2
  from django.urls import reverse
3
+ from django.core.exceptions import ValidationError
3
4
 
4
5
  from netbox.models import NetBoxModel
5
6
  from netbox.search import SearchIndex, register_search
7
+ from netbox.context import current_request
8
+ from utilities.exceptions import AbortRequest
6
9
 
7
10
 
8
11
  class View(NetBoxModel):
@@ -14,6 +17,9 @@ class View(NetBoxModel):
14
17
  max_length=200,
15
18
  blank=True,
16
19
  )
20
+ default_view = models.BooleanField(
21
+ default=False,
22
+ )
17
23
  tenant = models.ForeignKey(
18
24
  to="tenancy.Tenant",
19
25
  on_delete=models.PROTECT,
@@ -24,6 +30,10 @@ class View(NetBoxModel):
24
30
 
25
31
  clone_fields = ["name", "description"]
26
32
 
33
+ @classmethod
34
+ def get_default_view(cls):
35
+ return cls.objects.get(default_view=True)
36
+
27
37
  def get_absolute_url(self):
28
38
  return reverse("plugins:netbox_dns:view", kwargs={"pk": self.pk})
29
39
 
@@ -33,6 +43,49 @@ class View(NetBoxModel):
33
43
  class Meta:
34
44
  ordering = ("name",)
35
45
 
46
+ def delete(self, *args, **kwargs):
47
+ if self.default_view:
48
+ if current_request.get() is not None:
49
+ raise AbortRequest("The default view cannot be deleted")
50
+
51
+ raise ValidationError("The default view cannot be deleted")
52
+
53
+ super().delete(*args, **kwargs)
54
+
55
+ def clean(self, *args, old_state=None, **kwargs):
56
+ if self.pk is None:
57
+ return
58
+
59
+ old_state = View.objects.get(pk=self.pk)
60
+
61
+ if (
62
+ old_state.default_view
63
+ and not self.default_view
64
+ and not View.objects.filter(default_view=True).exclude(pk=self.pk).exists()
65
+ ):
66
+ raise ValidationError(
67
+ {
68
+ "default_view": "Please select a different view as default view to change this setting!"
69
+ }
70
+ )
71
+
72
+ super().clean(*args, **kwargs)
73
+
74
+ def save(self, *args, **kwargs):
75
+ self.clean()
76
+
77
+ old_state = None if self.pk is None else View.objects.get(pk=self.pk)
78
+
79
+ super().save(*args, **kwargs)
80
+
81
+ if (old_state is None and self.default_view) or (
82
+ old_state is not None and self.default_view and not old_state.default_view
83
+ ):
84
+ other_views = View.objects.filter(default_view=True).exclude(pk=self.pk)
85
+ for view in other_views:
86
+ view.default_view = False
87
+ view.save()
88
+
36
89
 
37
90
  @register_search
38
91
  class ViewIndex(SearchIndex):
netbox_dns/models/zone.py CHANGED
@@ -15,15 +15,15 @@ from django.db.models import Q, Max, ExpressionWrapper, BooleanField
15
15
  from django.urls import reverse
16
16
  from django.db.models.signals import m2m_changed
17
17
  from django.dispatch import receiver
18
+ from django.conf import settings
18
19
 
19
20
  from netbox.models import NetBoxModel
20
21
  from netbox.search import SearchIndex, register_search
22
+ from netbox.plugins.utils import get_plugin_config
21
23
  from utilities.querysets import RestrictedQuerySet
22
24
  from utilities.choices import ChoiceSet
23
25
  from ipam.models import IPAddress
24
26
 
25
- from netbox.plugins.utils import get_plugin_config
26
-
27
27
  from netbox_dns.fields import NetworkField, RFC2317NetworkField
28
28
  from netbox_dns.utilities import (
29
29
  arpa_to_prefix,
@@ -37,9 +37,11 @@ from netbox_dns.validators import (
37
37
  )
38
38
 
39
39
  # +
40
- # This is a hack designed to break cyclic imports between Record and Zone
40
+ # This is a hack designed to break cyclic imports between View, Record and Zone
41
41
  # -
42
42
  import netbox_dns.models.record as record
43
+ import netbox_dns.models.view as view
44
+ import netbox_dns.models.nameserver as nameserver
43
45
 
44
46
 
45
47
  class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
@@ -81,8 +83,7 @@ class Zone(NetBoxModel):
81
83
  view = models.ForeignKey(
82
84
  to="View",
83
85
  on_delete=models.PROTECT,
84
- blank=True,
85
- null=True,
86
+ null=False,
86
87
  )
87
88
  name = models.CharField(
88
89
  max_length=255,
@@ -285,11 +286,20 @@ class Zone(NetBoxModel):
285
286
  except dns_name.IDNAException:
286
287
  name = self.name
287
288
 
288
- if self.view:
289
+ if not self.view.default_view:
289
290
  return f"[{self.view}] {name}"
290
291
 
291
292
  return str(name)
292
293
 
294
+ @staticmethod
295
+ def get_defaults():
296
+ return {
297
+ field[5:]: value
298
+ for field, value in settings.PLUGINS_CONFIG.get("netbox_dns").items()
299
+ if field.startswith("zone_")
300
+ and field not in ("zone_soa_mname", "zone_nameservers")
301
+ }
302
+
293
303
  @property
294
304
  def display_name(self):
295
305
  return name_to_unicode(self.name)
@@ -317,11 +327,11 @@ class Zone(NetBoxModel):
317
327
 
318
328
  def get_rfc2317_parent_zone(self):
319
329
  if not self.is_rfc2317_zone:
320
- return
330
+ return None
321
331
 
322
332
  return (
323
333
  Zone.objects.filter(
324
- self.view_filter,
334
+ view=self.view,
325
335
  arpa_network__net_contains=self.rfc2317_prefix,
326
336
  )
327
337
  .order_by("arpa_network__net_mask_length")
@@ -342,12 +352,6 @@ class Zone(NetBoxModel):
342
352
  )
343
353
  )
344
354
 
345
- @property
346
- def view_filter(self):
347
- if self.view is None:
348
- return Q(view__isnull=True)
349
- return Q(view=self.view)
350
-
351
355
  def record_count(self, managed=False):
352
356
  return record.Record.objects.filter(zone=self, managed=managed).count()
353
357
 
@@ -417,19 +421,15 @@ class Zone(NetBoxModel):
417
421
  if not nameservers:
418
422
  ns_errors.append(f"No nameservers are configured for zone {self}")
419
423
 
420
- for nameserver in nameservers:
421
- name = dns_name.from_text(nameserver.name, origin=None)
424
+ for _nameserver in nameservers:
425
+ name = dns_name.from_text(_nameserver.name, origin=None)
422
426
  parent = name.parent()
423
427
 
424
428
  if len(parent) < 2:
425
429
  continue
426
430
 
427
- view_condition = (
428
- Q(view__isnull=True) if self.view is None else Q(view_id=self.view.pk)
429
- )
430
-
431
431
  try:
432
- ns_zone = Zone.objects.get(view_condition, name=parent.to_text())
432
+ ns_zone = Zone.objects.get(view_id=self.view.pk, name=parent.to_text())
433
433
  except ObjectDoesNotExist:
434
434
  continue
435
435
 
@@ -437,7 +437,7 @@ class Zone(NetBoxModel):
437
437
  address_records = record.Record.objects.filter(
438
438
  Q(zone=ns_zone),
439
439
  Q(status__in=record.Record.ACTIVE_STATUS_LIST),
440
- Q(Q(name=f"{nameserver.name}.") | Q(name=relative_name)),
440
+ Q(Q(name=f"{_nameserver.name}.") | Q(name=relative_name)),
441
441
  Q(
442
442
  Q(type=record.RecordTypeChoices.A)
443
443
  | Q(type=record.RecordTypeChoices.AAAA)
@@ -446,11 +446,23 @@ class Zone(NetBoxModel):
446
446
 
447
447
  if not address_records:
448
448
  ns_warnings.append(
449
- f"Nameserver {nameserver.name} does not have an active address record in zone {ns_zone}"
449
+ f"Nameserver {_nameserver.name} does not have an active address record in zone {ns_zone}"
450
450
  )
451
451
 
452
452
  return ns_warnings, ns_errors
453
453
 
454
+ def check_soa_serial_increment(self, old_serial, new_serial):
455
+ MAX_SOA_SERIAL_INCREMENT = 2**31 - 1
456
+ SOA_SERIAL_WRAP = 2**32
457
+
458
+ if old_serial is None:
459
+ return
460
+
461
+ if (new_serial - old_serial) % SOA_SERIAL_WRAP > MAX_SOA_SERIAL_INCREMENT:
462
+ raise ValidationError(
463
+ {"soa_serial": f"soa_serial must not decrease for zone {self.name}."}
464
+ )
465
+
454
466
  def get_auto_serial(self):
455
467
  records = record.Record.objects.filter(zone_id=self.pk).exclude(
456
468
  type=record.RecordTypeChoices.SOA
@@ -492,19 +504,6 @@ class Zone(NetBoxModel):
492
504
  def network_from_name(self):
493
505
  return arpa_to_prefix(self.name)
494
506
 
495
- def check_name_conflict(self):
496
- if self.view is None:
497
- if (
498
- Zone.objects.exclude(pk=self.pk)
499
- .filter(name=self.name.rstrip("."), view__isnull=True)
500
- .exists()
501
- ):
502
- raise ValidationError(
503
- {
504
- "name": f"A zone with name {self.name} and no view already exists."
505
- }
506
- )
507
-
508
507
  def update_rfc2317_parent_zone(self):
509
508
  if not self.is_rfc2317_zone:
510
509
  return
@@ -555,8 +554,33 @@ class Zone(NetBoxModel):
555
554
  ptr_zone.save_soa_serial()
556
555
  ptr_zone.update_soa_record()
557
556
 
557
+ def clean_fields(self, exclude=None):
558
+ defaults = settings.PLUGINS_CONFIG.get("netbox_dns")
559
+
560
+ if self.view_id is None:
561
+ self.view_id = view.View.get_default_view().pk
562
+
563
+ for field, value in self.get_defaults().items():
564
+ if getattr(self, field) in (None, ""):
565
+ if value not in (None, ""):
566
+ setattr(self, field, value)
567
+
568
+ if self.soa_mname_id is None:
569
+ default_soa_mname = defaults.get("zone_soa_mname")
570
+ try:
571
+ self.soa_mname = nameserver.NameServer.objects.get(
572
+ name=default_soa_mname
573
+ )
574
+ except nameserver.NameServer.DoesNotExist:
575
+ raise ValidationError(
576
+ f"Default soa_mname instance {default_soa_mname} does not exist"
577
+ )
578
+
579
+ super().clean_fields(exclude=exclude)
580
+
558
581
  def clean(self, *args, **kwargs):
559
- self.check_name_conflict()
582
+ if self.soa_ttl is None:
583
+ self.soa_ttl = self.default_ttl
560
584
 
561
585
  try:
562
586
  self.name = normalize_name(self.name)
@@ -576,6 +600,8 @@ class Zone(NetBoxModel):
576
600
  }
577
601
  ) from None
578
602
 
603
+ if self.soa_rname in (None, ""):
604
+ raise ValidationError("soa_rname not set and no default value defined")
579
605
  try:
580
606
  dns_name.from_text(self.soa_rname, origin=dns_name.root)
581
607
  validate_fqdn(self.soa_rname)
@@ -586,12 +612,29 @@ class Zone(NetBoxModel):
586
612
  }
587
613
  ) from None
588
614
 
589
- if self.soa_serial is None and not self.soa_serial_auto:
590
- raise ValidationError(
591
- {
592
- "soa_serial": f"soa_serial is not defined and soa_serial_auto is disabled for zone {self.name}."
593
- }
594
- )
615
+ if not self.soa_serial_auto:
616
+ if self.soa_serial is None:
617
+ raise ValidationError(
618
+ {
619
+ "soa_serial": f"soa_serial is not defined and soa_serial_auto is disabled for zone {self.name}."
620
+ }
621
+ )
622
+
623
+ if self.pk is not None:
624
+ old_zone = Zone.objects.get(pk=self.pk)
625
+ if not self.soa_serial_auto:
626
+ self.check_soa_serial_increment(old_zone.soa_serial, self.soa_serial)
627
+ else:
628
+ try:
629
+ self.check_soa_serial_increment(
630
+ old_zone.soa_serial, self.get_auto_serial()
631
+ )
632
+ except ValidationError:
633
+ raise ValidationError(
634
+ {
635
+ "soa_serial_auto": f"Enabling soa_serial_auto would decrease soa_serial for zone {self.name}."
636
+ }
637
+ )
595
638
 
596
639
  if self.is_reverse_zone:
597
640
  self.arpa_network = self.network_from_name
@@ -619,7 +662,7 @@ class Zone(NetBoxModel):
619
662
  self.rfc2317_parent_zone = None
620
663
 
621
664
  overlapping_zones = Zone.objects.filter(
622
- self.view_filter,
665
+ view=self.view,
623
666
  rfc2317_prefix__net_overlap=self.rfc2317_prefix,
624
667
  active=True,
625
668
  ).exclude(pk=self.pk)
@@ -635,6 +678,8 @@ class Zone(NetBoxModel):
635
678
  self.rfc2317_parent_managed = False
636
679
  self.rfc2317_parent_zone = None
637
680
 
681
+ super().clean(*args, **kwargs)
682
+
638
683
  def save(self, *args, **kwargs):
639
684
  self.full_clean()
640
685
 
@@ -659,7 +704,7 @@ class Zone(NetBoxModel):
659
704
  new_zone or name_changed or view_changed or status_changed
660
705
  ) and self.is_reverse_zone:
661
706
  zones = Zone.objects.filter(
662
- self.view_filter,
707
+ view=self.view,
663
708
  arpa_network__net_contains_or_equals=self.arpa_network,
664
709
  )
665
710
  address_records = record.Record.objects.filter(
@@ -695,7 +740,7 @@ class Zone(NetBoxModel):
695
740
  or rfc2317_changed
696
741
  ) and self.is_rfc2317_zone:
697
742
  zones = Zone.objects.filter(
698
- self.view_filter,
743
+ view=self.view,
699
744
  arpa_network__net_contains=self.rfc2317_prefix,
700
745
  )
701
746
  address_records = record.Record.objects.filter(
@@ -5,6 +5,7 @@ from rest_framework.exceptions import PermissionDenied as APIPermissionDenied
5
5
 
6
6
  from netbox.signals import post_clean
7
7
  from netbox.context import current_request
8
+ from netbox.plugins.utils import get_plugin_config
8
9
  from ipam.models import IPAddress
9
10
 
10
11
  from netbox_dns.models import Zone
@@ -18,8 +19,6 @@ from netbox_dns.utilities.ipam_coupling import (
18
19
  DNSPermissionDenied,
19
20
  )
20
21
 
21
- from netbox.plugins.utils import get_plugin_config
22
-
23
22
 
24
23
  @receiver(post_clean, sender=IPAddress)
25
24
  def ip_address_check_permissions_save(instance, **kwargs):
netbox_dns/tables/view.py CHANGED
@@ -10,9 +10,19 @@ class ViewTable(TenancyColumnsMixin, NetBoxTable):
10
10
  name = tables.Column(
11
11
  linkify=True,
12
12
  )
13
+ default_view = tables.Column(
14
+ verbose_name="Default View",
15
+ )
13
16
  tags = TagColumn(url_name="plugins:netbox_dns:view_list")
14
17
 
15
18
  class Meta(NetBoxTable.Meta):
16
19
  model = View
17
- fields = ("name", "description", "tenant", "tenant_group", "tags")
18
- default_columns = ("name",)
20
+ fields = (
21
+ "name",
22
+ "default_view",
23
+ "description",
24
+ "tenant",
25
+ "tenant_group",
26
+ "tags",
27
+ )
28
+ default_columns = ("name", "default_view")
@@ -117,7 +117,7 @@ class RelatedDNSObjects(PluginTemplateExtension):
117
117
  )
118
118
 
119
119
 
120
- template_extensions = list()
120
+ template_extensions = []
121
121
 
122
122
  if version.parse(settings.VERSION) < version.parse("3.7.0"):
123
123
  template_extensions.append(RelatedDNSObjects)
@@ -56,7 +56,7 @@
56
56
  <td><a href="{% url 'plugins:netbox_dns:zone_records' pk=object.zone.pk %}">{{ object.zone }}</a></td>
57
57
  {% endif %}
58
58
  </tr>
59
- {% if not object.managed %}
59
+ {% if not object.managed or object.tenant %}
60
60
  <tr>
61
61
  <th scope="row">Tenant</th>
62
62
  <td>