netbox-plugin-dns 1.0b1__py3-none-any.whl → 1.0.1__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/api/serializers_/zone.py +0 -11
- 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/management/commands/cleanup_rrset_ttl.py +1 -1
- 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/models/nameserver.py +3 -1
- netbox_dns/models/record.py +86 -32
- netbox_dns/models/view.py +53 -0
- netbox_dns/models/zone.py +91 -46
- 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 +9 -24
- {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/METADATA +27 -13
- {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/RECORD +39 -30
- netbox_dns/urls.py +0 -297
- {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/LICENSE +0 -0
- {netbox_plugin_dns-1.0b1.dist-info → netbox_plugin_dns-1.0.1.dist-info}/WHEEL +0 -0
netbox_dns/models/nameserver.py
CHANGED
|
@@ -55,7 +55,7 @@ class NameServer(NetBoxModel):
|
|
|
55
55
|
def get_absolute_url(self):
|
|
56
56
|
return reverse("plugins:netbox_dns:nameserver", kwargs={"pk": self.pk})
|
|
57
57
|
|
|
58
|
-
def clean(self):
|
|
58
|
+
def clean(self, *args, **kwargs):
|
|
59
59
|
try:
|
|
60
60
|
self.name = normalize_name(self.name)
|
|
61
61
|
except NameFormatError as exc:
|
|
@@ -74,6 +74,8 @@ class NameServer(NetBoxModel):
|
|
|
74
74
|
}
|
|
75
75
|
) from None
|
|
76
76
|
|
|
77
|
+
super().clean(*args, **kwargs)
|
|
78
|
+
|
|
77
79
|
def save(self, *args, **kwargs):
|
|
78
80
|
self.full_clean()
|
|
79
81
|
|
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,6 +23,7 @@ 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
28
|
|
|
29
29
|
# +
|
|
@@ -234,8 +234,8 @@ class Record(NetBoxModel):
|
|
|
234
234
|
if self.type != RecordTypeChoices.CNAME:
|
|
235
235
|
return None
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
value_fqdn = dns_name.from_text(self.value, origin=
|
|
237
|
+
_zone = dns_name.from_text(self.zone.name)
|
|
238
|
+
value_fqdn = dns_name.from_text(self.value, origin=_zone)
|
|
239
239
|
|
|
240
240
|
return value_fqdn.to_text()
|
|
241
241
|
|
|
@@ -287,12 +287,14 @@ class Record(NetBoxModel):
|
|
|
287
287
|
dns_name.from_text(self.ptr_record.zone.rfc2317_parent_zone.name)
|
|
288
288
|
)
|
|
289
289
|
|
|
290
|
+
return None
|
|
291
|
+
|
|
290
292
|
@property
|
|
291
293
|
def ptr_zone(self):
|
|
292
294
|
if self.type == RecordTypeChoices.A:
|
|
293
295
|
ptr_zone = (
|
|
294
296
|
zone.Zone.objects.filter(
|
|
295
|
-
self.zone.
|
|
297
|
+
view=self.zone.view,
|
|
296
298
|
rfc2317_prefix__net_contains=self.value,
|
|
297
299
|
)
|
|
298
300
|
.order_by("rfc2317_prefix__net_mask_length")
|
|
@@ -304,7 +306,7 @@ class Record(NetBoxModel):
|
|
|
304
306
|
|
|
305
307
|
ptr_zone = (
|
|
306
308
|
zone.Zone.objects.filter(
|
|
307
|
-
self.zone.
|
|
309
|
+
view=self.zone.view, arpa_network__net_contains=self.value
|
|
308
310
|
)
|
|
309
311
|
.order_by("arpa_network__net_mask_length")
|
|
310
312
|
.last()
|
|
@@ -319,7 +321,7 @@ class Record(NetBoxModel):
|
|
|
319
321
|
ptr_zone is None
|
|
320
322
|
or self.disable_ptr
|
|
321
323
|
or not self.is_active
|
|
322
|
-
or self.name
|
|
324
|
+
or self.name.startswith("*")
|
|
323
325
|
):
|
|
324
326
|
if self.ptr_record is not None:
|
|
325
327
|
with transaction.atomic():
|
|
@@ -418,10 +420,8 @@ class Record(NetBoxModel):
|
|
|
418
420
|
self.rfc2317_cname_record.save(save_zone_serial=save_zone_serial)
|
|
419
421
|
|
|
420
422
|
return
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
save_zone_serial=save_zone_serial
|
|
424
|
-
)
|
|
423
|
+
|
|
424
|
+
self.remove_from_rfc2317_cname_record(save_zone_serial=save_zone_serial)
|
|
425
425
|
|
|
426
426
|
rfc2317_cname_record = Record.objects.filter(
|
|
427
427
|
name=cname_name,
|
|
@@ -462,14 +462,14 @@ class Record(NetBoxModel):
|
|
|
462
462
|
|
|
463
463
|
def validate_name(self):
|
|
464
464
|
try:
|
|
465
|
-
|
|
465
|
+
_zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
|
|
466
466
|
name = dns_name.from_text(self.name, origin=None)
|
|
467
|
-
fqdn = dns_name.from_text(self.name, origin=
|
|
467
|
+
fqdn = dns_name.from_text(self.name, origin=_zone)
|
|
468
468
|
|
|
469
|
-
|
|
469
|
+
_zone.to_unicode()
|
|
470
470
|
name.to_unicode()
|
|
471
471
|
|
|
472
|
-
self.name = name.relativize(
|
|
472
|
+
self.name = name.relativize(_zone).to_text()
|
|
473
473
|
self.fqdn = fqdn.to_text()
|
|
474
474
|
|
|
475
475
|
except dns.exception.DNSException as exc:
|
|
@@ -479,7 +479,7 @@ class Record(NetBoxModel):
|
|
|
479
479
|
}
|
|
480
480
|
)
|
|
481
481
|
|
|
482
|
-
if not fqdn.is_subdomain(
|
|
482
|
+
if not fqdn.is_subdomain(_zone):
|
|
483
483
|
raise ValidationError(
|
|
484
484
|
{
|
|
485
485
|
"name": f"{self.name} is not a name in {self.zone.name}",
|
|
@@ -487,7 +487,7 @@ class Record(NetBoxModel):
|
|
|
487
487
|
)
|
|
488
488
|
|
|
489
489
|
if self.type not in get_plugin_config(
|
|
490
|
-
"netbox_dns", "tolerate_non_rfc1035_types", default=
|
|
490
|
+
"netbox_dns", "tolerate_non_rfc1035_types", default=[]
|
|
491
491
|
):
|
|
492
492
|
try:
|
|
493
493
|
validate_extended_hostname(
|
|
@@ -497,7 +497,7 @@ class Record(NetBoxModel):
|
|
|
497
497
|
in get_plugin_config(
|
|
498
498
|
"netbox_dns",
|
|
499
499
|
"tolerate_leading_underscore_types",
|
|
500
|
-
default=
|
|
500
|
+
default=[],
|
|
501
501
|
)
|
|
502
502
|
),
|
|
503
503
|
)
|
|
@@ -509,18 +509,16 @@ class Record(NetBoxModel):
|
|
|
509
509
|
) from None
|
|
510
510
|
|
|
511
511
|
def validate_value(self):
|
|
512
|
-
|
|
512
|
+
def _validate_idn(name):
|
|
513
513
|
try:
|
|
514
|
-
|
|
515
|
-
except
|
|
514
|
+
name.to_unicode()
|
|
515
|
+
except dns_name.IDNAException as exc:
|
|
516
516
|
raise ValidationError(
|
|
517
|
-
{
|
|
518
|
-
"value": exc,
|
|
519
|
-
}
|
|
517
|
+
f"{name.to_text()} is not a valid IDN: {exc}."
|
|
520
518
|
) from None
|
|
521
519
|
|
|
522
520
|
try:
|
|
523
|
-
rdata.from_text(RecordClassChoices.IN, self.type, self.value)
|
|
521
|
+
rr = rdata.from_text(RecordClassChoices.IN, self.type, self.value)
|
|
524
522
|
except dns.exception.SyntaxError as exc:
|
|
525
523
|
raise ValidationError(
|
|
526
524
|
{
|
|
@@ -528,6 +526,59 @@ class Record(NetBoxModel):
|
|
|
528
526
|
}
|
|
529
527
|
) from None
|
|
530
528
|
|
|
529
|
+
try:
|
|
530
|
+
match self.type:
|
|
531
|
+
case RecordTypeChoices.CNAME:
|
|
532
|
+
_validate_idn(rr.target)
|
|
533
|
+
validate_domain_name(
|
|
534
|
+
rr.target.to_text(),
|
|
535
|
+
always_tolerant=True,
|
|
536
|
+
allow_empty_label=True,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
case (
|
|
540
|
+
RecordTypeChoices.DNAME
|
|
541
|
+
| RecordTypeChoices.NS
|
|
542
|
+
| RecordTypeChoices.HTTPS
|
|
543
|
+
| RecordTypeChoices.SRV
|
|
544
|
+
| RecordTypeChoices.SVCB
|
|
545
|
+
):
|
|
546
|
+
_validate_idn(rr.target)
|
|
547
|
+
validate_domain_name(rr.target.to_text(), always_tolerant=True)
|
|
548
|
+
|
|
549
|
+
case RecordTypeChoices.PTR | RecordTypeChoices.NSAP_PTR:
|
|
550
|
+
_validate_idn(rr.target)
|
|
551
|
+
validate_fqdn(rr.target.to_text(), always_tolerant=True)
|
|
552
|
+
|
|
553
|
+
case RecordTypeChoices.MX | RecordTypeChoices.RT | RecordTypeChoices.KX:
|
|
554
|
+
_validate_idn(rr.exchange)
|
|
555
|
+
validate_domain_name(rr.exchange.to_text(), always_tolerant=True)
|
|
556
|
+
|
|
557
|
+
case RecordTypeChoices.NSEC:
|
|
558
|
+
_validate_idn(rr.next)
|
|
559
|
+
validate_domain_name(rr.next.to_text(), always_tolerant=True)
|
|
560
|
+
|
|
561
|
+
case RecordTypeChoices.RP:
|
|
562
|
+
_validate_idn(rr.mbox)
|
|
563
|
+
validate_domain_name(rr.mbox.to_text(), always_tolerant=True)
|
|
564
|
+
_validate_idn(rr.txt)
|
|
565
|
+
validate_domain_name(rr.txt.to_text(), always_tolerant=True)
|
|
566
|
+
|
|
567
|
+
case RecordTypeChoices.NAPTR:
|
|
568
|
+
_validate_idn(rr.replacement)
|
|
569
|
+
validate_extended_hostname(
|
|
570
|
+
rr.replacement.to_text(), always_tolerant=True
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
case RecordTypeChoices.PX:
|
|
574
|
+
_validate_idn(rr.map822)
|
|
575
|
+
validate_domain_name(rr.map822.to_text(), always_tolerant=True)
|
|
576
|
+
_validate_idn(rr.mapx400)
|
|
577
|
+
validate_domain_name(rr.mapx400.to_text(), always_tolerant=True)
|
|
578
|
+
|
|
579
|
+
except ValidationError as exc:
|
|
580
|
+
raise ValidationError({"value": exc}) from None
|
|
581
|
+
|
|
531
582
|
def check_unique_record(self):
|
|
532
583
|
if not get_plugin_config("netbox_dns", "enforce_unique_records", False):
|
|
533
584
|
return
|
|
@@ -695,6 +746,8 @@ class Record(NetBoxModel):
|
|
|
695
746
|
}
|
|
696
747
|
) from None
|
|
697
748
|
|
|
749
|
+
super().clean(*args, **kwargs)
|
|
750
|
+
|
|
698
751
|
def save(
|
|
699
752
|
self,
|
|
700
753
|
*args,
|
|
@@ -732,9 +785,9 @@ class Record(NetBoxModel):
|
|
|
732
785
|
|
|
733
786
|
super().save(*args, **kwargs)
|
|
734
787
|
|
|
735
|
-
|
|
736
|
-
if self.type != RecordTypeChoices.SOA and
|
|
737
|
-
|
|
788
|
+
_zone = self.zone
|
|
789
|
+
if self.type != RecordTypeChoices.SOA and _zone.soa_serial_auto:
|
|
790
|
+
_zone.update_serial(save_zone_serial=save_zone_serial)
|
|
738
791
|
|
|
739
792
|
def delete(self, *args, save_zone_serial=True, **kwargs):
|
|
740
793
|
if self.rfc2317_cname_record:
|
|
@@ -745,16 +798,17 @@ class Record(NetBoxModel):
|
|
|
745
798
|
|
|
746
799
|
super().delete(*args, **kwargs)
|
|
747
800
|
|
|
748
|
-
|
|
749
|
-
if
|
|
750
|
-
|
|
801
|
+
_zone = self.zone
|
|
802
|
+
if _zone.soa_serial_auto:
|
|
803
|
+
_zone.update_serial(save_zone_serial=save_zone_serial)
|
|
751
804
|
|
|
752
805
|
|
|
753
806
|
@register_search
|
|
754
807
|
class RecordIndex(SearchIndex):
|
|
755
808
|
model = Record
|
|
756
809
|
fields = (
|
|
757
|
-
("
|
|
810
|
+
("fqdn", 100),
|
|
811
|
+
("name", 120),
|
|
758
812
|
("value", 150),
|
|
759
813
|
("zone", 200),
|
|
760
814
|
("type", 200),
|
netbox_dns/models/view.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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
|
|
|
7
10
|
|
|
8
11
|
class View(NetBoxModel):
|
|
@@ -14,6 +17,9 @@ class View(NetBoxModel):
|
|
|
14
17
|
max_length=200,
|
|
15
18
|
blank=True,
|
|
16
19
|
)
|
|
20
|
+
default_view = models.BooleanField(
|
|
21
|
+
default=False,
|
|
22
|
+
)
|
|
17
23
|
tenant = models.ForeignKey(
|
|
18
24
|
to="tenancy.Tenant",
|
|
19
25
|
on_delete=models.PROTECT,
|
|
@@ -24,6 +30,10 @@ class View(NetBoxModel):
|
|
|
24
30
|
|
|
25
31
|
clone_fields = ["name", "description"]
|
|
26
32
|
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_default_view(cls):
|
|
35
|
+
return cls.objects.get(default_view=True)
|
|
36
|
+
|
|
27
37
|
def get_absolute_url(self):
|
|
28
38
|
return reverse("plugins:netbox_dns:view", kwargs={"pk": self.pk})
|
|
29
39
|
|
|
@@ -33,6 +43,49 @@ class View(NetBoxModel):
|
|
|
33
43
|
class Meta:
|
|
34
44
|
ordering = ("name",)
|
|
35
45
|
|
|
46
|
+
def delete(self, *args, **kwargs):
|
|
47
|
+
if self.default_view:
|
|
48
|
+
if current_request.get() is not None:
|
|
49
|
+
raise AbortRequest("The default view cannot be deleted")
|
|
50
|
+
|
|
51
|
+
raise ValidationError("The default view cannot be deleted")
|
|
52
|
+
|
|
53
|
+
super().delete(*args, **kwargs)
|
|
54
|
+
|
|
55
|
+
def clean(self, *args, old_state=None, **kwargs):
|
|
56
|
+
if self.pk is None:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
old_state = View.objects.get(pk=self.pk)
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
old_state.default_view
|
|
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
|
+
old_state = None if self.pk is None else View.objects.get(pk=self.pk)
|
|
78
|
+
|
|
79
|
+
super().save(*args, **kwargs)
|
|
80
|
+
|
|
81
|
+
if (old_state is None and self.default_view) or (
|
|
82
|
+
old_state is not None and self.default_view and not old_state.default_view
|
|
83
|
+
):
|
|
84
|
+
other_views = View.objects.filter(default_view=True).exclude(pk=self.pk)
|
|
85
|
+
for view in other_views:
|
|
86
|
+
view.default_view = False
|
|
87
|
+
view.save()
|
|
88
|
+
|
|
36
89
|
|
|
37
90
|
@register_search
|
|
38
91
|
class ViewIndex(SearchIndex):
|
netbox_dns/models/zone.py
CHANGED
|
@@ -15,15 +15,15 @@ from django.db.models import Q, Max, ExpressionWrapper, BooleanField
|
|
|
15
15
|
from django.urls import reverse
|
|
16
16
|
from django.db.models.signals import m2m_changed
|
|
17
17
|
from django.dispatch import receiver
|
|
18
|
+
from django.conf import settings
|
|
18
19
|
|
|
19
20
|
from netbox.models import NetBoxModel
|
|
20
21
|
from netbox.search import SearchIndex, register_search
|
|
22
|
+
from netbox.plugins.utils import get_plugin_config
|
|
21
23
|
from utilities.querysets import RestrictedQuerySet
|
|
22
24
|
from utilities.choices import ChoiceSet
|
|
23
25
|
from ipam.models import IPAddress
|
|
24
26
|
|
|
25
|
-
from netbox.plugins.utils import get_plugin_config
|
|
26
|
-
|
|
27
27
|
from netbox_dns.fields import NetworkField, RFC2317NetworkField
|
|
28
28
|
from netbox_dns.utilities import (
|
|
29
29
|
arpa_to_prefix,
|
|
@@ -37,9 +37,11 @@ from netbox_dns.validators import (
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
# +
|
|
40
|
-
# This is a hack designed to break cyclic imports between Record and Zone
|
|
40
|
+
# This is a hack designed to break cyclic imports between View, Record and Zone
|
|
41
41
|
# -
|
|
42
42
|
import netbox_dns.models.record as record
|
|
43
|
+
import netbox_dns.models.view as view
|
|
44
|
+
import netbox_dns.models.nameserver as nameserver
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
|
@@ -81,8 +83,7 @@ class Zone(NetBoxModel):
|
|
|
81
83
|
view = models.ForeignKey(
|
|
82
84
|
to="View",
|
|
83
85
|
on_delete=models.PROTECT,
|
|
84
|
-
|
|
85
|
-
null=True,
|
|
86
|
+
null=False,
|
|
86
87
|
)
|
|
87
88
|
name = models.CharField(
|
|
88
89
|
max_length=255,
|
|
@@ -285,11 +286,20 @@ class Zone(NetBoxModel):
|
|
|
285
286
|
except dns_name.IDNAException:
|
|
286
287
|
name = self.name
|
|
287
288
|
|
|
288
|
-
if self.view:
|
|
289
|
+
if not self.view.default_view:
|
|
289
290
|
return f"[{self.view}] {name}"
|
|
290
291
|
|
|
291
292
|
return str(name)
|
|
292
293
|
|
|
294
|
+
@staticmethod
|
|
295
|
+
def get_defaults():
|
|
296
|
+
return {
|
|
297
|
+
field[5:]: value
|
|
298
|
+
for field, value in settings.PLUGINS_CONFIG.get("netbox_dns").items()
|
|
299
|
+
if field.startswith("zone_")
|
|
300
|
+
and field not in ("zone_soa_mname", "zone_nameservers")
|
|
301
|
+
}
|
|
302
|
+
|
|
293
303
|
@property
|
|
294
304
|
def display_name(self):
|
|
295
305
|
return name_to_unicode(self.name)
|
|
@@ -317,11 +327,11 @@ class Zone(NetBoxModel):
|
|
|
317
327
|
|
|
318
328
|
def get_rfc2317_parent_zone(self):
|
|
319
329
|
if not self.is_rfc2317_zone:
|
|
320
|
-
return
|
|
330
|
+
return None
|
|
321
331
|
|
|
322
332
|
return (
|
|
323
333
|
Zone.objects.filter(
|
|
324
|
-
self.
|
|
334
|
+
view=self.view,
|
|
325
335
|
arpa_network__net_contains=self.rfc2317_prefix,
|
|
326
336
|
)
|
|
327
337
|
.order_by("arpa_network__net_mask_length")
|
|
@@ -342,12 +352,6 @@ class Zone(NetBoxModel):
|
|
|
342
352
|
)
|
|
343
353
|
)
|
|
344
354
|
|
|
345
|
-
@property
|
|
346
|
-
def view_filter(self):
|
|
347
|
-
if self.view is None:
|
|
348
|
-
return Q(view__isnull=True)
|
|
349
|
-
return Q(view=self.view)
|
|
350
|
-
|
|
351
355
|
def record_count(self, managed=False):
|
|
352
356
|
return record.Record.objects.filter(zone=self, managed=managed).count()
|
|
353
357
|
|
|
@@ -417,19 +421,15 @@ class Zone(NetBoxModel):
|
|
|
417
421
|
if not nameservers:
|
|
418
422
|
ns_errors.append(f"No nameservers are configured for zone {self}")
|
|
419
423
|
|
|
420
|
-
for
|
|
421
|
-
name = dns_name.from_text(
|
|
424
|
+
for _nameserver in nameservers:
|
|
425
|
+
name = dns_name.from_text(_nameserver.name, origin=None)
|
|
422
426
|
parent = name.parent()
|
|
423
427
|
|
|
424
428
|
if len(parent) < 2:
|
|
425
429
|
continue
|
|
426
430
|
|
|
427
|
-
view_condition = (
|
|
428
|
-
Q(view__isnull=True) if self.view is None else Q(view_id=self.view.pk)
|
|
429
|
-
)
|
|
430
|
-
|
|
431
431
|
try:
|
|
432
|
-
ns_zone = Zone.objects.get(
|
|
432
|
+
ns_zone = Zone.objects.get(view_id=self.view.pk, name=parent.to_text())
|
|
433
433
|
except ObjectDoesNotExist:
|
|
434
434
|
continue
|
|
435
435
|
|
|
@@ -437,7 +437,7 @@ class Zone(NetBoxModel):
|
|
|
437
437
|
address_records = record.Record.objects.filter(
|
|
438
438
|
Q(zone=ns_zone),
|
|
439
439
|
Q(status__in=record.Record.ACTIVE_STATUS_LIST),
|
|
440
|
-
Q(Q(name=f"{
|
|
440
|
+
Q(Q(name=f"{_nameserver.name}.") | Q(name=relative_name)),
|
|
441
441
|
Q(
|
|
442
442
|
Q(type=record.RecordTypeChoices.A)
|
|
443
443
|
| Q(type=record.RecordTypeChoices.AAAA)
|
|
@@ -446,11 +446,23 @@ class Zone(NetBoxModel):
|
|
|
446
446
|
|
|
447
447
|
if not address_records:
|
|
448
448
|
ns_warnings.append(
|
|
449
|
-
f"Nameserver {
|
|
449
|
+
f"Nameserver {_nameserver.name} does not have an active address record in zone {ns_zone}"
|
|
450
450
|
)
|
|
451
451
|
|
|
452
452
|
return ns_warnings, ns_errors
|
|
453
453
|
|
|
454
|
+
def check_soa_serial_increment(self, old_serial, new_serial):
|
|
455
|
+
MAX_SOA_SERIAL_INCREMENT = 2**31 - 1
|
|
456
|
+
SOA_SERIAL_WRAP = 2**32
|
|
457
|
+
|
|
458
|
+
if old_serial is None:
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
if (new_serial - old_serial) % SOA_SERIAL_WRAP > MAX_SOA_SERIAL_INCREMENT:
|
|
462
|
+
raise ValidationError(
|
|
463
|
+
{"soa_serial": f"soa_serial must not decrease for zone {self.name}."}
|
|
464
|
+
)
|
|
465
|
+
|
|
454
466
|
def get_auto_serial(self):
|
|
455
467
|
records = record.Record.objects.filter(zone_id=self.pk).exclude(
|
|
456
468
|
type=record.RecordTypeChoices.SOA
|
|
@@ -492,19 +504,6 @@ class Zone(NetBoxModel):
|
|
|
492
504
|
def network_from_name(self):
|
|
493
505
|
return arpa_to_prefix(self.name)
|
|
494
506
|
|
|
495
|
-
def check_name_conflict(self):
|
|
496
|
-
if self.view is None:
|
|
497
|
-
if (
|
|
498
|
-
Zone.objects.exclude(pk=self.pk)
|
|
499
|
-
.filter(name=self.name.rstrip("."), view__isnull=True)
|
|
500
|
-
.exists()
|
|
501
|
-
):
|
|
502
|
-
raise ValidationError(
|
|
503
|
-
{
|
|
504
|
-
"name": f"A zone with name {self.name} and no view already exists."
|
|
505
|
-
}
|
|
506
|
-
)
|
|
507
|
-
|
|
508
507
|
def update_rfc2317_parent_zone(self):
|
|
509
508
|
if not self.is_rfc2317_zone:
|
|
510
509
|
return
|
|
@@ -555,8 +554,33 @@ class Zone(NetBoxModel):
|
|
|
555
554
|
ptr_zone.save_soa_serial()
|
|
556
555
|
ptr_zone.update_soa_record()
|
|
557
556
|
|
|
557
|
+
def clean_fields(self, exclude=None):
|
|
558
|
+
defaults = settings.PLUGINS_CONFIG.get("netbox_dns")
|
|
559
|
+
|
|
560
|
+
if self.view_id is None:
|
|
561
|
+
self.view_id = view.View.get_default_view().pk
|
|
562
|
+
|
|
563
|
+
for field, value in self.get_defaults().items():
|
|
564
|
+
if getattr(self, field) in (None, ""):
|
|
565
|
+
if value not in (None, ""):
|
|
566
|
+
setattr(self, field, value)
|
|
567
|
+
|
|
568
|
+
if self.soa_mname_id is None:
|
|
569
|
+
default_soa_mname = defaults.get("zone_soa_mname")
|
|
570
|
+
try:
|
|
571
|
+
self.soa_mname = nameserver.NameServer.objects.get(
|
|
572
|
+
name=default_soa_mname
|
|
573
|
+
)
|
|
574
|
+
except nameserver.NameServer.DoesNotExist:
|
|
575
|
+
raise ValidationError(
|
|
576
|
+
f"Default soa_mname instance {default_soa_mname} does not exist"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
super().clean_fields(exclude=exclude)
|
|
580
|
+
|
|
558
581
|
def clean(self, *args, **kwargs):
|
|
559
|
-
self.
|
|
582
|
+
if self.soa_ttl is None:
|
|
583
|
+
self.soa_ttl = self.default_ttl
|
|
560
584
|
|
|
561
585
|
try:
|
|
562
586
|
self.name = normalize_name(self.name)
|
|
@@ -576,6 +600,8 @@ class Zone(NetBoxModel):
|
|
|
576
600
|
}
|
|
577
601
|
) from None
|
|
578
602
|
|
|
603
|
+
if self.soa_rname in (None, ""):
|
|
604
|
+
raise ValidationError("soa_rname not set and no default value defined")
|
|
579
605
|
try:
|
|
580
606
|
dns_name.from_text(self.soa_rname, origin=dns_name.root)
|
|
581
607
|
validate_fqdn(self.soa_rname)
|
|
@@ -586,12 +612,29 @@ class Zone(NetBoxModel):
|
|
|
586
612
|
}
|
|
587
613
|
) from None
|
|
588
614
|
|
|
589
|
-
if
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
615
|
+
if not self.soa_serial_auto:
|
|
616
|
+
if self.soa_serial is None:
|
|
617
|
+
raise ValidationError(
|
|
618
|
+
{
|
|
619
|
+
"soa_serial": f"soa_serial is not defined and soa_serial_auto is disabled for zone {self.name}."
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
if self.pk is not None:
|
|
624
|
+
old_zone = Zone.objects.get(pk=self.pk)
|
|
625
|
+
if not self.soa_serial_auto:
|
|
626
|
+
self.check_soa_serial_increment(old_zone.soa_serial, self.soa_serial)
|
|
627
|
+
else:
|
|
628
|
+
try:
|
|
629
|
+
self.check_soa_serial_increment(
|
|
630
|
+
old_zone.soa_serial, self.get_auto_serial()
|
|
631
|
+
)
|
|
632
|
+
except ValidationError:
|
|
633
|
+
raise ValidationError(
|
|
634
|
+
{
|
|
635
|
+
"soa_serial_auto": f"Enabling soa_serial_auto would decrease soa_serial for zone {self.name}."
|
|
636
|
+
}
|
|
637
|
+
)
|
|
595
638
|
|
|
596
639
|
if self.is_reverse_zone:
|
|
597
640
|
self.arpa_network = self.network_from_name
|
|
@@ -619,7 +662,7 @@ class Zone(NetBoxModel):
|
|
|
619
662
|
self.rfc2317_parent_zone = None
|
|
620
663
|
|
|
621
664
|
overlapping_zones = Zone.objects.filter(
|
|
622
|
-
self.
|
|
665
|
+
view=self.view,
|
|
623
666
|
rfc2317_prefix__net_overlap=self.rfc2317_prefix,
|
|
624
667
|
active=True,
|
|
625
668
|
).exclude(pk=self.pk)
|
|
@@ -635,6 +678,8 @@ class Zone(NetBoxModel):
|
|
|
635
678
|
self.rfc2317_parent_managed = False
|
|
636
679
|
self.rfc2317_parent_zone = None
|
|
637
680
|
|
|
681
|
+
super().clean(*args, **kwargs)
|
|
682
|
+
|
|
638
683
|
def save(self, *args, **kwargs):
|
|
639
684
|
self.full_clean()
|
|
640
685
|
|
|
@@ -659,7 +704,7 @@ class Zone(NetBoxModel):
|
|
|
659
704
|
new_zone or name_changed or view_changed or status_changed
|
|
660
705
|
) and self.is_reverse_zone:
|
|
661
706
|
zones = Zone.objects.filter(
|
|
662
|
-
self.
|
|
707
|
+
view=self.view,
|
|
663
708
|
arpa_network__net_contains_or_equals=self.arpa_network,
|
|
664
709
|
)
|
|
665
710
|
address_records = record.Record.objects.filter(
|
|
@@ -695,7 +740,7 @@ class Zone(NetBoxModel):
|
|
|
695
740
|
or rfc2317_changed
|
|
696
741
|
) and self.is_rfc2317_zone:
|
|
697
742
|
zones = Zone.objects.filter(
|
|
698
|
-
self.
|
|
743
|
+
view=self.view,
|
|
699
744
|
arpa_network__net_contains=self.rfc2317_prefix,
|
|
700
745
|
)
|
|
701
746
|
address_records = record.Record.objects.filter(
|
|
@@ -5,6 +5,7 @@ from rest_framework.exceptions import PermissionDenied as APIPermissionDenied
|
|
|
5
5
|
|
|
6
6
|
from netbox.signals import post_clean
|
|
7
7
|
from netbox.context import current_request
|
|
8
|
+
from netbox.plugins.utils import get_plugin_config
|
|
8
9
|
from ipam.models import IPAddress
|
|
9
10
|
|
|
10
11
|
from netbox_dns.models import Zone
|
|
@@ -18,8 +19,6 @@ from netbox_dns.utilities.ipam_coupling import (
|
|
|
18
19
|
DNSPermissionDenied,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
|
-
from netbox.plugins.utils import get_plugin_config
|
|
22
|
-
|
|
23
22
|
|
|
24
23
|
@receiver(post_clean, sender=IPAddress)
|
|
25
24
|
def ip_address_check_permissions_save(instance, **kwargs):
|
netbox_dns/tables/view.py
CHANGED
|
@@ -10,9 +10,19 @@ class ViewTable(TenancyColumnsMixin, NetBoxTable):
|
|
|
10
10
|
name = tables.Column(
|
|
11
11
|
linkify=True,
|
|
12
12
|
)
|
|
13
|
+
default_view = tables.Column(
|
|
14
|
+
verbose_name="Default View",
|
|
15
|
+
)
|
|
13
16
|
tags = TagColumn(url_name="plugins:netbox_dns:view_list")
|
|
14
17
|
|
|
15
18
|
class Meta(NetBoxTable.Meta):
|
|
16
19
|
model = View
|
|
17
|
-
fields = (
|
|
18
|
-
|
|
20
|
+
fields = (
|
|
21
|
+
"name",
|
|
22
|
+
"default_view",
|
|
23
|
+
"description",
|
|
24
|
+
"tenant",
|
|
25
|
+
"tenant_group",
|
|
26
|
+
"tags",
|
|
27
|
+
)
|
|
28
|
+
default_columns = ("name", "default_view")
|
netbox_dns/template_content.py
CHANGED