netbox-plugin-dns 1.0.6__py3-none-any.whl → 1.1.0__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 (94) hide show
  1. netbox_dns/__init__.py +23 -4
  2. netbox_dns/api/nested_serializers.py +17 -16
  3. netbox_dns/api/serializers.py +2 -1
  4. netbox_dns/api/serializers_/prefix.py +18 -0
  5. netbox_dns/api/serializers_/record.py +1 -0
  6. netbox_dns/api/serializers_/{contact.py → registration_contact.py} +5 -5
  7. netbox_dns/api/serializers_/view.py +34 -2
  8. netbox_dns/api/serializers_/zone.py +5 -5
  9. netbox_dns/api/serializers_/zone_template.py +5 -5
  10. netbox_dns/api/urls.py +5 -2
  11. netbox_dns/api/views.py +17 -35
  12. netbox_dns/fields/__init__.py +1 -0
  13. netbox_dns/fields/ipam.py +15 -0
  14. netbox_dns/filtersets/__init__.py +1 -1
  15. netbox_dns/filtersets/record.py +1 -1
  16. netbox_dns/filtersets/{contact.py → registration_contact.py} +4 -4
  17. netbox_dns/filtersets/view.py +16 -0
  18. netbox_dns/filtersets/zone.py +15 -15
  19. netbox_dns/filtersets/zone_template.py +15 -15
  20. netbox_dns/forms/__init__.py +1 -1
  21. netbox_dns/forms/{contact.py → registration_contact.py} +16 -16
  22. netbox_dns/forms/view.py +204 -4
  23. netbox_dns/forms/zone.py +15 -18
  24. netbox_dns/forms/zone_template.py +13 -13
  25. netbox_dns/graphql/__init__.py +2 -2
  26. netbox_dns/graphql/filters.py +5 -5
  27. netbox_dns/graphql/schema.py +24 -44
  28. netbox_dns/graphql/types.py +41 -12
  29. netbox_dns/management/commands/rebuild_dnssync.py +18 -0
  30. netbox_dns/management/commands/setup_dnssync.py +140 -0
  31. netbox_dns/migrations/0007_alter_ordering_options.py +25 -0
  32. netbox_dns/migrations/0008_view_prefixes.py +18 -0
  33. netbox_dns/migrations/0009_rename_contact_registrationcontact.py +27 -0
  34. netbox_dns/models/__init__.py +1 -3
  35. netbox_dns/models/nameserver.py +8 -3
  36. netbox_dns/models/record.py +154 -24
  37. netbox_dns/models/record_template.py +4 -1
  38. netbox_dns/models/registrar.py +7 -1
  39. netbox_dns/models/{contact.py → registration_contact.py} +15 -9
  40. netbox_dns/models/view.py +14 -2
  41. netbox_dns/models/zone.py +76 -35
  42. netbox_dns/models/zone_template.py +12 -9
  43. netbox_dns/navigation.py +7 -7
  44. netbox_dns/signals/ipam_dnssync.py +224 -0
  45. netbox_dns/tables/__init__.py +1 -1
  46. netbox_dns/tables/ipam_dnssync.py +11 -0
  47. netbox_dns/tables/nameserver.py +1 -7
  48. netbox_dns/tables/record.py +43 -30
  49. netbox_dns/tables/record_template.py +0 -17
  50. netbox_dns/tables/registrar.py +0 -2
  51. netbox_dns/tables/{contact.py → registration_contact.py} +5 -6
  52. netbox_dns/tables/view.py +19 -4
  53. netbox_dns/tables/zone.py +0 -15
  54. netbox_dns/tables/zone_template.py +2 -16
  55. netbox_dns/template_content.py +41 -40
  56. netbox_dns/templates/netbox_dns/record.html +6 -6
  57. netbox_dns/templates/netbox_dns/{contact.html → registrationcontact.html} +1 -1
  58. netbox_dns/templates/netbox_dns/view/button.html +9 -0
  59. netbox_dns/templates/netbox_dns/view/prefix.html +41 -0
  60. netbox_dns/templates/netbox_dns/view/related.html +17 -0
  61. netbox_dns/templates/netbox_dns/view.html +25 -0
  62. netbox_dns/urls/__init__.py +2 -2
  63. netbox_dns/urls/nameserver.py +14 -38
  64. netbox_dns/urls/record.py +7 -19
  65. netbox_dns/urls/record_template.py +18 -27
  66. netbox_dns/urls/registrar.py +11 -35
  67. netbox_dns/urls/registration_contact.py +60 -0
  68. netbox_dns/urls/view.py +12 -20
  69. netbox_dns/urls/zone.py +8 -46
  70. netbox_dns/urls/zone_template.py +16 -26
  71. netbox_dns/utilities/__init__.py +2 -74
  72. netbox_dns/utilities/conversions.py +83 -0
  73. netbox_dns/utilities/ipam_dnssync.py +295 -0
  74. netbox_dns/validators/dns_name.py +9 -0
  75. netbox_dns/views/__init__.py +1 -1
  76. netbox_dns/views/nameserver.py +7 -3
  77. netbox_dns/views/record.py +12 -7
  78. netbox_dns/views/record_template.py +1 -1
  79. netbox_dns/views/registrar.py +0 -1
  80. netbox_dns/views/registration_contact.py +94 -0
  81. netbox_dns/views/view.py +32 -2
  82. netbox_dns/views/zone.py +7 -6
  83. netbox_dns/views/zone_template.py +2 -2
  84. {netbox_plugin_dns-1.0.6.dist-info → netbox_plugin_dns-1.1.0.dist-info}/METADATA +2 -1
  85. netbox_plugin_dns-1.1.0.dist-info/RECORD +146 -0
  86. netbox_dns/management/commands/setup_coupling.py +0 -109
  87. netbox_dns/signals/ipam_coupling.py +0 -168
  88. netbox_dns/templates/netbox_dns/related_dns_objects.html +0 -21
  89. netbox_dns/urls/contact.py +0 -51
  90. netbox_dns/utilities/ipam_coupling.py +0 -112
  91. netbox_dns/views/contact.py +0 -95
  92. netbox_plugin_dns-1.0.6.dist-info/RECORD +0 -136
  93. {netbox_plugin_dns-1.0.6.dist-info → netbox_plugin_dns-1.1.0.dist-info}/LICENSE +0 -0
  94. {netbox_plugin_dns-1.0.6.dist-info → netbox_plugin_dns-1.1.0.dist-info}/WHEEL +0 -0
