netbox-plugin-dns 1.1.3__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of netbox-plugin-dns might be problematic. Click here for more details.
- netbox_dns/__init__.py +26 -5
- netbox_dns/api/views.py +1 -1
- netbox_dns/choices/zone.py +2 -0
- netbox_dns/fields/address.py +3 -21
- netbox_dns/filtersets/record.py +3 -0
- netbox_dns/filtersets/zone.py +1 -2
- netbox_dns/forms/record.py +29 -13
- netbox_dns/forms/view.py +2 -3
- netbox_dns/forms/zone.py +15 -10
- netbox_dns/forms/zone_template.py +5 -5
- netbox_dns/locale/de/LC_MESSAGES/django.mo +0 -0
- netbox_dns/models/nameserver.py +4 -8
- netbox_dns/models/record.py +26 -41
- netbox_dns/models/record_template.py +5 -5
- netbox_dns/models/view.py +2 -3
- netbox_dns/models/zone.py +96 -39
- netbox_dns/signals/ipam_dnssync.py +1 -1
- netbox_dns/tables/record.py +14 -2
- netbox_dns/tables/zone.py +1 -2
- netbox_dns/template_content.py +16 -0
- netbox_dns/templates/netbox_dns/record.html +12 -0
- netbox_dns/templates/netbox_dns/view.html +1 -1
- netbox_dns/templates/netbox_dns/zone/delegation_record.html +18 -0
- netbox_dns/templates/netbox_dns/zone.html +1 -1
- netbox_dns/utilities/__init__.py +1 -0
- netbox_dns/utilities/dns.py +12 -0
- netbox_dns/utilities/ipam_dnssync.py +10 -13
- netbox_dns/validators/dns_value.py +47 -6
- netbox_dns/views/nameserver.py +3 -3
- netbox_dns/views/record.py +44 -11
- netbox_dns/views/registrar.py +1 -1
- netbox_dns/views/registration_contact.py +1 -1
- netbox_dns/views/view.py +2 -2
- netbox_dns/views/zone.py +49 -20
- {netbox_plugin_dns-1.1.3.dist-info → netbox_plugin_dns-1.1.5.dist-info}/METADATA +3 -2
- {netbox_plugin_dns-1.1.3.dist-info → netbox_plugin_dns-1.1.5.dist-info}/RECORD +39 -37
- {netbox_plugin_dns-1.1.3.dist-info → netbox_plugin_dns-1.1.5.dist-info}/WHEEL +1 -1
- {netbox_plugin_dns-1.1.3.dist-info → netbox_plugin_dns-1.1.5.dist-info}/LICENSE +0 -0
- {netbox_plugin_dns-1.1.3.dist-info → netbox_plugin_dns-1.1.5.dist-info}/top_level.txt +0 -0
|
@@ -133,17 +133,17 @@ class RecordTemplate(NetBoxModel):
|
|
|
133
133
|
{
|
|
134
134
|
"record_name": exc,
|
|
135
135
|
}
|
|
136
|
-
)
|
|
136
|
+
)
|
|
137
137
|
|
|
138
138
|
def validate_value(self):
|
|
139
139
|
try:
|
|
140
|
-
validate_record_value(self
|
|
140
|
+
validate_record_value(self)
|
|
141
141
|
except ValidationError as exc:
|
|
142
|
-
raise ValidationError({"value": exc})
|
|
142
|
+
raise ValidationError({"value": exc})
|
|
143
143
|
|
|
144
144
|
def matching_records(self, zone):
|
|
145
|
-
return
|
|
146
|
-
|
|
145
|
+
return zone.record_set.filter(
|
|
146
|
+
name=self.record_name, type=self.type, value=self.value
|
|
147
147
|
)
|
|
148
148
|
|
|
149
149
|
def create_record(self, zone):
|
netbox_dns/models/view.py
CHANGED
|
@@ -2,7 +2,6 @@ from django.db import models
|
|
|
2
2
|
from django.urls import reverse
|
|
3
3
|
from django.core.exceptions import ValidationError
|
|
4
4
|
from django.utils.translation import gettext_lazy as _
|
|
5
|
-
from django.utils.translation import pgettext_lazy as _p
|
|
6
5
|
|
|
7
6
|
from netbox.models import NetBoxModel
|
|
8
7
|
from netbox.models.features import ContactsMixin
|
|
@@ -77,8 +76,8 @@ class View(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
77
76
|
return str(self.name)
|
|
78
77
|
|
|
79
78
|
class Meta:
|
|
80
|
-
verbose_name =
|
|
81
|
-
verbose_name_plural =
|
|
79
|
+
verbose_name = _("View")
|
|
80
|
+
verbose_name_plural = _("Views")
|
|
82
81
|
|
|
83
82
|
ordering = ("name",)
|
|
84
83
|
|
netbox_dns/models/zone.py
CHANGED
|
@@ -12,12 +12,12 @@ from django.core.validators import (
|
|
|
12
12
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
13
13
|
from django.db import models, transaction
|
|
14
14
|
from django.db.models import Q, Max, ExpressionWrapper, BooleanField
|
|
15
|
-
from django.
|
|
15
|
+
from django.db.models.functions import Length
|
|
16
16
|
from django.db.models.signals import m2m_changed
|
|
17
|
+
from django.urls import reverse
|
|
17
18
|
from django.dispatch import receiver
|
|
18
19
|
from django.conf import settings
|
|
19
20
|
from django.utils.translation import gettext_lazy as _
|
|
20
|
-
from django.utils.translation import pgettext_lazy as _p
|
|
21
21
|
|
|
22
22
|
from netbox.models import NetBoxModel
|
|
23
23
|
from netbox.models.features import ContactsMixin
|
|
@@ -35,6 +35,7 @@ from netbox_dns.utilities import (
|
|
|
35
35
|
arpa_to_prefix,
|
|
36
36
|
name_to_unicode,
|
|
37
37
|
normalize_name,
|
|
38
|
+
get_parent_zone_names,
|
|
38
39
|
NameFormatError,
|
|
39
40
|
)
|
|
40
41
|
from netbox_dns.validators import (
|
|
@@ -53,6 +54,9 @@ __all__ = (
|
|
|
53
54
|
"ZoneIndex",
|
|
54
55
|
)
|
|
55
56
|
|
|
57
|
+
ZONE_ACTIVE_STATUS_LIST = get_plugin_config("netbox_dns", "zone_active_status")
|
|
58
|
+
RECORD_ACTIVE_STATUS_LIST = get_plugin_config("netbox_dns", "record_active_status")
|
|
59
|
+
|
|
56
60
|
|
|
57
61
|
class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
|
58
62
|
"""
|
|
@@ -65,15 +69,13 @@ class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
|
|
65
69
|
.get_queryset()
|
|
66
70
|
.annotate(
|
|
67
71
|
active=ExpressionWrapper(
|
|
68
|
-
Q(status__in=
|
|
72
|
+
Q(status__in=ZONE_ACTIVE_STATUS_LIST), output_field=BooleanField()
|
|
69
73
|
)
|
|
70
74
|
)
|
|
71
75
|
)
|
|
72
76
|
|
|
73
77
|
|
|
74
78
|
class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
75
|
-
ACTIVE_STATUS_LIST = (ZoneStatusChoices.STATUS_ACTIVE,)
|
|
76
|
-
|
|
77
79
|
def __init__(self, *args, **kwargs):
|
|
78
80
|
kwargs.pop("template", None)
|
|
79
81
|
|
|
@@ -83,7 +85,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
83
85
|
self._ip_addresses_checked = False
|
|
84
86
|
|
|
85
87
|
view = models.ForeignKey(
|
|
86
|
-
verbose_name=
|
|
88
|
+
verbose_name=_("View"),
|
|
87
89
|
to="View",
|
|
88
90
|
on_delete=models.PROTECT,
|
|
89
91
|
null=False,
|
|
@@ -297,13 +299,27 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
297
299
|
|
|
298
300
|
return str(name)
|
|
299
301
|
|
|
302
|
+
@property
|
|
303
|
+
def fqdn(self):
|
|
304
|
+
return f"{self.name}."
|
|
305
|
+
|
|
300
306
|
@staticmethod
|
|
301
307
|
def get_defaults():
|
|
308
|
+
default_fields = (
|
|
309
|
+
"zone_default_ttl",
|
|
310
|
+
"zone_soa_ttl",
|
|
311
|
+
"zone_soa_serial",
|
|
312
|
+
"zone_soa_refresh",
|
|
313
|
+
"zone_soa_retry",
|
|
314
|
+
"zone_soa_expire",
|
|
315
|
+
"zone_soa_minimum",
|
|
316
|
+
"zone_soa_rname",
|
|
317
|
+
)
|
|
318
|
+
|
|
302
319
|
return {
|
|
303
320
|
field[5:]: value
|
|
304
321
|
for field, value in settings.PLUGINS_CONFIG.get("netbox_dns").items()
|
|
305
|
-
if field
|
|
306
|
-
and field not in ("zone_soa_mname", "zone_nameservers")
|
|
322
|
+
if field in default_fields
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
@property
|
|
@@ -337,7 +353,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
337
353
|
|
|
338
354
|
@property
|
|
339
355
|
def is_active(self):
|
|
340
|
-
return self.status in
|
|
356
|
+
return self.status in ZONE_ACTIVE_STATUS_LIST
|
|
341
357
|
|
|
342
358
|
@property
|
|
343
359
|
def is_reverse_zone(self):
|
|
@@ -352,10 +368,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
352
368
|
return None
|
|
353
369
|
|
|
354
370
|
return (
|
|
355
|
-
|
|
356
|
-
view=self.view,
|
|
357
|
-
arpa_network__net_contains=self.rfc2317_prefix,
|
|
358
|
-
)
|
|
371
|
+
self.view.zone_set.filter(arpa_network__net_contains=self.rfc2317_prefix)
|
|
359
372
|
.order_by("arpa_network__net_mask_length")
|
|
360
373
|
.last()
|
|
361
374
|
)
|
|
@@ -376,25 +389,73 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
376
389
|
|
|
377
390
|
@property
|
|
378
391
|
def child_zones(self):
|
|
379
|
-
return
|
|
380
|
-
name__iregex=rf"^[^.]+\.{re.escape(self.name)}$"
|
|
392
|
+
return self.view.zone_set.filter(
|
|
393
|
+
name__iregex=rf"^[^.]+\.{re.escape(self.name)}$"
|
|
381
394
|
)
|
|
382
395
|
|
|
396
|
+
@property
|
|
397
|
+
def descendant_zones(self):
|
|
398
|
+
return self.view.zone_set.filter(name__endswith=f".{self.name}")
|
|
399
|
+
|
|
383
400
|
@property
|
|
384
401
|
def parent_zone(self):
|
|
385
|
-
parent_name = (
|
|
386
|
-
dns_name.from_text(self.name).parent().relativize(dns_name.root).to_text()
|
|
387
|
-
)
|
|
388
402
|
try:
|
|
389
|
-
return
|
|
390
|
-
except Zone.DoesNotExist:
|
|
403
|
+
return self.view.zone_set.get(name=get_parent_zone_names(self.name)[-1])
|
|
404
|
+
except (Zone.DoesNotExist, IndexError):
|
|
391
405
|
return None
|
|
392
406
|
|
|
393
|
-
|
|
394
|
-
|
|
407
|
+
@property
|
|
408
|
+
def ancestor_zones(self):
|
|
409
|
+
return (
|
|
410
|
+
self.view.zone_set.annotate(name_length=Length("name"))
|
|
411
|
+
.filter(name__in=get_parent_zone_names(self.name))
|
|
412
|
+
.order_by("name_length")
|
|
413
|
+
)
|
|
395
414
|
|
|
396
|
-
|
|
397
|
-
|
|
415
|
+
@property
|
|
416
|
+
def delegation_records(self):
|
|
417
|
+
descendant_zone_names = [
|
|
418
|
+
f"{name}." for name in self.descendant_zones.values_list("name", flat=True)
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
ns_records = (
|
|
422
|
+
self.record_set.filter(type=RecordTypeChoices.NS)
|
|
423
|
+
.exclude(fqdn=self.fqdn)
|
|
424
|
+
.filter(fqdn__in=descendant_zone_names)
|
|
425
|
+
)
|
|
426
|
+
ns_values = [record.value_fqdn for record in ns_records]
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
ns_records
|
|
430
|
+
| self.record_set.filter(type=RecordTypeChoices.DS)
|
|
431
|
+
| self.record_set.filter(
|
|
432
|
+
type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
|
|
433
|
+
fqdn__in=ns_values,
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def ancestor_delegation_records(self):
|
|
439
|
+
ancestor_zones = self.ancestor_zones
|
|
440
|
+
|
|
441
|
+
ns_records = Record.objects.filter(
|
|
442
|
+
type=RecordTypeChoices.NS, zone__in=ancestor_zones, fqdn=self.fqdn
|
|
443
|
+
)
|
|
444
|
+
ns_values = [record.value_fqdn for record in ns_records]
|
|
445
|
+
|
|
446
|
+
ds_records = Record.objects.filter(
|
|
447
|
+
type=RecordTypeChoices.DS, zone__in=ancestor_zones, fqdn=self.fqdn
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
ns_records
|
|
452
|
+
| ds_records
|
|
453
|
+
| Record.objects.filter(
|
|
454
|
+
zone__in=ancestor_zones,
|
|
455
|
+
type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
|
|
456
|
+
fqdn__in=ns_values,
|
|
457
|
+
)
|
|
458
|
+
)
|
|
398
459
|
|
|
399
460
|
def update_soa_record(self):
|
|
400
461
|
soa_name = "@"
|
|
@@ -472,9 +533,8 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
472
533
|
continue
|
|
473
534
|
|
|
474
535
|
relative_name = name.relativize(parent).to_text()
|
|
475
|
-
address_records =
|
|
476
|
-
Q(
|
|
477
|
-
Q(status__in=Record.ACTIVE_STATUS_LIST),
|
|
536
|
+
address_records = ns_zone.record_set.filter(
|
|
537
|
+
Q(status__in=RECORD_ACTIVE_STATUS_LIST),
|
|
478
538
|
Q(Q(name=f"{_nameserver.name}.") | Q(name=relative_name)),
|
|
479
539
|
Q(Q(type=RecordTypeChoices.A) | Q(type=RecordTypeChoices.AAAA)),
|
|
480
540
|
)
|
|
@@ -630,7 +690,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
630
690
|
{
|
|
631
691
|
"name": str(exc),
|
|
632
692
|
}
|
|
633
|
-
)
|
|
693
|
+
)
|
|
634
694
|
|
|
635
695
|
try:
|
|
636
696
|
validate_domain_name(self.name, zone_name=True)
|
|
@@ -639,7 +699,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
639
699
|
{
|
|
640
700
|
"name": exc,
|
|
641
701
|
}
|
|
642
|
-
)
|
|
702
|
+
)
|
|
643
703
|
|
|
644
704
|
if self.soa_rname in (None, ""):
|
|
645
705
|
raise ValidationError(_("soa_rname not set and no default value defined"))
|
|
@@ -651,7 +711,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
651
711
|
{
|
|
652
712
|
"soa_rname": exc,
|
|
653
713
|
}
|
|
654
|
-
)
|
|
714
|
+
)
|
|
655
715
|
|
|
656
716
|
if not self.soa_serial_auto:
|
|
657
717
|
if self.soa_serial is None:
|
|
@@ -735,8 +795,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
735
795
|
else:
|
|
736
796
|
self.rfc2317_parent_zone = None
|
|
737
797
|
|
|
738
|
-
overlapping_zones =
|
|
739
|
-
view=self.view,
|
|
798
|
+
overlapping_zones = self.view.zone_set.filter(
|
|
740
799
|
rfc2317_prefix__net_overlap=self.rfc2317_prefix,
|
|
741
800
|
active=True,
|
|
742
801
|
).exclude(pk=self.pk)
|
|
@@ -769,9 +828,8 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
769
828
|
if (
|
|
770
829
|
changed_fields is None or {"name", "view", "status"} & changed_fields
|
|
771
830
|
) and self.is_reverse_zone:
|
|
772
|
-
zones =
|
|
773
|
-
|
|
774
|
-
arpa_network__net_contains_or_equals=self.arpa_network,
|
|
831
|
+
zones = self.view.zone_set.filter(
|
|
832
|
+
arpa_network__net_contains_or_equals=self.arpa_network
|
|
775
833
|
)
|
|
776
834
|
address_records = Record.objects.filter(
|
|
777
835
|
Q(ptr_record__isnull=True) | Q(ptr_record__zone__in=zones),
|
|
@@ -800,9 +858,8 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
800
858
|
or {"name", "view", "status", "rfc2317_prefix", "rfc2317_parent_managed"}
|
|
801
859
|
& changed_fields
|
|
802
860
|
) and self.is_rfc2317_zone:
|
|
803
|
-
zones =
|
|
804
|
-
|
|
805
|
-
arpa_network__net_contains=self.rfc2317_prefix,
|
|
861
|
+
zones = self.view.zone_set.filter(
|
|
862
|
+
arpa_network__net_contains=self.rfc2317_prefix
|
|
806
863
|
)
|
|
807
864
|
address_records = Record.objects.filter(
|
|
808
865
|
Q(ptr_record__isnull=True)
|
|
@@ -883,7 +940,7 @@ class Zone(ObjectModificationMixin, ContactsMixin, NetBoxModel):
|
|
|
883
940
|
cname_zone.update_soa_record()
|
|
884
941
|
|
|
885
942
|
rfc2317_child_zones = list(
|
|
886
|
-
self.rfc2317_child_zones.
|
|
943
|
+
self.rfc2317_child_zones.values_list("pk", flat=True)
|
|
887
944
|
)
|
|
888
945
|
|
|
889
946
|
ipam_ip_addresses = list(
|
|
@@ -63,7 +63,7 @@ def ipam_dnssync_ipaddress_post_clean(instance, **kwargs):
|
|
|
63
63
|
{
|
|
64
64
|
"dns_name": _(
|
|
65
65
|
"Unique DNS records are enforced and there is already "
|
|
66
|
-
"an active IP address {address} with DNS name {name}.
|
|
66
|
+
"an active IP address {address} with DNS name {name}. Please choose "
|
|
67
67
|
"a different name or disable record creation for this IP address."
|
|
68
68
|
).format(address=instance.address, name=instance.dns_name)
|
|
69
69
|
}
|
netbox_dns/tables/record.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import django_tables2 as tables
|
|
2
2
|
from django.utils.html import format_html
|
|
3
3
|
from django.utils.translation import gettext_lazy as _
|
|
4
|
-
from django.utils.translation import pgettext_lazy as _p
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
from netbox.tables import (
|
|
@@ -20,6 +19,7 @@ __all__ = (
|
|
|
20
19
|
"RecordTable",
|
|
21
20
|
"ManagedRecordTable",
|
|
22
21
|
"RelatedRecordTable",
|
|
22
|
+
"DelegationRecordTable",
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ class RecordBaseTable(TenancyColumnsMixin, NetBoxTable):
|
|
|
29
29
|
linkify=True,
|
|
30
30
|
)
|
|
31
31
|
view = tables.Column(
|
|
32
|
-
verbose_name=
|
|
32
|
+
verbose_name=_("View"),
|
|
33
33
|
accessor="zone__view",
|
|
34
34
|
linkify=True,
|
|
35
35
|
)
|
|
@@ -162,3 +162,15 @@ class RelatedRecordTable(RecordBaseTable):
|
|
|
162
162
|
"type",
|
|
163
163
|
"value",
|
|
164
164
|
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class DelegationRecordTable(RecordBaseTable):
|
|
168
|
+
class Meta(NetBoxTable.Meta):
|
|
169
|
+
model = Record
|
|
170
|
+
fields = ()
|
|
171
|
+
default_columns = (
|
|
172
|
+
"name",
|
|
173
|
+
"zone",
|
|
174
|
+
"type",
|
|
175
|
+
"value",
|
|
176
|
+
)
|
netbox_dns/tables/zone.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import django_tables2 as tables
|
|
2
2
|
from django.utils.translation import gettext_lazy as _
|
|
3
|
-
from django.utils.translation import pgettext_lazy as _p
|
|
4
3
|
|
|
5
4
|
from netbox.tables import (
|
|
6
5
|
ChoiceFieldColumn,
|
|
@@ -21,7 +20,7 @@ class ZoneTable(TenancyColumnsMixin, NetBoxTable):
|
|
|
21
20
|
linkify=True,
|
|
22
21
|
)
|
|
23
22
|
view = tables.Column(
|
|
24
|
-
verbose_name=
|
|
23
|
+
verbose_name=_("View"),
|
|
25
24
|
linkify=True,
|
|
26
25
|
)
|
|
27
26
|
soa_mname = tables.Column(
|
netbox_dns/template_content.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import django_tables2 as tables
|
|
2
|
+
|
|
1
3
|
from django.conf import settings
|
|
2
4
|
from django.urls import reverse
|
|
3
5
|
|
|
4
6
|
from netbox.plugins.utils import get_plugin_config
|
|
5
7
|
from netbox.plugins import PluginTemplateExtension
|
|
8
|
+
from utilities.tables import register_table_column
|
|
9
|
+
from ipam.tables import IPAddressTable
|
|
6
10
|
|
|
7
11
|
from netbox_dns.models import Record
|
|
8
12
|
from netbox_dns.choices import RecordTypeChoices
|
|
@@ -114,8 +118,20 @@ class IPRelatedDNSRecords(PluginTemplateExtension):
|
|
|
114
118
|
)
|
|
115
119
|
|
|
116
120
|
|
|
121
|
+
address_records = tables.ManyToManyColumn(
|
|
122
|
+
verbose_name="DNS Address Records",
|
|
123
|
+
accessor="netbox_dns_records",
|
|
124
|
+
linkify_item=True,
|
|
125
|
+
transform=lambda obj: (
|
|
126
|
+
obj.fqdn.rstrip(".")
|
|
127
|
+
if obj.zone.view.default_view
|
|
128
|
+
else f"[{obj.zone.view.name}] {obj.fqdn.rstrip('.')}"
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
|
|
117
132
|
if not settings.PLUGINS_CONFIG["netbox_dns"].get("dnssync_disabled"):
|
|
118
133
|
template_extensions = [RelatedDNSRecords, RelatedDNSViews]
|
|
134
|
+
register_table_column(address_records, "address_records", IPAddressTable)
|
|
119
135
|
else:
|
|
120
136
|
template_extensions = []
|
|
121
137
|
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
<th scope="row">{% trans "Name" %}</th>
|
|
44
44
|
<td style="word-break:break-all;">{{ object.name }}</td>
|
|
45
45
|
</tr>
|
|
46
|
+
{% if mask_warning %}
|
|
47
|
+
<tr class="text-warning">
|
|
48
|
+
<th scope="row">{% trans "Warning" %}</th>
|
|
49
|
+
<td>{{ mask_warning }}</td>
|
|
50
|
+
</tr>
|
|
51
|
+
{% endif %}
|
|
46
52
|
{% if unicode_name %}
|
|
47
53
|
<tr>
|
|
48
54
|
<th scope="row">{% trans "IDN" %}</th>
|
|
@@ -76,6 +82,12 @@
|
|
|
76
82
|
<th scope="row">{% trans "Value" %}</th>
|
|
77
83
|
<td style="word-break:break-all;">{{ object.value }}</td>
|
|
78
84
|
</tr>
|
|
85
|
+
{% if cname_warning %}
|
|
86
|
+
<tr class="text-warning">
|
|
87
|
+
<th scope="row">{% trans "Warning" %}</th>
|
|
88
|
+
<td>{{ cname_warning }}</td>
|
|
89
|
+
</tr>
|
|
90
|
+
{% endif %}
|
|
79
91
|
{% if unicode_value %}
|
|
80
92
|
<tr>
|
|
81
93
|
<th scope="row">{% trans "Unicode Value" %}</th>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<div class="row">
|
|
6
6
|
<div class="col col-md-6">
|
|
7
7
|
<div class="card">
|
|
8
|
-
<h5 class="card-header">{% trans "View"
|
|
8
|
+
<h5 class="card-header">{% trans "View" %}</h5>
|
|
9
9
|
<table class="table table-hover attr-table">
|
|
10
10
|
<tr>
|
|
11
11
|
<th scope="row">{% trans "Name" %}</th>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% extends 'netbox_dns/zone/base.html' %}
|
|
2
|
+
{% load helpers %}
|
|
3
|
+
{% load render_table from django_tables2 %}
|
|
4
|
+
{% load perms %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
{% include 'inc/table_controls_htmx.html' with table_modal="DelegationRecordTable_config" %}
|
|
8
|
+
<div class="card">
|
|
9
|
+
<div class="htmx-container table-responsive" id="object_list">
|
|
10
|
+
{% include 'htmx/table.html' %}
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
{% endblock %}
|
|
14
|
+
|
|
15
|
+
{% block modals %}
|
|
16
|
+
{{ block.super }}
|
|
17
|
+
{% table_config_form table %}
|
|
18
|
+
{% endblock modals %}
|
netbox_dns/utilities/__init__.py
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dns import name as dns_name
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = ("get_parent_zone_names",)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_parent_zone_names(name, min_labels=1, include_self=False):
|
|
8
|
+
fqdn = dns_name.from_text(name)
|
|
9
|
+
return [
|
|
10
|
+
fqdn.split(i)[1].to_text().rstrip(".")
|
|
11
|
+
for i in range(min_labels + 1, len(fqdn.labels) + include_self)
|
|
12
|
+
]
|
|
@@ -12,6 +12,8 @@ from ipam.models import IPAddress, Prefix
|
|
|
12
12
|
|
|
13
13
|
from netbox_dns.choices import RecordStatusChoices
|
|
14
14
|
|
|
15
|
+
from .dns import get_parent_zone_names
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
__all__ = (
|
|
17
19
|
"get_zones",
|
|
@@ -85,15 +87,12 @@ def get_zones(ip_address, view=None, old_zone=None):
|
|
|
85
87
|
min_labels = settings.PLUGINS_CONFIG["netbox_dns"].get(
|
|
86
88
|
"dnssync_minimum_zone_labels", 2
|
|
87
89
|
)
|
|
88
|
-
fqdn = dns_name.from_text(ip_address.dns_name)
|
|
89
|
-
zone_name_candidates = [
|
|
90
|
-
fqdn.split(i)[1].to_text().rstrip(".")
|
|
91
|
-
for i in range(min_labels + 1, len(fqdn.labels) + 1)
|
|
92
|
-
]
|
|
93
90
|
|
|
94
91
|
zones = Zone.objects.filter(
|
|
95
92
|
view__in=views,
|
|
96
|
-
name__in=
|
|
93
|
+
name__in=get_parent_zone_names(
|
|
94
|
+
ip_address.dns_name, min_labels=min_labels, include_self=True
|
|
95
|
+
),
|
|
97
96
|
active=True,
|
|
98
97
|
)
|
|
99
98
|
|
|
@@ -194,9 +193,7 @@ def update_dns_records(ip_address, view=None, force=False):
|
|
|
194
193
|
updated = True
|
|
195
194
|
|
|
196
195
|
zones = Zone.objects.filter(pk__in=[zone.pk for zone in zones]).exclude(
|
|
197
|
-
pk__in=set(
|
|
198
|
-
ip_address.netbox_dns_records.all().values_list("zone", flat=True)
|
|
199
|
-
)
|
|
196
|
+
pk__in=set(ip_address.netbox_dns_records.values_list("zone", flat=True))
|
|
200
197
|
)
|
|
201
198
|
|
|
202
199
|
for zone in zones:
|
|
@@ -213,18 +210,18 @@ def update_dns_records(ip_address, view=None, force=False):
|
|
|
213
210
|
|
|
214
211
|
|
|
215
212
|
def delete_dns_records(ip_address, view=None):
|
|
216
|
-
deleted = False
|
|
217
|
-
|
|
218
213
|
if view is None:
|
|
219
214
|
address_records = ip_address.netbox_dns_records.all()
|
|
220
215
|
else:
|
|
221
216
|
address_records = ip_address.netbox_dns_records.filter(zone__view=view)
|
|
222
217
|
|
|
218
|
+
if not address_records.exists():
|
|
219
|
+
return False
|
|
220
|
+
|
|
223
221
|
for record in address_records:
|
|
224
222
|
record.delete()
|
|
225
|
-
deleted = True
|
|
226
223
|
|
|
227
|
-
return
|
|
224
|
+
return True
|
|
228
225
|
|
|
229
226
|
|
|
230
227
|
def get_views_by_prefix(prefix):
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import re
|
|
2
|
+
import textwrap
|
|
3
|
+
|
|
2
4
|
from dns import rdata, name as dns_name
|
|
5
|
+
from dns.exception import SyntaxError
|
|
3
6
|
|
|
4
7
|
from django.core.exceptions import ValidationError
|
|
5
8
|
from django.utils.translation import gettext as _
|
|
@@ -11,11 +14,12 @@ from netbox_dns.validators import (
|
|
|
11
14
|
validate_generic_name,
|
|
12
15
|
)
|
|
13
16
|
|
|
17
|
+
MAX_TXT_LENGTH = 255
|
|
14
18
|
|
|
15
19
|
__all__ = ("validate_record_value",)
|
|
16
20
|
|
|
17
21
|
|
|
18
|
-
def validate_record_value(
|
|
22
|
+
def validate_record_value(record):
|
|
19
23
|
def _validate_idn(name):
|
|
20
24
|
try:
|
|
21
25
|
name.to_unicode()
|
|
@@ -26,16 +30,53 @@ def validate_record_value(record_type, value):
|
|
|
26
30
|
)
|
|
27
31
|
)
|
|
28
32
|
|
|
33
|
+
def _split_text_value(value):
|
|
34
|
+
# +
|
|
35
|
+
# Text values longer than 255 characters need to be broken up for TXT and
|
|
36
|
+
# SPF records.
|
|
37
|
+
# First, in case they had been split into separate strings, reassemble the
|
|
38
|
+
# original (long) value, then split it into chunks of a maximum length of
|
|
39
|
+
# 255 (preferably at word boundaries), and then build a sequence of partial
|
|
40
|
+
# strings enclosed in double quotes and separated by space.
|
|
41
|
+
#
|
|
42
|
+
# See https://datatracker.ietf.org/doc/html/rfc4408#section-3.1.3 for details.
|
|
43
|
+
# -
|
|
44
|
+
raw_value = "".join(re.findall(r'"([^"]+)"', value))
|
|
45
|
+
if not raw_value:
|
|
46
|
+
raw_value = value
|
|
47
|
+
|
|
48
|
+
return " ".join(
|
|
49
|
+
f'"{part}"'
|
|
50
|
+
for part in textwrap.wrap(raw_value, MAX_TXT_LENGTH, drop_whitespace=False)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if record.type in (RecordTypeChoices.TXT, RecordTypeChoices.SPF):
|
|
54
|
+
if not (record.value.isascii() and record.value.isprintable()):
|
|
55
|
+
raise ValidationError(
|
|
56
|
+
_(
|
|
57
|
+
"Record value {value} for a type {type} record is not a printable ASCII string."
|
|
58
|
+
).format(value=record.value, type=record.type)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if len(record.value) <= MAX_TXT_LENGTH:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
rr = rdata.from_text(RecordClassChoices.IN, record.type, record.value)
|
|
66
|
+
except SyntaxError as exc:
|
|
67
|
+
if str(exc) == "string too long":
|
|
68
|
+
record.value = _split_text_value(record.value)
|
|
69
|
+
|
|
29
70
|
try:
|
|
30
|
-
rr = rdata.from_text(RecordClassChoices.IN,
|
|
31
|
-
except
|
|
71
|
+
rr = rdata.from_text(RecordClassChoices.IN, record.type, record.value)
|
|
72
|
+
except SyntaxError as exc:
|
|
32
73
|
raise ValidationError(
|
|
33
74
|
_(
|
|
34
75
|
"Record value {value} is not a valid value for a {type} record: {error}."
|
|
35
|
-
).format(value=value, type=
|
|
76
|
+
).format(value=record.value, type=record.type, error=exc)
|
|
36
77
|
)
|
|
37
78
|
|
|
38
|
-
match
|
|
79
|
+
match record.type:
|
|
39
80
|
case RecordTypeChoices.CNAME:
|
|
40
81
|
_validate_idn(rr.target)
|
|
41
82
|
validate_domain_name(
|
netbox_dns/views/nameserver.py
CHANGED
|
@@ -36,7 +36,7 @@ class NameServerListView(generic.ObjectListView):
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class NameServerView(generic.ObjectView):
|
|
39
|
-
queryset = NameServer.objects.
|
|
39
|
+
queryset = NameServer.objects.prefetch_related("zones")
|
|
40
40
|
|
|
41
41
|
def get_extra_context(self, request, instance):
|
|
42
42
|
name = dns_name.from_text(instance.name)
|
|
@@ -85,7 +85,7 @@ class NameServerContactsView(ObjectContactsView):
|
|
|
85
85
|
|
|
86
86
|
@register_model_view(NameServer, "zones")
|
|
87
87
|
class NameServerZoneListView(generic.ObjectChildrenView):
|
|
88
|
-
queryset = NameServer.objects.
|
|
88
|
+
queryset = NameServer.objects.prefetch_related("zones")
|
|
89
89
|
child_model = Zone
|
|
90
90
|
table = ZoneTable
|
|
91
91
|
filterset = ZoneFilterSet
|
|
@@ -105,7 +105,7 @@ class NameServerZoneListView(generic.ObjectChildrenView):
|
|
|
105
105
|
|
|
106
106
|
@register_model_view(NameServer, "soa_zones")
|
|
107
107
|
class NameServerSOAZoneListView(generic.ObjectChildrenView):
|
|
108
|
-
queryset = NameServer.objects.
|
|
108
|
+
queryset = NameServer.objects.prefetch_related("zones_soa")
|
|
109
109
|
child_model = Zone
|
|
110
110
|
table = ZoneTable
|
|
111
111
|
filterset = ZoneFilterSet
|