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.

Files changed (40) hide show
  1. netbox_dns/__init__.py +3 -3
  2. netbox_dns/api/serializers_/view.py +6 -1
  3. netbox_dns/fields/network.py +20 -21
  4. netbox_dns/fields/rfc2317.py +2 -2
  5. netbox_dns/filtersets/view.py +1 -1
  6. netbox_dns/filtersets/zone.py +4 -4
  7. netbox_dns/forms/record.py +30 -2
  8. netbox_dns/forms/view.py +6 -3
  9. netbox_dns/forms/zone.py +71 -102
  10. netbox_dns/graphql/types.py +1 -4
  11. netbox_dns/migrations/0001_squashed_netbox_dns_0_22.py +4 -2
  12. netbox_dns/migrations/0003_default_view.py +15 -0
  13. netbox_dns/migrations/0004_create_and_assign_default_view.py +26 -0
  14. netbox_dns/migrations/0005_alter_zone_view_not_null.py +18 -0
  15. netbox_dns/mixins/__init__.py +1 -0
  16. netbox_dns/mixins/object_modification.py +26 -0
  17. netbox_dns/models/nameserver.py +7 -6
  18. netbox_dns/models/record.py +94 -35
  19. netbox_dns/models/view.py +56 -1
  20. netbox_dns/models/zone.py +101 -67
  21. netbox_dns/signals/ipam_coupling.py +1 -2
  22. netbox_dns/tables/view.py +12 -2
  23. netbox_dns/template_content.py +1 -1
  24. netbox_dns/templates/netbox_dns/record.html +1 -1
  25. netbox_dns/templates/netbox_dns/view.html +4 -0
  26. netbox_dns/templates/netbox_dns/zone.html +2 -4
  27. netbox_dns/urls/__init__.py +17 -0
  28. netbox_dns/urls/contact.py +51 -0
  29. netbox_dns/urls/nameserver.py +69 -0
  30. netbox_dns/urls/record.py +41 -0
  31. netbox_dns/urls/registrar.py +63 -0
  32. netbox_dns/urls/view.py +39 -0
  33. netbox_dns/urls/zone.py +57 -0
  34. netbox_dns/validators/dns_name.py +24 -11
  35. netbox_dns/views/record.py +10 -18
  36. {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/LICENSE +2 -1
  37. {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/METADATA +15 -14
  38. {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/RECORD +39 -28
  39. netbox_dns/urls.py +0 -297
  40. {netbox_plugin_dns-1.0b2.dist-info → netbox_plugin_dns-1.0.2.dist-info}/WHEEL +0 -0
@@ -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
- name_changed = (
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 name_changed:
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()
@@ -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
- name = dns_name.from_text(self.fqdn).relativize(dns_name.root).to_unicode()
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 = self.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
- zone = dns_name.from_text(self.zone.name)
238
- value_fqdn = dns_name.from_text(self.value, origin=zone)
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.view_filter,
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.view_filter, arpa_network__net_contains=self.value
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
- else:
422
- self.remove_from_rfc2317_cname_record(
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
- zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
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=zone)
471
+ fqdn = dns_name.from_text(self.name, origin=_zone)
468
472
 
469
- zone.to_unicode()
473
+ _zone.to_unicode()
470
474
  name.to_unicode()
471
475
 
472
- self.name = name.relativize(zone).to_text()
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(zone):
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=list()
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=list(),
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
- if self.type in (RecordTypeChoices.PTR):
516
+ def _validate_idn(name):
513
517
  try:
514
- validate_fqdn(self.value)
515
- except ValidationError as exc:
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
- super().save(*args, **kwargs)
790
+ changed_fields = self.changed_fields
791
+ if changed_fields is None or changed_fields:
792
+ super().save(*args, **kwargs)
734
793
 
735
- zone = self.zone
736
- if self.type != RecordTypeChoices.SOA and zone.soa_serial_auto:
737
- zone.update_serial(save_zone_serial=save_zone_serial)
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
- zone = self.zone
749
- if zone.soa_serial_auto:
750
- zone.update_serial(save_zone_serial=save_zone_serial)
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
- class View(NetBoxModel):
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):