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.
- netbox_dns/__init__.py +23 -4
- netbox_dns/api/nested_serializers.py +17 -16
- netbox_dns/api/serializers.py +2 -1
- netbox_dns/api/serializers_/prefix.py +18 -0
- netbox_dns/api/serializers_/record.py +1 -0
- netbox_dns/api/serializers_/{contact.py → registration_contact.py} +5 -5
- netbox_dns/api/serializers_/view.py +34 -2
- netbox_dns/api/serializers_/zone.py +5 -5
- netbox_dns/api/serializers_/zone_template.py +5 -5
- netbox_dns/api/urls.py +5 -2
- netbox_dns/api/views.py +17 -35
- netbox_dns/fields/__init__.py +1 -0
- netbox_dns/fields/ipam.py +15 -0
- netbox_dns/filtersets/__init__.py +1 -1
- netbox_dns/filtersets/record.py +1 -1
- netbox_dns/filtersets/{contact.py → registration_contact.py} +4 -4
- netbox_dns/filtersets/view.py +16 -0
- netbox_dns/filtersets/zone.py +15 -15
- netbox_dns/filtersets/zone_template.py +15 -15
- netbox_dns/forms/__init__.py +1 -1
- netbox_dns/forms/{contact.py → registration_contact.py} +16 -16
- netbox_dns/forms/view.py +204 -4
- netbox_dns/forms/zone.py +15 -18
- netbox_dns/forms/zone_template.py +13 -13
- netbox_dns/graphql/__init__.py +2 -2
- netbox_dns/graphql/filters.py +5 -5
- netbox_dns/graphql/schema.py +24 -44
- netbox_dns/graphql/types.py +41 -12
- netbox_dns/management/commands/rebuild_dnssync.py +18 -0
- netbox_dns/management/commands/setup_dnssync.py +140 -0
- netbox_dns/migrations/0007_alter_ordering_options.py +25 -0
- netbox_dns/migrations/0008_view_prefixes.py +18 -0
- netbox_dns/migrations/0009_rename_contact_registrationcontact.py +27 -0
- netbox_dns/models/__init__.py +1 -3
- netbox_dns/models/nameserver.py +8 -3
- netbox_dns/models/record.py +154 -24
- netbox_dns/models/record_template.py +4 -1
- netbox_dns/models/registrar.py +7 -1
- netbox_dns/models/{contact.py → registration_contact.py} +15 -9
- netbox_dns/models/view.py +14 -2
- netbox_dns/models/zone.py +76 -35
- netbox_dns/models/zone_template.py +12 -9
- netbox_dns/navigation.py +7 -7
- netbox_dns/signals/ipam_dnssync.py +224 -0
- netbox_dns/tables/__init__.py +1 -1
- netbox_dns/tables/ipam_dnssync.py +11 -0
- netbox_dns/tables/nameserver.py +1 -7
- netbox_dns/tables/record.py +43 -30
- netbox_dns/tables/record_template.py +0 -17
- netbox_dns/tables/registrar.py +0 -2
- netbox_dns/tables/{contact.py → registration_contact.py} +5 -6
- netbox_dns/tables/view.py +19 -4
- netbox_dns/tables/zone.py +0 -15
- netbox_dns/tables/zone_template.py +2 -16
- netbox_dns/template_content.py +41 -40
- netbox_dns/templates/netbox_dns/record.html +6 -6
- netbox_dns/templates/netbox_dns/{contact.html → registrationcontact.html} +1 -1
- netbox_dns/templates/netbox_dns/view/button.html +9 -0
- netbox_dns/templates/netbox_dns/view/prefix.html +41 -0
- netbox_dns/templates/netbox_dns/view/related.html +17 -0
- netbox_dns/templates/netbox_dns/view.html +25 -0
- netbox_dns/urls/__init__.py +2 -2
- netbox_dns/urls/nameserver.py +14 -38
- netbox_dns/urls/record.py +7 -19
- netbox_dns/urls/record_template.py +18 -27
- netbox_dns/urls/registrar.py +11 -35
- netbox_dns/urls/registration_contact.py +60 -0
- netbox_dns/urls/view.py +12 -20
- netbox_dns/urls/zone.py +8 -46
- netbox_dns/urls/zone_template.py +16 -26
- netbox_dns/utilities/__init__.py +2 -74
- netbox_dns/utilities/conversions.py +83 -0
- netbox_dns/utilities/ipam_dnssync.py +295 -0
- netbox_dns/validators/dns_name.py +9 -0
- netbox_dns/views/__init__.py +1 -1
- netbox_dns/views/nameserver.py +7 -3
- netbox_dns/views/record.py +12 -7
- netbox_dns/views/record_template.py +1 -1
- netbox_dns/views/registrar.py +0 -1
- netbox_dns/views/registration_contact.py +94 -0
- netbox_dns/views/view.py +32 -2
- netbox_dns/views/zone.py +7 -6
- netbox_dns/views/zone_template.py +2 -2
- {netbox_plugin_dns-1.0.6.dist-info → netbox_plugin_dns-1.1.0.dist-info}/METADATA +2 -1
- netbox_plugin_dns-1.1.0.dist-info/RECORD +146 -0
- netbox_dns/management/commands/setup_coupling.py +0 -109
- netbox_dns/signals/ipam_coupling.py +0 -168
- netbox_dns/templates/netbox_dns/related_dns_objects.html +0 -21
- netbox_dns/urls/contact.py +0 -51
- netbox_dns/utilities/ipam_coupling.py +0 -112
- netbox_dns/views/contact.py +0 -95
- netbox_plugin_dns-1.0.6.dist-info/RECORD +0 -136
- {netbox_plugin_dns-1.0.6.dist-info → netbox_plugin_dns-1.1.0.dist-info}/LICENSE +0 -0
- {netbox_plugin_dns-1.0.6.dist-info → netbox_plugin_dns-1.1.0.dist-info}/WHEEL +0 -0
netbox_dns/models/record.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
433
|
-
name.to_unicode()
|
|
479
|
+
data = record_data_from_ip_address(ip_address, zone)
|
|
434
480
|
|
|
435
|
-
|
|
436
|
-
self.
|
|
481
|
+
if data is None:
|
|
482
|
+
self.delete()
|
|
483
|
+
return
|
|
437
484
|
|
|
438
|
-
|
|
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":
|
|
518
|
+
"name": f"{self.name} is not a name in {zone.name}",
|
|
442
519
|
}
|
|
443
520
|
)
|
|
444
521
|
|
|
445
|
-
|
|
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":
|
|
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=
|
|
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
|
|
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(
|
|
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,
|
netbox_dns/models/registrar.py
CHANGED
|
@@ -59,7 +59,13 @@ class Registrar(NetBoxModel):
|
|
|
59
59
|
return str(self.name)
|
|
60
60
|
|
|
61
61
|
class Meta:
|
|
62
|
-
|
|
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
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"RegistrationContact",
|
|
12
|
+
"RegistrationContactIndex",
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class
|
|
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:
|
|
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
|
-
|
|
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
|
|
131
|
-
model =
|
|
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 =
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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="
|
|
212
|
+
to="RegistrationContact",
|
|
209
213
|
on_delete=models.SET_NULL,
|
|
210
|
-
verbose_name="
|
|
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="
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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:
|