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
netbox_dns/urls/zone.py CHANGED
@@ -1,63 +1,25 @@
1
- from django.urls import path
1
+ from django.urls import include, path
2
2
 
3
- from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
3
+ from utilities.urls import get_model_urls
4
4
 
5
- from netbox_dns.models import Zone
6
5
  from netbox_dns.views import (
7
- ZoneListView,
8
6
  ZoneView,
9
- ZoneDeleteView,
7
+ ZoneListView,
10
8
  ZoneEditView,
9
+ ZoneDeleteView,
11
10
  ZoneBulkImportView,
12
11
  ZoneBulkEditView,
13
12
  ZoneBulkDeleteView,
14
- ZoneRecordListView,
15
- ZoneManagedRecordListView,
16
- ZoneRegistrationView,
17
- ZoneRFC2317ChildZoneListView,
18
- ZoneChildZoneListView,
19
13
  )
20
14
 
21
15
  zone_urlpatterns = [
16
+ path("zones/<int:pk>/", ZoneView.as_view(), name="zone"),
22
17
  path("zones/", ZoneListView.as_view(), name="zone_list"),
23
18
  path("zones/add/", ZoneEditView.as_view(), name="zone_add"),
19
+ path("zones/<int:pk>/edit/", ZoneEditView.as_view(), name="zone_edit"),
20
+ path("zones/<int:pk>/delete/", ZoneDeleteView.as_view(), name="zone_delete"),
24
21
  path("zones/import/", ZoneBulkImportView.as_view(), name="zone_import"),
25
22
  path("zones/edit/", ZoneBulkEditView.as_view(), name="zone_bulk_edit"),
26
23
  path("zones/delete/", ZoneBulkDeleteView.as_view(), name="zone_bulk_delete"),
27
- path("zones/<int:pk>/", ZoneView.as_view(), name="zone"),
28
- path("zones/<int:pk>/delete/", ZoneDeleteView.as_view(), name="zone_delete"),
29
- path("zones/<int:pk>/edit/", ZoneEditView.as_view(), name="zone_edit"),
30
- path("zones/<int:pk>/records/", ZoneRecordListView.as_view(), name="zone_records"),
31
- path(
32
- "zones/<int:pk>/managedrecords/",
33
- ZoneManagedRecordListView.as_view(),
34
- name="zone_managed_records",
35
- ),
36
- path(
37
- "zones/<int:pk>/rfc2317childzones/",
38
- ZoneRFC2317ChildZoneListView.as_view(),
39
- name="zone_rfc2317_child_zones",
40
- ),
41
- path(
42
- "zones/<int:pk>/childzones/",
43
- ZoneChildZoneListView.as_view(),
44
- name="zone_child_zones",
45
- ),
46
- path(
47
- "zones/<int:pk>/registration/",
48
- ZoneRegistrationView.as_view(),
49
- name="zone_registration",
50
- ),
51
- path(
52
- "zones/<int:pk>/journal/",
53
- ObjectJournalView.as_view(),
54
- name="zone_journal",
55
- kwargs={"model": Zone},
56
- ),
57
- path(
58
- "zones/<int:pk>/changelog/",
59
- ObjectChangeLogView.as_view(),
60
- name="zone_changelog",
61
- kwargs={"model": Zone},
62
- ),
24
+ path("zones/<int:pk>/", include(get_model_urls("netbox_dns", "zone"))),
63
25
  ]
@@ -1,21 +1,31 @@
1
- from django.urls import path
1
+ from django.urls import include, path
2
2
 
3
- from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
3
+ from utilities.urls import get_model_urls
4
4
 
5
- from netbox_dns.models import ZoneTemplate
6
5
  from netbox_dns.views import (
7
- ZoneTemplateListView,
8
6
  ZoneTemplateView,
9
- ZoneTemplateDeleteView,
7
+ ZoneTemplateListView,
10
8
  ZoneTemplateEditView,
9
+ ZoneTemplateDeleteView,
11
10
  ZoneTemplateBulkImportView,
12
11
  ZoneTemplateBulkEditView,
13
12
  ZoneTemplateBulkDeleteView,
14
13
  )
15
14
 
