netbox-plugin-dns 0.21.4__py3-none-any.whl → 1.4.7__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.
Files changed (232) hide show
  1. netbox_dns/__init__.py +106 -41
  2. netbox_dns/api/field_serializers.py +25 -0
  3. netbox_dns/api/nested_serializers.py +95 -52
  4. netbox_dns/api/serializers.py +14 -296
  5. netbox_dns/api/serializers_/__init__.py +0 -0
  6. netbox_dns/api/serializers_/dnssec_key_template.py +69 -0
  7. netbox_dns/api/serializers_/dnssec_policy.py +165 -0
  8. netbox_dns/api/serializers_/nameserver.py +56 -0
  9. netbox_dns/api/serializers_/prefix.py +18 -0
  10. netbox_dns/api/serializers_/record.py +105 -0
  11. netbox_dns/api/serializers_/record_template.py +71 -0
  12. netbox_dns/api/serializers_/registrar.py +45 -0
  13. netbox_dns/api/serializers_/registration_contact.py +50 -0
  14. netbox_dns/api/serializers_/view.py +81 -0
  15. netbox_dns/api/serializers_/zone.py +247 -0
  16. netbox_dns/api/serializers_/zone_template.py +157 -0
  17. netbox_dns/api/urls.py +13 -2
  18. netbox_dns/api/views.py +96 -58
  19. netbox_dns/choices/__init__.py +4 -0
  20. netbox_dns/choices/dnssec_key_template.py +67 -0
  21. netbox_dns/choices/dnssec_policy.py +40 -0
  22. netbox_dns/choices/record.py +104 -0
  23. netbox_dns/choices/utilities.py +4 -0
  24. netbox_dns/choices/zone.py +119 -0
  25. netbox_dns/fields/__init__.py +4 -0
  26. netbox_dns/fields/address.py +22 -16
  27. netbox_dns/fields/choice_array.py +33 -0
  28. netbox_dns/fields/ipam.py +15 -0
  29. netbox_dns/fields/network.py +42 -18
  30. netbox_dns/fields/rfc2317.py +97 -0
  31. netbox_dns/fields/timeperiod.py +33 -0
  32. netbox_dns/filters.py +7 -0
  33. netbox_dns/filtersets/__init__.py +12 -0
  34. netbox_dns/filtersets/dnssec_key_template.py +57 -0
  35. netbox_dns/filtersets/dnssec_policy.py +101 -0
  36. netbox_dns/filtersets/nameserver.py +46 -0
  37. netbox_dns/filtersets/record.py +135 -0
  38. netbox_dns/filtersets/record_template.py +59 -0
  39. netbox_dns/{filters → filtersets}/registrar.py +8 -1
  40. netbox_dns/{filters/contact.py → filtersets/registration_contact.py} +9 -3
  41. netbox_dns/filtersets/view.py +45 -0
  42. netbox_dns/filtersets/zone.py +254 -0
  43. netbox_dns/filtersets/zone_template.py +165 -0
  44. netbox_dns/forms/__init__.py +5 -1
  45. netbox_dns/forms/dnssec_key_template.py +250 -0
  46. netbox_dns/forms/dnssec_policy.py +654 -0
  47. netbox_dns/forms/nameserver.py +121 -27
  48. netbox_dns/forms/record.py +215 -104
  49. netbox_dns/forms/record_template.py +285 -0
  50. netbox_dns/forms/registrar.py +108 -31
  51. netbox_dns/forms/registration_contact.py +282 -0
  52. netbox_dns/forms/view.py +331 -20
  53. netbox_dns/forms/zone.py +769 -373
  54. netbox_dns/forms/zone_template.py +463 -0
  55. netbox_dns/graphql/__init__.py +25 -22
  56. netbox_dns/graphql/enums.py +41 -0
  57. netbox_dns/graphql/filter_lookups.py +13 -0
  58. netbox_dns/graphql/filters/__init__.py +12 -0
  59. netbox_dns/graphql/filters/dnssec_key_template.py +63 -0
  60. netbox_dns/graphql/filters/dnssec_policy.py +124 -0
  61. netbox_dns/graphql/filters/nameserver.py +32 -0
  62. netbox_dns/graphql/filters/record.py +89 -0
  63. netbox_dns/graphql/filters/record_template.py +55 -0
  64. netbox_dns/graphql/filters/registrar.py +30 -0
  65. netbox_dns/graphql/filters/registration_contact.py +27 -0
  66. netbox_dns/graphql/filters/view.py +28 -0
  67. netbox_dns/graphql/filters/zone.py +147 -0
  68. netbox_dns/graphql/filters/zone_template.py +97 -0
  69. netbox_dns/graphql/schema.py +89 -7
  70. netbox_dns/graphql/types.py +355 -0
  71. netbox_dns/locale/de/LC_MESSAGES/django.mo +0 -0
  72. netbox_dns/locale/en/LC_MESSAGES/django.mo +0 -0
  73. netbox_dns/locale/fr/LC_MESSAGES/django.mo +0 -0
  74. netbox_dns/management/commands/cleanup_database.py +175 -156
  75. netbox_dns/management/commands/cleanup_rrset_ttl.py +64 -0
  76. netbox_dns/management/commands/rebuild_dnssync.py +23 -0
  77. netbox_dns/management/commands/setup_dnssync.py +140 -0
  78. netbox_dns/migrations/0001_squashed_netbox_dns_0_15.py +0 -27
  79. netbox_dns/migrations/0001_squashed_netbox_dns_0_22.py +557 -0
  80. netbox_dns/migrations/{0013_add_nameserver_zone_record_description.py → 0002_contact_description_registrar_description.py} +4 -9
  81. netbox_dns/migrations/0003_default_view.py +15 -0
  82. netbox_dns/migrations/0004_create_and_assign_default_view.py +26 -0
  83. netbox_dns/migrations/0005_alter_zone_view_not_null.py +18 -0
  84. netbox_dns/migrations/0006_templating.py +172 -0
  85. netbox_dns/migrations/0007_alter_ordering_options.py +25 -0
  86. netbox_dns/migrations/0008_view_prefixes.py +18 -0
  87. netbox_dns/migrations/0009_rename_contact_registrationcontact.py +36 -0
  88. netbox_dns/migrations/0010_view_ip_address_filter.py +18 -0
  89. netbox_dns/migrations/0011_rename_related_fields.py +63 -0
  90. netbox_dns/migrations/0012_natural_ordering.py +88 -0
  91. netbox_dns/migrations/0013_zonetemplate_soa_mname_zonetemplate_soa_rname.py +30 -0
  92. netbox_dns/migrations/0014_alter_unique_constraints_lowercase.py +42 -0
  93. netbox_dns/migrations/0015_dnssec.py +168 -0
  94. netbox_dns/migrations/{0015_add_record_status.py → 0016_dnssec_policy_status.py} +5 -4
  95. netbox_dns/migrations/0017_dnssec_policy_zone_zone_template.py +41 -0
  96. netbox_dns/migrations/0018_zone_domain_status_zone_expiration_date.py +23 -0
  97. netbox_dns/migrations/0019_dnssecpolicy_parental_agents.py +25 -0
  98. netbox_dns/migrations/0020_netbox_3_4.py +1 -1
  99. netbox_dns/migrations/0020_remove_dnssecpolicy_parental_agents_and_more.py +29 -0
  100. netbox_dns/migrations/0021_alter_record_ptr_record.py +25 -0
  101. netbox_dns/migrations/0021_record_ip_address.py +1 -1
  102. netbox_dns/migrations/0022_alter_record_ipam_ip_address.py +26 -0
  103. netbox_dns/migrations/0023_disable_ptr_false.py +27 -0
  104. netbox_dns/migrations/0024_zonetemplate_parental_agents.py +25 -0
  105. netbox_dns/migrations/0025_remove_zone_inline_signing_and_more.py +22 -0
  106. netbox_dns/migrations/0026_alter_dnssecpolicy_nsec3_opt_out.py +18 -0
  107. netbox_dns/migrations/0026_domain_registration.py +1 -1
  108. netbox_dns/migrations/0027_zone_comments.py +18 -0
  109. netbox_dns/migrations/0028_alter_zone_default_ttl_alter_zone_soa_minimum_and_more.py +54 -0
  110. netbox_dns/migrations/0028_rfc2317_fields.py +44 -0
  111. netbox_dns/migrations/0029_alter_registrationcontact_street.py +18 -0
  112. netbox_dns/migrations/0029_record_fqdn.py +30 -0
  113. netbox_dns/mixins/__init__.py +1 -0
  114. netbox_dns/mixins/object_modification.py +57 -0
  115. netbox_dns/models/__init__.py +5 -1
  116. netbox_dns/models/dnssec_key_template.py +114 -0
  117. netbox_dns/models/dnssec_policy.py +203 -0
  118. netbox_dns/models/nameserver.py +61 -30
  119. netbox_dns/models/record.py +781 -234
  120. netbox_dns/models/record_template.py +198 -0
  121. netbox_dns/models/registrar.py +34 -15
  122. netbox_dns/models/{contact.py → registration_contact.py} +72 -43
  123. netbox_dns/models/view.py +129 -9
  124. netbox_dns/models/zone.py +806 -242
  125. netbox_dns/models/zone_template.py +209 -0
  126. netbox_dns/navigation.py +176 -76
  127. netbox_dns/signals/__init__.py +0 -0
  128. netbox_dns/signals/dnssec.py +32 -0
  129. netbox_dns/signals/ipam_dnssync.py +216 -0
  130. netbox_dns/tables/__init__.py +5 -1
  131. netbox_dns/tables/dnssec_key_template.py +49 -0
  132. netbox_dns/tables/dnssec_policy.py +140 -0
  133. netbox_dns/tables/ipam_dnssync.py +12 -0
  134. netbox_dns/tables/nameserver.py +14 -17
  135. netbox_dns/tables/record.py +117 -59
  136. netbox_dns/tables/record_template.py +91 -0
  137. netbox_dns/tables/registrar.py +20 -10
  138. netbox_dns/tables/{contact.py → registration_contact.py} +22 -11
  139. netbox_dns/tables/view.py +47 -3
  140. netbox_dns/tables/zone.py +62 -31
  141. netbox_dns/tables/zone_template.py +78 -0
  142. netbox_dns/template_content.py +124 -38
  143. netbox_dns/templates/netbox_dns/dnsseckeytemplate.html +70 -0
  144. netbox_dns/templates/netbox_dns/dnssecpolicy.html +163 -0
  145. netbox_dns/templates/netbox_dns/nameserver.html +31 -28
  146. netbox_dns/templates/netbox_dns/record/managed.html +2 -1
  147. netbox_dns/templates/netbox_dns/record/related.html +17 -6
  148. netbox_dns/templates/netbox_dns/record.html +140 -93
  149. netbox_dns/templates/netbox_dns/recordtemplate.html +96 -0
  150. netbox_dns/templates/netbox_dns/registrar.html +41 -34
  151. netbox_dns/templates/netbox_dns/registrationcontact.html +76 -0
  152. netbox_dns/templates/netbox_dns/view/button.html +10 -0
  153. netbox_dns/templates/netbox_dns/view/prefix.html +44 -0
  154. netbox_dns/templates/netbox_dns/view/related.html +33 -0
  155. netbox_dns/templates/netbox_dns/view.html +62 -18
  156. netbox_dns/templates/netbox_dns/zone/base.html +6 -3
  157. netbox_dns/templates/netbox_dns/zone/child.html +6 -5
  158. netbox_dns/templates/netbox_dns/zone/child_zone.html +18 -0
  159. netbox_dns/templates/netbox_dns/zone/delegation_record.html +18 -0
  160. netbox_dns/templates/netbox_dns/zone/managed_record.html +1 -1
  161. netbox_dns/templates/netbox_dns/zone/record.html +6 -5
  162. netbox_dns/templates/netbox_dns/zone/registration.html +43 -24
  163. netbox_dns/templates/netbox_dns/zone/rfc2317_child_zone.html +18 -0
  164. netbox_dns/templates/netbox_dns/zone.html +178 -119
  165. netbox_dns/templates/netbox_dns/zonetemplate/child.html +46 -0
  166. netbox_dns/templates/netbox_dns/zonetemplate.html +124 -0
  167. netbox_dns/templatetags/netbox_dns.py +10 -0
  168. netbox_dns/urls.py +50 -210
  169. netbox_dns/utilities/__init__.py +3 -0
  170. netbox_dns/{utilities.py → utilities/conversions.py} +55 -7
  171. netbox_dns/utilities/dns.py +11 -0
  172. netbox_dns/utilities/ipam_dnssync.py +370 -0
  173. netbox_dns/validators/__init__.py +4 -0
  174. netbox_dns/validators/dns_name.py +116 -0
  175. netbox_dns/validators/dns_value.py +147 -0
  176. netbox_dns/validators/dnssec.py +148 -0
  177. netbox_dns/validators/rfc2317.py +28 -0
  178. netbox_dns/views/__init__.py +5 -1
  179. netbox_dns/views/dnssec_key_template.py +78 -0
  180. netbox_dns/views/dnssec_policy.py +146 -0
  181. netbox_dns/views/nameserver.py +34 -15
  182. netbox_dns/views/record.py +156 -15
  183. netbox_dns/views/record_template.py +93 -0
  184. netbox_dns/views/registrar.py +32 -13
  185. netbox_dns/views/registration_contact.py +101 -0
  186. netbox_dns/views/view.py +58 -14
  187. netbox_dns/views/zone.py +130 -33
  188. netbox_dns/views/zone_template.py +82 -0
  189. netbox_plugin_dns-1.4.7.dist-info/METADATA +132 -0
  190. netbox_plugin_dns-1.4.7.dist-info/RECORD +201 -0
  191. {netbox_plugin_dns-0.21.4.dist-info → netbox_plugin_dns-1.4.7.dist-info}/WHEEL +2 -1
  192. {netbox_plugin_dns-0.21.4.dist-info → netbox_plugin_dns-1.4.7.dist-info/licenses}/LICENSE +2 -1
  193. netbox_plugin_dns-1.4.7.dist-info/top_level.txt +1 -0
  194. netbox_dns/filters/__init__.py +0 -6
  195. netbox_dns/filters/nameserver.py +0 -18
  196. netbox_dns/filters/record.py +0 -53
  197. netbox_dns/filters/view.py +0 -18
  198. netbox_dns/filters/zone.py +0 -112
  199. netbox_dns/forms/contact.py +0 -211
  200. netbox_dns/graphql/contact.py +0 -19
  201. netbox_dns/graphql/nameserver.py +0 -19
  202. netbox_dns/graphql/record.py +0 -19
  203. netbox_dns/graphql/registrar.py +0 -19
  204. netbox_dns/graphql/view.py +0 -19
  205. netbox_dns/graphql/zone.py +0 -19
  206. netbox_dns/management/commands/setup_coupling.py +0 -75
  207. netbox_dns/management/commands/update_soa.py +0 -22
  208. netbox_dns/middleware.py +0 -226
  209. netbox_dns/migrations/0001_initial.py +0 -115
  210. netbox_dns/migrations/0002_zone_default_ttl.py +0 -18
  211. netbox_dns/migrations/0003_soa_managed_records.py +0 -112
  212. netbox_dns/migrations/0004_create_ptr_for_a_aaaa_records.py +0 -80
  213. netbox_dns/migrations/0005_update_ns_records.py +0 -41
  214. netbox_dns/migrations/0006_zone_soa_serial_auto.py +0 -29
  215. netbox_dns/migrations/0007_alter_zone_soa_serial_auto.py +0 -17
  216. netbox_dns/migrations/0008_zone_status_names.py +0 -21
  217. netbox_dns/migrations/0009_netbox32.py +0 -71
  218. netbox_dns/migrations/0010_update_soa_records.py +0 -58
  219. netbox_dns/migrations/0011_add_view_model.py +0 -70
  220. netbox_dns/migrations/0012_adjust_zone_and_record.py +0 -17
  221. netbox_dns/migrations/0014_add_view_description.py +0 -16
  222. netbox_dns/migrations/0016_cleanup_ptr_records.py +0 -38
  223. netbox_dns/migrations/0017_alter_record_ttl.py +0 -17
  224. netbox_dns/migrations/0018_zone_arpa_network.py +0 -51
  225. netbox_dns/migrations/0019_update_ns_ttl.py +0 -19
  226. netbox_dns/templates/netbox_dns/contact.html +0 -71
  227. netbox_dns/templates/netbox_dns/related_dns_objects.html +0 -21
  228. netbox_dns/templatetags/view_helpers.py +0 -15
  229. netbox_dns/validators.py +0 -57
  230. netbox_dns/views/contact.py +0 -83
  231. netbox_plugin_dns-0.21.4.dist-info/METADATA +0 -101
  232. netbox_plugin_dns-0.21.4.dist-info/RECORD +0 -110
