netbox-plugin-dns 1.0.7__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/serializers.py +2 -1
- netbox_dns/api/serializers_/prefix.py +18 -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 -7
- netbox_dns/fields/__init__.py +1 -0
- netbox_dns/fields/ipam.py +15 -0
- netbox_dns/filtersets/__init__.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 +9 -5
- 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/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/record.py +139 -20
- netbox_dns/models/{contact.py → registration_contact.py} +8 -8
- netbox_dns/models/view.py +5 -0
- netbox_dns/models/zone.py +66 -30
- netbox_dns/models/zone_template.py +4 -4
- 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/record.py +33 -0
- netbox_dns/tables/{contact.py → registration_contact.py} +5 -5
- netbox_dns/tables/view.py +24 -2
- 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/registration_contact.py +60 -0
- netbox_dns/urls/view.py +6 -0
- netbox_dns/utilities/__init__.py +2 -74
- netbox_dns/utilities/conversions.py +83 -0
- netbox_dns/utilities/ipam_dnssync.py +295 -0
- netbox_dns/views/__init__.py +1 -1
- netbox_dns/views/record.py +3 -5
- netbox_dns/views/registration_contact.py +94 -0
- netbox_dns/views/view.py +26 -1
- {netbox_plugin_dns-1.0.7.dist-info → netbox_plugin_dns-1.1.0.dist-info}/METADATA +2 -1
- {netbox_plugin_dns-1.0.7.dist-info → netbox_plugin_dns-1.1.0.dist-info}/RECORD +63 -54
- 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 -29
- netbox_dns/utilities/ipam_coupling.py +0 -112
- netbox_dns/views/contact.py +0 -94
- {netbox_plugin_dns-1.0.7.dist-info → netbox_plugin_dns-1.1.0.dist-info}/LICENSE +0 -0
- {netbox_plugin_dns-1.0.7.dist-info → netbox_plugin_dns-1.1.0.dist-info}/WHEEL +0 -0
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,
|
|
@@ -28,13 +27,15 @@ from ipam.models import IPAddress
|
|
|
28
27
|
from netbox_dns.choices import RecordClassChoices, RecordTypeChoices, ZoneStatusChoices
|
|
29
28
|
from netbox_dns.fields import NetworkField, RFC2317NetworkField
|
|
30
29
|
from netbox_dns.utilities import (
|
|
30
|
+
update_dns_records,
|
|
31
|
+
check_dns_records,
|
|
32
|
+
get_ip_addresses_by_zone,
|
|
31
33
|
arpa_to_prefix,
|
|
32
34
|
name_to_unicode,
|
|
33
35
|
normalize_name,
|
|
34
36
|
NameFormatError,
|
|
35
37
|
)
|
|
36
38
|
from netbox_dns.validators import (
|
|
37
|
-
validate_fqdn,
|
|
38
39
|
validate_rname,
|
|
39
40
|
validate_domain_name,
|
|
40
41
|
)
|
|
@@ -77,6 +78,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
77
78
|
super().__init__(*args, **kwargs)
|
|
78
79
|
|
|
79
80
|
self._soa_serial_dirty = False
|
|
81
|
+
self._ip_addresses_checked = False
|
|
80
82
|
|
|
81
83
|
view = models.ForeignKey(
|
|
82
84
|
to="View",
|
|
@@ -190,7 +192,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
190
192
|
null=True,
|
|
191
193
|
)
|
|
192
194
|
registrant = models.ForeignKey(
|
|
193
|
-
to="
|
|
195
|
+
to="RegistrationContact",
|
|
194
196
|
on_delete=models.SET_NULL,
|
|
195
197
|
verbose_name="Registrant",
|
|
196
198
|
help_text="The owner of the domain",
|
|
@@ -198,7 +200,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
198
200
|
null=True,
|
|
199
201
|
)
|
|
200
202
|
admin_c = models.ForeignKey(
|
|
201
|
-
to="
|
|
203
|
+
to="RegistrationContact",
|
|
202
204
|
on_delete=models.SET_NULL,
|
|
203
205
|
verbose_name="Admin Contact",
|
|
204
206
|
related_name="admin_c_zones",
|
|
@@ -207,16 +209,16 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
207
209
|
null=True,
|
|
208
210
|
)
|
|
209
211
|
tech_c = models.ForeignKey(
|
|
210
|
-
to="
|
|
212
|
+
to="RegistrationContact",
|
|
211
213
|
on_delete=models.SET_NULL,
|
|
212
|
-
verbose_name="
|
|
214
|
+
verbose_name="Technical Contact",
|
|
213
215
|
related_name="tech_c_zones",
|
|
214
216
|
help_text="The technical contact for the domain",
|
|
215
217
|
blank=True,
|
|
216
218
|
null=True,
|
|
217
219
|
)
|
|
218
220
|
billing_c = models.ForeignKey(
|
|
219
|
-
to="
|
|
221
|
+
to="RegistrationContact",
|
|
220
222
|
on_delete=models.SET_NULL,
|
|
221
223
|
verbose_name="Billing Contact",
|
|
222
224
|
related_name="billing_c_zones",
|
|
@@ -282,7 +284,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
282
284
|
else:
|
|
283
285
|
try:
|
|
284
286
|
name = dns_name.from_text(self.name, origin=None).to_unicode()
|
|
285
|
-
except
|
|
287
|
+
except DNSException:
|
|
286
288
|
name = self.name
|
|
287
289
|
|
|
288
290
|
try:
|
|
@@ -310,6 +312,14 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
310
312
|
def soa_serial_dirty(self, soa_serial_dirty):
|
|
311
313
|
self._soa_serial_dirty = soa_serial_dirty
|
|
312
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
|
+
|
|
313
323
|
@property
|
|
314
324
|
def display_name(self):
|
|
315
325
|
return name_to_unicode(self.name)
|
|
@@ -655,6 +665,26 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
655
665
|
}
|
|
656
666
|
)
|
|
657
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
|
+
|
|
658
688
|
if self.is_reverse_zone:
|
|
659
689
|
self.arpa_network = self.network_from_name
|
|
660
690
|
|
|
@@ -769,23 +799,13 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
769
799
|
|
|
770
800
|
elif changed_fields is not None and {"name", "view", "status"} & changed_fields:
|
|
771
801
|
for address_record in self.record_set.filter(
|
|
772
|
-
type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA)
|
|
802
|
+
type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
|
|
803
|
+
ipam_ip_address__isnull=True,
|
|
773
804
|
):
|
|
774
805
|
address_record.save(update_fields=["ptr_record"])
|
|
775
806
|
|
|
776
|
-
# Fix name in IP Address when zone name is changed
|
|
777
|
-
if (
|
|
778
|
-
get_plugin_config("netbox_dns", "feature_ipam_coupling")
|
|
779
|
-
and "name" in changed_fields
|
|
780
|
-
):
|
|
781
|
-
for ip in IPAddress.objects.filter(
|
|
782
|
-
custom_field_data__ipaddress_dns_zone_id=self.pk
|
|
783
|
-
):
|
|
784
|
-
ip.dns_name = f'{ip.custom_field_data["ipaddress_dns_record_name"]}.{self.name}'
|
|
785
|
-
ip.save(update_fields=["dns_name"])
|
|
786
|
-
|
|
787
807
|
if changed_fields is not None and "name" in changed_fields:
|
|
788
|
-
for _record in self.record_set.
|
|
808
|
+
for _record in self.record_set.filter(ipam_ip_address__isnull=True):
|
|
789
809
|
_record.save(
|
|
790
810
|
update_fields=["fqdn"],
|
|
791
811
|
save_zone_serial=False,
|
|
@@ -793,6 +813,17 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
793
813
|
update_rfc2317_cname=False,
|
|
794
814
|
)
|
|
795
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
|
+
|
|
796
827
|
self.save_soa_serial()
|
|
797
828
|
self.update_soa_record()
|
|
798
829
|
|
|
@@ -828,14 +859,15 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
828
859
|
self.rfc2317_child_zones.all().values_list("pk", flat=True)
|
|
829
860
|
)
|
|
830
861
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
+
)
|
|
839
871
|
|
|
840
872
|
super().delete(*args, **kwargs)
|
|
841
873
|
|
|
@@ -849,6 +881,10 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
849
881
|
address_zone.save_soa_serial()
|
|
850
882
|
address_zone.update_soa_record()
|
|
851
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
|
+
|
|
852
888
|
rfc2317_child_zones = Zone.objects.filter(pk__in=rfc2317_child_zones)
|
|
853
889
|
if rfc2317_child_zones:
|
|
854
890
|
for child_zone in rfc2317_child_zones:
|
|
@@ -47,7 +47,7 @@ class ZoneTemplate(NetBoxModel):
|
|
|
47
47
|
null=True,
|
|
48
48
|
)
|
|
49
49
|
registrant = models.ForeignKey(
|
|
50
|
-
to="
|
|
50
|
+
to="RegistrationContact",
|
|
51
51
|
on_delete=models.SET_NULL,
|
|
52
52
|
related_name="+",
|
|
53
53
|
help_text="The owner of the domain",
|
|
@@ -55,7 +55,7 @@ class ZoneTemplate(NetBoxModel):
|
|
|
55
55
|
null=True,
|
|
56
56
|
)
|
|
57
57
|
admin_c = models.ForeignKey(
|
|
58
|
-
to="
|
|
58
|
+
to="RegistrationContact",
|
|
59
59
|
on_delete=models.SET_NULL,
|
|
60
60
|
verbose_name="Admin contact",
|
|
61
61
|
related_name="+",
|
|
@@ -64,7 +64,7 @@ class ZoneTemplate(NetBoxModel):
|
|
|
64
64
|
null=True,
|
|
65
65
|
)
|
|
66
66
|
tech_c = models.ForeignKey(
|
|
67
|
-
to="
|
|
67
|
+
to="RegistrationContact",
|
|
68
68
|
on_delete=models.SET_NULL,
|
|
69
69
|
verbose_name="Tech contact",
|
|
70
70
|
related_name="+",
|
|
@@ -73,7 +73,7 @@ class ZoneTemplate(NetBoxModel):
|
|
|
73
73
|
null=True,
|
|
74
74
|
)
|
|
75
75
|
billing_c = models.ForeignKey(
|
|
76
|
-
to="
|
|
76
|
+
to="RegistrationContact",
|
|
77
77
|
on_delete=models.SET_NULL,
|
|
78
78
|
verbose_name="Billing contact",
|
|
79
79
|
related_name="+",
|
netbox_dns/navigation.py
CHANGED
|
@@ -151,21 +151,21 @@ registrar_menu_item = PluginMenuItem(
|
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
contact_menu_item = PluginMenuItem(
|
|
154
|
-
link="plugins:netbox_dns:
|
|
155
|
-
link_text="Contacts",
|
|
156
|
-
permissions=["netbox_dns.
|
|
154
|
+
link="plugins:netbox_dns:registrationcontact_list",
|
|
155
|
+
link_text="Registration Contacts",
|
|
156
|
+
permissions=["netbox_dns.view_registrationcontact"],
|
|
157
157
|
buttons=(
|
|
158
158
|
PluginMenuButton(
|
|
159
|
-
"plugins:netbox_dns:
|
|
159
|
+
"plugins:netbox_dns:registrationcontact_add",
|
|
160
160
|
"Add",
|
|
161
161
|
"mdi mdi-plus-thick",
|
|
162
|
-
permissions=["netbox_dns.
|
|
162
|
+
permissions=["netbox_dns.add_registrationcontact"],
|
|
163
163
|
),
|
|
164
164
|
PluginMenuButton(
|
|
165
|
-
"plugins:netbox_dns:
|
|
165
|
+
"plugins:netbox_dns:registrationcontact_import",
|
|
166
166
|
"Import",
|
|
167
167
|
"mdi mdi-upload",
|
|
168
|
-
permissions=["netbox_dns.
|
|
168
|
+
permissions=["netbox_dns.add_registrationcontact"],
|
|
169
169
|
),
|
|
170
170
|
),
|
|
171
171
|
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from netaddr import IPNetwork
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.dispatch import receiver
|
|
5
|
+
from django.db.models.signals import pre_delete, pre_save, post_save, m2m_changed
|
|
6
|
+
from django.core.exceptions import ValidationError
|
|
7
|
+
|
|
8
|
+
from netbox.context import current_request
|
|
9
|
+
from netbox.signals import post_clean
|
|
10
|
+
from ipam.models import IPAddress, Prefix
|
|
11
|
+
from utilities.exceptions import AbortRequest
|
|
12
|
+
|
|
13
|
+
from netbox_dns.models import view as _view
|
|
14
|
+
from netbox_dns.utilities import (
|
|
15
|
+
check_dns_records,
|
|
16
|
+
check_record_permission,
|
|
17
|
+
update_dns_records,
|
|
18
|
+
delete_dns_records,
|
|
19
|
+
get_views_by_prefix,
|
|
20
|
+
get_ip_addresses_by_prefix,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DNSSYNC_CUSTOM_FIELDS = {
|
|
24
|
+
"ipaddress_dns_disabled": False,
|
|
25
|
+
"ipaddress_dns_record_ttl": None,
|
|
26
|
+
"ipaddress_dns_record_disable_ptr": False,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
IPADDRESS_ACTIVE_STATUS = settings.PLUGINS_CONFIG["netbox_dns"][
|
|
30
|
+
"dnssync_ipaddress_active_status"
|
|
31
|
+
]
|
|
32
|
+
ENFORCE_UNIQUE_RECORDS = settings.PLUGINS_CONFIG["netbox_dns"]["enforce_unique_records"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@receiver(post_clean, sender=IPAddress)
|
|
36
|
+
def ipam_dnssync_ipaddress_post_clean(instance, **kwargs):
|
|
37
|
+
if not isinstance(instance.address, IPNetwork):
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if instance.custom_field_data.get("ipaddress_dns_disabled"):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# +
|
|
44
|
+
# Check for uniqueness of IP address and dns_name. If unique records are
|
|
45
|
+
# enforced, report an error when trying to create the same IP address with
|
|
46
|
+
# the same dns_name. Ignore existing IP addresses that have their CF
|
|
47
|
+
# "ipaddress_dns_disabled" set to "True".
|
|
48
|
+
# -
|
|
49
|
+
duplicate_addresses = IPAddress.objects.filter(
|
|
50
|
+
address=instance.address,
|
|
51
|
+
vrf=instance.vrf,
|
|
52
|
+
dns_name=instance.dns_name,
|
|
53
|
+
status__in=IPADDRESS_ACTIVE_STATUS,
|
|
54
|
+
)
|
|
55
|
+
if instance.pk is not None:
|
|
56
|
+
duplicate_addresses = duplicate_addresses.exclude(pk=instance.pk)
|
|
57
|
+
|
|
58
|
+
if ENFORCE_UNIQUE_RECORDS and instance.status in IPADDRESS_ACTIVE_STATUS:
|
|
59
|
+
for ip_address in duplicate_addresses.only("custom_field_data"):
|
|
60
|
+
if not ip_address.custom_field_data.get("ipaddress_dns_disabled"):
|
|
61
|
+
raise ValidationError(
|
|
62
|
+
{
|
|
63
|
+
"dns_name": "Unique DNS records are enforced and there is already "
|
|
64
|
+
f"an active IP address {instance.address} with DNS name {instance.dns_name}. "
|
|
65
|
+
"Plesase choose a different name or disable record creation for this "
|
|
66
|
+
"IP address."
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# +
|
|
71
|
+
# Check NetBox DNS record permission for changes to IPAddress custom fields
|
|
72
|
+
#
|
|
73
|
+
# Normally, as the modfication of DNS fields
|
|
74
|
+
if (request := current_request.get()) is not None:
|
|
75
|
+
cf_data = instance.custom_field_data
|
|
76
|
+
if (
|
|
77
|
+
instance.pk is not None
|
|
78
|
+
and any(
|
|
79
|
+
(
|
|
80
|
+
cf_data.get(cf, cf_default)
|
|
81
|
+
!= IPAddress.objects.get(pk=instance.pk).custom_field_data.get(
|
|
82
|
+
cf, cf_default
|
|
83
|
+
)
|
|
84
|
+
for cf, cf_default in DNSSYNC_CUSTOM_FIELDS.items()
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
and not check_record_permission()
|
|
88
|
+
) or (
|
|
89
|
+
instance.pk is None
|
|
90
|
+
and any(
|
|
91
|
+
(
|
|
92
|
+
cf_data.get(cf, cf_default) != cf_default
|
|
93
|
+
for cf, cf_default in DNSSYNC_CUSTOM_FIELDS.items()
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
and not check_record_permission(change=False, delete=False)
|
|
97
|
+
):
|
|
98
|
+
raise ValidationError(
|
|
99
|
+
f"User '{request.user}' is not allowed to alter DNSsync custom fields"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
check_dns_records(instance)
|
|
104
|
+
except ValidationError as exc:
|
|
105
|
+
raise ValidationError({"dns_name": exc.messages})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@receiver(pre_delete, sender=IPAddress)
|
|
109
|
+
def ipam_dnssync_ipaddress_pre_delete(instance, **kwargs):
|
|
110
|
+
delete_dns_records(instance)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@receiver(pre_save, sender=IPAddress)
|
|
114
|
+
def ipam_dnssync_ipaddress_pre_save(instance, **kwargs):
|
|
115
|
+
check_dns_records(instance)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@receiver(post_save, sender=IPAddress)
|
|
119
|
+
def ipam_dnssync_ipaddress_post_save(instance, **kwargs):
|
|
120
|
+
update_dns_records(instance)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@receiver(pre_save, sender=Prefix)
|
|
124
|
+
def ipam_dnssync_prefix_pre_save(instance, **kwargs):
|
|
125
|
+
"""
|
|
126
|
+
Changes that modify the prefix hierarchy cannot be validated properly before
|
|
127
|
+
commiting them. So the solution in this case is to ask the user to deassign
|
|
128
|
+
the prefix from any views it is assigned to and retry.
|
|
129
|
+
"""
|
|
130
|
+
request = current_request.get()
|
|
131
|
+
|
|
132
|
+
if instance.pk is None or not instance.netbox_dns_views.exists():
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
saved_prefix = Prefix.objects.prefetch_related("netbox_dns_views").get(
|
|
136
|
+
pk=instance.pk
|
|
137
|
+
)
|
|
138
|
+
if saved_prefix.prefix != instance.prefix or saved_prefix.vrf != instance.vrf:
|
|
139
|
+
dns_views = ", ".join([view.name for view in instance.netbox_dns_views.all()])
|
|
140
|
+
if request is not None:
|
|
141
|
+
raise AbortRequest(
|
|
142
|
+
f"This prefix is currently assigned to the following DNS views: {dns_views}"
|
|
143
|
+
f"Please deassign it from these views before making changes to the prefix "
|
|
144
|
+
f"or VRF."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
raise ValidationError(
|
|
148
|
+
f"Prefix is assigned to DNS views {dns_views}. Prefix and VRF must not be changed"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@receiver(pre_delete, sender=Prefix)
|
|
153
|
+
def ipam_dnssync_prefix_pre_delete(instance, **kwargs):
|
|
154
|
+
parent = instance.get_parents().last()
|
|
155
|
+
request = current_request.get()
|
|
156
|
+
|
|
157
|
+
if parent is not None and get_views_by_prefix(instance) != get_views_by_prefix(
|
|
158
|
+
parent
|
|
159
|
+
):
|
|
160
|
+
try:
|
|
161
|
+
for prefix in instance.get_children().filter(
|
|
162
|
+
_depth=instance.depth + 1, netbox_dns_views__isnull=True
|
|
163
|
+
):
|
|
164
|
+
for ip_address in get_ip_addresses_by_prefix(prefix):
|
|
165
|
+
check_dns_records(ip_address)
|
|
166
|
+
except ValidationError as exc:
|
|
167
|
+
if request is not None:
|
|
168
|
+
raise AbortRequest(
|
|
169
|
+
f"Prefix deletion would cause DNS errors: {exc.messages[0]} "
|
|
170
|
+
"Please review DNS View assignments for this and the parent prefix"
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
raise exc
|
|
174
|
+
|
|
175
|
+
# +
|
|
176
|
+
# CAUTION: This only works because the NetBox workaround for an ancient
|
|
177
|
+
# Django bug (see https://code.djangoproject.com/ticket/17688) has already
|
|
178
|
+
# removed the relations between the prefix and the views when this signal
|
|
179
|
+
# handler runs.
|
|
180
|
+
#
|
|
181
|
+
# Should anything be fixed, this code will stop working and need to be
|
|
182
|
+
# revisited.
|
|
183
|
+
#
|
|
184
|
+
# The NetBox workaround only works for requests, not for model level
|
|
185
|
+
# operations. The following code replicates it for non-requests.
|
|
186
|
+
# -
|
|
187
|
+
if request is None:
|
|
188
|
+
for view in instance.netbox_dns_views.all():
|
|
189
|
+
view.snapshot()
|
|
190
|
+
view.prefixes.remove(instance)
|
|
191
|
+
|
|
192
|
+
for ip_address in get_ip_addresses_by_prefix(instance):
|
|
193
|
+
update_dns_records(ip_address)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@receiver(m2m_changed, sender=_view.View.prefixes.through)
|
|
197
|
+
def ipam_dnssync_view_prefix_changed(**kwargs):
|
|
198
|
+
action = kwargs.get("action")
|
|
199
|
+
request = current_request.get()
|
|
200
|
+
|
|
201
|
+
# +
|
|
202
|
+
# Handle all post_add and post_remove signals except the ones directly
|
|
203
|
+
# handled by the pre_delete handler for the Prefix model.
|
|
204
|
+
#
|
|
205
|
+
# Yes. This IS ugly.
|
|
206
|
+
# -
|
|
207
|
+
if action not in ("post_add", "post_remove") or (
|
|
208
|
+
request is not None
|
|
209
|
+
and action == "post_remove"
|
|
210
|
+
and (
|
|
211
|
+
request.path.startswith("/ipam/prefixes/")
|
|
212
|
+
or request.path.startswith("/api/ipam/prefixes/")
|
|
213
|
+
)
|
|
214
|
+
):
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
check_view = action != "post_remove"
|
|
218
|
+
|
|
219
|
+
ip_addresses = IPAddress.objects.none()
|
|
220
|
+
for prefix in Prefix.objects.filter(pk__in=kwargs.get("pk_set")):
|
|
221
|
+
ip_addresses |= get_ip_addresses_by_prefix(prefix, check_view=check_view)
|
|
222
|
+
|
|
223
|
+
for ip_address in ip_addresses.distinct():
|
|
224
|
+
update_dns_records(ip_address)
|
netbox_dns/tables/__init__.py
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import django_tables2 as tables
|
|
2
|
+
|
|
3
|
+
from ipam.tables import PrefixTable
|
|
4
|
+
from utilities.tables import register_table_column
|
|
5
|
+
|
|
6
|
+
views = tables.ManyToManyColumn(
|
|
7
|
+
verbose_name="DNS Views",
|
|
8
|
+
linkify_item=True,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
register_table_column(views, "netbox_dns_views", PrefixTable)
|
netbox_dns/tables/record.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import django_tables2 as tables
|
|
2
|
+
from django.utils.html import format_html
|
|
3
|
+
|
|
2
4
|
|
|
3
5
|
from netbox.tables import (
|
|
4
6
|
NetBoxTable,
|
|
@@ -11,6 +13,10 @@ from tenancy.tables import TenancyColumnsMixin
|
|
|
11
13
|
from netbox_dns.models import Record
|
|
12
14
|
from netbox_dns.utilities import value_to_unicode
|
|
13
15
|
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("netbox_dns")
|
|
19
|
+
|
|
14
20
|
|
|
15
21
|
__all__ = (
|
|
16
22
|
"RecordTable",
|
|
@@ -96,6 +102,11 @@ class ManagedRecordTable(RecordBaseTable):
|
|
|
96
102
|
verbose_name="IPAM IP Address",
|
|
97
103
|
linkify=True,
|
|
98
104
|
)
|
|
105
|
+
related_ip_address = tables.Column(
|
|
106
|
+
verbose_name="Related IP Address",
|
|
107
|
+
empty_values=(),
|
|
108
|
+
orderable=False,
|
|
109
|
+
)
|
|
99
110
|
actions = ActionsColumn(actions=("changelog",))
|
|
100
111
|
|
|
101
112
|
class Meta(NetBoxTable.Meta):
|
|
@@ -110,6 +121,28 @@ class ManagedRecordTable(RecordBaseTable):
|
|
|
110
121
|
"active",
|
|
111
122
|
)
|
|
112
123
|
|
|
124
|
+
def render_related_ip_address(self, record):
|
|
125
|
+
if record.ipam_ip_address is not None:
|
|
126
|
+
address = record.ipam_ip_address
|
|
127
|
+
elif (
|
|
128
|
+
hasattr(record, "address_record")
|
|
129
|
+
and record.address_record.ipam_ip_address is not None
|
|
130
|
+
):
|
|
131
|
+
address = record.address_record.ipam_ip_address
|
|
132
|
+
else:
|
|
133
|
+
return format_html("—")
|
|
134
|
+
|
|
135
|
+
return format_html(f"<a href='{address.get_absolute_url()}'>{address}</a>")
|
|
136
|
+
|
|
137
|
+
def value_related_ip_address(self, record):
|
|
138
|
+
if record.ipam_ip_address is not None:
|
|
139
|
+
return record.ipam_ip_address
|
|
140
|
+
elif (
|
|
141
|
+
hasattr(record, "address_record")
|
|
142
|
+
and record.address_record.ipam_ip_address is not None
|
|
143
|
+
):
|
|
144
|
+
return record.address_record.ipam_ip_address
|
|
145
|
+
|
|
113
146
|
|
|
114
147
|
class RelatedRecordTable(RecordBaseTable):
|
|
115
148
|
actions = ActionsColumn(actions=())
|
|
@@ -2,22 +2,22 @@ import django_tables2 as tables
|
|
|
2
2
|
|
|
3
3
|
from netbox.tables import NetBoxTable, TagColumn
|
|
4
4
|
|
|
5
|
-
from netbox_dns.models import
|
|
5
|
+
from netbox_dns.models import RegistrationContact
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
__all__ = ("
|
|
8
|
+
__all__ = ("RegistrationContactTable",)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
11
|
+
class RegistrationContactTable(NetBoxTable):
|
|
12
12
|
contact_id = tables.Column(
|
|
13
13
|
linkify=True,
|
|
14
14
|
)
|
|
15
15
|
tags = TagColumn(
|
|
16
|
-
url_name="plugins:netbox_dns:
|
|
16
|
+
url_name="plugins:netbox_dns:registrationcontact_list",
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
class Meta(NetBoxTable.Meta):
|
|
20
|
-
model =
|
|
20
|
+
model = RegistrationContact
|
|
21
21
|
fields = (
|
|
22
22
|
"name",
|
|
23
23
|
"description",
|
netbox_dns/tables/view.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import django_tables2 as tables
|
|
2
2
|
|
|
3
|
-
from netbox.tables import NetBoxTable, TagColumn
|
|
3
|
+
from netbox.tables import NetBoxTable, TagColumn, ActionsColumn
|
|
4
4
|
from tenancy.tables import TenancyColumnsMixin
|
|
5
5
|
|
|
6
6
|
from netbox_dns.models import View
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
__all__ = (
|
|
9
|
+
__all__ = (
|
|
10
|
+
"ViewTable",
|
|
11
|
+
"RelatedViewTable",
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class ViewTable(TenancyColumnsMixin, NetBoxTable):
|
|
@@ -22,3 +25,22 @@ class ViewTable(TenancyColumnsMixin, NetBoxTable):
|
|
|
22
25
|
model = View
|
|
23
26
|
fields = ("description",)
|
|
24
27
|
default_columns = ("name", "default_view")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RelatedViewTable(TenancyColumnsMixin, NetBoxTable):
|
|
31
|
+
actions = ActionsColumn(actions=())
|
|
32
|
+
|
|
33
|
+
name = tables.Column(
|
|
34
|
+
linkify=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
class Meta(NetBoxTable.Meta):
|
|
38
|
+
model = View
|
|
39
|
+
fields = (
|
|
40
|
+
"name",
|
|
41
|
+
"description",
|
|
42
|
+
"tenant",
|
|
43
|
+
"tenant_group",
|
|
44
|
+
"tags",
|
|
45
|
+
)
|
|
46
|
+
default_columns = ("name", "description")
|