16
15
  zonetemplate_urlpatterns = [
16
+ path("zonetemplates/<int:pk>/", ZoneTemplateView.as_view(), name="zonetemplate"),
17
17
  path("zonetemplates/", ZoneTemplateListView.as_view(), name="zonetemplate_list"),
18
18
  path("zonetemplates/add/", ZoneTemplateEditView.as_view(), name="zonetemplate_add"),
19
+ path(
20
+ "zonetemplates/<int:pk>/edit/",
21
+ ZoneTemplateEditView.as_view(),
22
+ name="zonetemplate_edit",
23
+ ),
24
+ path(
25
+ "zonetemplates/<int:pk>/delete/",
26
+ ZoneTemplateDeleteView.as_view(),
27
+ name="zonetemplate_delete",
28
+ ),
19
29
  path(
20
30
  "zonetemplates/import/",
21
31
  ZoneTemplateBulkImportView.as_view(),
@@ -31,27 +41,7 @@ zonetemplate_urlpatterns = [
31
41
  ZoneTemplateBulkDeleteView.as_view(),
32
42
  name="zonetemplate_bulk_delete",
33
43
  ),
34
- path("zonetemplates/<int:pk>/", ZoneTemplateView.as_view(), name="zonetemplate"),
35
- path(
36
- "zonetemplates/<int:pk>/delete/",
37
- ZoneTemplateDeleteView.as_view(),
38
- name="zonetemplate_delete",
39
- ),
40
- path(
41
- "zonetemplates/<int:pk>/edit/",
42
- ZoneTemplateEditView.as_view(),
43
- name="zonetemplate_edit",
44
- ),
45
- path(
46
- "zonetemplates/<int:pk>/journal/",
47
- ObjectJournalView.as_view(),
48
- name="zonetemplate_journal",
49
- kwargs={"model": ZoneTemplate},
50
- ),
51
44
  path(
52
- "zonetemplates/<int:pk>/changelog/",
53
- ObjectChangeLogView.as_view(),
54
- name="zonetemplate_changelog",
55
- kwargs={"model": ZoneTemplate},
45
+ "zonetemplates/<int:pk>/", include(get_model_urls("netbox_dns", "zonetemplate"))
56
46
  ),
57
47
  ]
@@ -1,74 +1,2 @@
1
- import re
2
-
3
- from dns import name as dns_name
4
- from dns.exception import DNSException
5
- from netaddr import IPNetwork, AddrFormatError
6
-
7
- from netbox.plugins.utils import get_plugin_config
8
-
9
-
10
- class NameFormatError(Exception):
11
- pass
12
-
13
-
14
- def arpa_to_prefix(arpa_name):
15
- name = arpa_name.rstrip(".")
16
-
17
- if name.endswith(".in-addr.arpa"):
18
- address = ".".join(reversed(name.replace(".in-addr.arpa", "").split(".")))
19
- mask = len(address.split(".")) * 8
20
- address = address + (32 - mask) // 8 * ".0"
21
-
22
- try:
23
- return IPNetwork(f"{address}/{mask}")
24
- except (AddrFormatError, ValueError):
25
- return None
26
-
27
- elif name.endswith("ip6.arpa"):
28
- address = "".join(reversed(name.replace(".ip6.arpa", "").split(".")))
29
- mask = len(address)
30
- address = address + "0" * (32 - mask)
31
-
32
- try:
33
- return IPNetwork(
34
- f"{':'.join([(address[i:i+4]) for i in range(0, 32, 4)])}/{mask*4}"
35
- )
36
- except AddrFormatError:
37
- return None
38
-
39
- else:
40
- return None
41
-
42
-
43
- def name_to_unicode(name):
44
- if name == "." and get_plugin_config("netbox_dns", "enable_root_zones"):
45
- return "."
46
-
47
- try:
48
- return dns_name.from_text(name, origin=None).to_unicode()
49
- except dns_name.IDNAException:
50
- return name
51
-
52
-
53
- def value_to_unicode(value):
54
- return re.sub(
55
- r"xn--[0-9a-z-_.]*",
56
- lambda x: name_to_unicode(x.group(0)),
57
- value,
58
- flags=re.IGNORECASE,
59
- )
60
-
61
-
62
- def normalize_name(name):
63
- if name == "." and get_plugin_config("netbox_dns", "enable_root_zones"):
64
- return "."
65
-
66
- try:
67
- return (
68
- dns_name.from_text(name, origin=dns_name.root)
69
- .relativize(dns_name.root)
70
- .to_text()
71
- )
72
-
73
- except DNSException as exc:
74
- raise NameFormatError from exc
1
+ from .conversions import *
2
+ from .ipam_dnssync import *
@@ -0,0 +1,83 @@
1
+ import re
2
+
3
+ from dns import name as dns_name
4
+ from dns.exception import DNSException
5
+ from netaddr import IPNetwork, AddrFormatError
6
+
7
+ from netbox.plugins.utils import get_plugin_config
8
+
9
+
10
+ __all__ = (
11
+ "NameFormatError",
12
+ "arpa_to_prefix",
13
+ "name_to_unicode",
14
+ "value_to_unicode",
15
+ "normalize_name",
16
+ )
17
+
18
+
19
+ class NameFormatError(Exception):
20
+ pass
21
+
22
+
23
+ def arpa_to_prefix(arpa_name):
24
+ name = arpa_name.rstrip(".")
25
+
26
+ if name.endswith(".in-addr.arpa"):
27
+ address = ".".join(reversed(name.replace(".in-addr.arpa", "").split(".")))
28
+ mask = len(address.split(".")) * 8
29
+ address = address + (32 - mask) // 8 * ".0"
30
+
31
+ try:
32
+ return IPNetwork(f"{address}/{mask}")
33
+ except (AddrFormatError, ValueError):
34
+ return None
35
+
36
+ elif name.endswith("ip6.arpa"):
37
+ address = "".join(reversed(name.replace(".ip6.arpa", "").split(".")))
38
+ mask = len(address)
39
+ address = address + "0" * (32 - mask)
40
+
41
+ try:
42
+ return IPNetwork(
43
+ f"{':'.join([(address[i:i+4]) for i in range(0, 32, 4)])}/{mask*4}"
44
+ )
45
+ except AddrFormatError:
46
+ return None
47
+
48
+ else:
49
+ return None
50
+
51
+
52
+ def name_to_unicode(name):
53
+ if name == "." and get_plugin_config("netbox_dns", "enable_root_zones"):
54
+ return "."
55
+
56
+ try:
57
+ return dns_name.from_text(name, origin=None).to_unicode()
58
+ except dns_name.IDNAException:
59
+ return name
60
+
61
+
62
+ def value_to_unicode(value):
63
+ return re.sub(
64
+ r"xn--[0-9a-z-_.]*",
65
+ lambda x: name_to_unicode(x.group(0)),
66
+ value,
67
+ flags=re.IGNORECASE,
68
+ )
69
+
70
+
71
+ def normalize_name(name):
72
+ if name == "." and get_plugin_config("netbox_dns", "enable_root_zones"):
73
+ return "."
74
+
75
+ try:
76
+ return (
77
+ dns_name.from_text(name, origin=dns_name.root)
78
+ .relativize(dns_name.root)
79
+ .to_text()
80
+ )
81
+
82
+ except DNSException as exc:
83
+ raise NameFormatError from exc
@@ -0,0 +1,295 @@
1
+ import re
2
+
3
+ from collections import defaultdict
4
+
5
+ from dns import name as dns_name
6
+
7
+ from django.conf import settings
8
+ from django.db.models import Q
9
+
10
+ from netbox.context import current_request
11
+ from ipam.models import IPAddress, Prefix
12
+
13
+ from netbox_dns.models import zone as _zone
14
+ from netbox_dns.models import record as _record
15
+ from netbox_dns.models import view as _view
16
+ from netbox_dns.choices import RecordStatusChoices
17
+
18
+
19
+ __all__ = (
20
+ "get_zones",
21
+ "check_dns_records",
22
+ "update_dns_records",
23
+ "delete_dns_records",
24
+ "get_views_by_prefix",
25
+ "get_ip_addresses_by_prefix",
26
+ "get_ip_addresses_by_view",
27
+ "get_ip_addresses_by_zone",
28
+ "check_record_permission",
29
+ )
30
+
31
+
32
+ def _get_assigned_views(ip_address):
33
+ longest_prefix = Prefix.objects.filter(
34
+ vrf=ip_address.vrf,
35
+ prefix__net_contains_or_equals=str(ip_address.address.ip),
36
+ netbox_dns_views__isnull=False,
37
+ ).last()
38
+
39
+ if longest_prefix is None:
40
+ return []
41
+
42
+ return longest_prefix.netbox_dns_views.all()
43
+
44
+
45
+ def _get_record_status(ip_address):
46
+ return (
47
+ RecordStatusChoices.STATUS_ACTIVE
48
+ if ip_address.status
49
+ in settings.PLUGINS_CONFIG["netbox_dns"].get(
50
+ "dnssync_ipaddress_active_status", []
51
+ )
52
+ else RecordStatusChoices.STATUS_INACTIVE
53
+ )
54
+
55
+
56
+ def _valid_entry(ip_address, zone):
57
+ return zone.view in _get_assigned_views(ip_address) and dns_name.from_text(
58
+ ip_address.dns_name
59
+ ).is_subdomain(dns_name.from_text(zone.name))
60
+
61
+
62
+ def _match_data(ip_address, record):
63
+ cf_disable_ptr = ip_address.custom_field_data.get(
64
+ "ipaddress_dns_record_disable_ptr"
65
+ )
66
+
67
+ return (
68
+ record.fqdn.rstrip(".") == ip_address.dns_name.rstrip(".")
69
+ and record.value == str(ip_address.address.ip)
70
+ and record.status == _get_record_status(ip_address)
71
+ and record.ttl == ip_address.custom_field_data.get("ipaddress_dns_record_ttl")
72
+ and (cf_disable_ptr is None or record.disable_ptr == cf_disable_ptr)
73
+ )
74
+
75
+
76
+ def get_zones(ip_address, view=None, old_zone=None):
77
+ if view is None:
78
+ views = _get_assigned_views(ip_address)
79
+ if not views:
80
+ return []
81
+
82
+ else:
83
+ views = [view]
84
+
85
+ min_labels = settings.PLUGINS_CONFIG["netbox_dns"].get(
86
+ "dnssync_minimum_zone_labels", 2
87
+ )
88
+ fqdn = dns_name.from_text(ip_address.dns_name)
89
+ zone_name_candidates = [
90
+ fqdn.split(i)[1].to_text().rstrip(".")
91
+ for i in range(min_labels + 1, len(fqdn.labels))
92
+ ]
93
+
94
+ zones = _zone.Zone.objects.filter(
95
+ view__in=views,
96
+ name__in=zone_name_candidates,
97
+ active=True,
98
+ )
99
+
100
+ zone_map = defaultdict(list)
101
+
102
+ if old_zone is not None:
103
+ zones = zones.exclude(pk=old_zone.pk)
104
+ if _valid_entry(ip_address, old_zone):
105
+ zone_map[old_zone.view].append(old_zone)
106
+
107
+ for zone in zones:
108
+ zone_map[zone.view].append(zone)
109
+
110
+ return [
111
+ sorted(zones_per_view, key=lambda x: len(x.name))[-1]
112
+ for zones_per_view in zone_map.values()
113
+ ]
114
+
115
+
116
+ def check_dns_records(ip_address, zone=None, view=None):
117
+ if ip_address.dns_name == "":
118
+ return
119
+
120
+ if zone is None:
121
+ zones = get_zones(ip_address, view=view)
122
+
123
+ if ip_address.pk is not None:
124
+ for record in ip_address.netbox_dns_records.filter(zone__in=zones):
125
+ if not _match_data(ip_address, record):
126
+ record.update_from_ip_address(ip_address)
127
+
128
+ if record is not None:
129
+ record.clean()
130
+
131
+ zones = _zone.Zone.objects.filter(
132
+ pk__in=[zone.pk for zone in zones]
133
+ ).exclude(
134
+ pk__in=set(ip_address.netbox_dns_records.values_list("zone", flat=True))
135
+ )
136
+
137
+ for zone in zones:
138
+ record = _record.Record.create_from_ip_address(
139
+ ip_address,
140
+ zone,
141
+ )
142
+
143
+ if record is not None:
144
+ record.clean()
145
+
146
+ if ip_address.pk is None:
147
+ return
148
+
149
+ try:
150
+ new_zone = get_zones(ip_address, old_zone=zone)[0]
151
+ except IndexError:
152
+ return
153
+
154
+ for record in ip_address.netbox_dns_records.filter(zone=zone):
155
+ record.update_from_ip_address(ip_address, new_zone)
156
+
157
+ if record is not None:
158
+ record.clean(new_zone=new_zone)
159
+
160
+
161
+ def update_dns_records(ip_address):
162
+ if ip_address.dns_name == "":
163
+ delete_dns_records(ip_address)
164
+ return
165
+
166
+ zones = get_zones(ip_address)
167
+
168
+ if ip_address.pk is not None:
169
+ for record in ip_address.netbox_dns_records.all():
170
+ if record.zone not in zones or ip_address.custom_field_data.get(
171
+ "ipaddress_dns_disabled"
172
+ ):
173
+ record.delete()
174
+ continue
175
+
176
+ record.update_fqdn()
177
+ if not _match_data(ip_address, record):
178
+ record.update_from_ip_address(ip_address)
179
+
180
+ if record is not None:
181
+ record.save()
182
+
183
+ zones = _zone.Zone.objects.filter(pk__in=[zone.pk for zone in zones]).exclude(
184
+ pk__in=set(
185
+ ip_address.netbox_dns_records.all().values_list("zone", flat=True)
186
+ )
187
+ )
188
+
189
+ for zone in zones:
190
+ record = _record.Record.create_from_ip_address(
191
+ ip_address,
192
+ zone,
193
+ )
194
+
195
+ if record is not None:
196
+ record.save()
197
+
198
+
199
+ def delete_dns_records(ip_address):
200
+ for record in ip_address.netbox_dns_records.all():
201
+ record.delete()
202
+
203
+
204
+ def get_views_by_prefix(prefix):
205
+ if (views := prefix.netbox_dns_views.all()).exists():
206
+ return views
207
+
208
+ if (parent := prefix.get_parents().filter(netbox_dns_views__isnull=False)).exists():
209
+ return parent.last().netbox_dns_views.all()
210
+
211
+ return _view.View.objects.none()
212
+
213
+
214
+ def get_ip_addresses_by_prefix(prefix, check_view=True):
215
+ """
216
+ Find all IPAddress objects that are in a given prefix, provided that prefix
217
+ is assigned to NetBox DNS view. IPAddress objects belonging to a sub-prefix
218
+ that is assigned to a NetBox DNS view itself are excluded, because the zones
219
+ that are relevant for them are depending on the view set of the sub-prefix.
220
+
221
+ If neither the prefix nor any parent prefix is assigned to a view, the list
222
+ of IPAddress objects returned is empty.
223
+ """
224
+ if check_view and not get_views_by_prefix(prefix):
225
+ return IPAddress.objects.none()
226
+
227
+ queryset = IPAddress.objects.filter(
228
+ vrf=prefix.vrf, address__net_host_contained=prefix.prefix
229
+ )
230
+
231
+ for exclude_child in (
232
+ prefix.get_children().filter(netbox_dns_views__isnull=False).distinct()
233
+ ):
234
+ queryset = queryset.exclude(
235
+ vrf=exclude_child.vrf,
236
+ address__net_host_contained=exclude_child.prefix,
237
+ )
238
+
239
+ return queryset
240
+
241
+
242
+ def get_ip_addresses_by_view(view):
243
+ """
244
+ Find all IPAddress objects that are within prefixes that have a NetBox DNS
245
+ view assigned to them, or that inherit a view from their parent prefix.
246
+
247
+ Inheritance is defined recursively if the prefix is assigned to the view or
248
+ if it is a child prefix of the prefix that is not assigned to a view directly
249
+ or by inheritance.
250
+ """
251
+ queryset = IPAddress.objects.none()
252
+ for prefix in Prefix.objects.filter(netbox_dns_views__in=[view]):
253
+ sub_queryset = IPAddress.objects.filter(
254
+ vrf=prefix.vrf, address__net_host_contained=prefix.prefix
255
+ )
256
+ for exclude_child in prefix.get_children().exclude(
257
+ Q(netbox_dns_views__isnull=True) | Q(netbox_dns_views__in=[view])
258
+ ):
259
+ sub_queryset = sub_queryset.exclude(
260
+ vrf=exclude_child.vrf,
261
+ address__net_host_contained=exclude_child.prefix,
262
+ )
263
+ queryset |= sub_queryset
264
+
265
+ return queryset
266
+
267
+
268
+ def get_ip_addresses_by_zone(zone):
269
+ """
270
+ Find all IPAddress objects that are relevant for a NetBox DNS zone. These
271
+ are the IPAddress objects in prefixes assigned to the same view, if the
272
+ 'dns_name' attribute of the IPAddress object ends in the zone's name.
273
+ """
274
+ queryset = get_ip_addresses_by_view(zone.view).filter(
275
+ dns_name__regex=rf"\.{re.escape(zone.name)}\.?$"
276
+ )
277
+
278
+ return queryset
279
+
280
+
281
+ def check_record_permission(add=True, change=True, delete=True):
282
+ checks = locals().copy()
283
+
284
+ request = current_request.get()
285
+
286
+ if request is None:
287
+ return True
288
+
289
+ return all(
290
+ (
291
+ request.user.has_perm(f"netbox_dns.{perm}_record")
292
+ for perm, check in checks.items()
293
+ if check
294
+ )
295
+ )
@@ -7,6 +7,7 @@ from netbox.plugins.utils import get_plugin_config
7
7
 
8
8
  __all__ = (
9
9
  "validate_fqdn",
10
+ "validate_rname",
10
11
  "validate_generic_name",
11
12
  "validate_domain_name",
12
13
  )
@@ -57,6 +58,14 @@ def validate_fqdn(name, always_tolerant=False):
57
58
  raise ValidationError(f"{name} is not a valid fully qualified DNS host name")
58
59
 
59
60
 
61
+ def validate_rname(name, always_tolerant=False):
62
+ label, zone_label = _get_label(always_tolerant=always_tolerant)
63
+ regex = rf"^(\*|{label})(\\\.{label})*(\.{zone_label}){{2,}}\.?$"
64
+
65
+ if not re.match(regex, name, flags=re.IGNORECASE) or _has_invalid_double_dash(name):
66
+ raise ValidationError(f"{name} is not a valid RNAME")
67
+
68
+
60
69
  def validate_generic_name(
61
70
  name, tolerate_leading_underscores=False, always_tolerant=False
62
71
  ):
@@ -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 *
@@ -2,6 +2,7 @@ from dns import name as dns_name
2
2
 
3
3
  from netbox.views import generic
4
4
  from utilities.views import ViewTab, register_model_view
5
+ from tenancy.views import ObjectContactsView
5
6
 
6
7
  from netbox_dns.filtersets import NameServerFilterSet, ZoneFilterSet
7
8
  from netbox_dns.forms import (
@@ -15,15 +16,13 @@ from netbox_dns.tables import NameServerTable, ZoneTable
15
16
 
16
17
 
17
18
  __all__ = (
18
- "NameServerListView",
19
19
  "NameServerView",
20
+ "NameServerListView",
20
21
  "NameServerEditView",
21
22
  "NameServerDeleteView",
22
23
  "NameServerBulkEditView",
23
24
  "NameServerBulkImportView",
24
25
  "NameServerBulkDeleteView",
25
- "NameServerZoneListView",
26
- "NameServerSOAZoneListView",
27
26
  )
28
27
 
29
28
 
@@ -77,6 +76,11 @@ class NameServerBulkDeleteView(generic.BulkDeleteView):
77
76
  table = NameServerTable
78
77
 
79
78
 
79
+ @register_model_view(NameServer, "contacts")
80
+ class NameServerContactsView(ObjectContactsView):
81
+ queryset = NameServer.objects.all()
82
+
83
+
80
84
  @register_model_view(NameServer, "zones")
81
85
  class NameServerZoneListView(generic.ObjectChildrenView):
82
86
  queryset = NameServer.objects.all().prefetch_related("zones")