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