@@ -7,8 +7,10 @@ from django.core.exceptions import ValidationError
7
7
  from django.db import transaction, models
8
8
  from django.db.models import Q, ExpressionWrapper, BooleanField, Min
9
9
  from django.urls import reverse
10
+ from django.conf import settings
10
11
 
11
12
  from netbox.models import NetBoxModel
13
+ from netbox.models.features import ContactsMixin
12
14
  from netbox.search import SearchIndex, register_search
13
15
  from netbox.plugins.utils import get_plugin_config
14
16
  from utilities.querysets import RestrictedQuerySet
@@ -35,6 +37,43 @@ def min_ttl(*ttl_list):
35
37
  return min((ttl for ttl in ttl_list if ttl is not None), default=None)
36
38
 
37
39
 
40
+ def record_data_from_ip_address(ip_address, zone):
41
+ cf_data = ip_address.custom_field_data
42
+
43
+ if cf_data.get("ipaddress_dns_disabled"):
44
+ return None
45
+
46
+ data = {
47
+ "name": (
48
+ dns_name.from_text(ip_address.dns_name)
49
+ .relativize(dns_name.from_text(zone.name))
50
+ .to_text()
51
+ ),
52
+ "type": (
53
+ RecordTypeChoices.A
54
+ if ip_address.address.version == 4
55
+ else RecordTypeChoices.AAAA
56
+ ),
57
+ "value": str(ip_address.address.ip),
58
+ "status": (
59
+ RecordStatusChoices.STATUS_ACTIVE
60
+ if ip_address.status
61
+ in settings.PLUGINS_CONFIG["netbox_dns"].get(
62
+ "dnssync_ipaddress_active_status", []
63
+ )
64
+ else RecordStatusChoices.STATUS_INACTIVE
65
+ ),
66
+ }
67
+
68
+ if "ipaddress_dns_record_ttl" in cf_data:
69
+ data["ttl"] = cf_data.get("ipaddress_dns_record_ttl")
70
+
71
+ if (disable_ptr := cf_data.get("ipaddress_dns_record_disable_ptr")) is not None:
72
+ data["disable_ptr"] = disable_ptr
73
+
74
+ return data
75
+
76
+
38
77
  class RecordManager(models.Manager.from_queryset(RestrictedQuerySet)):
