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
netbox_dns/models/zone.py CHANGED
@@ -1,54 +1,74 @@
1
+ import re
1
2
  from math import ceil
2
- from datetime import datetime
3
+ from datetime import datetime, date
3
4
 
4
5
  from dns import name as dns_name
5
- from dns.rdtypes.ANY import SOA
6
6
  from dns.exception import DNSException
7
-
7
+ from dns.rdtypes.ANY import SOA
8
8
  from django.core.validators import (
9
9
  MinValueValidator,
10
10
  MaxValueValidator,
11
11
  )
12
12
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
13
13
  from django.db import models, transaction
14
- from django.db.models import Q, Max, ExpressionWrapper, BooleanField
15
- from django.urls import reverse
14
+ from django.db.models import Q, Max, ExpressionWrapper, BooleanField, UniqueConstraint
15
+ from django.db.models.functions import Length, Lower
16
16
  from django.db.models.signals import m2m_changed
17
17
  from django.dispatch import receiver
18
+ from django.conf import settings
19
+ from django.contrib.postgres.fields import ArrayField
20
+ from django.utils.translation import gettext_lazy as _
18
21
 
19
22
  from netbox.models import NetBoxModel
23
+ from netbox.models.features import ContactsMixin
20
24
  from netbox.search import SearchIndex, register_search
25
+ from netbox.plugins.utils import get_plugin_config
21
26
  from utilities.querysets import RestrictedQuerySet
22
- from utilities.choices import ChoiceSet
23
27
  from ipam.models import IPAddress
28
+ from ipam.choices import IPAddressFamilyChoices
24
29
 
25
- try:
26
- # NetBox 3.5.0 - 3.5.7, 3.5.9+
27
- from extras.plugins import get_plugin_config
28
- except ImportError:
29
- # NetBox 3.5.8
30
- from extras.plugins.utils import get_plugin_config
31
-
32
- from netbox_dns.fields import NetworkField
30
+ from netbox_dns.choices import (
31
+ RecordClassChoices,
32
+ RecordTypeChoices,
33
+ ZoneStatusChoices,
34
+ ZoneEPPStatusChoices,
35
+ )
36
+ from netbox_dns.fields import NetworkField, RFC2317NetworkField
33
37
  from netbox_dns.utilities import (
38
+ update_dns_records,
39
+ check_dns_records,
40
+ get_ip_addresses_by_zone,
34
41
  arpa_to_prefix,
35
42
  name_to_unicode,
36
43
  normalize_name,
44
+ get_parent_zone_names,
45
+ regex_from_list,
37
46
  NameFormatError,
38
47
  )
39
48
  from netbox_dns.validators import (
40
- validate_fqdn,
49
+ validate_rname,
41
50
  validate_domain_name,
42
51
  )
52
+ from netbox_dns.mixins import ObjectModificationMixin
43
53
 
44
- # +
45
- # This is a hack designed to break cyclic imports between Record and Zone
46
- # -
47
- import netbox_dns.models.record as record
54
+ from .record import Record
55
+ from .view import View
56
+ from .nameserver import NameServer
57
+
58
+
59
+ __all__ = (
60
+ "Zone",
61
+ "ZoneIndex",
62
+ )
63
+
64
+ ZONE_ACTIVE_STATUS_LIST = get_plugin_config("netbox_dns", "zone_active_status")
65
+ RECORD_ACTIVE_STATUS_LIST = get_plugin_config("netbox_dns", "record_active_status")
48
66
 
49
67
 
50
68
  class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
51
- """Special Manager for zones providing the activity status annotation"""
69
+ """
70
+ Custom manager for zones providing the activity status annotation
71
+ """
52
72
 
53
73
  def get_queryset(self):
54
74
  return (
@@ -56,220 +76,336 @@ class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
56
76
  .get_queryset()
57
77
  .annotate(
58
78
  active=ExpressionWrapper(
59
- Q(status__in=Zone.ACTIVE_STATUS_LIST), output_field=BooleanField()
79
+ Q(status__in=ZONE_ACTIVE_STATUS_LIST), output_field=BooleanField()
60
80
  )
61
81
  )
62
82
  )
63
83
 
64
84
 
65
- class ZoneStatusChoices(ChoiceSet):
66
- key = "Zone.status"
85
+ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
86
+ class Meta:
87
+ verbose_name = _("Zone")
88
+ verbose_name_plural = _("Zones")
67
89
 
68
- STATUS_ACTIVE = "active"
69
- STATUS_RESERVED = "reserved"
70
- STATUS_DEPRECATED = "deprecated"
71
- STATUS_PARKED = "parked"
90
+ ordering = (
91
+ "view",
92
+ "name",
93
+ )
72
94
 
73
- CHOICES = [
74
- (STATUS_ACTIVE, "Active", "blue"),
75
- (STATUS_RESERVED, "Reserved", "cyan"),
76
- (STATUS_DEPRECATED, "Deprecated", "red"),
77
- (STATUS_PARKED, "Parked", "gray"),
78
- ]
95
+ constraints = [
96
+ UniqueConstraint(
97
+ Lower("name"),
98
+ "view",
99
+ name="name_view_unique_ci",
100
+ violation_error_message=_(
101
+ "There is already a zone with the same name in this view"
102
+ ),
103
+ ),
104
+ ]
105
+
106
+ clone_fields = (
107
+ "view",
108
+ "name",
109
+ "description",
110
+ "status",
111
+ "nameservers",
112
+ "default_ttl",
113
+ "soa_ttl",
114
+ "soa_mname",
115
+ "soa_rname",
116
+ "soa_refresh",
117
+ "soa_retry",
118
+ "soa_expire",
119
+ "soa_minimum",
120
+ "tenant",
121
+ )
79
122
 
123
+ soa_clean_fields = {
124
+ "description",
125
+ "status",
126
+ "dnssec_policy",
127
+ "parental_agents",
128
+ "registrar",
129
+ "registry_domain_id",
130
+ "expiration_date",
131
+ "domain_status",
132
+ "registrant",
133
+ "admin_c",
134
+ "tech_c",
135
+ "billing_c",
136
+ "rfc2317_parent_managed",
137
+ "tenant",
138
+ "comments",
139
+ }
80
140
 
81
- class Zone(NetBoxModel):
82
- ACTIVE_STATUS_LIST = (ZoneStatusChoices.STATUS_ACTIVE,)
141
+ objects = ZoneManager()
142
+
143
+ def __str__(self):
144
+ if self.name == "." and get_plugin_config("netbox_dns", "enable_root_zones"):
145
+ name = ". (root zone)"
146
+ else:
147
+ try:
148
+ name = dns_name.from_text(self.name, origin=None).to_unicode()
149
+ except DNSException:
150
+ name = self.name
151
+
152
+ try:
153
+ if not self.view.default_view:
154
+ return f"[{self.view}] {name}"
155
+ except ObjectDoesNotExist:
156
+ return f"[<no view assigned>] {name}"
157
+
158
+ return str(name)
159
+
160
+ def __init__(self, *args, **kwargs):
161
+ kwargs.pop("template", None)
162
+
163
+ super().__init__(*args, **kwargs)
164
+
165
+ self._soa_serial_dirty = False
166
+ self._ip_addresses_checked = False
83
167
 
84
168
  view = models.ForeignKey(
169
+ verbose_name=_("View"),
85
170
  to="View",
86
171
  on_delete=models.PROTECT,
87
- blank=True,
88
- null=True,
172
+ related_name="zones",
173
+ null=False,
89
174
  )
90
175
  name = models.CharField(
176
+ verbose_name=_("Name"),
91
177
  max_length=255,
178
+ db_collation="natural_sort",
179
+ )
180
+ description = models.CharField(
181
+ verbose_name=_("Description"),
182
+ max_length=200,
183
+ blank=True,
92
184
  )
93
185
  status = models.CharField(
186
+ verbose_name=_("Status"),
94
187
  max_length=50,
95
188
  choices=ZoneStatusChoices,
96
189
  default=ZoneStatusChoices.STATUS_ACTIVE,
97
190
  blank=True,
98
191
  )
99
192
  nameservers = models.ManyToManyField(
193
+ verbose_name=_("Nameserver"),
100
194
  to="NameServer",
101
195
  related_name="zones",
102
196
  blank=True,
103
197
  )
104
198
  default_ttl = models.PositiveIntegerField(
199
+ verbose_name=_("Default TTL"),
105
200
  blank=True,
106
- verbose_name="Default TTL",
107
- validators=[MinValueValidator(1)],
201
+ validators=[MaxValueValidator(2147483647)],
108
202
  )
109
203
  soa_ttl = models.PositiveIntegerField(
204
+ verbose_name=_("SOA TTL"),
110
205
  blank=False,
111
206
  null=False,
112
- verbose_name="SOA TTL",
113
- validators=[MinValueValidator(1)],
207
+ validators=[MaxValueValidator(2147483647)],
114
208
  )
115
209
  soa_mname = models.ForeignKey(
210
+ verbose_name=_("SOA MName"),
116
211
  to="NameServer",
117
- related_name="zones_soa",
118
- verbose_name="SOA MName",
212
+ related_name="soa_zones",
119
213
  on_delete=models.PROTECT,
120
214
  blank=False,
121
215
  null=False,
122
216
  )
123
217
  soa_rname = models.CharField(
218
+ verbose_name=_("SOA RName"),
124
219
  max_length=255,
125
220
  blank=False,
126
221
  null=False,
127
- verbose_name="SOA RName",
128
222
  )
129
223
  soa_serial = models.BigIntegerField(
224
+ verbose_name=_("SOA Serial"),
130
225
  blank=True,
131
226
  null=True,
132
- verbose_name="SOA Serial",
133
227
  validators=[MinValueValidator(1), MaxValueValidator(4294967295)],
134
228
  )
135
229
  soa_refresh = models.PositiveIntegerField(
230
+ verbose_name=_("SOA Refresh"),
136
231
  blank=False,
137
232
  null=False,
138
- verbose_name="SOA Refresh",
139
233
  validators=[MinValueValidator(1)],
140
234
  )
141
235
  soa_retry = models.PositiveIntegerField(
236
+ verbose_name=_("SOA Retry"),
142
237
  blank=False,
143
238
  null=False,
144
- verbose_name="SOA Retry",
145
239
  validators=[MinValueValidator(1)],
146
240
  )
147
241
  soa_expire = models.PositiveIntegerField(
242
+ verbose_name=_("SOA Expire"),
148
243
  blank=False,
149
244
  null=False,
150
- verbose_name="SOA Expire",
151
245
  validators=[MinValueValidator(1)],
152
246
  )
153
247
  soa_minimum = models.PositiveIntegerField(
248
+ verbose_name=_("SOA Minimum TTL"),
154
249
  blank=False,
155
250
  null=False,
156
- verbose_name="SOA Minimum TTL",
157
- validators=[MinValueValidator(1)],
251
+ validators=[MaxValueValidator(2147483647)],
158
252
  )
159
253
  soa_serial_auto = models.BooleanField(
160
- verbose_name="Generate SOA Serial",
161
- help_text="Automatically generate the SOA Serial field",
254
+ verbose_name=_("Generate SOA Serial"),
255
+ help_text=_("Automatically generate the SOA serial number"),
162
256
  default=True,
163
257
  )
164
- description = models.CharField(
165
- max_length=200,
166
- blank=True,
167
- )
168
- arpa_network = NetworkField(
169
- verbose_name="ARPA Network",
170
- help_text="Network related to a reverse lookup zone (.arpa)",
258
+ dnssec_policy = models.ForeignKey(
259
+ verbose_name=_("DNSSEC Policy"),
260
+ to="DNSSECPolicy",
261
+ on_delete=models.PROTECT,
262
+ related_name="zones",
171
263
  blank=True,
172
264
  null=True,
173
265
  )
174
- tenant = models.ForeignKey(
175
- to="tenancy.Tenant",
176
- on_delete=models.PROTECT,
177
- related_name="netbox_dns_zones",
266
+ parental_agents = ArrayField(
267
+ base_field=models.GenericIPAddressField(
268
+ protocol="both",
269
+ ),
178
270
  blank=True,
179
271
  null=True,
272
+ default=list,
180
273
  )
181
274
  registrar = models.ForeignKey(
275
+ verbose_name=_("Registrar"),
182
276
  to="Registrar",
183
277
  on_delete=models.SET_NULL,
184
- verbose_name="Registrar",
185
- help_text="The external registrar the domain is registered with",
278
+ related_name="zones",
186
279
  blank=True,
187
280
  null=True,
188
281
  )
189
282
  registry_domain_id = models.CharField(
190
- verbose_name="Registry Domain ID",
191
- help_text="The ID of the domain assigned by the registry",
283
+ verbose_name=_("Registry Domain ID"),
192
284
  max_length=50,
193
285
  blank=True,
194
286
  null=True,
195
287
  )
288
+ expiration_date = models.DateField(
289
+ verbose_name=_("Expiration Date"),
290
+ blank=True,
291
+ null=True,
292
+ )
293
+ domain_status = models.CharField(
294
+ verbose_name=_("Domain Status"),
295
+ max_length=50,
296
+ choices=ZoneEPPStatusChoices,
297
+ blank=True,
298
+ null=True,
299
+ )
196
300
  registrant = models.ForeignKey(
197
- to="Contact",
301
+ verbose_name=_("Registrant"),
302
+ to="RegistrationContact",
198
303
  on_delete=models.SET_NULL,
199
- verbose_name="Registrant",
200
- help_text="The owner of the domain",
304
+ related_name="registrant_zones",
201
305
  blank=True,
202
306
  null=True,
203
307
  )
204
308
  admin_c = models.ForeignKey(
205
- to="Contact",
309
+ verbose_name=_("Administrative Contact"),
310
+ to="RegistrationContact",
206
311
  on_delete=models.SET_NULL,
207
- verbose_name="Admin Contact",
208
312
  related_name="admin_c_zones",
209
- help_text="The administrative contact for the domain",
210
313
  blank=True,
211
314
  null=True,
212
315
  )
213
316
  tech_c = models.ForeignKey(
214
- to="Contact",
317
+ verbose_name=_("Technical Contact"),
318
+ to="RegistrationContact",
215
319
  on_delete=models.SET_NULL,
216
- verbose_name="Tech Contact",
217
320
  related_name="tech_c_zones",
218
- help_text="The technical contact for the domain",
219
321
  blank=True,
220
322
  null=True,
221
323
  )
222
324
  billing_c = models.ForeignKey(
223
- to="Contact",
325
+ verbose_name=_("Billing Contact"),
326
+ to="RegistrationContact",
224
327
  on_delete=models.SET_NULL,
225
- verbose_name="Billing Contact",
226
328
  related_name="billing_c_zones",
227
- help_text="The billing contact for the domain",
228
329
  blank=True,
229
330
  null=True,
230
331
  )
332
+ rfc2317_prefix = RFC2317NetworkField(
333
+ verbose_name=_("RFC2317 Prefix"),
334
+ help_text=_("RFC2317 IPv4 prefix with a length of at least 25 bits"),
335
+ blank=True,
336
+ null=True,
337
+ )
338
+ rfc2317_parent_managed = models.BooleanField(
339
+ verbose_name=_("RFC2317 Parent Managed"),
340
+ help_text=_("The parent zone for the RFC2317 zone is managed by NetBox DNS"),
341
+ default=False,
342
+ )
343
+ rfc2317_parent_zone = models.ForeignKey(
344
+ verbose_name=_("RFC2317 Parent Zone"),
345
+ to="self",
346
+ on_delete=models.SET_NULL,
347
+ related_name="rfc2317_child_zones",
348
+ help_text=_("Parent zone for RFC2317 reverse zones"),
349
+ blank=True,
350
+ null=True,
351
+ )
352
+ arpa_network = NetworkField(
353
+ verbose_name=_("ARPA Network"),
354
+ help_text=_("Network related to a reverse lookup zone (.arpa)"),
355
+ blank=True,
356
+ null=True,
357
+ )
358
+ tenant = models.ForeignKey(
359
+ verbose_name=_("Tenant"),
360
+ to="tenancy.Tenant",
361
+ on_delete=models.PROTECT,
362
+ related_name="netbox_dns_zones",
363
+ blank=True,
364
+ null=True,
365
+ )
366
+ comments = models.TextField(
367
+ verbose_name=_("Comments"),
368
+ blank=True,
369
+ )
231
370
 
232
- objects = ZoneManager()
371
+ @property
372
+ def fqdn(self):
373
+ return f"{self.name}."
374
+
375
+ @staticmethod
376
+ def get_defaults():
377
+ default_fields = (
378
+ "zone_default_ttl",
379
+ "zone_soa_ttl",
380
+ "zone_soa_serial",
381
+ "zone_soa_refresh",
382
+ "zone_soa_retry",
383
+ "zone_soa_expire",
384
+ "zone_soa_minimum",
385
+ "zone_soa_rname",
386
+ )
233
387
 
234
- clone_fields = [
235
- "view",
236
- "name",
237
- "status",
238
- "nameservers",
239
- "default_ttl",
240
- "soa_ttl",
241
- "soa_mname",
242
- "soa_rname",
243
- "soa_refresh",
244
- "soa_retry",
245
- "soa_expire",
246
- "soa_minimum",
247
- "description",
248
- ]
388
+ return {
389
+ field[5:]: value
390
+ for field, value in settings.PLUGINS_CONFIG.get("netbox_dns").items()
391
+ if field in default_fields
392
+ }
249
393
 
250
- class Meta:
251
- ordering = (
252
- "view",
253
- "name",
254
- )
255
- unique_together = (
256
- "view",
257
- "name",
258
- )
394
+ @property
395
+ def soa_serial_dirty(self):
396
+ return self._soa_serial_dirty
259
397
 
260
- def __str__(self):
261
- if self.name == "." and get_plugin_config("netbox_dns", "enable_root_zones"):
262
- name = ". (root zone)"
263
- else:
264
- try:
265
- name = dns_name.from_text(self.name, origin=None).to_unicode()
266
- except dns_name.IDNAException:
267
- name = self.name
398
+ @soa_serial_dirty.setter
399
+ def soa_serial_dirty(self, soa_serial_dirty):
400
+ self._soa_serial_dirty = soa_serial_dirty
268
401
 
269
- if self.view:
270
- return f"[{self.view}] {name}"
402
+ @property
403
+ def ip_addresses_checked(self):
404
+ return self._ip_addresses_checked
271
405
 
272
- return str(name)
406
+ @ip_addresses_checked.setter
407
+ def ip_addresses_checked(self, ip_addresses_checked):
408
+ self._ip_addresses_checked = ip_addresses_checked
273
409
 
274
410
  @property
275
411
  def display_name(self):
@@ -278,20 +414,41 @@ class Zone(NetBoxModel):
278
414
  def get_status_color(self):
279
415
  return ZoneStatusChoices.colors.get(self.status)
280
416
 
281
- def get_absolute_url(self):
282
- return reverse("plugins:netbox_dns:zone", kwargs={"pk": self.pk})
417
+ def get_domain_status_color(self):
418
+ return ZoneEPPStatusChoices.colors.get(self.domain_status)
283
419
 
284
420
  def get_status_class(self):
285
421
  return self.CSS_CLASSES.get(self.status)
286
422
 
287
423
  @property
288
424
  def is_active(self):
289
- return self.status in Zone.ACTIVE_STATUS_LIST
425
+ return self.status in ZONE_ACTIVE_STATUS_LIST
290
426
 
291
427
  @property
292
428
  def is_reverse_zone(self):
293
429
  return self.name.endswith(".arpa") and self.network_from_name is not None
294
430
 
431
+ @property
432
+ def is_rfc2317_zone(self):
433
+ return self.rfc2317_prefix is not None
434
+
435
+ @property
436
+ def inline_signing(self):
437
+ if self.dnssec_policy is None:
438
+ return None
439
+
440
+ return self.dnssec_policy.inline_signing
441
+
442
+ def get_rfc2317_parent_zone(self):
443
+ if not self.is_rfc2317_zone:
444
+ return None
445
+
446
+ return (
447
+ self.view.zones.filter(arpa_network__net_contains=self.rfc2317_prefix)
448
+ .order_by("arpa_network__net_mask_length")
449
+ .last()
450
+ )
451
+
295
452
  @property
296
453
  def is_registered(self):
297
454
  return any(
@@ -303,24 +460,91 @@ class Zone(NetBoxModel):
303
460
  self.admin_c,
304
461
  self.tech_c,
305
462
  self.billing_c,
463
+ self.expiration_date,
464
+ self.domain_status,
306
465
  )
307
466
  )
308
467
 
309
468
  @property
310
- def view_filter(self):
311
- if self.view is None:
312
- return Q(view__isnull=True)
313
- return Q(view=self.view)
469
+ def child_zones(self):
470
+ return self.view.zones.filter(name__iregex=rf"^[^.]+\.{re.escape(self.name)}$")
314
471
 
315
- def record_count(self, managed=False):
316
- return record.Record.objects.filter(zone=self, managed=managed).count()
472
+ @property
473
+ def descendant_zones(self):
474
+ return self.view.zones.filter(name__iendswith=f".{self.name}")
475
+
476
+ @property
477
+ def parent_zone(self):
478
+ try:
479
+ return self.view.zones.get(
480
+ name__iexact=get_parent_zone_names(self.name)[-1]
481
+ )
482
+ except (Zone.DoesNotExist, IndexError):
483
+ return None
484
+
485
+ @property
486
+ def ancestor_zones(self):
487
+ return (
488
+ self.view.zones.annotate(name_length=Length("name"))
489
+ .filter(name__iregex=regex_from_list(get_parent_zone_names(self.name)))
490
+ .order_by("name_length")
491
+ )
492
+
493
+ @property
494
+ def delegation_records(self):
495
+ descendant_zone_names = [
496
+ rf"{name}."
497
+ for name in (
498
+ name.lower()
499
+ for name in self.descendant_zones.values_list("name", flat=True)
500
+ )
501
+ ]
502
+
503
+ ns_records = (
504
+ self.records.filter(type=RecordTypeChoices.NS)
505
+ .exclude(fqdn__iexact=self.fqdn)
506
+ .filter(fqdn__iregex=regex_from_list(descendant_zone_names))
507
+ )
508
+ ns_values = [record.value_fqdn for record in ns_records]
509
+
510
+ return (
511
+ ns_records
512
+ | self.records.filter(type=RecordTypeChoices.DS)
513
+ | self.records.filter(
514
+ type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
515
+ fqdn__in=ns_values,
516
+ )
517
+ )
518
+
519
+ @property
520
+ def ancestor_delegation_records(self):
521
+ ancestor_zones = self.ancestor_zones
522
+
523
+ ns_records = Record.objects.filter(
524
+ type=RecordTypeChoices.NS, zone__in=ancestor_zones, fqdn=self.fqdn
525
+ )
526
+ ns_values = [record.value_fqdn for record in ns_records]
527
+
528
+ ds_records = Record.objects.filter(
529
+ type=RecordTypeChoices.DS, zone__in=ancestor_zones, fqdn=self.fqdn
530
+ )
531
+
532
+ return (
533
+ ns_records
534
+ | ds_records
535
+ | Record.objects.filter(
536
+ zone__in=ancestor_zones,
537
+ type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
538
+ fqdn__in=ns_values,
539
+ )
540
+ )
317
541
 
318
542
  def update_soa_record(self):
319
543
  soa_name = "@"
320
544
  soa_ttl = self.soa_ttl
321
545
  soa_rdata = SOA.SOA(
322
- rdclass=record.RecordClassChoices.IN,
323
- rdtype=record.RecordTypeChoices.SOA,
546
+ rdclass=RecordClassChoices.IN,
547
+ rdtype=RecordTypeChoices.SOA,
324
548
  mname=self.soa_mname.name,
325
549
  rname=self.soa_rname,
326
550
  serial=self.soa_serial,
@@ -331,20 +555,23 @@ class Zone(NetBoxModel):
331
555
  )
332
556
 
333
557
  try:
334
- soa_record = self.record_set.get(
335
- type=record.RecordTypeChoices.SOA, name=soa_name
336
- )
558
+ soa_record = self.records.get(type=RecordTypeChoices.SOA, name=soa_name)
337
559
 
338
- if soa_record.ttl != soa_ttl or soa_record.value != soa_rdata.to_text():
560
+ if (
561
+ soa_record.ttl != soa_ttl
562
+ or soa_record.value != soa_rdata.to_text()
563
+ or not soa_record.managed
564
+ ):
565
+ soa_record.snapshot()
339
566
  soa_record.ttl = soa_ttl
340
567
  soa_record.value = soa_rdata.to_text()
341
568
  soa_record.managed = True
342
569
  soa_record.save()
343
570
 
344
- except record.Record.DoesNotExist:
345
- record.Record.objects.create(
571
+ except Record.DoesNotExist:
572
+ Record.objects.create(
346
573
  zone_id=self.pk,
347
- type=record.RecordTypeChoices.SOA,
574
+ type=RecordTypeChoices.SOA,
348
575
  name=soa_name,
349
576
  ttl=soa_ttl,
350
577
  value=soa_rdata.to_text(),
@@ -356,19 +583,44 @@ class Zone(NetBoxModel):
356
583
 
357
584
  nameservers = [f"{nameserver.name}." for nameserver in self.nameservers.all()]
358
585
 
359
- self.record_set.filter(type=record.RecordTypeChoices.NS, managed=True).exclude(
360
- value__in=nameservers
361
- ).delete()
586
+ for ns_record in self.records.filter(
587
+ type=RecordTypeChoices.NS, managed=True
588
+ ).exclude(value__in=nameservers):
589
+ ns_record.delete()
362
590
 
363
591
  for ns in nameservers:
364
- record.Record.raw_objects.update_or_create(
592
+ Record.raw_objects.update_or_create(
365
593
  zone_id=self.pk,
366
- type=record.RecordTypeChoices.NS,
594
+ type=RecordTypeChoices.NS,
367
595
  name=ns_name,
368
596
  value=ns,
369
597
  managed=True,
370
598
  )
371
599
 
600
+ def _check_nameserver_address_records(self, nameserver):
601
+ name = dns_name.from_text(nameserver.name, origin=None)
602
+ parent = name.parent()
603
+
604
+ if len(parent) < 2:
605
+ return None
606
+
607
+ try:
608
+ ns_zone = Zone.objects.get(view_id=self.view.pk, name=parent.to_text())
609
+ except ObjectDoesNotExist:
610
+ return None
611
+
612
+ relative_name = name.relativize(parent).to_text()
613
+ address_records = ns_zone.records.filter(
614
+ Q(status__in=RECORD_ACTIVE_STATUS_LIST),
615
+ Q(Q(name=f"{nameserver.name}.") | Q(name=relative_name)),
616
+ Q(Q(type=RecordTypeChoices.A) | Q(type=RecordTypeChoices.AAAA)),
617
+ )
618
+
619
+ if not address_records.exists():
620
+ return _(
621
+ "Nameserver {ns} does not have an active address record in zone {zone}"
622
+ ).format(ns=nameserver.name, zone=ns_zone)
623
+
372
624
  def check_nameservers(self):
373
625
  nameservers = self.nameservers.all()
374
626
 
@@ -376,45 +628,59 @@ class Zone(NetBoxModel):
376
628
  ns_errors = []
377
629
 
378
630
  if not nameservers:
379
- ns_errors.append(f"No nameservers are configured for zone {self}")
631
+ ns_errors.append(
632
+ _("No nameservers are configured for zone {zone}").format(zone=self)
633
+ )
380
634
 
381
- for nameserver in nameservers:
382
- name = dns_name.from_text(nameserver.name, origin=None)
383
- parent = name.parent()
635
+ for _nameserver in nameservers:
636
+ warning = self._check_nameserver_address_records(_nameserver)
637
+ if warning is not None:
638
+ ns_warnings.append(warning)
384
639
 
385
- if len(parent) < 2:
386
- continue
640
+ return ns_warnings, ns_errors
387
641
 
388
- view_condition = (
389
- Q(view__isnull=True) if self.view is None else Q(view_id=self.view.pk)
390
- )
642
+ def check_soa_mname(self):
643
+ return self._check_nameserver_address_records(self.soa_mname)
391
644
 
392
- try:
393
- ns_zone = Zone.objects.get(view_condition, name=parent.to_text())
394
- except ObjectDoesNotExist:
395
- continue
396
-
397
- relative_name = name.relativize(parent).to_text()
398
- address_records = record.Record.objects.filter(
399
- Q(zone=ns_zone),
400
- Q(status__in=record.Record.ACTIVE_STATUS_LIST),
401
- Q(Q(name=f"{nameserver.name}.") | Q(name=relative_name)),
402
- Q(
403
- Q(type=record.RecordTypeChoices.A)
404
- | Q(type=record.RecordTypeChoices.AAAA)
405
- ),
645
+ def check_expiration(self):
646
+ if self.expiration_date is None:
647
+ return None, None
648
+
649
+ expiration_warning = None
650
+ expiration_error = None
651
+
652
+ expiration_warning_days = get_plugin_config(
653
+ "netbox_dns", "zone_expiration_warning_days"
654
+ )
655
+
656
+ if self.expiration_date < date.today():
657
+ expiration_error = _("The registration for this domain has expired.")
658
+ elif (self.expiration_date - date.today()).days < expiration_warning_days:
659
+ expiration_warning = _(
660
+ f"The registration for this domain will expire in less than {expiration_warning_days} days."
406
661
  )
407
662
 
408
- if not address_records:
409
- ns_warnings.append(
410
- f"Nameserver {nameserver.name} does not have an active address record in zone {ns_zone}"
411
- )
663
+ return expiration_warning, expiration_error
412
664
 
413
- return ns_warnings, ns_errors
665
+ def check_soa_serial_increment(self, old_serial, new_serial):
666
+ MAX_SOA_SERIAL_INCREMENT = 2**31 - 1
667
+ SOA_SERIAL_WRAP = 2**32
668
+
669
+ if old_serial is None:
670
+ return
671
+
672
+ if (new_serial - old_serial) % SOA_SERIAL_WRAP > MAX_SOA_SERIAL_INCREMENT:
673
+ raise ValidationError(
674
+ {
675
+ "soa_serial": _(
676
+ "soa_serial must not decrease for zone {zone}."
677
+ ).format(zone=self.name)
678
+ }
679
+ )
414
680
 
415
681
  def get_auto_serial(self):
416
- records = record.Record.objects.filter(zone=self).exclude(
417
- type=record.RecordTypeChoices.SOA
682
+ records = Record.objects.filter(zone_id=self.pk).exclude(
683
+ type=RecordTypeChoices.SOA
418
684
  )
419
685
  if records:
420
686
  soa_serial = (
@@ -430,31 +696,129 @@ class Zone(NetBoxModel):
430
696
 
431
697
  return soa_serial
432
698
 
433
- def update_serial(self):
699
+ def update_serial(self, save_zone_serial=True):
700
+ if not self.soa_serial_auto:
701
+ return
702
+
434
703
  self.last_updated = datetime.now()
435
704
  self.soa_serial = ceil(datetime.now().timestamp())
436
- self.update_soa_record()
437
- super().save()
705
+
706
+ if save_zone_serial:
707
+ super().save(update_fields=["soa_serial", "last_updated"])
708
+ self.soa_serial_dirty = False
709
+ self.update_soa_record()
710
+ else:
711
+ self.soa_serial_dirty = True
712
+
713
+ def save_soa_serial(self):
714
+ if self.soa_serial_auto and self.soa_serial_dirty:
715
+ super().save(update_fields=["soa_serial", "last_updated"])
716
+ self.soa_serial_dirty = False
438
717
 
439
718
  @property
440
719
  def network_from_name(self):
441
720
  return arpa_to_prefix(self.name)
442
721
 
443
- def check_name_conflict(self):
444
- if self.view is None:
445
- if (
446
- Zone.objects.exclude(pk=self.pk)
447
- .filter(name=self.name.rstrip("."), view__isnull=True)
448
- .exists()
449
- ):
722
+ def update_rfc2317_parent_zone(self):
723
+ if not self.is_rfc2317_zone:
724
+ return
725
+
726
+ if self.rfc2317_parent_managed:
727
+ rfc2317_parent_zone = self.get_rfc2317_parent_zone()
728
+
729
+ if rfc2317_parent_zone is None:
730
+ self.rfc2317_parent_managed = False
731
+ self.rfc2317_parent_zone = None
732
+ self.save(
733
+ update_fields=["rfc2317_parent_zone", "rfc2317_parent_managed"]
734
+ )
735
+
736
+ elif self.rfc2317_parent_zone != rfc2317_parent_zone:
737
+ self.rfc2317_parent_zone = rfc2317_parent_zone
738
+ self.save(update_fields=["rfc2317_parent_zone"])
739
+
740
+ ptr_records = self.records.filter(type=RecordTypeChoices.PTR).prefetch_related(
741
+ "zone", "rfc2317_cname_record"
742
+ )
743
+ ptr_zones = {ptr_record.zone for ptr_record in ptr_records}
744
+
745
+ if self.rfc2317_parent_managed:
746
+ for ptr_record in ptr_records:
747
+ ptr_record.save(save_zone_serial=False)
748
+
749
+ self.rfc2317_parent_zone.save_soa_serial()
750
+ self.rfc2317_parent_zone.update_soa_record()
751
+ else:
752
+ cname_records = {
753
+ ptr_record.rfc2317_cname_record
754
+ for ptr_record in ptr_records
755
+ if ptr_record.rfc2317_cname_record is not None
756
+ }
757
+ cname_zones = {cname_record.zone for cname_record in cname_records}
758
+
759
+ for ptr_record in ptr_records:
760
+ ptr_record.save(update_rfc2317_cname=False, save_zone_serial=False)
761
+ for cname_record in cname_records:
762
+ cname_record.delete(save_zone_serial=False)
763
+
764
+ for cname_zone in cname_zones:
765
+ cname_zone.save_soa_serial()
766
+ cname_zone.update_soa_record()
767
+
768
+ for ptr_zone in ptr_zones:
769
+ ptr_zone.save_soa_serial()
770
+ ptr_zone.update_soa_record()
771
+
772
+ def clean_fields(self, exclude=None):
773
+ defaults = settings.PLUGINS_CONFIG.get("netbox_dns")
774
+
775
+ if get_plugin_config("netbox_dns", "convert_names_to_lowercase", False):
776
+ self.name = self.name.lower()
777
+
778
+ if self.view_id is None:
779
+ self.view_id = View.get_default_view().pk
780
+
781
+ for field, value in self.get_defaults().items():
782
+ if getattr(self, field) in (None, ""):
783
+ if value not in (None, ""):
784
+ setattr(self, field, value)
785
+
786
+ if self.soa_mname_id is None and "soa_mname" not in exclude:
787
+ if default_soa_mname := defaults.get("zone_soa_mname"):
788
+ try:
789
+ self.soa_mname = NameServer.objects.get(name=default_soa_mname)
790
+ except NameServer.DoesNotExist:
791
+ raise ValidationError(
792
+ {
793
+ "soa_mname": _(
794
+ "Default soa_mname instance {nameserver} does not exist"
795
+ ).format(nameserver=default_soa_mname)
796
+ }
797
+ )
798
+ else:
450
799
  raise ValidationError(
451
800
  {
452
- "name": f"A zone with name {self.name} and no view already exists."
801
+ "soa_mname": _(
802
+ "soa_mname not set and no template or default value defined"
803
+ )
453
804
  }
454
805
  )
455
806
 
807
+ super().clean_fields(exclude=exclude)
808
+
456
809
  def clean(self, *args, **kwargs):
457
- self.check_name_conflict()
810
+ if not self.dnssec_policy:
811
+ self.parental_agents = self._meta.get_field("parental_agents").get_default()
812
+
813
+ if not self.registrar:
814
+ self.registry_domain_id = self._meta.get_field(
815
+ "registry_domain_id"
816
+ ).get_default()
817
+ self.expiration_date = self._meta.get_field("expiration_date").get_default()
818
+ self.domain_status = self._meta.get_field("domain_status").get_default()
819
+
820
+ if self.soa_ttl is None:
821
+ self.soa_ttl = self.default_ttl
458
822
 
459
823
  try:
460
824
  self.name = normalize_name(self.name)
@@ -463,115 +827,314 @@ class Zone(NetBoxModel):
463
827
  {
464
828
  "name": str(exc),
465
829
  }
466
- ) from None
830
+ )
467
831
 
468
832
  try:
469
- validate_domain_name(self.name)
833
+ validate_domain_name(self.name, zone_name=True)
470
834
  except ValidationError as exc:
471
835
  raise ValidationError(
472
836
  {
473
837
  "name": exc,
474
838
  }
475
- ) from None
839
+ )
476
840
 
841
+ if self.soa_rname in (None, ""):
842
+ raise ValidationError(
843
+ {
844
+ "soa_rname": _(
845
+ "soa_rname not set and no template or default value defined"
846
+ ),
847
+ }
848
+ )
477
849
  try:
478
850
  dns_name.from_text(self.soa_rname, origin=dns_name.root)
479
- validate_fqdn(self.soa_rname)
851
+ validate_rname(self.soa_rname)
480
852
  except (DNSException, ValidationError) as exc:
481
853
  raise ValidationError(
482
854
  {
483
855
  "soa_rname": exc,
484
856
  }
485
- ) from None
486
-
487
- if self.soa_serial is None and not self.soa_serial_auto:
488
- raise ValidationError(
489
- {
490
- "soa_serial": f"soa_serial is not defined and soa_serial_auto is disabled for zone {self.name}."
491
- }
492
857
  )
493
858
 
494
- def save(self, *args, **kwargs):
495
- self.full_clean()
859
+ if not self.soa_serial_auto:
860
+ if self.soa_serial is None:
861
+ raise ValidationError(
862
+ {
863
+ "soa_serial": _(
864
+ "soa_serial is not defined and soa_serial_auto is disabled for zone {zone}."
865
+ ).format(zone=self.name)
866
+ }
867
+ )
496
868
 
497
- new_zone = self.pk is None
498
- if not new_zone:
499
- old_zone = Zone.objects.get(pk=self.pk)
869
+ if not self._state.adding:
870
+ old_soa_serial = self.get_saved_value("soa_serial")
871
+ old_soa_serial_auto = self.get_saved_value("soa_serial_auto")
872
+
873
+ if not self.soa_serial_auto:
874
+ self.check_soa_serial_increment(old_soa_serial, self.soa_serial)
875
+ elif not old_soa_serial_auto:
876
+ try:
877
+ self.check_soa_serial_increment(
878
+ old_soa_serial, self.get_auto_serial()
879
+ )
880
+ except ValidationError:
881
+ raise ValidationError(
882
+ {
883
+ "soa_serial_auto": _(
884
+ "Enabling soa_serial_auto would decrease soa_serial for zone {zone}."
885
+ ).format(zone=self.name)
886
+ }
887
+ )
888
+
889
+ old_name = self.get_saved_value("name")
890
+ old_view_id = self.get_saved_value("view_id")
500
891
 
501
- name_changed = not new_zone and old_zone.name != self.name
502
- view_changed = not new_zone and old_zone.view != self.view
503
- status_changed = not new_zone and old_zone.status != self.status
892
+ if (
893
+ not self.ip_addresses_checked
894
+ and old_name != self.name
895
+ or old_view_id != self.view_id
896
+ ):
897
+ ip_addresses = IPAddress.objects.filter(
898
+ netbox_dns_records__in=self.records.filter(
899
+ ipam_ip_address__isnull=False
900
+ )
901
+ )
902
+ ip_addresses |= get_ip_addresses_by_zone(self)
504
903
 
505
- if self.soa_serial_auto:
506
- self.soa_serial = self.get_auto_serial()
904
+ for ip_address in ip_addresses.distinct():
905
+ try:
906
+ check_dns_records(ip_address, zone=self)
907
+ except ValidationError as exc:
908
+ raise ValidationError(exc.messages)
909
+
910
+ self.ip_addresses_checked = True
507
911
 
508
912
  if self.is_reverse_zone:
509
913
  self.arpa_network = self.network_from_name
510
914
 
915
+ if self.is_rfc2317_zone:
916
+ if self.arpa_network is not None:
917
+ raise ValidationError(
918
+ {
919
+ "rfc2317_prefix": _(
920
+ "A regular reverse zone can not be used as an RFC2317 zone."
921
+ )
922
+ }
923
+ )
924
+
925
+ if self.rfc2317_parent_managed:
926
+ rfc2317_parent_zone = self.get_rfc2317_parent_zone()
927
+
928
+ if rfc2317_parent_zone is None:
929
+ raise ValidationError(
930
+ {
931
+ "rfc2317_parent_managed": _(
932
+ "Parent zone not found in view {view}."
933
+ ).format(view=self.view)
934
+ }
935
+ )
936
+
937
+ self.rfc2317_parent_zone = rfc2317_parent_zone
938
+ else:
939
+ self.rfc2317_parent_zone = None
940
+
941
+ overlapping_zones = self.view.zones.filter(
942
+ rfc2317_prefix__net_overlap=self.rfc2317_prefix,
943
+ active=True,
944
+ ).exclude(pk=self.pk)
945
+
946
+ if overlapping_zones.exists():
947
+ raise ValidationError(
948
+ {
949
+ "rfc2317_prefix": _(
950
+ "RFC2317 prefix overlaps with zone {zone}."
951
+ ).format(zone=overlapping_zones.first())
952
+ }
953
+ )
954
+
955
+ else:
956
+ self.rfc2317_parent_managed = False
957
+ self.rfc2317_parent_zone = None
958
+
959
+ super().clean(*args, **kwargs)
960
+
961
+ def save(self, *args, **kwargs):
962
+ self.full_clean()
963
+
964
+ changed_fields = self.changed_fields
965
+
966
+ if self.soa_serial_auto and (
967
+ changed_fields is None or changed_fields - self.soa_clean_fields
968
+ ):
969
+ self.soa_serial = self.get_auto_serial()
970
+
511
971
  super().save(*args, **kwargs)
512
972
 
513
973
  if (
514
- new_zone or name_changed or view_changed or status_changed
974
+ changed_fields is None or {"name", "view", "status"} & changed_fields
515
975
  ) and self.is_reverse_zone:
516
- zones = Zone.objects.filter(
517
- self.view_filter,
518
- arpa_network__net_contains_or_equals=self.arpa_network,
976
+ zones = self.view.zones.filter(
977
+ arpa_network__net_contains_or_equals=self.arpa_network
519
978
  )
520
- address_records = record.Record.objects.filter(
521
- Q(ptr_record__isnull=True) | Q(ptr_record__zone__in=zones),
522
- type__in=(record.RecordTypeChoices.A, record.RecordTypeChoices.AAAA),
979
+
980
+ if self.arpa_network.version == IPAddressFamilyChoices.FAMILY_4:
981
+ record_type = RecordTypeChoices.A
982
+ else:
983
+ record_type = RecordTypeChoices.AAAA
984
+
985
+ address_records = Record.objects.filter(
986
+ Q(
987
+ ptr_record__isnull=True,
988
+ zone__view=self.view,
989
+ ip_address__isnull=False,
990
+ ip_address__contained=self.arpa_network,
991
+ type=record_type,
992
+ )
993
+ | Q(ptr_record__zone__in=zones),
523
994
  disable_ptr=False,
524
995
  )
996
+
525
997
  for address_record in address_records:
526
- address_record.update_ptr_record()
998
+ address_record.save(
999
+ update_fields=["ptr_record"], save_zone_serial=False
1000
+ )
527
1001
 
528
- elif name_changed or view_changed or status_changed:
529
- for address_record in self.record_set.filter(
530
- type__in=(record.RecordTypeChoices.A, record.RecordTypeChoices.AAAA)
531
- ):
532
- address_record.update_ptr_record()
1002
+ for zone in zones:
1003
+ zone.save_soa_serial()
533
1004
 
534
- # Fix name in IP Address when zone name is changed
535
- if (
536
- get_plugin_config("netbox_dns", "feature_ipam_coupling")
537
- and name_changed
1005
+ if self.arpa_network.version == 4:
1006
+ rfc2317_child_zones = Zone.objects.filter(
1007
+ rfc2317_prefix__net_contained=self.arpa_network,
1008
+ rfc2317_parent_managed=True,
1009
+ )
1010
+ for child_zone in rfc2317_child_zones:
1011
+ child_zone.update_rfc2317_parent_zone()
1012
+
1013
+ if (
1014
+ changed_fields is None
1015
+ or {"name", "view", "status", "rfc2317_prefix", "rfc2317_parent_managed"}
1016
+ & changed_fields
1017
+ ) and self.is_rfc2317_zone:
1018
+ zones = self.view.zones.filter(
1019
+ arpa_network__net_contains=self.rfc2317_prefix
1020
+ )
1021
+ address_records = Record.objects.filter(
1022
+ Q(ptr_record__isnull=True, ip_address__contained=self.rfc2317_prefix)
1023
+ | Q(ptr_record__zone__in=zones)
1024
+ | Q(ptr_record__zone=self),
1025
+ type=RecordTypeChoices.A,
1026
+ disable_ptr=False,
1027
+ )
1028
+
1029
+ for address_record in address_records:
1030
+ address_record.save(
1031
+ update_fields=["ptr_record"],
1032
+ update_rfc2317_cname=False,
1033
+ save_zone_serial=False,
1034
+ )
1035
+
1036
+ for zone in zones:
1037
+ zone.save_soa_serial()
1038
+
1039
+ self.update_rfc2317_parent_zone()
1040
+
1041
+ elif changed_fields is not None and {"name", "view", "status"} & changed_fields:
1042
+ for address_record in self.records.filter(
1043
+ type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
1044
+ ipam_ip_address__isnull=True,
538
1045
  ):
539
- for ip in IPAddress.objects.filter(
540
- custom_field_data__ipaddress_dns_zone_id=self.pk
541
- ):
542
- ip.dns_name = f'{ip.custom_field_data["ipaddress_dns_record_name"]}.{self.name}'
543
- ip.save(update_fields=["dns_name"])
1046
+ address_record.save(update_fields=["ptr_record"])
1047
+
1048
+ if changed_fields is not None and "name" in changed_fields:
1049
+ for _record in self.records.filter(ipam_ip_address__isnull=True):
1050
+ _record.save(
1051
+ update_fields=["fqdn"],
1052
+ save_zone_serial=False,
1053
+ update_rrset_ttl=False,
1054
+ update_rfc2317_cname=False,
1055
+ )
1056
+
1057
+ if changed_fields is None or {"name", "view"} & changed_fields:
1058
+ ip_addresses = IPAddress.objects.filter(
1059
+ netbox_dns_records__in=self.records.filter(
1060
+ ipam_ip_address__isnull=False
1061
+ )
1062
+ )
1063
+ ip_addresses |= get_ip_addresses_by_zone(self)
544
1064
 
1065
+ for ip_address in ip_addresses.distinct():
1066
+ update_dns_records(ip_address)
1067
+
1068
+ self.save_soa_serial()
545
1069
  self.update_soa_record()
546
1070
 
547
1071
  def delete(self, *args, **kwargs):
548
1072
  with transaction.atomic():
549
- address_records = self.record_set.filter(ptr_record__isnull=False)
1073
+ address_records = self.records.filter(
1074
+ ptr_record__isnull=False
1075
+ ).prefetch_related("ptr_record")
550
1076
  for address_record in address_records:
551
1077
  address_record.ptr_record.delete()
552
1078
 
553
- ptr_records = self.record_set.filter(address_record__isnull=False)
554
- update_records = [
555
- address_record.pk
556
- for address_record in record.Record.objects.filter(
557
- ptr_record__in=ptr_records
1079
+ ptr_records = self.records.filter(address_records__isnull=False)
1080
+ update_records = list(
1081
+ Record.objects.filter(ptr_record__in=ptr_records).values_list(
1082
+ "pk", flat=True
1083
+ )
1084
+ )
1085
+
1086
+ cname_records = {
1087
+ ptr_record.rfc2317_cname_record
1088
+ for ptr_record in ptr_records
1089
+ if ptr_record.rfc2317_cname_record is not None
1090
+ }
1091
+ cname_zones = {cname_record.zone for cname_record in cname_records}
1092
+
1093
+ for cname_record in cname_records:
1094
+ cname_record.delete(save_zone_serial=False)
1095
+ for cname_zone in cname_zones:
1096
+ cname_zone.save_soa_serial()
1097
+ cname_zone.update_soa_record()
1098
+
1099
+ rfc2317_child_zones = list(
1100
+ self.rfc2317_child_zones.values_list("pk", flat=True)
1101
+ )
1102
+
1103
+ ipam_ip_addresses = list(
1104
+ IPAddress.objects.filter(
1105
+ netbox_dns_records__in=self.records.filter(
1106
+ ipam_ip_address__isnull=False
1107
+ )
558
1108
  )
559
- ]
560
-
561
- if get_plugin_config("netbox_dns", "feature_ipam_coupling"):
562
- # Remove coupling from IPAddress to DNS record when zone is deleted
563
- for ip in IPAddress.objects.filter(
564
- custom_field_data__ipaddress_dns_zone_id=self.pk
565
- ):
566
- ip.dns_name = ""
567
- ip.custom_field_data["ipaddress_dns_record_name"] = None
568
- ip.custom_field_data["ipaddress_dns_zone_id"] = None
569
- ip.save(update_fields=["dns_name", "custom_field_data"])
1109
+ .distinct()
1110
+ .values_list("pk", flat=True)
1111
+ )
570
1112
 
571
1113
  super().delete(*args, **kwargs)
572
1114
 
573
- for address_record in record.Record.objects.filter(pk__in=update_records):
574
- address_record.update_ptr_record()
1115
+ address_records = Record.objects.filter(pk__in=update_records).prefetch_related(
1116
+ "zone"
1117
+ )
1118
+
1119
+ for address_record in address_records:
1120
+ address_record.save(save_zone_serial=False)
1121
+ for address_zone in {address_record.zone for address_record in address_records}:
1122
+ address_zone.save_soa_serial()
1123
+ address_zone.update_soa_record()
1124
+
1125
+ ip_addresses = IPAddress.objects.filter(pk__in=ipam_ip_addresses)
1126
+ for ip_address in ip_addresses:
1127
+ update_dns_records(ip_address)
1128
+
1129
+ rfc2317_child_zones = Zone.objects.filter(pk__in=rfc2317_child_zones)
1130
+ if rfc2317_child_zones:
1131
+ for child_zone in rfc2317_child_zones:
1132
+ child_zone.update_rfc2317_parent_zone()
1133
+
1134
+ new_rfc2317_parent_zone = rfc2317_child_zones.first().rfc2317_parent_zone
1135
+ if new_rfc2317_parent_zone is not None:
1136
+ new_rfc2317_parent_zone.save_soa_serial()
1137
+ new_rfc2317_parent_zone.update_soa_record()
575
1138
 
576
1139
 
577
1140
  @receiver(m2m_changed, sender=Zone.nameservers.through)
@@ -586,6 +1149,7 @@ def update_ns_records(**kwargs):
586
1149
  @register_search
587
1150
  class ZoneIndex(SearchIndex):
588
1151
  model = Zone
1152
+
589
1153
  fields = (
590
1154
  ("name", 100),
591
1155
  ("view", 150),