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.

Files changed (69) hide show
  1. netbox_dns/__init__.py +23 -4
  2. netbox_dns/api/serializers.py +2 -1
  3. netbox_dns/api/serializers_/prefix.py +18 -0
  4. netbox_dns/api/serializers_/{contact.py → registration_contact.py} +5 -5
  5. netbox_dns/api/serializers_/view.py +34 -2
  6. netbox_dns/api/serializers_/zone.py +5 -5
  7. netbox_dns/api/serializers_/zone_template.py +5 -5
  8. netbox_dns/api/urls.py +5 -2
  9. netbox_dns/api/views.py +17 -7
  10. netbox_dns/fields/__init__.py +1 -0
  11. netbox_dns/fields/ipam.py +15 -0
  12. netbox_dns/filtersets/__init__.py +1 -1
  13. netbox_dns/filtersets/{contact.py → registration_contact.py} +4 -4
  14. netbox_dns/filtersets/view.py +16 -0
  15. netbox_dns/filtersets/zone.py +15 -15
  16. netbox_dns/filtersets/zone_template.py +15 -15
  17. netbox_dns/forms/__init__.py +1 -1
  18. netbox_dns/forms/{contact.py → registration_contact.py} +16 -16
  19. netbox_dns/forms/view.py +204 -4
  20. netbox_dns/forms/zone.py +15 -18
  21. netbox_dns/forms/zone_template.py +13 -13
  22. netbox_dns/graphql/__init__.py +2 -2
  23. netbox_dns/graphql/filters.py +5 -5
  24. netbox_dns/graphql/schema.py +9 -5
  25. netbox_dns/graphql/types.py +41 -12
  26. netbox_dns/management/commands/rebuild_dnssync.py +18 -0
  27. netbox_dns/management/commands/setup_dnssync.py +140 -0
  28. netbox_dns/migrations/0008_view_prefixes.py +18 -0
  29. netbox_dns/migrations/0009_rename_contact_registrationcontact.py +27 -0
  30. netbox_dns/models/__init__.py +1 -3
  31. netbox_dns/models/record.py +139 -20
  32. netbox_dns/models/{contact.py → registration_contact.py} +8 -8
  33. netbox_dns/models/view.py +5 -0
  34. netbox_dns/models/zone.py +66 -30
  35. netbox_dns/models/zone_template.py +4 -4
  36. netbox_dns/navigation.py +7 -7
  37. netbox_dns/signals/ipam_dnssync.py +224 -0
  38. netbox_dns/tables/__init__.py +1 -1
  39. netbox_dns/tables/ipam_dnssync.py +11 -0
  40. netbox_dns/tables/record.py +33 -0
  41. netbox_dns/tables/{contact.py → registration_contact.py} +5 -5
  42. netbox_dns/tables/view.py +24 -2
  43. netbox_dns/template_content.py +41 -40
  44. netbox_dns/templates/netbox_dns/record.html +6 -6
  45. netbox_dns/templates/netbox_dns/{contact.html → registrationcontact.html} +1 -1
  46. netbox_dns/templates/netbox_dns/view/button.html +9 -0
  47. netbox_dns/templates/netbox_dns/view/prefix.html +41 -0
  48. netbox_dns/templates/netbox_dns/view/related.html +17 -0
  49. netbox_dns/templates/netbox_dns/view.html +25 -0
  50. netbox_dns/urls/__init__.py +2 -2
  51. netbox_dns/urls/registration_contact.py +60 -0
  52. netbox_dns/urls/view.py +6 -0
  53. netbox_dns/utilities/__init__.py +2 -74
  54. netbox_dns/utilities/conversions.py +83 -0
  55. netbox_dns/utilities/ipam_dnssync.py +295 -0
  56. netbox_dns/views/__init__.py +1 -1
  57. netbox_dns/views/record.py +3 -5
  58. netbox_dns/views/registration_contact.py +94 -0
  59. netbox_dns/views/view.py +26 -1
  60. {netbox_plugin_dns-1.0.7.dist-info → netbox_plugin_dns-1.1.0.dist-info}/METADATA +2 -1
  61. {netbox_plugin_dns-1.0.7.dist-info → netbox_plugin_dns-1.1.0.dist-info}/RECORD +63 -54
  62. netbox_dns/management/commands/setup_coupling.py +0 -109
  63. netbox_dns/signals/ipam_coupling.py +0 -168
  64. netbox_dns/templates/netbox_dns/related_dns_objects.html +0 -21
  65. netbox_dns/urls/contact.py +0 -29
  66. netbox_dns/utilities/ipam_coupling.py +0 -112
  67. netbox_dns/views/contact.py +0 -94
  68. {netbox_plugin_dns-1.0.7.dist-info → netbox_plugin_dns-1.1.0.dist-info}/LICENSE +0 -0
  69. {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="Contact",
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="Contact",
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="Contact",
212
+ to="RegistrationContact",
211
213
  on_delete=models.SET_NULL,
212
- verbose_name="Tech Contact",
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="Contact",
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 dns_name.IDNAException:
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.all():
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
- if get_plugin_config("netbox_dns", "feature_ipam_coupling"):
832
- for ip in IPAddress.objects.filter(
833
- custom_field_data__ipaddress_dns_zone_id=self.pk
834
- ):
835
- ip.dns_name = ""
836
- ip.custom_field_data["ipaddress_dns_record_name"] = None
837
- ip.custom_field_data["ipaddress_dns_zone_id"] = None
838
- 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
+ )
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="Contact",
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="Contact",
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="Contact",
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="Contact",
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:contact_list",
155
- link_text="Contacts",
156
- permissions=["netbox_dns.view_contact"],
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:contact_add",
159
+ "plugins:netbox_dns:registrationcontact_add",
160
160
  "Add",
161
161
  "mdi mdi-plus-thick",
162
- permissions=["netbox_dns.add_contact"],
162
+ permissions=["netbox_dns.add_registrationcontact"],
163
163
  ),
164
164
  PluginMenuButton(
165
- "plugins:netbox_dns:contact_import",
165
+ "plugins:netbox_dns:registrationcontact_import",
166
166
  "Import",
167
167
  "mdi mdi-upload",
168
- permissions=["netbox_dns.add_contact"],
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)
@@ -2,7 +2,7 @@ from .view import *
2
2
  from .zone import *
3
3
  from .nameserver import *
4
4
  from .record import *
5
- from .contact import *
5
+ from .registration_contact import *
6
6
  from .registrar import *
7
7
  from .zone_template import *
8
8
  from .record_template import *
@@ -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)
@@ -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 Contact
5
+ from netbox_dns.models import RegistrationContact
6
6
 
7
7
 
8
- __all__ = ("ContactTable",)
8
+ __all__ = ("RegistrationContactTable",)
9
9
 
10
10
 
11
- class ContactTable(NetBoxTable):
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:contact_list",
16
+ url_name="plugins:netbox_dns:registrationcontact_list",
17
17
  )
18
18
 
19
19
  class Meta(NetBoxTable.Meta):
20
- model = Contact
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__ = ("ViewTable",)
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")