39
78
  """
40
79
  Custom manager for records providing the activity status annotation
@@ -62,7 +101,7 @@ class RecordManager(models.Manager.from_queryset(RestrictedQuerySet)):
62
101
  )
63
102
 
64
103
 
65
- class Record(ObjectModificationMixin, NetBoxModel):
104
+ class Record(ObjectModificationMixin, ContactsMixin, NetBoxModel):
66
105
  ACTIVE_STATUS_LIST = (RecordStatusChoices.STATUS_ACTIVE,)
67
106
 
68
107
  unique_ptr_qs = Q(
@@ -155,7 +194,7 @@ class Record(ObjectModificationMixin, NetBoxModel):
155
194
  objects = RecordManager()
156
195
  raw_objects = RestrictedQuerySet.as_manager()
157
196
 
158
- clone_fields = [
197
+ clone_fields = (
159
198
  "zone",
160
199
  "type",
161
200
  "name",
@@ -164,10 +203,20 @@ class Record(ObjectModificationMixin, NetBoxModel):
164
203
  "ttl",
165
204
  "disable_ptr",
166
205
  "description",
167
- ]
206
+ )
168
207
 
169
208
  class Meta:
170
- ordering = ("zone", "name", "type", "value", "status")
209
+ verbose_name = "Record"
210
+ verbose_name_plural = "Records"
211
+
212
+ ordering = (
213
+ "fqdn",
214
+ "zone",
215
+ "name",
216
+ "type",
217
+ "value",
218
+ "status",
219
+ )
171
220
 
172
221
  def __str__(self):
173
222
  try:
@@ -423,29 +472,70 @@ class Record(ObjectModificationMixin, NetBoxModel):
423
472
  self.rfc2317_cname_record.delete(save_zone_serial=save_zone_serial)
424
473
  self.rfc2317_cname_record = None
425
474
 
426
- def validate_name(self):
427
- try:
428
- _zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
429
- name = dns_name.from_text(self.name, origin=None)
430
- fqdn = dns_name.from_text(self.name, origin=_zone)
475
+ def update_from_ip_address(self, ip_address, zone=None):
476
+ if zone is None:
477
+ zone = self.zone
431
478
 
432
- _zone.to_unicode()
433
- name.to_unicode()
479
+ data = record_data_from_ip_address(ip_address, zone)
434
480
 
435
- self.name = name.relativize(_zone).to_text()
436
- self.fqdn = fqdn.to_text()
481
+ if data is None:
482
+ self.delete()
483
+ return
437
484
 
438
- except dns.exception.DNSException as exc:
485
+ if all((getattr(self, attr) == data[attr] for attr in data.keys())):
486
+ return
487
+
488
+ for attr, value in data.items():
489
+ setattr(self, attr, value)
490
+
491
+ return self
492
+
493
+ @classmethod
494
+ def create_from_ip_address(cls, ip_address, zone):
495
+ data = record_data_from_ip_address(ip_address, zone)
496
+
497
+ if data is None:
498
+ return
499
+
500
+ return Record(
501
+ zone=zone,
502
+ managed=True,
503
+ ipam_ip_address=ip_address,
504
+ **data,
505
+ )
506
+
507
+ def update_fqdn(self, zone=None):
508
+ if zone is None:
509
+ zone = self.zone
510
+
511
+ _zone = dns_name.from_text(zone.name, origin=dns_name.root)
512
+ name = dns_name.from_text(self.name, origin=None)
513
+ fqdn = dns_name.from_text(self.name, origin=_zone)
514
+
515
+ if not fqdn.is_subdomain(_zone):
439
516
  raise ValidationError(
440
517
  {
441
- "name": str(exc),
518
+ "name": f"{self.name} is not a name in {zone.name}",
442
519
  }
443
520
  )
444
521
 
445
- if not fqdn.is_subdomain(_zone):
522
+ _zone.to_unicode()
523
+ name.to_unicode()
524
+
525
+ self.name = name.relativize(_zone).to_text()
526
+ self.fqdn = fqdn.to_text()
527
+
528
+ def validate_name(self, new_zone=None):
529
+ if new_zone is None:
530
+ new_zone = self.zone
531
+
532
+ try:
533
+ self.update_fqdn(zone=new_zone)
534
+
535
+ except dns.exception.DNSException as exc:
446
536
  raise ValidationError(
447
537
  {
448
- "name": f"{self.name} is not a name in {self.zone.name}",
538
+ "name": str(exc),
449
539
  }
450
540
  )
451
541
 
@@ -477,15 +567,18 @@ class Record(ObjectModificationMixin, NetBoxModel):
477
567
  except ValidationError as exc:
478
568
  raise ValidationError({"value": exc}) from None
479
569
 
480
- def check_unique_record(self):
570
+ def check_unique_record(self, new_zone=None):
481
571
  if not get_plugin_config("netbox_dns", "enforce_unique_records", False):
482
572
  return
483
573
 
484
574
  if not self.is_active:
485
575
  return
486
576
 
577
+ if new_zone is None:
578
+ new_zone = self.zone
579
+
487
580
  records = Record.objects.filter(
488
- zone=self.zone,
581
+ zone=new_zone,
489
582
  name=self.name,
490
583
  type=self.type,
491
584
  value=self.value,
@@ -495,13 +588,41 @@ class Record(ObjectModificationMixin, NetBoxModel):
495
588
  if self.pk is not None:
496
589
  records = records.exclude(pk=self.pk)
497
590
 
498
- if len(records):
591
+ if records.exists():
592
+ if self.ipam_ip_address is not None:
593
+ if not records.filter(
594
+ ipam_ip_address__isnull=True
595
+ ).exists() or get_plugin_config(
596
+ "netbox_dns", "dnssync_conflict_deactivate", False
597
+ ):
598
+ return
599
+
499
600
  raise ValidationError(
500
601
  {
501
602
  "value": f"There is already an active {self.type} record for name {self.name} in zone {self.zone} with value {self.value}."
502
603
  }
503
604
  ) from None
504
605
 
606
+ def handle_conflicting_address_records(self):
607
+ if self.ipam_ip_address is None or not self.is_active:
608
+ return
609
+
610
+ if not get_plugin_config("netbox_dns", "dnssync_conflict_deactivate", False):
611
+ return
612
+
613
+ records = Record.objects.filter(
614
+ zone=self.zone,
615
+ name=self.name,
616
+ type=self.type,
617
+ value=self.value,
618
+ status__in=Record.ACTIVE_STATUS_LIST,
619
+ ipam_ip_address__isnull=True,
620
+ )
621
+
622
+ for record in records:
623
+ record.status = RecordStatusChoices.STATUS_INACTIVE
624
+ record.save(update_fields=["status"])
625
+
505
626
  def check_unique_rrset_ttl(self):
506
627
  if self.pk is not None:
507
628
  return
@@ -509,6 +630,11 @@ class Record(ObjectModificationMixin, NetBoxModel):
509
630
  if not get_plugin_config("netbox_dns", "enforce_unique_rrset_ttl", False):
510
631
  return
511
632
 
633
+ if self.ipam_ip_address is not None and get_plugin_config(
634
+ "netbox_dns", "dnssync_conflict_deactivate", False
635
+ ):
636
+ return
637
+
512
638
  if self.type == RecordTypeChoices.PTR and self.managed:
513
639
  return
514
640
 
@@ -523,10 +649,13 @@ class Record(ObjectModificationMixin, NetBoxModel):
523
649
  .exclude(status=RecordStatusChoices.STATUS_INACTIVE)
524
650
  )
525
651
 
652
+ if self.ipam_ip_address is not None:
653
+ records = records.exclude(ipam_ip_address__isnull=False)
654
+
526
655
  if not records.exists():
527
656
  return
528
657
 
529
- conflicting_ttls = ", ".join(set(str(record.ttl) for record in records))
658
+ conflicting_ttls = ", ".join({str(record.ttl) for record in records})
530
659
  raise ValidationError(
531
660
  {
532
661
  "ttl": f"There is at least one active {self.type} record for name {self.name} in zone {self.zone} and TTL is different ({conflicting_ttls})."
@@ -566,10 +695,10 @@ class Record(ObjectModificationMixin, NetBoxModel):
566
695
  self.type = self.type.upper()
567
696
  super().clean_fields(*args, **kwargs)
568
697
 
569
- def clean(self, *args, **kwargs):
570
- self.validate_name()
698
+ def clean(self, *args, new_zone=None, **kwargs):
699
+ self.validate_name(new_zone=new_zone)
571
700
  self.validate_value()
572
- self.check_unique_record()
701
+ self.check_unique_record(new_zone=new_zone)
573
702
  if self.pk is None:
574
703
  self.check_unique_rrset_ttl()
575
704
 
@@ -675,6 +804,7 @@ class Record(ObjectModificationMixin, NetBoxModel):
675
804
  self.ip_address = None
676
805
 
677
806
  if self.is_address_record:
807
+ self.handle_conflicting_address_records()
678
808
  self.update_ptr_record(
679
809
  update_rfc2317_cname=update_rfc2317_cname,
680
810
  save_zone_serial=save_zone_serial,
@@ -85,7 +85,10 @@ class RecordTemplate(NetBoxModel):
85
85
  )
86
86
 
87
87
  class Meta:
88
- ordering = ["name"]
88
+ verbose_name = "Record Template"
89
+ verbose_name_plural = "Record Templates"
90
+
91
+ ordering = ("name",)
89
92
 
90
93
  def __str__(self):
91
94
  return str(self.name)
@@ -59,7 +59,13 @@ class Registrar(NetBoxModel):
59
59
  return str(self.name)
60
60
 
61
61
  class Meta:
62
- ordering = ("name", "iana_id")
62
+ verbose_name = "Registrar"
63
+ verbose_name_plural = "Registrars"
64
+
65
+ ordering = (
66
+ "name",
67
+ "iana_id",
68
+ )
63
69
 
64
70
 
65
71
  @register_search
@@ -8,12 +8,12 @@ from taggit.managers import TaggableManager
8
8
 
9
9
 
10
10
  __all__ = (
11
- "Contact",
12
- "ContactIndex",
11
+ "RegistrationContact",
12
+ "RegistrationContactIndex",
13
13
  )
14
14
 
15
15
 
16
- class Contact(NetBoxModel):
16
+ class RegistrationContact(NetBoxModel):
17
17
  # +
18
18
  # Data fields according to https://www.icann.org/resources/pages/rdds-labeling-policy-2017-02-01-en
19
19
  # -
@@ -87,7 +87,7 @@ class Contact(NetBoxModel):
87
87
  related_name="netbox_dns_contact_set",
88
88
  )
89
89
 
90
- clone_fields = [
90
+ clone_fields = (
91
91
  "name",
92
92
  "description",
93
93
  "organization",
@@ -102,10 +102,10 @@ class Contact(NetBoxModel):
102
102
  "fax_ext",
103
103
  "email",
104
104
  "tags",
105
- ]
105
+ )
106
106
 
107
107
  def get_absolute_url(self):
108
- return reverse("plugins:netbox_dns:contact", kwargs={"pk": self.pk})
108
+ return reverse("plugins:netbox_dns:registrationcontact", kwargs={"pk": self.pk})
109
109
 
110
110
  def __str__(self):
111
111
  if self.name is not None:
@@ -114,7 +114,13 @@ class Contact(NetBoxModel):
114
114
  return self.contact_id
115
115
 
116
116
  class Meta:
117
- ordering = ("name", "contact_id")
117
+ verbose_name = "Registration Contact"
118
+ verbose_name_plural = "Registration Contacts"
119
+
120
+ ordering = (
121
+ "name",
122
+ "contact_id",
123
+ )
118
124
 
119
125
  @property
120
126
  def zones(self):
@@ -127,8 +133,8 @@ class Contact(NetBoxModel):
127
133
 
128
134
 
129
135
  @register_search
130
- class ContactIndex(SearchIndex):
131
- model = Contact
136
+ class RegistrationContactIndex(SearchIndex):
137
+ model = RegistrationContact
132
138
  fields = (
133
139
  ("name", 100),
134
140
  ("contact_id", 100),
netbox_dns/models/view.py CHANGED
@@ -3,6 +3,7 @@ from django.urls import reverse
3
3
  from django.core.exceptions import ValidationError
4
4
 
5
5
  from netbox.models import NetBoxModel
6
+ from netbox.models.features import ContactsMixin
6
7
  from netbox.search import SearchIndex, register_search
7
8
  from netbox.context import current_request
8
9
  from utilities.exceptions import AbortRequest
@@ -16,7 +17,7 @@ __all__ = (
16
17
  )
17
18
 
18
19
 
19
- class View(ObjectModificationMixin, NetBoxModel):
20
+ class View(ObjectModificationMixin, ContactsMixin, NetBoxModel):
20
21
  name = models.CharField(
21
22
  unique=True,
22
23
  max_length=255,
@@ -28,6 +29,11 @@ class View(ObjectModificationMixin, NetBoxModel):
28
29
  default_view = models.BooleanField(
29
30
  default=False,
30
31
  )
32
+ prefixes = models.ManyToManyField(
33
+ to="ipam.Prefix",
34
+ related_name="netbox_dns_views",
35
+ blank=True,
36
+ )
31
37
  tenant = models.ForeignKey(
32
38
  to="tenancy.Tenant",
33
39
  on_delete=models.PROTECT,
@@ -36,7 +42,10 @@ class View(ObjectModificationMixin, NetBoxModel):
36
42
  null=True,
37
43
  )
38
44
 
39
- clone_fields = ["name", "description"]
45
+ clone_fields = (
46
+ "name",
47
+ "description",
48
+ )
40
49
 
41
50
  @classmethod
42
51
  def get_default_view(cls):
@@ -49,6 +58,9 @@ class View(ObjectModificationMixin, NetBoxModel):
49
58
  return str(self.name)
50
59
 
51
60
  class Meta:
61
+ verbose_name = "View"
62
+ verbose_name_plural = "Views"
63
+
52
64
  ordering = ("name",)
53
65
 
54
66
  def delete(self, *args, **kwargs):
netbox_dns/models/zone.py CHANGED
@@ -3,9 +3,8 @@ from math import ceil
3
3
  from datetime import datetime
4
4
 
5
5
  from dns import name as dns_name
6
- from dns.rdtypes.ANY import SOA
7
6
  from dns.exception import DNSException
8
-
7
+ from dns.rdtypes.ANY import SOA
9
8
  from django.core.validators import (
10
9
  MinValueValidator,
11
10
  MaxValueValidator,
@@ -19,6 +18,7 @@ from django.dispatch import receiver
19
18
  from django.conf import settings
20
19
 
21
20
  from netbox.models import NetBoxModel
21
+ from netbox.models.features import ContactsMixin
22
22
  from netbox.search import SearchIndex, register_search
23
23
  from netbox.plugins.utils import get_plugin_config
24
24
  from utilities.querysets import RestrictedQuerySet
@@ -27,13 +27,16 @@ from ipam.models import IPAddress
27
27
  from netbox_dns.choices import RecordClassChoices, RecordTypeChoices, ZoneStatusChoices
28
28
  from netbox_dns.fields import NetworkField, RFC2317NetworkField
29
29
  from netbox_dns.utilities import (
30
+ update_dns_records,
31
+ check_dns_records,
32
+ get_ip_addresses_by_zone,
30
33
  arpa_to_prefix,
31
34
  name_to_unicode,
32
35
  normalize_name,
33
36
  NameFormatError,
34
37
  )
35
38
  from netbox_dns.validators import (
36
- validate_fqdn,
39
+ validate_rname,
37
40
  validate_domain_name,
38
41
  )
39
42
  from netbox_dns.mixins import ObjectModificationMixin
@@ -66,7 +69,7 @@ class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
66
69
  )
67
70
 
68
71
 
69
- class Zone(ObjectModificationMixin, NetBoxModel):
72
+ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
70
73
  ACTIVE_STATUS_LIST = (ZoneStatusChoices.STATUS_ACTIVE,)
71
74
 
72
75
  def __init__(self, *args, **kwargs):
@@ -75,6 +78,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
75
78
  super().__init__(*args, **kwargs)
76
79
 
77
80
  self._soa_serial_dirty = False
81
+ self._ip_addresses_checked = False
78
82
 
79
83
  view = models.ForeignKey(
80
84
  to="View",
@@ -188,7 +192,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
188
192
  null=True,
189
193
  )
190
194
  registrant = models.ForeignKey(
191
- to="Contact",
195
+ to="RegistrationContact",
192
196
  on_delete=models.SET_NULL,
193
197
  verbose_name="Registrant",
194
198
  help_text="The owner of the domain",
@@ -196,7 +200,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
196
200
  null=True,
197
201
  )
198
202
  admin_c = models.ForeignKey(
199
- to="Contact",
203
+ to="RegistrationContact",
200
204
  on_delete=models.SET_NULL,
201
205
  verbose_name="Admin Contact",
202
206
  related_name="admin_c_zones",
@@ -205,16 +209,16 @@ class Zone(ObjectModificationMixin, NetBoxModel):
205
209
  null=True,
206
210
  )
207
211
  tech_c = models.ForeignKey(
208
- to="Contact",
212
+ to="RegistrationContact",
209
213
  on_delete=models.SET_NULL,
210
- verbose_name="Tech Contact",
214
+ verbose_name="Technical Contact",
211
215
  related_name="tech_c_zones",
212
216
  help_text="The technical contact for the domain",
213
217
  blank=True,
214
218
  null=True,
215
219
  )
216
220
  billing_c = models.ForeignKey(
217
- to="Contact",
221
+ to="RegistrationContact",
218
222
  on_delete=models.SET_NULL,
219
223
  verbose_name="Billing Contact",
220
224
  related_name="billing_c_zones",
@@ -245,7 +249,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
245
249
 
246
250
  objects = ZoneManager()
247
251
 
248
- clone_fields = [
252
+ clone_fields = (
249
253
  "view",
250
254
  "name",
251
255
  "status",
@@ -259,9 +263,12 @@ class Zone(ObjectModificationMixin, NetBoxModel):
259
263
  "soa_expire",
260
264
  "soa_minimum",
261
265
  "description",
262
- ]
266
+ )
263
267
 
264
268
  class Meta:
269
+ verbose_name = "Zone"
270
+ verbose_name_plural = "Zones"
271
+
265
272
  ordering = (
266
273
  "view",
267
274
  "name",
@@ -277,7 +284,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
277
284
  else:
278
285
  try:
279
286
  name = dns_name.from_text(self.name, origin=None).to_unicode()
280
- except dns_name.IDNAException:
287
+ except DNSException:
281
288
  name = self.name
282
289
 
283
290
  try:
@@ -305,6 +312,14 @@ class Zone(ObjectModificationMixin, NetBoxModel):
305
312
  def soa_serial_dirty(self, soa_serial_dirty):
306
313
  self._soa_serial_dirty = soa_serial_dirty
307
314
 
315
+ @property
316
+ def ip_addresses_checked(self):
317
+ return self._ip_addresses_checked
318
+
319
+ @ip_addresses_checked.setter
320
+ def ip_addresses_checked(self, ip_addresses_checked):
321
+ self._ip_addresses_checked = ip_addresses_checked
322
+
308
323
  @property
309
324
  def display_name(self):
310
325
  return name_to_unicode(self.name)
@@ -618,7 +633,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
618
633
  raise ValidationError("soa_rname not set and no default value defined")
619
634
  try:
620
635
  dns_name.from_text(self.soa_rname, origin=dns_name.root)
621
- validate_fqdn(self.soa_rname)
636
+ validate_rname(self.soa_rname)
622
637
  except (DNSException, ValidationError) as exc:
623
638
  raise ValidationError(
624
639
  {
@@ -638,7 +653,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
638
653
  old_zone = Zone.objects.get(pk=self.pk)
639
654
  if not self.soa_serial_auto:
640
655
  self.check_soa_serial_increment(old_zone.soa_serial, self.soa_serial)
641
- else:
656
+ elif not old_zone.soa_serial_auto:
642
657
  try:
643
658
  self.check_soa_serial_increment(
644
659
  old_zone.soa_serial, self.get_auto_serial()
@@ -650,6 +665,26 @@ class Zone(ObjectModificationMixin, NetBoxModel):
650
665
  }
651
666
  )
652
667
 
668
+ if (
669
+ not self.ip_addresses_checked
670
+ and old_zone.name != self.name
671
+ or old_zone.view != self.view
672
+ ):
673
+ ip_addresses = IPAddress.objects.filter(
674
+ netbox_dns_records__in=self.record_set.filter(
675
+ ipam_ip_address__isnull=False
676
+ )
677
+ )
678
+ ip_addresses |= get_ip_addresses_by_zone(self)
679
+
680
+ for ip_address in ip_addresses.distinct():
681
+ try:
682
+ check_dns_records(ip_address, zone=self)
683
+ except ValidationError as exc:
684
+ raise ValidationError(exc.messages)
685
+
686
+ self.ip_addresses_checked = True
687
+
653
688
  if self.is_reverse_zone:
654
689
  self.arpa_network = self.network_from_name
655
690
 
@@ -764,23 +799,13 @@ class Zone(ObjectModificationMixin, NetBoxModel):
764
799
 
765
800
  elif changed_fields is not None and {"name", "view", "status"} & changed_fields:
766
801
  for address_record in self.record_set.filter(
767
- type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA)
802
+ type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
803
+ ipam_ip_address__isnull=True,
768
804
  ):
769
805
  address_record.save(update_fields=["ptr_record"])
770
806
 
771
- # Fix name in IP Address when zone name is changed
772
- if (
773
- get_plugin_config("netbox_dns", "feature_ipam_coupling")
774
- and "name" in changed_fields
775
- ):
776
- for ip in IPAddress.objects.filter(
777
- custom_field_data__ipaddress_dns_zone_id=self.pk
778
- ):
779
- ip.dns_name = f'{ip.custom_field_data["ipaddress_dns_record_name"]}.{self.name}'
780
- ip.save(update_fields=["dns_name"])
781
-
782
807
  if changed_fields is not None and "name" in changed_fields:
783
- for _record in self.record_set.all():
808
+ for _record in self.record_set.filter(ipam_ip_address__isnull=True):
784
809
  _record.save(
785
810
  update_fields=["fqdn"],
786
811
  save_zone_serial=False,
@@ -788,6 +813,17 @@ class Zone(ObjectModificationMixin, NetBoxModel):
788
813
  update_rfc2317_cname=False,
789
814
  )
790
815
 
816
+ if changed_fields is None or {"name", "view"} & changed_fields:
817
+ ip_addresses = IPAddress.objects.filter(
818
+ netbox_dns_records__in=self.record_set.filter(
819
+ ipam_ip_address__isnull=False
820
+ )
821
+ )
822
+ ip_addresses |= get_ip_addresses_by_zone(self)
823
+
824
+ for ip_address in ip_addresses.distinct():
825
+ update_dns_records(ip_address)
826
+
791
827
  self.save_soa_serial()
792
828
  self.update_soa_record()
793
829
 
@@ -823,14 +859,15 @@ class Zone(ObjectModificationMixin, NetBoxModel):
823
859
  self.rfc2317_child_zones.all().values_list("pk", flat=True)
824
860
  )
825
861
 
826
- if get_plugin_config("netbox_dns", "feature_ipam_coupling"):
827
- for ip in IPAddress.objects.filter(
828
- custom_field_data__ipaddress_dns_zone_id=self.pk
829
- ):
830
- ip.dns_name = ""
831
- ip.custom_field_data["ipaddress_dns_record_name"] = None
832
- ip.custom_field_data["ipaddress_dns_zone_id"] = None
833
- ip.save(update_fields=["dns_name", "custom_field_data"])
862
+ ipam_ip_addresses = list(
863
+ IPAddress.objects.filter(
864
+ netbox_dns_records__in=self.record_set.filter(
865
+ ipam_ip_address__isnull=False
866
+ )
867
+ )
868
+ .distinct()
869
+ .values_list("pk", flat=True)
870
+ )
834
871
 
835
872
  super().delete(*args, **kwargs)
836
873
 
@@ -844,6 +881,10 @@ class Zone(ObjectModificationMixin, NetBoxModel):
844
881
  address_zone.save_soa_serial()
845
882
  address_zone.update_soa_record()
846
883
 
884
+ ip_addresses = IPAddress.objects.filter(pk__in=ipam_ip_addresses)
885
+ for ip_address in ip_addresses:
886
+ update_dns_records(ip_address)
887
+
847
888
  rfc2317_child_zones = Zone.objects.filter(pk__in=rfc2317_child_zones)
848
889
  if rfc2317_child_zones:
849
890
  for child_zone in rfc2317_child_zones: