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
@@ -1,45 +1,95 @@
1
1
  import ipaddress
2
+ import netaddr
2
3
 
3
4
  import dns
4
- from dns import rdata, rdatatype, rdataclass
5
5
  from dns import name as dns_name
6
+ from dns import rdata
6
7
 
7
8
  from django.core.exceptions import ValidationError
8
- from django.db import transaction, models
9
- from django.db.models import Q, ExpressionWrapper, BooleanField
10
- from django.db.models.functions import Length
11
- from django.urls import reverse
9
+ from django.db import models
10
+ from django.db.models import Q, ExpressionWrapper, BooleanField, Min
11
+ from django.conf import settings
12
+ from django.utils.translation import gettext_lazy as _
13
+ from django.core.validators import MaxValueValidator
12
14
 
13
15
  from netbox.models import NetBoxModel
16
+ from netbox.models.features import ContactsMixin
14
17
  from netbox.search import SearchIndex, register_search
18
+ from netbox.plugins.utils import get_plugin_config
15
19
  from utilities.querysets import RestrictedQuerySet
16
- from utilities.choices import ChoiceSet
17
-
18
- try:
19
- # NetBox 3.5.0 - 3.5.7, 3.5.9+
20
- from extras.plugins import get_plugin_config
21
- except ImportError:
22
- # NetBox 3.5.8
23
- from extras.plugins.utils import get_plugin_config
24
20
 
25
21
  from netbox_dns.fields import AddressField
26
- from netbox_dns.utilities import (
27
- arpa_to_prefix,
28
- name_to_unicode,
22
+ from netbox_dns.utilities import arpa_to_prefix, name_to_unicode, check_filter
23
+ from netbox_dns.validators import validate_generic_name, validate_record_value
24
+ from netbox_dns.mixins import ObjectModificationMixin
25
+ from netbox_dns.choices import (
26
+ RecordTypeChoices,
27
+ RecordStatusChoices,
28
+ RecordClassChoices,
29
29
  )
30
- from netbox_dns.validators import (
31
- validate_fqdn,
32
- validate_extended_hostname,
30
+
31
+ __all__ = (
32
+ "Record",
33
+ "RecordIndex",
33
34
  )
34
35
 
35
- # +
36
- # This is a hack designed to break cyclic imports between Record and Zone
37
- # -
38
- import netbox_dns.models.zone as zone
36
+ ZONE_ACTIVE_STATUS_LIST = get_plugin_config("netbox_dns", "zone_active_status")
37
+ RECORD_ACTIVE_STATUS_LIST = get_plugin_config("netbox_dns", "record_active_status")
38
+
39
+
40
+ def min_ttl(*ttl_list):
41
+ return min((ttl for ttl in ttl_list if ttl is not None), default=None)
42
+
43
+
44
+ def record_data_from_ip_address(ip_address, zone):
45
+ cf_data = ip_address.custom_field_data
46
+
47
+ if cf_data.get("ipaddress_dns_disabled"):
48
+ # +
49
+ # DNS record creation disabled for this address
50
+ # -
51
+ return None
52
+
53
+ if zone.view.ip_address_filter is not None and not check_filter(
54
+ ip_address, zone.view.ip_address_filter
55
+ ):
56
+ return None
57
+
58
+ data = {
59
+ "name": (
60
+ dns_name.from_text(ip_address.dns_name)
61
+ .relativize(dns_name.from_text(zone.name))
62
+ .to_text()
63
+ ),
64
+ "type": (
65
+ RecordTypeChoices.A
66
+ if ip_address.address.version == 4
67
+ else RecordTypeChoices.AAAA
68
+ ),
69
+ "value": str(ip_address.address.ip),
70
+ "status": (
71
+ RecordStatusChoices.STATUS_ACTIVE
72
+ if ip_address.status
73
+ in settings.PLUGINS_CONFIG["netbox_dns"].get(
74
+ "dnssync_ipaddress_active_status", []
75
+ )
76
+ else RecordStatusChoices.STATUS_INACTIVE
77
+ ),
78
+ }
79
+
80
+ if "ipaddress_dns_record_ttl" in cf_data:
81
+ data["ttl"] = cf_data.get("ipaddress_dns_record_ttl")
82
+
83
+ if (disable_ptr := cf_data.get("ipaddress_dns_record_disable_ptr")) is not None:
84
+ data["disable_ptr"] = disable_ptr
85
+
86
+ return data
39
87
 
40
88
 
41
89
  class RecordManager(models.Manager.from_queryset(RestrictedQuerySet)):
42
- """Special Manager for records providing the activity status annotation"""
90
+ """
91
+ Custom manager for records providing the activity status annotation
92
+ """
43
93
 
44
94
  def get_queryset(self):
45
95
  return (
@@ -48,14 +98,8 @@ class RecordManager(models.Manager.from_queryset(RestrictedQuerySet)):
48
98
  .annotate(
49
99
  active=ExpressionWrapper(
50
100
  Q(
51
- Q(zone__status__in=zone.Zone.ACTIVE_STATUS_LIST)
52
- & Q(
53
- Q(address_record__isnull=True)
54
- | Q(
55
- address_record__zone__status__in=zone.Zone.ACTIVE_STATUS_LIST
56
- )
57
- )
58
- & Q(status__in=Record.ACTIVE_STATUS_LIST)
101
+ zone__status__in=ZONE_ACTIVE_STATUS_LIST,
102
+ status__in=RECORD_ACTIVE_STATUS_LIST,
59
103
  ),
60
104
  output_field=BooleanField(),
61
105
  )
@@ -63,100 +107,124 @@ class RecordManager(models.Manager.from_queryset(RestrictedQuerySet)):
63
107
  )
64
108
 
65
109
 
66
- def initialize_choice_names(cls):
67
- for choice in cls.CHOICES:
68
- setattr(cls, choice[0], choice[0])
69
- return cls
70
-
71
-
72
- @initialize_choice_names
73
- class RecordTypeChoices(ChoiceSet):
74
- CHOICES = [
75
- (rdtype.name, rdtype.name)
76
- for rdtype in sorted(rdatatype.RdataType, key=lambda a: a.name)
77
- if not rdatatype.is_metatype(rdtype)
78
- ]
79
- SINGLETONS = [
80
- rdtype.name for rdtype in rdatatype.RdataType if rdatatype.is_singleton(rdtype)
81
- ]
82
-
83
-
84
- @initialize_choice_names
85
- class RecordClassChoices(ChoiceSet):
86
- CHOICES = [
87
- (rdclass.name, rdclass.name)
88
- for rdclass in sorted(rdataclass.RdataClass)
89
- if not rdataclass.is_metaclass(rdclass)
90
- ]
110
+ class Record(ObjectModificationMixin, ContactsMixin, NetBoxModel):
111
+ class Meta:
112
+ verbose_name = _("Record")
113
+ verbose_name_plural = _("Records")
114
+
115
+ ordering = (
116
+ "fqdn",
117
+ "zone",
118
+ "name",
119
+ "type",
120
+ "value",
121
+ "status",
122
+ )
91
123
 
124
+ objects = RecordManager()
125
+ raw_objects = RestrictedQuerySet.as_manager()
92
126
 
93
- class RecordStatusChoices(ChoiceSet):
94
- key = "Record.status"
127
+ clone_fields = (
128
+ "zone",
129
+ "type",
130
+ "name",
131
+ "value",
132
+ "status",
133
+ "ttl",
134
+ "disable_ptr",
135
+ "description",
136
+ "tenant",
137
+ )
95
138
 
96
- STATUS_ACTIVE = "active"
97
- STATUS_INACTIVE = "inactive"
139
+ def __init__(self, *args, **kwargs):
140
+ super().__init__(*args, **kwargs)
98
141
 
99
- CHOICES = [
100
- (STATUS_ACTIVE, "Active", "blue"),
101
- (STATUS_INACTIVE, "Inactive", "red"),
102
- ]
142
+ self._cleanup_ptr_record = None
103
143
 
144
+ def __str__(self):
145
+ try:
146
+ fqdn = dns_name.from_text(
147
+ self.name, origin=dns_name.from_text(self.zone.name)
148
+ ).relativize(dns_name.root)
149
+ name = fqdn.to_unicode()
150
+ except dns_name.IDNAException:
151
+ name = fqdn.to_text()
152
+ except dns_name.LabelTooLong:
153
+ name = f"{self.name[:59]}..."
104
154
 
105
- class Record(NetBoxModel):
106
- ACTIVE_STATUS_LIST = (RecordStatusChoices.STATUS_ACTIVE,)
155
+ return f"{name} [{self.type}]"
107
156
 
108
157
  unique_ptr_qs = Q(
109
158
  Q(disable_ptr=False),
110
159
  Q(Q(type=RecordTypeChoices.A) | Q(type=RecordTypeChoices.AAAA)),
111
160
  )
112
161
 
162
+ name = models.CharField(
163
+ verbose_name=_("Name"),
164
+ max_length=255,
165
+ db_collation="natural_sort",
166
+ )
113
167
  zone = models.ForeignKey(
114
- "Zone",
168
+ verbose_name=_("Zone"),
169
+ to="Zone",
115
170
  on_delete=models.CASCADE,
171
+ related_name="records",
172
+ )
173
+ fqdn = models.CharField(
174
+ verbose_name=_("FQDN"),
175
+ max_length=255,
176
+ null=True,
177
+ blank=True,
178
+ default=None,
179
+ db_collation="natural_sort",
116
180
  )
117
181
  type = models.CharField(
182
+ verbose_name=_("Type"),
118
183
  choices=RecordTypeChoices,
119
184
  max_length=10,
120
185
  )
121
- name = models.CharField(
122
- max_length=255,
123
- )
124
186
  value = models.CharField(
187
+ verbose_name=_("Value"),
125
188
  max_length=65535,
126
189
  )
127
190
  status = models.CharField(
191
+ verbose_name=_("Status"),
128
192
  max_length=50,
129
193
  choices=RecordStatusChoices,
130
194
  default=RecordStatusChoices.STATUS_ACTIVE,
131
195
  blank=False,
132
196
  )
133
197
  ttl = models.PositiveIntegerField(
134
- verbose_name="TTL",
198
+ verbose_name=_("TTL"),
135
199
  null=True,
136
200
  blank=True,
201
+ validators=[MaxValueValidator(2147483647)],
137
202
  )
138
203
  managed = models.BooleanField(
204
+ verbose_name=_("Managed"),
139
205
  null=False,
140
206
  default=False,
141
207
  )
142
- ptr_record = models.OneToOneField(
143
- "self",
208
+ ptr_record = models.ForeignKey(
209
+ verbose_name=_("PTR Record"),
210
+ to="self",
144
211
  on_delete=models.SET_NULL,
145
- related_name="address_record",
146
- verbose_name="PTR record",
212
+ related_name="address_records",
147
213
  null=True,
148
214
  blank=True,
149
215
  )
150
216
  disable_ptr = models.BooleanField(
151
- verbose_name="Disable PTR",
152
- help_text="Disable PTR record creation",
217
+ verbose_name=_("Disable PTR"),
218
+ help_text=_("Disable PTR record creation"),
153
219
  default=False,
154
220
  )
155
221
  description = models.CharField(
222
+ verbose_name=_("Description"),
156
223
  max_length=200,
157
224
  blank=True,
158
225
  )
159
226
  tenant = models.ForeignKey(
227
+ verbose_name=_("Tenant"),
160
228
  to="tenancy.Tenant",
161
229
  on_delete=models.PROTECT,
162
230
  related_name="netbox_dns_records",
@@ -164,51 +232,35 @@ class Record(NetBoxModel):
164
232
  null=True,
165
233
  )
166
234
  ip_address = AddressField(
167
- verbose_name="Related IP Address",
168
- help_text="IP address related to an address (A/AAAA) or PTR record",
235
+ verbose_name=_("Related IP Address"),
236
+ help_text=_("IP address related to an address (A/AAAA) or PTR record"),
169
237
  blank=True,
170
238
  null=True,
171
239
  )
172
240
  ipam_ip_address = models.ForeignKey(
173
- verbose_name="IPAM IP Address",
241
+ verbose_name=_("IPAM IP Address"),
174
242
  to="ipam.IPAddress",
175
- on_delete=models.CASCADE,
243
+ on_delete=models.SET_NULL,
176
244
  related_name="netbox_dns_records",
177
245
  blank=True,
178
246
  null=True,
179
247
  )
180
- objects = RecordManager()
181
- raw_objects = RestrictedQuerySet.as_manager()
182
-
183
- clone_fields = [
184
- "zone",
185
- "type",
186
- "name",
187
- "value",
188
- "status",
189
- "ttl",
190
- "disable_ptr",
191
- "description",
192
- ]
193
-
194
- class Meta:
195
- ordering = ("zone", "name", "type", "value", "status")
248
+ rfc2317_cname_record = models.ForeignKey(
249
+ verbose_name=_("RFC2317 CNAME Record"),
250
+ to="self",
251
+ on_delete=models.SET_NULL,
252
+ related_name="rfc2317_ptr_records",
253
+ null=True,
254
+ blank=True,
255
+ )
196
256
 
197
- def __str__(self):
198
- try:
199
- name = (
200
- dns_name.from_text(
201
- self.name, origin=dns_name.from_text(self.zone.name, origin=None)
202
- )
203
- .relativize(dns_name.root)
204
- .to_unicode()
205
- )
206
- except dns_name.IDNAException:
207
- name = self.name
208
- except dns_name.LabelTooLong as exc:
209
- name = f"{self.name[:59]}..."
257
+ @property
258
+ def cleanup_ptr_record(self):
259
+ return self._cleanup_ptr_record
210
260
 
211
- return f"{name} [{self.type}]"
261
+ @cleanup_ptr_record.setter
262
+ def cleanup_ptr_record(self, ptr_record):
263
+ self._cleanup_ptr_record = ptr_record
212
264
 
213
265
  @property
214
266
  def display_name(self):
@@ -217,15 +269,15 @@ class Record(NetBoxModel):
217
269
  def get_status_color(self):
218
270
  return RecordStatusChoices.colors.get(self.status)
219
271
 
220
- def get_absolute_url(self):
221
- return reverse("plugins:netbox_dns:record", kwargs={"pk": self.id})
222
-
223
272
  @property
224
- def fqdn(self):
225
- zone = dns_name.from_text(self.zone.name)
226
- name = dns_name.from_text(self.name, origin=zone)
273
+ def value_fqdn(self):
274
+ if self.type not in (RecordTypeChoices.CNAME, RecordTypeChoices.NS):
275
+ return None
227
276
 
228
- return name.to_text()
277
+ _zone = dns_name.from_text(self.zone.name)
278
+ value_fqdn = dns_name.from_text(self.value, origin=_zone)
279
+
280
+ return value_fqdn.to_text()
229
281
 
230
282
  @property
231
283
  def address_from_name(self):
@@ -235,11 +287,19 @@ class Record(NetBoxModel):
235
287
 
236
288
  return None
237
289
 
290
+ @property
291
+ def address_from_rfc2317_name(self):
292
+ prefix = self.zone.rfc2317_prefix
293
+ if prefix is not None:
294
+ return ".".join(str(prefix.ip).split(".")[0:3] + [self.name])
295
+
296
+ return None
297
+
238
298
  @property
239
299
  def is_active(self):
240
300
  return (
241
- self.status in Record.ACTIVE_STATUS_LIST
242
- and self.zone.status in zone.Zone.ACTIVE_STATUS_LIST
301
+ self.status in RECORD_ACTIVE_STATUS_LIST
302
+ and self.zone.status in ZONE_ACTIVE_STATUS_LIST
243
303
  )
244
304
 
245
305
  @property
@@ -251,105 +311,357 @@ class Record(NetBoxModel):
251
311
  return self.type == RecordTypeChoices.PTR
252
312
 
253
313
  @property
254
- def ptr_zone(self):
255
- ptr_zones = zone.Zone.objects.filter(
256
- self.zone.view_filter, arpa_network__net_contains=self.value
257
- ).order_by(Length("name").desc())
314
+ def rfc2317_ptr_name(self):
315
+ return self.value.split(".")[-1]
258
316
 
259
- if len(ptr_zones):
260
- return ptr_zones[0]
317
+ @property
318
+ def rfc2317_ptr_cname_name(self):
319
+ assert self.type == RecordTypeChoices.A
320
+ if (
321
+ self.ptr_record is not None
322
+ and self.ptr_record.zone.rfc2317_parent_zone is not None
323
+ ):
324
+ return dns_name.from_text(
325
+ ipaddress.ip_address(self.value).reverse_pointer
326
+ ).relativize(
327
+ dns_name.from_text(self.ptr_record.zone.rfc2317_parent_zone.name)
328
+ )
261
329
 
262
330
  return None
263
331
 
264
- def update_ptr_record(self):
332
+ @property
333
+ def ptr_zone(self):
334
+ if self.type == RecordTypeChoices.A:
335
+ ptr_zone = (
336
+ self.zone.view.zones.filter(
337
+ rfc2317_prefix__net_contains=self.value,
338
+ )
339
+ .order_by("rfc2317_prefix__net_mask_length")
340
+ .last()
341
+ )
342
+
343
+ if ptr_zone is not None:
344
+ return ptr_zone
345
+
346
+ ptr_zone = (
347
+ self.zone.view.zones.filter(arpa_network__net_contains=self.value)
348
+ .order_by("arpa_network__net_mask_length")
349
+ .last()
350
+ )
351
+
352
+ return ptr_zone
353
+
354
+ @property
355
+ def is_delegation_record(self):
356
+ return self in self.zone.delegation_records
357
+
358
+ def refresh_ptr_record(
359
+ self, ptr_record=None, update_rfc2317_cname=True, save_zone_serial=True
360
+ ):
361
+ if ptr_record is None:
362
+ return
363
+
364
+ if not ptr_record.address_records.exists():
365
+ if ptr_record.rfc2317_cname_record is not None:
366
+ ptr_record.remove_from_rfc2317_cname_record()
367
+
368
+ ptr_record.delete(save_zone_serial=save_zone_serial)
369
+
370
+ elif update_rfc2317_cname:
371
+ ptr_record.update_rfc2317_cname_record(save_zone_serial=save_zone_serial)
372
+
373
+ def update_ptr_record(self, update_rfc2317_cname=True, save_zone_serial=True):
265
374
  ptr_zone = self.ptr_zone
266
375
 
376
+ # +
377
+ # Check whether a PTR record is optioned for and return if that is not the
378
+ # case.
379
+ # -
267
380
  if (
268
381
  ptr_zone is None
269
382
  or self.disable_ptr
270
383
  or not self.is_active
271
- or self.name == "*"
384
+ or self.name.startswith("*")
272
385
  ):
273
- if self.ptr_record is not None:
274
- with transaction.atomic():
275
- self.ptr_record.delete()
276
- self.ptr_record = None
386
+ self.cleanup_ptr_record = self.ptr_record
387
+ self.ptr_record = None
277
388
  return
278
389
 
279
- ptr_name = dns_name.from_text(
280
- ipaddress.ip_address(self.value).reverse_pointer
281
- ).relativize(dns_name.from_text(ptr_zone.name))
390
+ # +
391
+ # Determine the ptr_name and ptr_value related to the ptr_zone. RFC2317
392
+ # PTR names and zones need to be handled differently.
393
+ # -
394
+ if ptr_zone.is_rfc2317_zone:
395
+ ptr_name = self.rfc2317_ptr_name
396
+ else:
397
+ ptr_name = (
398
+ dns_name.from_text(ipaddress.ip_address(self.value).reverse_pointer)
399
+ .relativize(dns_name.from_text(ptr_zone.name))
400
+ .to_text()
401
+ )
282
402
  ptr_value = self.fqdn
283
- ptr_record = self.ptr_record
284
403
 
285
- with transaction.atomic():
286
- if ptr_record is not None:
287
- if ptr_record.zone.pk != ptr_zone.pk:
288
- ptr_record.delete()
289
- ptr_record = None
290
-
291
- else:
292
- if (
293
- ptr_record.name != ptr_name
294
- or ptr_record.value != ptr_value
295
- or ptr_record.ttl != self.ttl
296
- ):
297
- ptr_record.name = ptr_name
298
- ptr_record.value = ptr_value
299
- ptr_record.ttl = self.ttl
300
- ptr_record.save()
301
-
302
- if ptr_record is None:
303
- ptr_record = Record.objects.create(
304
- zone_id=ptr_zone.pk,
305
- type=RecordTypeChoices.PTR,
404
+ # +
405
+ # If there is an existing and matching PTR record there is nothing to be done.
406
+ # -
407
+ if (ptr_record := self.ptr_record) is not None:
408
+ if (
409
+ ptr_record.zone == ptr_zone
410
+ and ptr_record.name == ptr_name
411
+ and ptr_record.value == ptr_value
412
+ and ptr_record.ttl == self.ttl
413
+ ):
414
+ return
415
+
416
+ # +
417
+ # If the existing PTR record no longer matches the address record,
418
+ # check whether there is an existing PTR record that does. In that
419
+ # case, mark the old PTR record for cleanup and use the existing one.
420
+ # -
421
+ try:
422
+ existing_ptr_record = Record.objects.get(
306
423
  name=ptr_name,
307
- ttl=self.ttl,
424
+ zone=ptr_zone,
425
+ type=RecordTypeChoices.PTR,
308
426
  value=ptr_value,
309
- managed=True,
310
427
  )
311
428
 
312
- self.ptr_record = ptr_record
313
- if self.pk:
314
- super().save()
429
+ self.cleanup_ptr_record = self.ptr_record
430
+ self.ptr_record = existing_ptr_record
431
+ ptr_record = self.ptr_record
432
+
433
+ except Record.DoesNotExist:
434
+ pass
435
+
436
+ # +
437
+ # If there is an RFC2317 CNAME for the PTR record and it is either
438
+ # not required or needs to be changed, remove it.
439
+ # -
440
+ if (
441
+ ptr_record.zone.pk != ptr_zone.pk or not ptr_record.zone.is_rfc2317_zone
442
+ ) and ptr_record.rfc2317_cname_record is not None:
443
+ ptr_record.rfc2317_cname_record.delete(
444
+ save_zone_serial=save_zone_serial
445
+ )
446
+ ptr_record.rfc2317_cname_record = None
447
+
448
+ # +
449
+ # If the PTR record is used exclusively by the address record it can be
450
+ # modified to match the new name, zone, value and TTL.
451
+ # -
452
+ if ptr_record.address_records.count() == 1:
453
+ ptr_record.snapshot()
454
+ ptr_record.zone = ptr_zone
455
+ ptr_record.name = ptr_name
456
+ ptr_record.value = ptr_value
457
+ ptr_record.ttl = self.ttl
458
+ ptr_record.managed = True
459
+ ptr_record.save(
460
+ update_rfc2317_cname=update_rfc2317_cname,
461
+ save_zone_serial=save_zone_serial,
462
+ )
463
+ return
315
464
 
316
- def validate_name(self):
465
+ # +
466
+ # Either there was no PTR record or the existing PTR record could not be re-used,
467
+ # so we need to either find a matching PTR record or create a new one.
468
+ # -
317
469
  try:
318
- zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
319
- name = dns_name.from_text(self.name, origin=None)
320
- fqdn = dns_name.from_text(self.name, origin=zone)
470
+ ptr_record = Record.objects.get(
471
+ name=ptr_name,
472
+ zone=ptr_zone,
473
+ type=RecordTypeChoices.PTR,
474
+ value=ptr_value,
475
+ )
321
476
 
322
- zone.to_unicode()
323
- name.to_unicode()
477
+ # +
478
+ # If no existing PTR record could be found in the database, create a new
479
+ # one from scratch.
480
+ # -
481
+ except Record.DoesNotExist:
482
+ ptr_record = Record(
483
+ zone_id=ptr_zone.pk,
484
+ type=RecordTypeChoices.PTR,
485
+ name=ptr_name,
486
+ ttl=self.ttl,
487
+ value=ptr_value,
488
+ managed=True,
489
+ )
490
+ ptr_record.save(
491
+ update_rfc2317_cname=update_rfc2317_cname,
492
+ save_zone_serial=save_zone_serial,
493
+ )
324
494
 
325
- self.name = name.to_text()
495
+ self.ptr_record = ptr_record
326
496
 
327
- except dns.exception.DNSException as exc:
497
+ def remove_from_rfc2317_cname_record(self, save_zone_serial=True):
498
+ if self.rfc2317_cname_record.pk:
499
+ rfc2317_ptr_records = self.rfc2317_cname_record.rfc2317_ptr_records.exclude(
500
+ pk=self.pk
501
+ )
502
+
503
+ if rfc2317_ptr_records:
504
+ self.rfc2317_cname_record.ttl = rfc2317_ptr_records.aggregate(
505
+ Min("ttl")
506
+ ).get("ttl__min")
507
+ self.rfc2317_cname_record.save(
508
+ update_fields=["ttl"], save_zone_serial=save_zone_serial
509
+ )
510
+ else:
511
+ self.rfc2317_cname_record.delete()
512
+
513
+ def update_rfc2317_cname_record(self, save_zone_serial=True):
514
+ if self.zone.rfc2317_parent_managed:
515
+ cname_name = (
516
+ dns_name.from_text(
517
+ ipaddress.ip_address(self.ip_address).reverse_pointer
518
+ )
519
+ .relativize(dns_name.from_text(self.zone.rfc2317_parent_zone.name))
520
+ .to_text()
521
+ )
522
+
523
+ if self.rfc2317_cname_record is not None:
524
+ if self.rfc2317_cname_record.name == cname_name:
525
+ self.rfc2317_cname_record.zone = self.zone.rfc2317_parent_zone
526
+ self.rfc2317_cname_record.value = self.fqdn
527
+ self.rfc2317_cname_record.ttl = min_ttl(
528
+ self.rfc2317_cname_record.rfc2317_ptr_records.exclude(
529
+ pk=self.pk
530
+ )
531
+ .aggregate(Min("ttl"))
532
+ .get("ttl__min"),
533
+ self.ttl,
534
+ )
535
+ self.rfc2317_cname_record.save(save_zone_serial=save_zone_serial)
536
+
537
+ return
538
+
539
+ self.remove_from_rfc2317_cname_record(save_zone_serial=save_zone_serial)
540
+
541
+ rfc2317_cname_record = self.zone.rfc2317_parent_zone.records.filter(
542
+ name=cname_name,
543
+ type=RecordTypeChoices.CNAME,
544
+ managed=True,
545
+ value=self.fqdn,
546
+ ).first()
547
+
548
+ if rfc2317_cname_record is not None:
549
+ rfc2317_cname_record.ttl = min_ttl(
550
+ rfc2317_cname_record.rfc2317_ptr_records.exclude(pk=self.pk)
551
+ .aggregate(Min("ttl"))
552
+ .get("ttl__min"),
553
+ self.ttl,
554
+ )
555
+ rfc2317_cname_record.save(
556
+ update_fields=["ttl"], save_zone_serial=save_zone_serial
557
+ )
558
+
559
+ else:
560
+ rfc2317_cname_record = Record(
561
+ name=cname_name,
562
+ type=RecordTypeChoices.CNAME,
563
+ zone=self.zone.rfc2317_parent_zone,
564
+ managed=True,
565
+ value=self.fqdn,
566
+ ttl=self.ttl,
567
+ )
568
+ rfc2317_cname_record.save(save_zone_serial=save_zone_serial)
569
+
570
+ self.rfc2317_cname_record = rfc2317_cname_record
571
+
572
+ else:
573
+ if self.rfc2317_cname_record is not None:
574
+ self.rfc2317_cname_record.delete(save_zone_serial=save_zone_serial)
575
+ self.rfc2317_cname_record = None
576
+
577
+ def update_from_ip_address(self, ip_address, zone=None):
578
+ """
579
+ Update an address record according to data from an IPAddress object.
580
+
581
+ Returns a tuple of two booleans: (update, delete).
582
+
583
+ update: The record was updated and needs to be cleaned and/or saved
584
+ delete: The record is no longer needed and needs to be deleted
585
+ """
586
+ if zone is None:
587
+ zone = self.zone
588
+
589
+ data = record_data_from_ip_address(ip_address, zone)
590
+
591
+ if data is None:
592
+ return False, True
593
+
594
+ if all((getattr(self, attr) == data[attr] for attr in data.keys())):
595
+ return False, False
596
+
597
+ for attr, value in data.items():
598
+ setattr(self, attr, value)
599
+
600
+ return True, False
601
+
602
+ @classmethod
603
+ def create_from_ip_address(cls, ip_address, zone):
604
+ data = record_data_from_ip_address(ip_address, zone)
605
+
606
+ if data is None:
607
+ return
608
+
609
+ return Record(
610
+ zone=zone,
611
+ managed=True,
612
+ ipam_ip_address=ip_address,
613
+ **data,
614
+ )
615
+
616
+ def update_fqdn(self, zone=None):
617
+ if zone is None:
618
+ zone = self.zone
619
+
620
+ _zone = dns_name.from_text(zone.name, origin=dns_name.root)
621
+ name = dns_name.from_text(self.name, origin=None)
622
+ fqdn = dns_name.from_text(self.name, origin=_zone)
623
+
624
+ if not fqdn.is_subdomain(_zone):
328
625
  raise ValidationError(
329
626
  {
330
- "name": str(exc),
627
+ "name": _("{name} is not a name in {zone}").format(
628
+ name=self.name, zone=zone.name
629
+ ),
331
630
  }
332
631
  )
333
632
 
334
- if not fqdn.is_subdomain(zone):
633
+ _zone.to_unicode()
634
+ name.to_unicode()
635
+
636
+ self.name = name.relativize(_zone).to_text()
637
+ self.fqdn = fqdn.to_text()
638
+
639
+ def validate_name(self, new_zone=None):
640
+ if new_zone is None:
641
+ new_zone = self.zone
642
+
643
+ try:
644
+ self.update_fqdn(zone=new_zone)
645
+
646
+ except dns.exception.DNSException as exc:
335
647
  raise ValidationError(
336
648
  {
337
- "name": f"{self.name} is not a name in {self.zone.name}",
649
+ "name": str(exc),
338
650
  }
339
651
  )
340
652
 
341
653
  if self.type not in get_plugin_config(
342
- "netbox_dns", "tolerate_non_rfc1035_types", default=list()
654
+ "netbox_dns", "tolerate_non_rfc1035_types", default=[]
343
655
  ):
344
656
  try:
345
- validate_extended_hostname(
657
+ validate_generic_name(
346
658
  self.name,
347
659
  (
348
660
  self.type
349
661
  in get_plugin_config(
350
662
  "netbox_dns",
351
663
  "tolerate_leading_underscore_types",
352
- default=list(),
664
+ default=[],
353
665
  )
354
666
  ),
355
667
  )
@@ -358,74 +670,265 @@ class Record(NetBoxModel):
358
670
  {
359
671
  "name": exc,
360
672
  }
361
- ) from None
673
+ )
362
674
 
363
675
  def validate_value(self):
364
- if self.type in (RecordTypeChoices.PTR):
365
- try:
366
- validate_fqdn(self.value)
367
- except ValidationError as exc:
368
- raise ValidationError(
369
- {
370
- "value": exc,
371
- }
372
- ) from None
373
-
374
676
  try:
375
- rdata.from_text(RecordClassChoices.IN, self.type, self.value)
376
- except dns.exception.SyntaxError as exc:
377
- raise ValidationError(
378
- {
379
- "value": f"Record value {self.value} is not a valid value for a {self.type} record: {exc}."
380
- }
381
- ) from None
677
+ validate_record_value(self)
678
+ except ValidationError as exc:
679
+ raise ValidationError({"value": exc})
382
680
 
383
- def check_unique(self):
681
+ def check_unique_record(self, new_zone=None):
384
682
  if not get_plugin_config("netbox_dns", "enforce_unique_records", False):
385
683
  return
386
684
 
387
685
  if not self.is_active:
388
686
  return
389
687
 
390
- records = Record.objects.filter(
391
- zone=self.zone,
392
- name=self.name,
688
+ if new_zone is None:
689
+ new_zone = self.zone
690
+
691
+ records = new_zone.records.filter(
692
+ name__iexact=self.name,
393
693
  type=self.type,
394
694
  value=self.value,
395
- status__in=Record.ACTIVE_STATUS_LIST,
695
+ status__in=RECORD_ACTIVE_STATUS_LIST,
396
696
  )
397
- if len(records):
697
+
698
+ if not self._state.adding:
699
+ records = records.exclude(pk=self.pk)
700
+
701
+ if records.exists():
702
+ if self.ipam_ip_address is not None:
703
+ if not records.filter(
704
+ ipam_ip_address__isnull=True
705
+ ).exists() or get_plugin_config(
706
+ "netbox_dns", "dnssync_conflict_deactivate", False
707
+ ):
708
+ return
709
+
398
710
  raise ValidationError(
399
711
  {
400
- "value": f"There is already an active {self.type} record for name {self.name} in zone {self.zone} with value {self.value}."
712
+ "value": _(
713
+ "There is already an active {type} record for name {name} in zone {zone} with value {value}."
714
+ ).format(
715
+ type=self.type, name=self.name, zone=self.zone, value=self.value
716
+ )
401
717
  }
402
- ) from None
718
+ )
719
+
720
+ @property
721
+ def absolute_value(self):
722
+ if self.type in RecordTypeChoices.CUSTOM_TYPES:
723
+ return self.value
724
+
725
+ zone = dns_name.from_text(self.zone.name)
726
+ rr = rdata.from_text(RecordClassChoices.IN, self.type, self.value)
727
+
728
+ match self.type:
729
+ case (
730
+ RecordTypeChoices.CNAME
731
+ | RecordTypeChoices.DNAME
732
+ | RecordTypeChoices.NS
733
+ | RecordTypeChoices.HTTPS
734
+ | RecordTypeChoices.SRV
735
+ | RecordTypeChoices.SVCB
736
+ ):
737
+ return rr.replace(target=rr.target.derelativize(zone)).to_text()
738
+
739
+ case RecordTypeChoices.MX | RecordTypeChoices.RT | RecordTypeChoices.KX:
740
+ return rr.replace(exchange=rr.exchange.derelativize(zone)).to_text()
741
+
742
+ case RecordTypeChoices.RP:
743
+ return rr.replace(
744
+ mbox=rr.mbox.derelativize(zone), txt=rr.txt.derelativize(zone)
745
+ ).to_text()
746
+
747
+ case RecordTypeChoices.NAPTR:
748
+ return rr.replace(
749
+ replacement=rr.replacement.derelativize(zone)
750
+ ).to_text()
751
+
752
+ case RecordTypeChoices.PX:
753
+ return rr.replace(
754
+ map822=rr.map822.derelativize(zone),
755
+ mapx400=rr.mapx400.derelativize(zone),
756
+ ).to_text()
757
+
758
+ return self.value
759
+
760
+ def handle_conflicting_address_records(self):
761
+ if self.ipam_ip_address is None or not self.is_active:
762
+ return
763
+
764
+ if not get_plugin_config("netbox_dns", "dnssync_conflict_deactivate", False):
765
+ return
766
+
767
+ records = self.zone.records.filter(
768
+ name=self.name,
769
+ type=self.type,
770
+ value=self.value,
771
+ status__in=RECORD_ACTIVE_STATUS_LIST,
772
+ ipam_ip_address__isnull=True,
773
+ )
774
+
775
+ for record in records:
776
+ record.status = RecordStatusChoices.STATUS_INACTIVE
777
+ record.save(update_fields=["status"])
778
+
779
+ def check_unique_rrset_ttl(self):
780
+ if not self._state.adding:
781
+ return
782
+
783
+ if not get_plugin_config("netbox_dns", "enforce_unique_rrset_ttl", False):
784
+ return
785
+
786
+ if self.ipam_ip_address is not None and get_plugin_config(
787
+ "netbox_dns", "dnssync_conflict_deactivate", False
788
+ ):
789
+ return
790
+
791
+ if self.type == RecordTypeChoices.PTR and self.managed:
792
+ return
793
+
794
+ records = (
795
+ self.zone.records.filter(
796
+ name=self.name,
797
+ type=self.type,
798
+ )
799
+ .exclude(ttl=self.ttl)
800
+ .exclude(type=RecordTypeChoices.PTR, managed=True)
801
+ .exclude(status=RecordStatusChoices.STATUS_INACTIVE)
802
+ )
803
+
804
+ if self.ipam_ip_address is not None:
805
+ records = records.exclude(ipam_ip_address__isnull=False)
806
+
807
+ if not records.exists():
808
+ return
809
+
810
+ conflicting_ttls = {record.ttl for record in records}
811
+ if len(conflicting_ttls) == 1 and self.ttl is None:
812
+ self.ttl = conflicting_ttls.pop()
813
+ return
814
+
815
+ raise ValidationError(
816
+ {
817
+ "ttl": _(
818
+ "There is at least one active {type} record for name {name} in zone {zone} and TTL is different ({ttls})."
819
+ ).format(
820
+ type=self.type,
821
+ name=self.name,
822
+ zone=self.zone,
823
+ ttls=", ".join(str(ttl) for ttl in conflicting_ttls),
824
+ )
825
+ }
826
+ )
827
+
828
+ def update_rrset_ttl(self, ttl=None):
829
+ if self._state.adding:
830
+ return
831
+
832
+ if not get_plugin_config("netbox_dns", "enforce_unique_rrset_ttl", False):
833
+ return
834
+
835
+ if self.type == RecordTypeChoices.PTR and self.managed:
836
+ return
837
+
838
+ if ttl is None:
839
+ ttl = self.ttl
403
840
 
404
- def clean_fields(self, *args, **kwargs):
841
+ records = (
842
+ self.zone.records.filter(
843
+ name=self.name,
844
+ type=self.type,
845
+ )
846
+ .exclude(pk=self.pk)
847
+ .exclude(ttl=ttl)
848
+ .exclude(type=RecordTypeChoices.PTR, managed=True)
849
+ .exclude(status=RecordStatusChoices.STATUS_INACTIVE)
850
+ )
851
+
852
+ for record in records:
853
+ record.ttl = ttl
854
+ record.save(update_fields=["ttl"], update_rrset_ttl=False)
855
+
856
+ def clean_fields(self, exclude=None):
405
857
  self.type = self.type.upper()
406
- super().clean_fields(*args, **kwargs)
858
+ if get_plugin_config("netbox_dns", "convert_names_to_lowercase", False):
859
+ self.name = self.name.lower()
407
860
 
408
- def clean(self, *args, **kwargs):
409
- self.validate_name()
861
+ super().clean_fields(exclude=exclude)
862
+
863
+ def clean(self, *args, new_zone=None, **kwargs):
864
+ self.validate_name(new_zone=new_zone)
410
865
  self.validate_value()
411
- self.check_unique()
866
+ self.check_unique_record(new_zone=new_zone)
867
+ if self._state.adding:
868
+ self.check_unique_rrset_ttl()
869
+
870
+ if not self.is_address_record:
871
+ self.disable_ptr = False
412
872
 
413
873
  if not self.is_active:
414
874
  return
415
875
 
416
- records = (
417
- Record.objects.filter(name=self.name, zone=self.zone)
418
- .exclude(pk=self.pk)
419
- .exclude(active=False)
876
+ records = self.zone.records.filter(name=self.name, active=True).exclude(
877
+ pk=self.pk
420
878
  )
421
879
 
880
+ if self.type == RecordTypeChoices.A and not self.disable_ptr:
881
+ ptr_zone = self.ptr_zone
882
+
883
+ if (
884
+ ptr_zone is not None
885
+ and ptr_zone.is_rfc2317_zone
886
+ and ptr_zone.rfc2317_parent_managed
887
+ ):
888
+ ptr_cname_zone = ptr_zone.rfc2317_parent_zone
889
+ ptr_cname_name = self.rfc2317_ptr_cname_name
890
+ ptr_fqdn = dns_name.from_text(
891
+ self.rfc2317_ptr_name, origin=dns_name.from_text(ptr_zone.name)
892
+ )
893
+
894
+ if (
895
+ ptr_cname_zone.records.filter(
896
+ name=ptr_cname_name,
897
+ active=True,
898
+ )
899
+ .exclude(
900
+ type=RecordTypeChoices.CNAME,
901
+ value=ptr_fqdn,
902
+ )
903
+ .exclude(type=RecordTypeChoices.NSEC)
904
+ .exists()
905
+ ):
906
+ raise ValidationError(
907
+ {
908
+ "value": _(
909
+ "There is already an active record for name {name} in zone {zone}, RFC2317 CNAME is not allowed."
910
+ ).format(name=ptr_cname_name, zone=ptr_cname_zone)
911
+ }
912
+ )
913
+
914
+ if self.type == RecordTypeChoices.SOA and self.name != "@":
915
+ raise ValidationError(
916
+ {
917
+ "name": _(
918
+ "SOA records are only allowed with name @ and are created automatically by NetBox DNS"
919
+ )
920
+ }
921
+ )
922
+
422
923
  if self.type == RecordTypeChoices.CNAME:
423
924
  if records.exclude(type=RecordTypeChoices.NSEC).exists():
424
925
  raise ValidationError(
425
926
  {
426
- "type": f"There is already an active record for name {self.name} in zone {self.zone}, CNAME is not allowed."
927
+ "type": _(
928
+ "There is already an active record for name {name} in zone {zone}, CNAME is not allowed."
929
+ ).format(name=self.name, zone=self.zone)
427
930
  }
428
- ) from None
931
+ )
429
932
 
430
933
  elif (
431
934
  records.filter(type=RecordTypeChoices.CNAME).exists()
@@ -433,57 +936,101 @@ class Record(NetBoxModel):
433
936
  ):
434
937
  raise ValidationError(
435
938
  {
436
- "type": f"There is already an active CNAME record for name {self.name} in zone {self.zone}, no other record allowed."
939
+ "type": _(
940
+ "There is already an active CNAME record for name {name} in zone {zone}, no other record allowed."
941
+ ).format(name=self.name, zone=self.zone)
437
942
  }
438
- ) from None
943
+ )
439
944
 
440
945
  elif self.type in RecordTypeChoices.SINGLETONS:
441
946
  if records.filter(type=self.type).exists():
442
947
  raise ValidationError(
443
948
  {
444
- "type": f"There is already an active {self.type} record for name {self.name} in zone {self.zone}, more than one are not allowed."
949
+ "type": _(
950
+ "There is already an active {type} record for name {name} in zone {zone}, more than one are not allowed."
951
+ ).format(type=self.type, name=self.name, zone=self.zone)
445
952
  }
446
- ) from None
953
+ )
447
954
 
448
- def save(self, *args, **kwargs):
955
+ super().clean(*args, **kwargs)
956
+
957
+ def save(
958
+ self,
959
+ *args,
960
+ update_rfc2317_cname=True,
961
+ save_zone_serial=True,
962
+ update_rrset_ttl=True,
963
+ **kwargs,
964
+ ):
449
965
  self.full_clean()
450
966
 
967
+ if not self._state.adding and update_rrset_ttl:
968
+ self.update_rrset_ttl()
969
+
451
970
  if self.is_ptr_record:
452
- self.ip_address = self.address_from_name
971
+ if self.zone.is_rfc2317_zone:
972
+ self.ip_address = self.address_from_rfc2317_name
973
+ if update_rfc2317_cname:
974
+ self.update_rfc2317_cname_record(save_zone_serial=save_zone_serial)
975
+ else:
976
+ self.ip_address = self.address_from_name
977
+
453
978
  elif self.is_address_record:
454
- self.ip_address = self.value
979
+ self.ip_address = netaddr.IPAddress(self.value)
455
980
  else:
456
981
  self.ip_address = None
457
982
 
458
983
  if self.is_address_record:
459
- self.update_ptr_record()
984
+ self.handle_conflicting_address_records()
985
+ self.update_ptr_record(
986
+ update_rfc2317_cname=update_rfc2317_cname,
987
+ save_zone_serial=save_zone_serial,
988
+ )
460
989
  elif self.ptr_record is not None:
461
- self.ptr_record.delete()
990
+ self.cleanup_ptr_record = self.ptr_record
462
991
  self.ptr_record = None
463
992
 
464
- super().save(*args, **kwargs)
993
+ changed_fields = self.changed_fields
994
+ if changed_fields is None or changed_fields:
995
+ super().save(*args, **kwargs)
465
996
 
466
- zone = self.zone
467
- if self.type != RecordTypeChoices.SOA and zone.soa_serial_auto:
468
- zone.update_serial()
997
+ self.refresh_ptr_record(
998
+ self.cleanup_ptr_record,
999
+ update_rfc2317_cname=update_rfc2317_cname,
1000
+ save_zone_serial=save_zone_serial,
1001
+ )
1002
+
1003
+ if self.type != RecordTypeChoices.SOA and self.zone.soa_serial_auto:
1004
+ self.zone.update_serial(save_zone_serial=save_zone_serial)
469
1005
 
470
- def delete(self, *args, **kwargs):
471
- if self.ptr_record:
472
- self.ptr_record.delete()
1006
+ def delete(self, *args, save_zone_serial=True, **kwargs):
1007
+ if self.rfc2317_cname_record:
1008
+ self.remove_from_rfc2317_cname_record(save_zone_serial=save_zone_serial)
1009
+
1010
+ ptr_record = self.ptr_record
473
1011
 
474
1012
  super().delete(*args, **kwargs)
475
1013
 
476
- zone = self.zone
477
- if zone.soa_serial_auto:
478
- zone.update_serial()
1014
+ self.refresh_ptr_record(
1015
+ ptr_record,
1016
+ update_rfc2317_cname=True,
1017
+ save_zone_serial=save_zone_serial,
1018
+ )
1019
+
1020
+ _zone = self.zone
1021
+ if _zone.soa_serial_auto:
1022
+ _zone.update_serial(save_zone_serial=save_zone_serial)
479
1023
 
480
1024
 
481
1025
  @register_search
482
1026
  class RecordIndex(SearchIndex):
483
1027
  model = Record
1028
+
484
1029
  fields = (
485
- ("name", 100),
1030
+ ("fqdn", 100),
1031
+ ("name", 120),
486
1032
  ("value", 150),
487
1033
  ("zone", 200),
488
1034
  ("type", 200),
1035
+ ("description", 500),
489
1036
  )