netbox-plugin-dns 1.0b2__py3-none-any.whl → 1.0.2__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 +3 -3
- netbox_dns/api/serializers_/view.py +6 -1
- netbox_dns/fields/network.py +20 -21
- netbox_dns/fields/rfc2317.py +2 -2
- netbox_dns/filtersets/view.py +1 -1
- netbox_dns/filtersets/zone.py +4 -4
- netbox_dns/forms/record.py +30 -2
- netbox_dns/forms/view.py +6 -3
- netbox_dns/forms/zone.py +71 -102
- netbox_dns/graphql/types.py +1 -4
- netbox_dns/migrations/0001_squashed_netbox_dns_0_22.py +4 -2
- 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/mixins/__init__.py +1 -0
- netbox_dns/mixins/object_modification.py +26 -0
- netbox_dns/models/nameserver.py +7 -6
- netbox_dns/models/record.py +94 -35
- netbox_dns/models/view.py +56 -1
- netbox_dns/models/zone.py +101 -67
- netbox_dns/signals/ipam_coupling.py +1 -2
- netbox_dns/tables/view.py +12 -2
- netbox_dns/template_content.py +1 -1
- netbox_dns/templates/netbox_dns/record.html +1 -1
- netbox_dns/templates/netbox_dns/view.html +4 -0
- netbox_dns/templates/netbox_dns/zone.html +2 -4
- netbox_dns/urls/__init__.py +17 -0
- netbox_dns/urls/contact.py +51 -0
- netbox_dns/urls/nameserver.py +69 -0
- netbox_dns/urls/record.py +41 -0
- netbox_dns/urls/registrar.py +63 -0
- netbox_dns/urls/view.py +39 -0
- netbox_dns/urls/zone.py +57 -0
- netbox_dns/validators/dns_name.py +24 -11
- netbox_dns/views/record.py +10 -18
- {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/LICENSE +2 -1
- {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/METADATA +15 -14
- {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/RECORD +39 -28
- netbox_dns/urls.py +0 -297
- {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/WHEEL +0 -0
netbox_dns/models/nameserver.py
CHANGED
|
@@ -14,11 +14,12 @@ from netbox_dns.utilities import (
|
|
|
14
14
|
NameFormatError,
|
|
15
15
|
)
|
|
16
16
|
from netbox_dns.validators import validate_fqdn
|
|
17
|
+
from netbox_dns.mixins import ObjectModificationMixin
|
|
17
18
|
|
|
18
19
|
from .record import Record, RecordTypeChoices
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class NameServer(NetBoxModel):
|
|
22
|
+
class NameServer(ObjectModificationMixin, NetBoxModel):
|
|
22
23
|
name = models.CharField(
|
|
23
24
|
unique=True,
|
|
24
25
|
max_length=255,
|
|
@@ -55,7 +56,7 @@ class NameServer(NetBoxModel):
|
|
|
55
56
|
def get_absolute_url(self):
|
|
56
57
|
return reverse("plugins:netbox_dns:nameserver", kwargs={"pk": self.pk})
|
|
57
58
|
|
|
58
|
-
def clean(self):
|
|
59
|
+
def clean(self, *args, **kwargs):
|
|
59
60
|
try:
|
|
60
61
|
self.name = normalize_name(self.name)
|
|
61
62
|
except NameFormatError as exc:
|
|
@@ -74,17 +75,17 @@ class NameServer(NetBoxModel):
|
|
|
74
75
|
}
|
|
75
76
|
) from None
|
|
76
77
|
|
|
78
|
+
super().clean(*args, **kwargs)
|
|
79
|
+
|
|
77
80
|
def save(self, *args, **kwargs):
|
|
78
81
|
self.full_clean()
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
self.pk is not None and self.name != NameServer.objects.get(pk=self.pk).name
|
|
82
|
-
)
|
|
83
|
+
changed_fields = self.changed_fields
|
|
83
84
|
|
|
84
85
|
with transaction.atomic():
|
|
85
86
|
super().save(*args, **kwargs)
|
|
86
87
|
|
|
87
|
-
if
|
|
88
|
+
if changed_fields is not None and "name" in changed_fields:
|
|
88
89
|
soa_zones = self.zones_soa.all()
|
|
89
90
|
for soa_zone in soa_zones:
|
|
90
91
|
soa_zone.update_soa_record()
|
netbox_dns/models/record.py
CHANGED
|
@@ -11,11 +11,10 @@ from django.urls import reverse
|
|
|
11
11
|
|
|
12
12
|
from netbox.models import NetBoxModel
|
|
13
13
|
from netbox.search import SearchIndex, register_search
|
|
14
|
+
from netbox.plugins.utils import get_plugin_config
|
|
14
15
|
from utilities.querysets import RestrictedQuerySet
|
|
15
16
|
from utilities.choices import ChoiceSet
|
|
16
17
|
|
|
17
|
-
from netbox.plugins.utils import get_plugin_config
|
|
18
|
-
|
|
19
18
|
from netbox_dns.fields import AddressField
|
|
20
19
|
from netbox_dns.utilities import (
|
|
21
20
|
arpa_to_prefix,
|
|
@@ -24,7 +23,9 @@ from netbox_dns.utilities import (
|
|
|
24
23
|
from netbox_dns.validators import (
|
|
25
24
|
validate_fqdn,
|
|
26
25
|
validate_extended_hostname,
|
|
26
|
+
validate_domain_name,
|
|
27
27
|
)
|
|
28
|
+
from netbox_dns.mixins import ObjectModificationMixin
|
|
28
29
|
|
|
29
30
|
# +
|
|
30
31
|
# This is a hack designed to break cyclic imports between Record and Zone
|
|
@@ -102,7 +103,7 @@ class RecordStatusChoices(ChoiceSet):
|
|
|
102
103
|
]
|
|
103
104
|
|
|
104
105
|
|
|
105
|
-
class Record(NetBoxModel):
|
|
106
|
+
class Record(ObjectModificationMixin, NetBoxModel):
|
|
106
107
|
ACTIVE_STATUS_LIST = (RecordStatusChoices.STATUS_ACTIVE,)
|
|
107
108
|
|
|
108
109
|
unique_ptr_qs = Q(
|
|
@@ -211,9 +212,12 @@ class Record(NetBoxModel):
|
|
|
211
212
|
|
|
212
213
|
def __str__(self):
|
|
213
214
|
try:
|
|
214
|
-
|
|
215
|
+
fqdn = dns_name.from_text(
|
|
216
|
+
self.name, origin=dns_name.from_text(self.zone.name)
|
|
217
|
+
).relativize(dns_name.root)
|
|
218
|
+
name = fqdn.to_unicode()
|
|
215
219
|
except dns_name.IDNAException:
|
|
216
|
-
name =
|
|
220
|
+
name = fqdn.to_text()
|
|
217
221
|
except dns_name.LabelTooLong:
|
|
218
222
|
name = f"{self.name[:59]}..."
|
|
219
223
|
|
|
@@ -234,8 +238,8 @@ class Record(NetBoxModel):
|
|
|
234
238
|
if self.type != RecordTypeChoices.CNAME:
|
|
235
239
|
return None
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
value_fqdn = dns_name.from_text(self.value, origin=
|
|
241
|
+
_zone = dns_name.from_text(self.zone.name)
|
|
242
|
+
value_fqdn = dns_name.from_text(self.value, origin=_zone)
|
|
239
243
|
|
|
240
244
|
return value_fqdn.to_text()
|
|
241
245
|
|
|
@@ -287,12 +291,14 @@ class Record(NetBoxModel):
|
|
|
287
291
|
dns_name.from_text(self.ptr_record.zone.rfc2317_parent_zone.name)
|
|
288
292
|
)
|
|
289
293
|
|
|
294
|
+
return None
|
|
295
|
+
|
|
290
296
|
@property
|
|
291
297
|
def ptr_zone(self):
|
|
292
298
|
if self.type == RecordTypeChoices.A:
|
|
293
299
|
ptr_zone = (
|
|
294
300
|
zone.Zone.objects.filter(
|
|
295
|
-
self.zone.
|
|
301
|
+
view=self.zone.view,
|
|
296
302
|
rfc2317_prefix__net_contains=self.value,
|
|
297
303
|
)
|
|
298
304
|
.order_by("rfc2317_prefix__net_mask_length")
|
|
@@ -304,7 +310,7 @@ class Record(NetBoxModel):
|
|
|
304
310
|
|
|
305
311
|
ptr_zone = (
|
|
306
312
|
zone.Zone.objects.filter(
|
|
307
|
-
self.zone.
|
|
313
|
+
view=self.zone.view, arpa_network__net_contains=self.value
|
|
308
314
|
)
|
|
309
315
|
.order_by("arpa_network__net_mask_length")
|
|
310
316
|
.last()
|
|
@@ -319,7 +325,7 @@ class Record(NetBoxModel):
|
|
|
319
325
|
ptr_zone is None
|
|
320
326
|
or self.disable_ptr
|
|
321
327
|
or not self.is_active
|
|
322
|
-
or self.name
|
|
328
|
+
or self.name.startswith("*")
|
|
323
329
|
):
|
|
324
330
|
if self.ptr_record is not None:
|
|
325
331
|
with transaction.atomic():
|
|
@@ -418,10 +424,8 @@ class Record(NetBoxModel):
|
|
|
418
424
|
self.rfc2317_cname_record.save(save_zone_serial=save_zone_serial)
|
|
419
425
|
|
|
420
426
|
return
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
save_zone_serial=save_zone_serial
|
|
424
|
-
)
|
|
427
|
+
|
|
428
|
+
self.remove_from_rfc2317_cname_record(save_zone_serial=save_zone_serial)
|
|
425
429
|
|
|
426
430
|
rfc2317_cname_record = Record.objects.filter(
|
|
427
431
|
name=cname_name,
|
|
@@ -462,14 +466,14 @@ class Record(NetBoxModel):
|
|
|
462
466
|
|
|
463
467
|
def validate_name(self):
|
|
464
468
|
try:
|
|
465
|
-
|
|
469
|
+
_zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
|
|
466
470
|
name = dns_name.from_text(self.name, origin=None)
|
|
467
|
-
fqdn = dns_name.from_text(self.name, origin=
|
|
471
|
+
fqdn = dns_name.from_text(self.name, origin=_zone)
|
|
468
472
|
|
|
469
|
-
|
|
473
|
+
_zone.to_unicode()
|
|
470
474
|
name.to_unicode()
|
|
471
475
|
|
|
472
|
-
self.name = name.relativize(
|
|
476
|
+
self.name = name.relativize(_zone).to_text()
|
|
473
477
|
self.fqdn = fqdn.to_text()
|
|
474
478
|
|
|
475
479
|
except dns.exception.DNSException as exc:
|
|
@@ -479,7 +483,7 @@ class Record(NetBoxModel):
|
|
|
479
483
|
}
|
|
480
484
|
)
|
|
481
485
|
|
|
482
|
-
if not fqdn.is_subdomain(
|
|
486
|
+
if not fqdn.is_subdomain(_zone):
|
|
483
487
|
raise ValidationError(
|
|
484
488
|
{
|
|
485
489
|
"name": f"{self.name} is not a name in {self.zone.name}",
|
|
@@ -487,7 +491,7 @@ class Record(NetBoxModel):
|
|
|
487
491
|
)
|
|
488
492
|
|
|
489
493
|
if self.type not in get_plugin_config(
|
|
490
|
-
"netbox_dns", "tolerate_non_rfc1035_types", default=
|
|
494
|
+
"netbox_dns", "tolerate_non_rfc1035_types", default=[]
|
|
491
495
|
):
|
|
492
496
|
try:
|
|
493
497
|
validate_extended_hostname(
|
|
@@ -497,7 +501,7 @@ class Record(NetBoxModel):
|
|
|
497
501
|
in get_plugin_config(
|
|
498
502
|
"netbox_dns",
|
|
499
503
|
"tolerate_leading_underscore_types",
|
|
500
|
-
default=
|
|
504
|
+
default=[],
|
|
501
505
|
)
|
|
502
506
|
),
|
|
503
507
|
)
|
|
@@ -509,18 +513,16 @@ class Record(NetBoxModel):
|
|
|
509
513
|
) from None
|
|
510
514
|
|
|
511
515
|
def validate_value(self):
|
|
512
|
-
|
|
516
|
+
def _validate_idn(name):
|
|
513
517
|
try:
|
|
514
|
-
|
|
515
|
-
except
|
|
518
|
+
name.to_unicode()
|
|
519
|
+
except dns_name.IDNAException as exc:
|
|
516
520
|
raise ValidationError(
|
|
517
|
-
{
|
|
518
|
-
"value": exc,
|
|
519
|
-
}
|
|
521
|
+
f"{name.to_text()} is not a valid IDN: {exc}."
|
|
520
522
|
) from None
|
|
521
523
|
|
|
522
524
|
try:
|
|
523
|
-
rdata.from_text(RecordClassChoices.IN, self.type, self.value)
|
|
525
|
+
rr = rdata.from_text(RecordClassChoices.IN, self.type, self.value)
|
|
524
526
|
except dns.exception.SyntaxError as exc:
|
|
525
527
|
raise ValidationError(
|
|
526
528
|
{
|
|
@@ -528,6 +530,59 @@ class Record(NetBoxModel):
|
|
|
528
530
|
}
|
|
529
531
|
) from None
|
|
530
532
|
|
|
533
|
+
try:
|
|
534
|
+
match self.type:
|
|
535
|
+
case RecordTypeChoices.CNAME:
|
|
536
|
+
_validate_idn(rr.target)
|
|
537
|
+
validate_domain_name(
|
|
538
|
+
rr.target.to_text(),
|
|
539
|
+
always_tolerant=True,
|
|
540
|
+
allow_empty_label=True,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
case (
|
|
544
|
+
RecordTypeChoices.DNAME
|
|
545
|
+
| RecordTypeChoices.NS
|
|
546
|
+
| RecordTypeChoices.HTTPS
|
|
547
|
+
| RecordTypeChoices.SRV
|
|
548
|
+
| RecordTypeChoices.SVCB
|
|
549
|
+
):
|
|
550
|
+
_validate_idn(rr.target)
|
|
551
|
+
validate_domain_name(rr.target.to_text(), always_tolerant=True)
|
|
552
|
+
|
|
553
|
+
case RecordTypeChoices.PTR | RecordTypeChoices.NSAP_PTR:
|
|
554
|
+
_validate_idn(rr.target)
|
|
555
|
+
validate_fqdn(rr.target.to_text(), always_tolerant=True)
|
|
556
|
+
|
|
557
|
+
case RecordTypeChoices.MX | RecordTypeChoices.RT | RecordTypeChoices.KX:
|
|
558
|
+
_validate_idn(rr.exchange)
|
|
559
|
+
validate_domain_name(rr.exchange.to_text(), always_tolerant=True)
|
|
560
|
+
|
|
561
|
+
case RecordTypeChoices.NSEC:
|
|
562
|
+
_validate_idn(rr.next)
|
|
563
|
+
validate_domain_name(rr.next.to_text(), always_tolerant=True)
|
|
564
|
+
|
|
565
|
+
case RecordTypeChoices.RP:
|
|
566
|
+
_validate_idn(rr.mbox)
|
|
567
|
+
validate_domain_name(rr.mbox.to_text(), always_tolerant=True)
|
|
568
|
+
_validate_idn(rr.txt)
|
|
569
|
+
validate_domain_name(rr.txt.to_text(), always_tolerant=True)
|
|
570
|
+
|
|
571
|
+
case RecordTypeChoices.NAPTR:
|
|
572
|
+
_validate_idn(rr.replacement)
|
|
573
|
+
validate_extended_hostname(
|
|
574
|
+
rr.replacement.to_text(), always_tolerant=True
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
case RecordTypeChoices.PX:
|
|
578
|
+
_validate_idn(rr.map822)
|
|
579
|
+
validate_domain_name(rr.map822.to_text(), always_tolerant=True)
|
|
580
|
+
_validate_idn(rr.mapx400)
|
|
581
|
+
validate_domain_name(rr.mapx400.to_text(), always_tolerant=True)
|
|
582
|
+
|
|
583
|
+
except ValidationError as exc:
|
|
584
|
+
raise ValidationError({"value": exc}) from None
|
|
585
|
+
|
|
531
586
|
def check_unique_record(self):
|
|
532
587
|
if not get_plugin_config("netbox_dns", "enforce_unique_records", False):
|
|
533
588
|
return
|
|
@@ -695,6 +750,8 @@ class Record(NetBoxModel):
|
|
|
695
750
|
}
|
|
696
751
|
) from None
|
|
697
752
|
|
|
753
|
+
super().clean(*args, **kwargs)
|
|
754
|
+
|
|
698
755
|
def save(
|
|
699
756
|
self,
|
|
700
757
|
*args,
|
|
@@ -730,11 +787,13 @@ class Record(NetBoxModel):
|
|
|
730
787
|
self.ptr_record.delete()
|
|
731
788
|
self.ptr_record = None
|
|
732
789
|
|
|
733
|
-
|
|
790
|
+
changed_fields = self.changed_fields
|
|
791
|
+
if changed_fields is None or changed_fields:
|
|
792
|
+
super().save(*args, **kwargs)
|
|
734
793
|
|
|
735
|
-
|
|
736
|
-
if self.type != RecordTypeChoices.SOA and
|
|
737
|
-
|
|
794
|
+
_zone = self.zone
|
|
795
|
+
if self.type != RecordTypeChoices.SOA and _zone.soa_serial_auto:
|
|
796
|
+
_zone.update_serial(save_zone_serial=save_zone_serial)
|
|
738
797
|
|
|
739
798
|
def delete(self, *args, save_zone_serial=True, **kwargs):
|
|
740
799
|
if self.rfc2317_cname_record:
|
|
@@ -745,9 +804,9 @@ class Record(NetBoxModel):
|
|
|
745
804
|
|
|
746
805
|
super().delete(*args, **kwargs)
|
|
747
806
|
|
|
748
|
-
|
|
749
|
-
if
|
|
750
|
-
|
|
807
|
+
_zone = self.zone
|
|
808
|
+
if _zone.soa_serial_auto:
|
|
809
|
+
_zone.update_serial(save_zone_serial=save_zone_serial)
|
|
751
810
|
|
|
752
811
|
|
|
753
812
|
@register_search
|
netbox_dns/models/view.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
from django.db import models
|
|
2
2
|
from django.urls import reverse
|
|
3
|
+
from django.core.exceptions import ValidationError
|
|
3
4
|
|
|
4
5
|
from netbox.models import NetBoxModel
|
|
5
6
|
from netbox.search import SearchIndex, register_search
|
|
7
|
+
from netbox.context import current_request
|
|
8
|
+
from utilities.exceptions import AbortRequest
|
|
6
9
|
|
|
10
|
+
from netbox_dns.mixins import ObjectModificationMixin
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
|
|
13
|
+
class View(ObjectModificationMixin, NetBoxModel):
|
|
9
14
|
name = models.CharField(
|
|
10
15
|
unique=True,
|
|
11
16
|
max_length=255,
|
|
@@ -14,6 +19,9 @@ class View(NetBoxModel):
|
|
|
14
19
|
max_length=200,
|
|
15
20
|
blank=True,
|
|
16
21
|
)
|
|
22
|
+
default_view = models.BooleanField(
|
|
23
|
+
default=False,
|
|
24
|
+
)
|
|
17
25
|
tenant = models.ForeignKey(
|
|
18
26
|
to="tenancy.Tenant",
|
|
19
27
|
on_delete=models.PROTECT,
|
|
@@ -24,6 +32,10 @@ class View(NetBoxModel):
|
|
|
24
32
|
|
|
25
33
|
clone_fields = ["name", "description"]
|
|
26
34
|
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_default_view(cls):
|
|
37
|
+
return cls.objects.get(default_view=True)
|
|
38
|
+
|
|
27
39
|
def get_absolute_url(self):
|
|
28
40
|
return reverse("plugins:netbox_dns:view", kwargs={"pk": self.pk})
|
|
29
41
|
|
|
@@ -33,6 +45,49 @@ class View(NetBoxModel):
|
|
|
33
45
|
class Meta:
|
|
34
46
|
ordering = ("name",)
|
|
35
47
|
|
|
48
|
+
def delete(self, *args, **kwargs):
|
|
49
|
+
if self.default_view:
|
|
50
|
+
if current_request.get() is not None:
|
|
51
|
+
raise AbortRequest("The default view cannot be deleted")
|
|
52
|
+
|
|
53
|
+
raise ValidationError("The default view cannot be deleted")
|
|
54
|
+
|
|
55
|
+
super().delete(*args, **kwargs)
|
|
56
|
+
|
|
57
|
+
def clean(self, *args, old_state=None, **kwargs):
|
|
58
|
+
if (changed_fields := self.changed_fields) is None:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
"default_view" in changed_fields
|
|
63
|
+
and not self.default_view
|
|
64
|
+
and not View.objects.filter(default_view=True).exclude(pk=self.pk).exists()
|
|
65
|
+
):
|
|
66
|
+
raise ValidationError(
|
|
67
|
+
{
|
|
68
|
+
"default_view": "Please select a different view as default view to change this setting!"
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
super().clean(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
def save(self, *args, **kwargs):
|
|
75
|
+
self.clean()
|
|
76
|
+
|
|
77
|
+
changed_fields = self.changed_fields
|
|
78
|
+
|
|
79
|
+
super().save(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
if (changed_fields is None and self.default_view) or (
|
|
82
|
+
changed_fields is not None
|
|
83
|
+
and self.default_view
|
|
84
|
+
and "default_view" in changed_fields
|
|
85
|
+
):
|
|
86
|
+
other_views = View.objects.filter(default_view=True).exclude(pk=self.pk)
|
|
87
|
+
for view in other_views:
|
|
88
|
+
view.default_view = False
|
|
89
|
+
view.save()
|
|
90
|
+
|
|
36
91
|
|
|
37
92
|
@register_search
|
|
38
93
|
class ViewIndex(SearchIndex):
|