@@ -0,0 +1,370 @@
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.choices import RecordStatusChoices, RecordTypeChoices
14
+
15
+ from .dns import get_parent_zone_names
16
+ from .conversions import regex_from_list
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
+ "get_query_from_filter",
30
+ "check_filter",
31
+ )
32
+
33
+
34
+ def _get_assigned_views(ip_address):
35
+ longest_prefix = Prefix.objects.filter(
36
+ vrf=ip_address.vrf,
37
+ prefix__net_contains_or_equals=str(ip_address.address.ip),
38
+ netbox_dns_views__isnull=False,
39
+ ).last()
40
+
41
+ if longest_prefix is None:
42
+ return []
43
+
44
+ return longest_prefix.netbox_dns_views.all()
45
+
46
+
47
+ def _get_record_status(ip_address):
48
+ return (
49
+ RecordStatusChoices.STATUS_ACTIVE
50
+ if ip_address.status
51
+ in settings.PLUGINS_CONFIG["netbox_dns"].get(
52
+ "dnssync_ipaddress_active_status", []
53
+ )
54
+ else RecordStatusChoices.STATUS_INACTIVE
55
+ )
56
+
57
+
58
+ def _valid_entry(ip_address, zone):
59
+ return zone.view in _get_assigned_views(ip_address) and dns_name.from_text(
60
+ ip_address.dns_name
61
+ ).is_subdomain(dns_name.from_text(zone.name))
62
+
63
+
64
+ def _match_data(ip_address, record):
65
+ cf_disable_ptr = ip_address.custom_field_data.get(
66
+ "ipaddress_dns_record_disable_ptr"
67
+ )
68
+
69
+ return (
70
+ record.fqdn.rstrip(".") == ip_address.dns_name.rstrip(".")
71
+ and record.value == str(ip_address.address.ip)
72
+ and record.status == _get_record_status(ip_address)
73
+ and record.ttl == ip_address.custom_field_data.get("ipaddress_dns_record_ttl")
74
+ and (cf_disable_ptr is None or record.disable_ptr == cf_disable_ptr)
75
+ )
76
+
77
+
78
+ def get_zones(ip_address, view=None, old_zone=None):
79
+ from netbox_dns.models import Zone
80
+
81
+ if view is None:
82
+ views = _get_assigned_views(ip_address)
83
+ if not views:
84
+ return []
85
+
86
+ else:
87
+ views = [view]
88
+
89
+ min_labels = settings.PLUGINS_CONFIG["netbox_dns"].get(
90
+ "dnssync_minimum_zone_labels", 2
91
+ )
92
+
93
+ zones = Zone.objects.filter(
94
+ view__in=views,
95
+ name__iregex=regex_from_list(
96
+ get_parent_zone_names(
97
+ ip_address.dns_name, min_labels=min_labels, include_self=True
98
+ )
99
+ ),
100
+ active=True,
101
+ )
102
+
103
+ zone_map = defaultdict(list)
104
+
105
+ if old_zone is not None:
106
+ zones = zones.exclude(pk=old_zone.pk)
107
+ if _valid_entry(ip_address, old_zone):
108
+ zone_map[old_zone.view].append(old_zone)
109
+
110
+ for zone in zones:
111
+ zone_map[zone.view].append(zone)
112
+
113
+ return [
114
+ sorted(zones_per_view, key=lambda x: len(x.name))[-1]
115
+ for zones_per_view in zone_map.values()
116
+ ]
117
+
118
+
119
+ def check_dns_records(ip_address, zone=None, view=None):
120
+ from netbox_dns.models import Zone, Record
121
+
122
+ if ip_address.dns_name == "":
123
+ return
124
+
125
+ if zone is None:
126
+ zones = get_zones(ip_address, view=view)
127
+
128
+ if not ip_address._state.adding:
129
+ for record in ip_address.netbox_dns_records.filter(zone__in=zones):
130
+ if not _match_data(ip_address, record):
131
+ updated = record.update_from_ip_address(ip_address)
132
+
133
+ if updated:
134
+ record.clean()
135
+
136
+ zones = Zone.objects.filter(pk__in=[zone.pk for zone in zones]).exclude(
137
+ pk__in=set(ip_address.netbox_dns_records.values_list("zone", flat=True))
138
+ )
139
+
140
+ for zone in zones:
141
+ record = Record.create_from_ip_address(
142
+ ip_address,
143
+ zone,
144
+ )
145
+
146
+ if record is not None:
147
+ record.clean()
148
+
149
+ if ip_address._state.adding:
150
+ return
151
+
152
+ try:
153
+ new_zone = get_zones(ip_address, old_zone=zone)[0]
154
+ except IndexError:
155
+ return
156
+
157
+ for record in ip_address.netbox_dns_records.filter(zone=zone):
158
+ updated = record.update_from_ip_address(ip_address, new_zone)
159
+
160
+ if updated:
161
+ record.clean(new_zone=new_zone)
162
+
163
+
164
+ def update_dns_records(ip_address, view=None, force=False):
165
+ from netbox_dns.models import Zone, Record
166
+
167
+ updated = False
168
+
169
+ if ip_address.dns_name == "":
170
+ return delete_dns_records(ip_address)
171
+
172
+ zones = get_zones(ip_address, view=view)
173
+
174
+ if not ip_address._state.adding:
175
+ if view is None:
176
+ address_records = ip_address.netbox_dns_records.all()
177
+ else:
178
+ address_records = ip_address.netbox_dns_records.filter(zone__view=view)
179
+
180
+ for record in address_records:
181
+ record.snapshot()
182
+
183
+ if record.zone not in zones or ip_address.custom_field_data.get(
184
+ "ipaddress_dns_disabled"
185
+ ):
186
+ record.delete()
187
+ updated = True
188
+ continue
189
+
190
+ record.update_fqdn()
191
+ updated, deleted = record.update_from_ip_address(ip_address)
192
+
193
+ if deleted:
194
+ record.delete()
195
+ updated = True
196
+ elif updated:
197
+ record.save()
198
+ updated = True
199
+
200
+ zones = Zone.objects.filter(pk__in=[zone.pk for zone in zones]).exclude(
201
+ pk__in=set(ip_address.netbox_dns_records.values_list("zone", flat=True))
202
+ )
203
+
204
+ for zone in zones:
205
+ record = Record.create_from_ip_address(
206
+ ip_address,
207
+ zone,
208
+ )
209
+
210
+ if record is not None:
211
+ record.save()
212
+ updated = True
213
+
214
+ return updated
215
+
216
+
217
+ def delete_dns_records(ip_address, view=None):
218
+ from netbox_dns.models import Record
219
+
220
+ if current_request.get() is None:
221
+ address_records = ip_address.netbox_dns_records.all()
222
+ else:
223
+ # +
224
+ # This is a dirty hack made necessary by NetBox grand idea of manipulating
225
+ # objects in its event handling code, removing references to related objects
226
+ # in pre_delete() before our pre_delete() handler has the chance to handle
227
+ # them.
228
+ #
229
+ # TODO: Find something better. This is really awful.
230
+ # -
231
+ address_records = Record.objects.filter(
232
+ Q(
233
+ Q(ipam_ip_address=ip_address)
234
+ | Q(
235
+ type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
236
+ managed=True,
237
+ ip_address=ip_address.address,
238
+ ipam_ip_address__isnull=True,
239
+ )
240
+ ),
241
+ )
242
+
243
+ if view is not None:
244
+ address_records &= Record.objects.filter(zone__view=view)
245
+
246
+ if not address_records.exists():
247
+ return False
248
+
249
+ for record in address_records:
250
+ record.delete()
251
+
252
+ return True
253
+
254
+
255
+ def get_views_by_prefix(prefix):
256
+ from netbox_dns.models import View
257
+
258
+ if (views := prefix.netbox_dns_views.all()).exists():
259
+ return views
260
+
261
+ if (parent := prefix.get_parents().filter(netbox_dns_views__isnull=False)).exists():
262
+ return parent.last().netbox_dns_views.all()
263
+
264
+ return View.objects.none()
265
+
266
+
267
+ def get_ip_addresses_by_prefix(prefix, check_view=True):
268
+ """
269
+ Find all IPAddress objects that are in a given prefix, provided that prefix
270
+ is assigned to NetBox DNS view. IPAddress objects belonging to a sub-prefix
271
+ that is assigned to a NetBox DNS view itself are excluded, because the zones
272
+ that are relevant for them are depending on the view set of the sub-prefix.
273
+
274
+ If neither the prefix nor any parent prefix is assigned to a view, the list
275
+ of IPAddress objects returned is empty.
276
+ """
277
+ if check_view and not get_views_by_prefix(prefix):
278
+ return IPAddress.objects.none()
279
+
280
+ queryset = IPAddress.objects.filter(
281
+ vrf=prefix.vrf, address__net_host_contained=prefix.prefix
282
+ )
283
+
284
+ for exclude_child in (
285
+ prefix.get_children().filter(netbox_dns_views__isnull=False).distinct()
286
+ ):
287
+ queryset = queryset.exclude(
288
+ vrf=exclude_child.vrf,
289
+ address__net_host_contained=exclude_child.prefix,
290
+ )
291
+
292
+ return queryset
293
+
294
+
295
+ def get_ip_addresses_by_view(view):
296
+ """
297
+ Find all IPAddress objects that are within prefixes that have a NetBox DNS
298
+ view assigned to them, or that inherit a view from their parent prefix.
299
+
300
+ Inheritance is defined recursively if the prefix is assigned to the view or
301
+ if it is a child prefix of the prefix that is not assigned to a view directly
302
+ or by inheritance.
303
+ """
304
+ queryset = IPAddress.objects.none()
305
+ for prefix in Prefix.objects.filter(netbox_dns_views__in=[view]):
306
+ sub_queryset = IPAddress.objects.filter(
307
+ vrf=prefix.vrf, address__net_host_contained=prefix.prefix
308
+ )
309
+ for exclude_child in prefix.get_children().exclude(
310
+ Q(netbox_dns_views__isnull=True) | Q(netbox_dns_views__in=[view])
311
+ ):
312
+ sub_queryset = sub_queryset.exclude(
313
+ vrf=exclude_child.vrf,
314
+ address__net_host_contained=exclude_child.prefix,
315
+ )
316
+ queryset |= sub_queryset
317
+
318
+ return queryset
319
+
320
+
321
+ def get_ip_addresses_by_zone(zone):
322
+ """
323
+ Find all IPAddress objects that are relevant for a NetBox DNS zone. These
324
+ are the IPAddress objects in prefixes assigned to the same view, if the
325
+ 'dns_name' attribute of the IPAddress object ends in the zone's name.
326
+ """
327
+ queryset = get_ip_addresses_by_view(zone.view).filter(
328
+ dns_name__regex=rf"\.{re.escape(zone.name)}\.?$"
329
+ )
330
+
331
+ return queryset
332
+
333
+
334
+ def check_record_permission(add=True, change=True, delete=True):
335
+ checks = locals().copy()
336
+
337
+ request = current_request.get()
338
+
339
+ if request is None:
340
+ return True
341
+
342
+ return all(
343
+ (
344
+ request.user.has_perm(f"netbox_dns.{perm}_record")
345
+ for perm, check in checks.items()
346
+ if check
347
+ )
348
+ )
349
+
350
+
351
+ def get_query_from_filter(ip_address_filter):
352
+ query = Q()
353
+
354
+ if not isinstance(ip_address_filter, list):
355
+ ip_address_filter = [ip_address_filter]
356
+
357
+ for condition in ip_address_filter:
358
+ if condition:
359
+ query |= Q(**condition)
360
+ else:
361
+ return Q()
362
+
363
+ return query
364
+
365
+
366
+ def check_filter(ip_address, ip_address_filter):
367
+ query = get_query_from_filter(ip_address_filter)
368
+ against = ip_address._get_field_expression_map(meta=IPAddress._meta)
369
+
370
+ return query.check(against)
@@ -0,0 +1,4 @@
1
+ from .dns_name import *
2
+ from .dns_value import *
3
+ from .rfc2317 import *
4
+ from .dnssec import *
@@ -0,0 +1,116 @@
1
+ import re
2
+ from socket import inet_aton
3
+
4
+ from django.core.exceptions import ValidationError
5
+ from django.utils.translation import gettext as _
6
+
7
+ from netbox.plugins.utils import get_plugin_config
8
+
9
+
10
+ __all__ = (
11
+ "validate_fqdn",
12
+ "validate_rname",
13
+ "validate_generic_name",
14
+ "validate_domain_name",
15
+ )
16
+
17
+
18
+ def _get_label(tolerate_leading_underscores=False, always_tolerant=False):
19
+ tolerate_characters = re.escape(
20
+ get_plugin_config("netbox_dns", "tolerate_characters_in_zone_labels", "")
21
+ )
22
+ label_characters = rf"a-z0-9{tolerate_characters}"
23
+
24
+ if always_tolerant:
25
+ label = r"[a-z0-9_][a-z0-9_-]*(?<![-_])"
26
+ zone_label = rf"[{label_characters}_][{label_characters}_-]*(?<![-_])"
27
+
28
+ return label, zone_label
29
+
30
+ tolerate_underscores = get_plugin_config(
31
+ "netbox_dns", "tolerate_underscores_in_labels"
32
+ )
33
+
34
+ if tolerate_leading_underscores:
35
+ if tolerate_underscores:
36
+ label = r"[a-z0-9_][a-z0-9_-]*(?<![-_])"
37
+ zone_label = rf"[{label_characters}_][{label_characters}_-]*(?<![-_])"
38
+ else:
39
+ label = r"[a-z0-9_][a-z0-9-]*(?<!-)"
40
+ zone_label = rf"[{label_characters}_][{label_characters}-]*(?<!-)"
41
+ elif tolerate_underscores:
42
+ label = r"[a-z0-9][a-z0-9_-]*(?<![-_])"
43
+ zone_label = rf"[{label_characters}][{label_characters}_-]*(?<![-_])"
44
+ else:
45
+ label = r"[a-z0-9][a-z0-9-]*(?<!-)"
46
+ zone_label = rf"[{label_characters}][{label_characters}-]*(?<!-)"
47
+
48
+ return label, zone_label
49
+
50
+
51
+ def _has_invalid_double_dash(name):
52
+ return bool(re.findall(r"(^|\.)(?!xn)..--", name, re.IGNORECASE))
53
+
54
+
55
+ def validate_fqdn(name, always_tolerant=False):
56
+ label, zone_label = _get_label(always_tolerant=always_tolerant)
57
+ regex = rf"^(\*|{label})(\.{zone_label})+\.?$"
58
+
59
+ if not re.match(regex, name, flags=re.IGNORECASE) or _has_invalid_double_dash(name):
60
+ raise ValidationError(
61
+ _("{name} is not a valid fully qualified DNS host name").format(name=name)
62
+ )
63
+
64
+
65
+ def validate_rname(name, always_tolerant=False):
66
+ label, zone_label = _get_label(always_tolerant=always_tolerant)
67
+ regex = rf"^(\*|{label})(\\\.{label})*(\.{zone_label}){{2,}}\.?$"
68
+
69
+ if not re.match(regex, name, flags=re.IGNORECASE) or _has_invalid_double_dash(name):
70
+ raise ValidationError(_("{name} is not a valid RName").format(name=name))
71
+
72
+
73
+ def validate_generic_name(
74
+ name, tolerate_leading_underscores=False, always_tolerant=False
75
+ ):
76
+ label, zone_label = _get_label(
77
+ tolerate_leading_underscores=tolerate_leading_underscores,
78
+ always_tolerant=always_tolerant,
79
+ )
80
+ regex = rf"^([*@]|(\*\.)?{label}(\.{zone_label})*\.?)$"
81
+
82
+ if not re.match(regex, name, flags=re.IGNORECASE) or _has_invalid_double_dash(name):
83
+ raise ValidationError(
84
+ _("{name} is not a valid DNS host name").format(name=name)
85
+ )
86
+
87
+
88
+ def validate_domain_name(
89
+ name, always_tolerant=False, allow_empty_label=False, zone_name=False
90
+ ):
91
+ if name == "@" and allow_empty_label:
92
+ return
93
+
94
+ if name == "." and (
95
+ always_tolerant or get_plugin_config("netbox_dns", "enable_root_zones")
96
+ ):
97
+ return
98
+
99
+ try:
100
+ inet_aton(name)
101
+ raise ValidationError(
102
+ _("{name} is not a valid DNS domain name").format(name=name)
103
+ )
104
+ except OSError:
105
+ pass
106
+
107
+ label, zone_label = _get_label(always_tolerant=always_tolerant)
108
+ if zone_name:
109
+ regex = rf"^{zone_label}(\.{zone_label})*\.?$"
110
+ else:
111
+ regex = rf"^{label}(\.{zone_label})*\.?$"
112
+
113
+ if not re.match(regex, name, flags=re.IGNORECASE) or _has_invalid_double_dash(name):
114
+ raise ValidationError(
115
+ _("{name} is not a valid DNS domain name").format(name=name)
116
+ )
@@ -0,0 +1,147 @@
1
+ import re
2
+ import textwrap
3
+
4
+ from dns import rdata, name as dns_name
5
+ from dns.exception import SyntaxError
6
+
7
+ from django.core.exceptions import ValidationError
8
+ from django.utils.translation import gettext as _
9
+
10
+ from netbox.plugins.utils import get_plugin_config
11
+
12
+ from netbox_dns.choices import RecordClassChoices, RecordTypeChoices
13
+ from netbox_dns.validators import (
14
+ validate_fqdn,
15
+ validate_domain_name,
16
+ validate_generic_name,
17
+ )
18
+
19
+ MAX_TXT_LENGTH = 255
20
+
21
+ __all__ = ("validate_record_value",)
22
+
23
+
24
+ def validate_record_value(record):
25
+ def _validate_idn(name):
26
+ try:
27
+ name.to_unicode()
28
+ except dns_name.IDNAException as exc:
29
+ raise ValidationError(
30
+ "{name} is not a valid IDN: {error}.".format(
31
+ name=name.to_text(), error=exc
32
+ )
33
+ )
34
+
35
+ def _split_text_value(value):
36
+ # +
37
+ # Text values longer than 255 characters need to be broken up for TXT and
38
+ # SPF records.
39
+ # First, in case they had been split into separate strings, reassemble the
40
+ # original (long) value, then split it into chunks of a maximum length of
41
+ # 255 (preferably at word boundaries), and then build a sequence of partial
42
+ # strings enclosed in double quotes and separated by space.
43
+ #
44
+ # See https://datatracker.ietf.org/doc/html/rfc4408#section-3.1.3 for details.
45
+ # -
46
+ raw_value = "".join(re.findall(r'"([^"]+)"', value))
47
+ if not raw_value:
48
+ raw_value = value
49
+
50
+ return " ".join(
51
+ f'"{part}"'
52
+ for part in textwrap.wrap(raw_value, MAX_TXT_LENGTH, drop_whitespace=False)
53
+ )
54
+
55
+ if record.type in (RecordTypeChoices.CUSTOM_TYPES):
56
+ return
57
+
58
+ if record.type in (RecordTypeChoices.TXT, RecordTypeChoices.SPF):
59
+ if not (record.value.isascii() and record.value.isprintable()):
60
+ raise ValidationError(
61
+ _(
62
+ "Record value {value} for a type {type} record is not a printable ASCII string."
63
+ ).format(value=record.value, type=record.type)
64
+ )
65
+
66
+ if len(record.value) <= MAX_TXT_LENGTH:
67
+ return
68
+
69
+ try:
70
+ rr = rdata.from_text(RecordClassChoices.IN, record.type, record.value)
71
+ except SyntaxError as exc:
72
+ if str(exc) == "string too long":
73
+ record.value = _split_text_value(record.value)
74
+
75
+ try:
76
+ rr = rdata.from_text(RecordClassChoices.IN, record.type, record.value)
77
+ except SyntaxError as exc:
78
+ raise ValidationError(
79
+ _(
80
+ "Record value {value} is not a valid value for a {type} record: {error}."
81
+ ).format(value=record.value, type=record.type, error=exc)
82
+ )
83
+
84
+ skip_name_validation = record.type in get_plugin_config(
85
+ "netbox_dns", "tolerate_non_rfc1035_types", default=[]
86
+ )
87
+
88
+ match record.type:
89
+ case RecordTypeChoices.CNAME:
90
+ _validate_idn(rr.target)
91
+ if not skip_name_validation:
92
+ validate_domain_name(
93
+ rr.target.to_text(),
94
+ always_tolerant=True,
95
+ allow_empty_label=True,
96
+ )
97
+
98
+ case (
99
+ RecordTypeChoices.NS
100
+ | RecordTypeChoices.HTTPS
101
+ | RecordTypeChoices.SRV
102
+ | RecordTypeChoices.SVCB
103
+ ):
104
+ _validate_idn(rr.target)
105
+ if not skip_name_validation:
106
+ validate_domain_name(rr.target.to_text(), always_tolerant=True)
107
+
108
+ case RecordTypeChoices.DNAME:
109
+ _validate_idn(rr.target)
110
+ if not skip_name_validation:
111
+ validate_domain_name(
112
+ rr.target.to_text(), always_tolerant=True, zone_name=True
113
+ )
114
+
115
+ case RecordTypeChoices.PTR | RecordTypeChoices.NSAP_PTR:
116
+ _validate_idn(rr.target)
117
+ if not skip_name_validation:
118
+ validate_fqdn(rr.target.to_text(), always_tolerant=True)
119
+
120
+ case RecordTypeChoices.MX | RecordTypeChoices.RT | RecordTypeChoices.KX:
121
+ _validate_idn(rr.exchange)
122
+ if not skip_name_validation:
123
+ validate_domain_name(rr.exchange.to_text(), always_tolerant=True)
124
+
125
+ case RecordTypeChoices.NSEC:
126
+ _validate_idn(rr.next)
127
+ if not skip_name_validation:
128
+ validate_domain_name(rr.next.to_text(), always_tolerant=True)
129
+
130
+ case RecordTypeChoices.RP:
131
+ _validate_idn(rr.mbox)
132
+ _validate_idn(rr.txt)
133
+ if not skip_name_validation:
134
+ validate_domain_name(rr.mbox.to_text(), always_tolerant=True)
135
+ validate_domain_name(rr.txt.to_text(), always_tolerant=True)
136
+
137
+ case RecordTypeChoices.NAPTR:
138
+ _validate_idn(rr.replacement)
139
+ if not skip_name_validation:
140
+ validate_generic_name(rr.replacement.to_text(), always_tolerant=True)
141
+
142
+ case RecordTypeChoices.PX:
143
+ _validate_idn(rr.map822)
144
+ _validate_idn(rr.mapx400)
145
+ if not skip_name_validation:
146
+ validate_domain_name(rr.map822.to_text(), always_tolerant=True)
147
+ validate_domain_name(rr.mapx400.to_text(), always_tolerant=True)