netbox-plugin-dns 0.22.4__py3-none-any.whl → 0.22.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 CHANGED
@@ -10,7 +10,7 @@ except ImportError:
10
10
  # NetBox 3.5.8
11
11
  from extras.plugins.utils import get_plugin_config
12
12
 
13
- __version__ = "0.22.4"
13
+ __version__ = "0.22.5"
14
14
 
15
15
 
16
16
  class DNSConfig(PluginConfig):
@@ -38,7 +38,8 @@ class DNSConfig(PluginConfig):
38
38
  ],
39
39
  "tolerate_non_rfc1035_types": [],
40
40
  "enable_root_zones": False,
41
- "enforce_unique_records": False,
41
+ "enforce_unique_records": True,
42
+ "enforce_unique_rrset_ttl": True,
42
43
  }
43
44
  base_url = "netbox-dns"
44
45
 
@@ -272,6 +272,7 @@ class RegistrarSerializer(NetBoxModelSerializer):
272
272
  "display",
273
273
  "name",
274
274
  "iana_id",
275
+ "address",
275
276
  "referral_url",
276
277
  "whois_server",
277
278
  "abuse_email",
@@ -12,6 +12,7 @@ class RegistrarFilter(NetBoxModelFilterSet):
12
12
  "id",
13
13
  "name",
14
14
  "iana_id",
15
+ "address",
15
16
  "referral_url",
16
17
  "whois_server",
17
18
  "abuse_email",
@@ -17,6 +17,7 @@ class RegistrarForm(NetBoxModelForm):
17
17
  fields = (
18
18
  "name",
19
19
  "iana_id",
20
+ "address",
20
21
  "referral_url",
21
22
  "whois_server",
22
23
  "abuse_email",
@@ -29,12 +30,18 @@ class RegistrarFilterForm(NetBoxModelFilterSetForm):
29
30
  model = Registrar
30
31
  fieldsets = (
31
32
  (None, ("q", "name", "iana_id", "tags")),
32
- ("Contact", ("referral_url", "whois_server", "abuse_email", "abuse_phone")),
33
+ (
34
+ "Contact",
35
+ ("address", "referral_url", "whois_server", "abuse_email", "abuse_phone"),
36
+ ),
33
37
  )
34
38
 
35
39
  name = forms.CharField(
36
40
  required=False,
37
41
  )
42
+ address = forms.CharField(
43
+ required=False,
44
+ )
38
45
  iana_id = forms.IntegerField(
39
46
  required=False,
40
47
  label="IANA ID",
@@ -64,6 +71,7 @@ class RegistrarImportForm(NetBoxModelImportForm):
64
71
  fields = (
65
72
  "name",
66
73
  "iana_id",
74
+ "address",
67
75
  "referral_url",
68
76
  "whois_server",
69
77
  "abuse_email",
@@ -96,6 +104,7 @@ class RegistrarBulkEditForm(NetBoxModelBulkEditForm):
96
104
  (
97
105
  None,
98
106
  (
107
+ "address",
99
108
  "referral_url",
100
109
  "whois_server",
101
110
  "abuse_email",
@@ -105,6 +114,7 @@ class RegistrarBulkEditForm(NetBoxModelBulkEditForm):
105
114
  )
106
115
 
107
116
  nullable_fields = (
117
+ "address",
108
118
  "referral_url",
109
119
  "whois_server",
110
120
  "abuse_email",
netbox_dns/forms/zone.py CHANGED
@@ -315,7 +315,7 @@ class ZoneImportForm(NetBoxModelImportForm):
315
315
  required=False,
316
316
  help_text="Mailbox of the zone's administrator",
317
317
  )
318
- soa_serial_auto = forms.BooleanField(
318
+ soa_serial_auto = forms.NullBooleanField(
319
319
  required=False,
320
320
  help_text="Generate the SOA serial",
321
321
  )
@@ -443,26 +443,6 @@ class ZoneImportForm(NetBoxModelImportForm):
443
443
  def clean_soa_rname(self):
444
444
  return self._clean_field_with_defaults("soa_rname")
445
445
 
446
- def clean_soa_serial_auto(self):
447
- try:
448
- return self._clean_field_with_defaults("soa_serial_auto")
449
- except ValidationError:
450
- if self.cleaned_data["soa_serial"] or self._get_default_value("soa_serial"):
451
- return None
452
-
453
- raise
454
-
455
- def clean_soa_serial(self):
456
- try:
457
- return self._clean_field_with_defaults("soa_serial")
458
- except ValidationError:
459
- if self.cleaned_data["soa_serial_auto"] or self._get_default_value(
460
- "soa_serial_auto"
461
- ):
462
- return None
463
-
464
- raise
465
-
466
446
  def clean_soa_refresh(self):
467
447
  return self._clean_field_with_defaults("soa_refresh")
468
448
 
@@ -475,6 +455,36 @@ class ZoneImportForm(NetBoxModelImportForm):
475
455
  def clean_soa_minimum(self):
476
456
  return self._clean_field_with_defaults("soa_minimum")
477
457
 
458
+ def clean(self, *args, **kwargs):
459
+ super().clean(*args, **kwargs)
460
+
461
+ soa_serial_auto = self.cleaned_data.get("soa_serial_auto")
462
+ soa_serial = self.cleaned_data.get("soa_serial")
463
+
464
+ if soa_serial is None:
465
+ soa_serial = self._get_default_value("soa_serial")
466
+
467
+ if soa_serial_auto is None:
468
+ if self._get_default_value("soa_serial_auto") is not None:
469
+ soa_serial_auto = self._get_default_value("soa_serial_auto")
470
+
471
+ elif soa_serial is not None:
472
+ soa_serial_auto = False
473
+
474
+ else:
475
+ raise ValidationError(
476
+ "SOA Serial Auto not set and no default value and SOA Serial available"
477
+ )
478
+
479
+ if "soa_serial_auto" in self.cleaned_data:
480
+ self.cleaned_data["soa_serial_auto"] = soa_serial_auto
481
+
482
+ if "soa_serial" in self.cleaned_data:
483
+ if soa_serial_auto:
484
+ self.cleaned_data["soa_serial"] = None
485
+ else:
486
+ self.cleaned_data["soa_serial"] = soa_serial
487
+
478
488
  class Meta:
479
489
  model = Zone
480
490
 
@@ -0,0 +1,62 @@
1
+ from django.core.management.base import BaseCommand
2
+ from django.db.models import Max, Min
3
+
4
+ from netbox_dns.models import (
5
+ Record,
6
+ RecordTypeChoices,
7
+ )
8
+
9
+
10
+ class Command(BaseCommand):
11
+ help = "Clean up the TTLs for RRSets"
12
+
13
+ def add_arguments(self, parser):
14
+ min_max = parser.add_mutually_exclusive_group()
15
+ min_max.add_argument(
16
+ "--min",
17
+ action="store_true",
18
+ help="Use the minimum TTL of an RRSet for all Records",
19
+ )
20
+ min_max.add_argument(
21
+ "--max",
22
+ action="store_true",
23
+ help="Use the maximum TTL of an RRSet for all Records",
24
+ )
25
+
26
+ def handle(self, *model_names, **options):
27
+ self.cleanup_rrset_ttl(**options)
28
+
29
+ self.stdout.write("RRSet cleanup completed.")
30
+
31
+ def cleanup_rrset_ttl(self, **options):
32
+ verbose = options.get("verbosity") > 1
33
+
34
+ ttl_records = (
35
+ Record.objects.filter(ttl__isnull=False)
36
+ .exclude(type=RecordTypeChoices.SOA)
37
+ .exclude(type=RecordTypeChoices.PTR, maanged=True)
38
+ )
39
+ for record in ttl_records:
40
+ records = Record.objects.filter(
41
+ name=record.name,
42
+ zone=record.zone,
43
+ type=record.type,
44
+ ).exclude(type=RecordTypeChoices.PTR, maanged=True)
45
+
46
+ if records.count() == 1:
47
+ if options.get("verbosity") > 2:
48
+ self.stdout.write(f"Ignoring single record {record.id} ({record})")
49
+ continue
50
+
51
+ if options.get("max"):
52
+ ttl = records.aggregate(Max("ttl")).get("ttl__max")
53
+ else:
54
+ ttl = records.aggregate(Min("ttl")).get("ttl__min")
55
+
56
+ for record in records.exclude(ttl=ttl):
57
+ if options.get("verbosity") > 1:
58
+ self.stdout.write(
59
+ f"Updating TTL for record {record.id} ({record}) to {ttl}"
60
+ )
61
+ record.ttl = ttl
62
+ record.save(update_fields=["ttl"], update_rrset_ttl=False)
@@ -6,7 +6,7 @@ from dns import name as dns_name
6
6
 
7
7
  from django.core.exceptions import ValidationError
8
8
  from django.db import transaction, models
9
- from django.db.models import Q, ExpressionWrapper, BooleanField
9
+ from django.db.models import Q, ExpressionWrapper, BooleanField, Min
10
10
  from django.db.models.functions import Length
11
11
  from django.urls import reverse
12
12
 
@@ -38,6 +38,10 @@ from netbox_dns.validators import (
38
38
  import netbox_dns.models.zone as zone
39
39
 
40
40
 
41
+ def min_ttl(*ttl_list):
42
+ return min((ttl for ttl in ttl_list if ttl is not None), default=None)
43
+
44
+
41
45
  class RecordManager(models.Manager.from_queryset(RestrictedQuerySet)):
42
46
  """Special Manager for records providing the activity status annotation"""
43
47
 
@@ -207,7 +211,8 @@ class Record(NetBoxModel):
207
211
  try:
208
212
  name = (
209
213
  dns_name.from_text(
210
- self.name, origin=dns_name.from_text(self.zone.name, origin=None)
214
+ str(self.name),
215
+ origin=dns_name.from_text(self.zone.name, origin=None),
211
216
  )
212
217
  .relativize(dns_name.root)
213
218
  .to_unicode()
@@ -309,7 +314,7 @@ class Record(NetBoxModel):
309
314
 
310
315
  return ptr_zone
311
316
 
312
- def update_ptr_record(self, update_rfc2317_cname=True):
317
+ def update_ptr_record(self, update_rfc2317_cname=True, save_zone_serial=True):
313
318
  ptr_zone = self.ptr_zone
314
319
 
315
320
  if (
@@ -339,14 +344,16 @@ class Record(NetBoxModel):
339
344
  not ptr_record.zone.is_rfc2317_zone
340
345
  and ptr_record.rfc2317_cname_record is not None
341
346
  ):
342
- ptr_record.rfc2317_cname_record.delete()
347
+ ptr_record.rfc2317_cname_record.delete(
348
+ save_zone_serial=save_zone_serial
349
+ )
343
350
 
344
351
  with transaction.atomic():
345
352
  if ptr_record is not None:
346
353
  if ptr_record.zone.pk != ptr_zone.pk:
347
354
  if ptr_record.rfc2317_cname_record is not None:
348
355
  ptr_record.rfc2317_cname_record.delete()
349
- ptr_record.delete()
356
+ ptr_record.delete(save_zone_serial=save_zone_serial)
350
357
  ptr_record = None
351
358
 
352
359
  else:
@@ -358,7 +365,7 @@ class Record(NetBoxModel):
358
365
  ptr_record.name = ptr_name
359
366
  ptr_record.value = ptr_value
360
367
  ptr_record.ttl = self.ttl
361
- ptr_record.save()
368
+ ptr_record.save(save_zone_serial=save_zone_serial)
362
369
 
363
370
  if ptr_record is None:
364
371
  ptr_record = Record(
@@ -369,11 +376,14 @@ class Record(NetBoxModel):
369
376
  value=ptr_value,
370
377
  managed=True,
371
378
  )
372
- ptr_record.save(update_rfc2317_cname=update_rfc2317_cname)
379
+ ptr_record.save(
380
+ update_rfc2317_cname=update_rfc2317_cname,
381
+ save_zone_serial=save_zone_serial,
382
+ )
373
383
 
374
384
  self.ptr_record = ptr_record
375
385
 
376
- def update_rfc2317_cname_record(self):
386
+ def update_rfc2317_cname_record(self, save_zone_serial=True):
377
387
  if self.zone.rfc2317_parent_managed:
378
388
  cname_name = dns_name.from_text(
379
389
  ipaddress.ip_address(self.ip_address).reverse_pointer
@@ -383,7 +393,13 @@ class Record(NetBoxModel):
383
393
  self.rfc2317_cname_record.name = cname_name
384
394
  self.rfc2317_cname_record.zone = self.zone.rfc2317_parent_zone
385
395
  self.rfc2317_cname_record.value = self.fqdn
386
- self.rfc2317_cname_record.save()
396
+ self.rfc2317_cname_record.ttl = min_ttl(
397
+ self.rfc2317_cname_record.rfc2317_ptr_records.exclude(pk=self.pk)
398
+ .aggregate(Min("ttl"))
399
+ .get("ttl__min"),
400
+ self.ttl,
401
+ )
402
+ self.rfc2317_cname_record.save(save_zone_serial=save_zone_serial)
387
403
  else:
388
404
  rfc2317_cname_record = Record.objects.filter(
389
405
  name=cname_name,
@@ -392,20 +408,34 @@ class Record(NetBoxModel):
392
408
  managed=True,
393
409
  value=self.fqdn,
394
410
  ).first()
395
- if rfc2317_cname_record is None:
396
- rfc2317_cname_record = Record.objects.create(
411
+
412
+ if rfc2317_cname_record is not None:
413
+ rfc2317_cname_record.ttl = min_ttl(
414
+ rfc2317_cname_record.rfc2317_ptr_records.exclude(pk=self.pk)
415
+ .aggregate(Min("ttl"))
416
+ .get("ttl__min"),
417
+ self.ttl,
418
+ )
419
+ rfc2317_cname_record.save(
420
+ update_fields=["ttl"], save_zone_serial=save_zone_serial
421
+ )
422
+
423
+ else:
424
+ rfc2317_cname_record = Record(
397
425
  name=cname_name,
398
426
  type=RecordTypeChoices.CNAME,
399
427
  zone=self.zone.rfc2317_parent_zone,
400
428
  managed=True,
401
429
  value=self.fqdn,
430
+ ttl=self.ttl,
402
431
  )
432
+ rfc2317_cname_record.save(save_zone_serial=save_zone_serial)
403
433
 
404
434
  self.rfc2317_cname_record = rfc2317_cname_record
405
435
 
406
436
  else:
407
437
  if self.rfc2317_cname_record is not None:
408
- self.rfc2317_cname_record.delete()
438
+ self.rfc2317_cname_record.delete(save_zone_serial=save_zone_serial)
409
439
  self.rfc2317_cname_record = None
410
440
 
411
441
  def validate_name(self):
@@ -475,7 +505,7 @@ class Record(NetBoxModel):
475
505
  }
476
506
  ) from None
477
507
 
478
- def check_unique(self):
508
+ def check_unique_record(self):
479
509
  if not get_plugin_config("netbox_dns", "enforce_unique_records", False):
480
510
  return
481
511
 
@@ -489,6 +519,10 @@ class Record(NetBoxModel):
489
519
  value=self.value,
490
520
  status__in=Record.ACTIVE_STATUS_LIST,
491
521
  )
522
+
523
+ if self.pk is not None:
524
+ records = records.exclude(pk=self.pk)
525
+
492
526
  if len(records):
493
527
  raise ValidationError(
494
528
  {
@@ -496,6 +530,64 @@ class Record(NetBoxModel):
496
530
  }
497
531
  ) from None
498
532
 
533
+ def check_unique_rrset_ttl(self):
534
+ if self.pk is not None:
535
+ return
536
+
537
+ if not get_plugin_config("netbox_dns", "enforce_unique_rrset_ttl", False):
538
+ return
539
+
540
+ if self.type == RecordTypeChoices.PTR and self.managed:
541
+ return
542
+
543
+ records = (
544
+ Record.objects.filter(
545
+ zone=self.zone,
546
+ name=self.name,
547
+ type=self.type,
548
+ )
549
+ .exclude(ttl=self.ttl)
550
+ .exclude(type=RecordTypeChoices.PTR, managed=True)
551
+ )
552
+
553
+ if not records.exists():
554
+ return
555
+
556
+ conflicting_ttls = ", ".join(set(str(record.ttl) for record in records))
557
+ raise ValidationError(
558
+ {
559
+ "ttl": f"There is at least one active {self.type} record for name {self.name} in zone {self.zone} and TTL is different ({conflicting_ttls})."
560
+ }
561
+ ) from None
562
+
563
+ def update_rrset_ttl(self, ttl=None):
564
+ if self.pk is None:
565
+ return
566
+
567
+ if not get_plugin_config("netbox_dns", "enforce_unique_rrset_ttl", False):
568
+ return
569
+
570
+ if self.type == RecordTypeChoices.PTR and self.managed:
571
+ return
572
+
573
+ if ttl is None:
574
+ ttl = self.ttl
575
+
576
+ records = (
577
+ Record.objects.filter(
578
+ zone=self.zone,
579
+ name=self.name,
580
+ type=self.type,
581
+ )
582
+ .exclude(pk=self.pk)
583
+ .exclude(ttl=ttl)
584
+ .exclude(type=RecordTypeChoices.PTR, managed=True)
585
+ )
586
+
587
+ for record in records:
588
+ record.ttl = ttl
589
+ record.save(update_fields=["ttl"], update_rrset_ttl=False)
590
+
499
591
  def clean_fields(self, *args, **kwargs):
500
592
  self.type = self.type.upper()
501
593
  super().clean_fields(*args, **kwargs)
@@ -503,7 +595,9 @@ class Record(NetBoxModel):
503
595
  def clean(self, *args, **kwargs):
504
596
  self.validate_name()
505
597
  self.validate_value()
506
- self.check_unique()
598
+ self.check_unique_record()
599
+ if self.pk is None:
600
+ self.check_unique_rrset_ttl()
507
601
 
508
602
  if not self.is_active:
509
603
  return
@@ -578,14 +672,24 @@ class Record(NetBoxModel):
578
672
  }
579
673
  ) from None
580
674
 
581
- def save(self, *args, update_rfc2317_cname=True, **kwargs):
675
+ def save(
676
+ self,
677
+ *args,
678
+ update_rfc2317_cname=True,
679
+ save_zone_serial=True,
680
+ update_rrset_ttl=True,
681
+ **kwargs,
682
+ ):
582
683
  self.full_clean()
583
684
 
685
+ if self.pk is not None and update_rrset_ttl:
686
+ self.update_rrset_ttl()
687
+
584
688
  if self.is_ptr_record:
585
689
  if self.zone.is_rfc2317_zone:
586
690
  self.ip_address = self.address_from_rfc2317_name
587
691
  if update_rfc2317_cname:
588
- self.update_rfc2317_cname_record()
692
+ self.update_rfc2317_cname_record(save_zone_serial=save_zone_serial)
589
693
  else:
590
694
  self.ip_address = self.address_from_name
591
695
 
@@ -595,7 +699,10 @@ class Record(NetBoxModel):
595
699
  self.ip_address = None
596
700
 
597
701
  if self.is_address_record:
598
- self.update_ptr_record(update_rfc2317_cname=update_rfc2317_cname)
702
+ self.update_ptr_record(
703
+ update_rfc2317_cname=update_rfc2317_cname,
704
+ save_zone_serial=save_zone_serial,
705
+ )
599
706
  elif self.ptr_record is not None:
600
707
  self.ptr_record.delete()
601
708
  self.ptr_record = None
@@ -604,15 +711,24 @@ class Record(NetBoxModel):
604
711
 
605
712
  zone = self.zone
606
713
  if self.type != RecordTypeChoices.SOA and zone.soa_serial_auto:
607
- zone.update_serial()
714
+ zone.update_serial(save_zone_serial=save_zone_serial)
608
715
 
609
- def delete(self, *args, **kwargs):
716
+ def delete(self, *args, save_zone_serial=True, **kwargs):
610
717
  if self.rfc2317_cname_record:
611
- if (
612
- self.rfc2317_cname_record.pk
613
- and self.rfc2317_cname_record.rfc2317_ptr_records.count() == 1
614
- ):
615
- self.rfc2317_cname_record.delete()
718
+ if self.rfc2317_cname_record.pk:
719
+ if self.rfc2317_cname_record.rfc2317_ptr_records.count() == 1:
720
+ self.rfc2317_cname_record.delete()
721
+ else:
722
+ self.rfc2317_cname_record.ttl = (
723
+ self.rfc2317_cname_record.rfc2317_ptr_records.exclude(
724
+ pk=self.pk
725
+ )
726
+ .aggregate(Min("ttl"))
727
+ .get("ttl__min")
728
+ )
729
+ self.rfc2317_cname_record.save(
730
+ update_fields=["ttl"], save_zone_serial=save_zone_serial
731
+ )
616
732
 
617
733
  if self.ptr_record:
618
734
  self.ptr_record.delete()
@@ -621,7 +737,7 @@ class Record(NetBoxModel):
621
737
 
622
738
  zone = self.zone
623
739
  if zone.soa_serial_auto:
624
- zone.update_serial()
740
+ zone.update_serial(save_zone_serial=save_zone_serial)
625
741
 
626
742
 
627
743
  @register_search
netbox_dns/models/zone.py CHANGED
@@ -249,6 +249,8 @@ class Zone(NetBoxModel):
249
249
  null=True,
250
250
  )
251
251
 
252
+ soa_serial_dirty = False
253
+
252
254
  objects = ZoneManager()
253
255
 
254
256
  clone_fields = [
@@ -470,11 +472,24 @@ class Zone(NetBoxModel):
470
472
 
471
473
  return soa_serial
472
474
 
473
- def update_serial(self):
475
+ def update_serial(self, save_zone_serial=True):
476
+ if not self.soa_serial_auto:
477
+ return
478
+
474
479
  self.last_updated = datetime.now()
475
480
  self.soa_serial = ceil(datetime.now().timestamp())
476
- self.update_soa_record()
477
- super().save()
481
+
482
+ if save_zone_serial:
483
+ super().save(update_fields=["soa_serial", "last_updated"])
484
+ self.soa_serial_dirty = False
485
+ self.update_soa_record()
486
+ else:
487
+ self.soa_serial_dirty = True
488
+
489
+ def save_soa_serial(self):
490
+ if self.soa_serial_auto and self.soa_serial_dirty:
491
+ super().save(update_fields=["soa_serial", "last_updated"])
492
+ self.soa_serial_dirty = False
478
493
 
479
494
  @property
480
495
  def network_from_name(self):
@@ -503,28 +518,45 @@ class Zone(NetBoxModel):
503
518
  if rfc2317_parent_zone is None:
504
519
  self.rfc2317_parent_managed = False
505
520
  self.rfc2317_parent_zone = None
506
- self.save()
521
+ self.save(
522
+ update_fields=["rfc2317_parent_zone", "rfc2317_parent_managed"]
523
+ )
507
524
 
508
525
  elif self.rfc2317_parent_zone != rfc2317_parent_zone:
509
526
  self.rfc2317_parent_zone = rfc2317_parent_zone
510
- self.save()
527
+ self.save(update_fields=["rfc2317_parent_zone"])
511
528
 
512
- ptr_records = self.record_set.filter(type=record.RecordTypeChoices.PTR)
529
+ ptr_records = self.record_set.filter(
530
+ type=record.RecordTypeChoices.PTR
531
+ ).prefetch_related("zone", "rfc2317_cname_record")
532
+ ptr_zones = {ptr_record.zone for ptr_record in ptr_records}
513
533
 
514
534
  if self.rfc2317_parent_managed:
515
535
  for ptr_record in ptr_records:
516
- ptr_record.save()
536
+ ptr_record.save(save_zone_serial=False)
517
537
 
538
+ self.rfc2317_parent_zone.save_soa_serial()
539
+ self.rfc2317_parent_zone.update_soa_record()
518
540
  else:
519
541
  cname_records = {
520
542
  ptr_record.rfc2317_cname_record
521
543
  for ptr_record in ptr_records
522
544
  if ptr_record.rfc2317_cname_record is not None
523
545
  }
546
+ cname_zones = {cname_record.zone for cname_record in cname_records}
547
+
524
548
  for ptr_record in ptr_records:
525
- ptr_record.save(update_rfc2317_cname=False)
549
+ ptr_record.save(update_rfc2317_cname=False, save_zone_serial=False)
526
550
  for cname_record in cname_records:
527
- cname_record.delete()
551
+ cname_record.delete(save_zone_serial=False)
552
+
553
+ for cname_zone in cname_zones:
554
+ cname_zone.save_soa_serial()
555
+ cname_zone.update_soa_record()
556
+
557
+ for ptr_zone in ptr_zones:
558
+ ptr_zone.save_soa_serial()
559
+ ptr_zone.update_soa_record()
528
560
 
529
561
  def clean(self, *args, **kwargs):
530
562
  self.check_name_conflict()
@@ -643,7 +675,12 @@ class Zone(NetBoxModel):
643
675
  )
644
676
 
645
677
  for address_record in address_records:
646
- address_record.save(update_fields=["ptr_record"])
678
+ address_record.save(
679
+ update_fields=["ptr_record"], save_zone_serial=False
680
+ )
681
+
682
+ for zone in zones:
683
+ zone.save_soa_serial()
647
684
 
648
685
  if self.arpa_network.version == 4:
649
686
  rfc2317_child_zones = Zone.objects.filter(
@@ -674,9 +711,14 @@ class Zone(NetBoxModel):
674
711
 
675
712
  for address_record in address_records:
676
713
  address_record.save(
677
- update_fields=["ptr_record"], update_rfc2317_cname=False
714
+ update_fields=["ptr_record"],
715
+ update_rfc2317_cname=False,
716
+ save_zone_serial=False,
678
717
  )
679
718
 
719
+ for zone in zones:
720
+ zone.save_soa_serial()
721
+
680
722
  self.update_rfc2317_parent_zone()
681
723
 
682
724
  elif name_changed or view_changed or status_changed:
@@ -696,11 +738,14 @@ class Zone(NetBoxModel):
696
738
  ip.dns_name = f'{ip.custom_field_data["ipaddress_dns_record_name"]}.{self.name}'
697
739
  ip.save(update_fields=["dns_name"])
698
740
 
741
+ self.save_soa_serial()
699
742
  self.update_soa_record()
700
743
 
701
744
  def delete(self, *args, **kwargs):
702
745
  with transaction.atomic():
703
- address_records = self.record_set.filter(ptr_record__isnull=False)
746
+ address_records = self.record_set.filter(
747
+ ptr_record__isnull=False
748
+ ).prefetch_related("ptr_record")
704
749
  for address_record in address_records:
705
750
  address_record.ptr_record.delete()
706
751
 
@@ -717,8 +762,13 @@ class Zone(NetBoxModel):
717
762
  for ptr_record in ptr_records
718
763
  if ptr_record.rfc2317_cname_record is not None
719
764
  }
765
+ cname_zones = {cname_record.zone for cname_record in cname_records}
766
+
720
767
  for cname_record in cname_records:
721
- cname_record.delete()
768
+ cname_record.delete(save_zone_serial=False)
769
+ for cname_zone in cname_zones:
770
+ cname_zone.save_soa_serial()
771
+ cname_zone.update_soa_record()
722
772
 
723
773
  rfc2317_child_zones = [
724
774
  child_zone.pk for child_zone in self.rfc2317_child_zones.all()
@@ -735,11 +785,25 @@ class Zone(NetBoxModel):
735
785
 
736
786
  super().delete(*args, **kwargs)
737
787
 
738
- for address_record in record.Record.objects.filter(pk__in=update_records):
739
- address_record.save(update_fields=["ptr_record"])
740
-
741
- for child_zone in Zone.objects.filter(pk__in=rfc2317_child_zones):
742
- child_zone.update_rfc2317_parent_zone()
788
+ address_records = record.Record.objects.filter(
789
+ pk__in=update_records
790
+ ).prefetch_related("zone")
791
+
792
+ for address_record in address_records:
793
+ address_record.save(save_zone_serial=False)
794
+ for address_zone in {address_record.zone for address_record in address_records}:
795
+ address_zone.save_soa_serial()
796
+ address_zone.update_soa_record()
797
+
798
+ rfc2317_child_zones = Zone.objects.filter(pk__in=rfc2317_child_zones)
799
+ if rfc2317_child_zones:
800
+ for child_zone in rfc2317_child_zones:
801
+ child_zone.update_rfc2317_parent_zone()
802
+
803
+ new_rfc2317_parent_zone = rfc2317_child_zones.first().rfc2317_parent_zone
804
+ if new_rfc2317_parent_zone is not None:
805
+ new_rfc2317_parent_zone.save_soa_serial()
806
+ new_rfc2317_parent_zone.update_soa_record()
743
807
 
744
808
 
745
809
  @receiver(m2m_changed, sender=Zone.nameservers.through)
@@ -22,6 +22,10 @@
22
22
  <h5 class="card-header">Contact Details</h5>
23
23
  <div class="card-body">
24
24
  <table class="table table-hover attr-table">
25
+ <tr>
26
+ <th scope="row">Address</th>
27
+ <td>{{ object.address }}</td>
28
+ </tr>
25
29
  <tr>
26
30
  <th scope="row">Referral URL</th>
27
31
  <td><a href="{{ object.referral_url }}">{{ object.referral_url }}</a></td>
@@ -120,10 +120,17 @@
120
120
  <th scope="row">Responsible</th>
121
121
  <td>{{ object.soa_rname }}</td>
122
122
  </tr>
123
+ {% if object.soa_serial_auto %}
124
+ <tr>
125
+ <th scope="row">Serial (auto-generated)</th>
126
+ <td>{{ object.soa_serial }}</td>
127
+ </tr>
128
+ {% else %}
123
129
  <tr>
124
130
  <th scope="row">Serial</th>
125
131
  <td>{{ object.soa_serial }}</td>
126
132
  </tr>
133
+ {% endif %}
127
134
  <tr>
128
135
  <th scope="row">Refresh</th>
129
136
  <td>{{ object.soa_refresh }}</td>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: netbox-plugin-dns
3
- Version: 0.22.4
3
+ Version: 0.22.5
4
4
  Summary: NetBox DNS is a NetBox plugin for managing DNS data.
5
5
  Home-page: https://github.com/peteeckel/netbox-plugin-dns
6
6
  License: MIT
@@ -8,6 +8,7 @@ Keywords: netbox,netbox-plugin,dns
8
8
  Author: Peter Eckel
9
9
  Author-email: pete@netbox-dns.org
10
10
  Requires-Python: >=3.8,<4.0
11
+ Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: License :: OSI Approved :: MIT License
12
13
  Classifier: Programming Language :: Python :: 3
13
14
  Classifier: Programming Language :: Python :: 3.8
@@ -19,16 +20,15 @@ Requires-Dist: dnspython (>=2.2.1,<3.0.0)
19
20
  Project-URL: Repository, https://github.com/peteeckel/netbox-plugin-dns
20
21
  Description-Content-Type: text/markdown
21
22
 
22
- <h1 align="center">NetBox DNS</h1>
23
-
24
- <p align="center"><i>NetBox DNS is a NetBox plugin for managing DNS views, zones, name servers and records.</i></p>
23
+ # NetBox DNS
24
+ The NetBox DNS plugin enables NetBox to manage operational DNS data such as name servers, zones, records and views, as well as registration data for domains. It can automate tasks like creating PTR records, generating zone serial numbers, NS and SOA records, as well as validate names and values values for resource records to ensure zone data is consistent, current and conforming to the relevant RFCs.
25
25
 
26
26
  <div align="center">
27
27
  <a href="https://pypi.org/project/netbox-plugin-dns/"><img src="https://img.shields.io/pypi/v/netbox-plugin-dns" alt="PyPi"/></a>
28
- <a href="https://github.com/peteeckel/netbox-plugin-dns/stargazers"><img src="https://img.shields.io/github/stars/peteeckel/netbox-plugin-dns" alt="Stars Badge"/></a>
29
- <a href="https://github.com/peteeckel/netbox-plugin-dns/network/members"><img src="https://img.shields.io/github/forks/peteeckel/netbox-plugin-dns" alt="Forks Badge"/></a>
30
- <a href="https://github.com/peteeckel/netbox-plugin-dns/pulls"><img src="https://img.shields.io/github/issues-pr/peteeckel/netbox-plugin-dns" alt="Pull Requests Badge"/></a>
28
+ <a href="https://github.com/peteeckel/netbox-plugin-dns/stargazers"><img src="https://img.shields.io/github/stars/peteeckel/netbox-plugin-dns?style=flat" alt="Stars Badge"/></a>
29
+ <a href="https://github.com/peteeckel/netbox-plugin-dns/network/members"><img src="https://img.shields.io/github/forks/peteeckel/netbox-plugin-dns?style=flat" alt="Forks Badge"/></a>
31
30
  <a href="https://github.com/peteeckel/netbox-plugin-dns/issues"><img src="https://img.shields.io/github/issues/peteeckel/netbox-plugin-dns" alt="Issues Badge"/></a>
31
+ <a href="https://github.com/peteeckel/netbox-plugin-dns/pulls"><img src="https://img.shields.io/github/issues-pr/peteeckel/netbox-plugin-dns" alt="Pull Requests Badge"/></a>
32
32
  <a href="https://github.com/peteeckel/netbox-plugin-dns/graphs/contributors"><img alt="GitHub contributors" src="https://img.shields.io/github/contributors/peteeckel/netbox-plugin-dns?color=2b9348"></a>
33
33
  <a href="https://github.com/peteeckel/netbox-plugin-dns/blob/master/LICENSE"><img src="https://img.shields.io/github/license/peteeckel/netbox-plugin-dns?color=2b9348" alt="License Badge"/></a>
34
34
  <a href="https://pepy.tech/project/netbox-plugin-dns"><img alt="Downloads" src="https://static.pepy.tech/badge/netbox-plugin-dns"></a>
@@ -40,10 +40,10 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  * Manage name servers, zones and records
42
42
  * Automatically generate SOA and NS records for zones
43
- * Automatically create and update PTR records for IPv4 and IPv6 records
44
- * Optionally organize DNS zones in views for split horizon DNS and multi-site deployments
45
- * Optionally maintain domain registrar and registrant information for zones
46
- * Maintain RFC2317 reverse zones for IPv4 prefixes with a network mask length longer than 24 bits
43
+ * Automatically create and update PTR records for IPv4 and IPv6 address records
44
+ * Organize DNS zones in views for split horizon DNS and multi-site deployments
45
+ * Manage domain registrar and registrant information for domains related to zones
46
+ * Manage RFC2317 reverse zones for IPv4 prefixes with a network mask length longer than 24 bits
47
47
 
48
48
  NetBox DNS is using the standardized NetBox plugin interface, so it also takes advantage of the NetBox tagging and change log features.
49
49
 
@@ -1,6 +1,6 @@
1
- netbox_dns/__init__.py,sha256=cK957HlhJU2BDxxdHNq9c-NTm6OhvtfZuQH_DLp2BDs,2777
1
+ netbox_dns/__init__.py,sha256=Wnz89UxRISvgUOkm-cZI50tMo6taJh14iZnSb1Wx0L4,2818
2
2
  netbox_dns/api/nested_serializers.py,sha256=XB7bcCjVMPYrumJWgRicj06PukQ2UCBjdr84AIUJuVQ,3291
3
- netbox_dns/api/serializers.py,sha256=03BYJMUjIK3edvJv1kcU8jt440kIm-YH6ZAw2T374HM,8160
3
+ netbox_dns/api/serializers.py,sha256=H5Vm1O6C4SsxHVzdooYiu5ak3ykxXo-L1mT6ISWVRV8,8183
4
4
  netbox_dns/api/urls.py,sha256=R9VmmWtdrjvr35i5d_SfZK2lGn6JzmPuWEKTQlZ8MJo,575
5
5
  netbox_dns/api/views.py,sha256=DjovvTfS4F2Fq2Ahea6f4LBJiWr01ajk-wSZHNTya5I,3527
6
6
  netbox_dns/apps.py,sha256=JCW5eS-AQBUubDJve1DjP-IRFKTFGQh1NLGWzJpC5MI,151
@@ -12,16 +12,16 @@ netbox_dns/filters/__init__.py,sha256=Aw8HrCTjaJfu5JSwJsQRHfOUz4zKwAmZNByT9q6BrF
12
12
  netbox_dns/filters/contact.py,sha256=_onZ6G2KKgfvm9Emg_kng2RPETRMfbTyIR8Tvs9d2YM,1027
13
13
  netbox_dns/filters/nameserver.py,sha256=sDKsrluSmUZ0aTw_wagYJAh1g5fKzyzHXbJu6DaK1b8,522
14
14
  netbox_dns/filters/record.py,sha256=DH1x4bNkBAN_gVNklAjorcgHoJmH_AXEf1sBNvJS6Kc,1691
15
- netbox_dns/filters/registrar.py,sha256=kAvE2qNah0t5KtW-QniSw1HyhUgNnlvNSyVpHdUbSng,888
15
+ netbox_dns/filters/registrar.py,sha256=8UXJXUb0XmkPyPQ2oACAhzhmZtAmvM2c4hTWMDLuBEo,911
16
16
  netbox_dns/filters/view.py,sha256=Sji4DKy0VKk8BLEdk8xCe8su3rUBtXeJUsUAee0IsOs,497
17
17
  netbox_dns/filters/zone.py,sha256=gNKTLll0nvdkzjEVl5c4O1hncU13IFVKn61iXThLYJA,3556
18
18
  netbox_dns/forms/__init__.py,sha256=Aw8HrCTjaJfu5JSwJsQRHfOUz4zKwAmZNByT9q6BrFU,136
19
19
  netbox_dns/forms/contact.py,sha256=Z8llYOpcGns8ZerOLsldpY836pfjO7427rmAZ4T8Kyg,4492
20
20
  netbox_dns/forms/nameserver.py,sha256=LUYV_tna667flgPdQevDFniMdXZUYRUG8iqCx7HKdxQ,2027
21
21
  netbox_dns/forms/record.py,sha256=TAQ-rZ55lmWE_piogiE5TwTv1v4r5T2yZpNT8K7GrPU,6171
22
- netbox_dns/forms/registrar.py,sha256=9yGh1BSUbgdSGxM1QTOUeff8ExThX6QnDh_GpVTHjX4,2437
22
+ netbox_dns/forms/registrar.py,sha256=Vkw8cafXQRDHf7K7OFPn62R-_BzjNOofnhLmL2Hw7mg,2636
23
23
  netbox_dns/forms/view.py,sha256=KEWlUo8-oXP_QsqT5fW_UK-ZoX9g-f4CqRnJ_P6LHco,1627
24
- netbox_dns/forms/zone.py,sha256=gJTDPq9WLYCa8nHDzjWrLRFmTEBM4JFqgAtHhV1L-Zk,21241
24
+ netbox_dns/forms/zone.py,sha256=Y9KfeSCzFrJU1LxQjxPcLANu3NEcaHjvxnyMqgna_FY,21702
25
25
  netbox_dns/graphql/__init__.py,sha256=v3lLvVoP-JQbEZ1NY49u1TNqvanF-4TqOtjHXrbsTvU,811
26
26
  netbox_dns/graphql/contact.py,sha256=2iyuvCxG09CLDUMhan5vR3uFZxCVzKNZBrotr_zoUVY,497
27
27
  netbox_dns/graphql/nameserver.py,sha256=mlQw1Po_Ax_fjyyXVBetyxlFLrCqmptYDgOspZvYtP4,527
@@ -31,6 +31,7 @@ netbox_dns/graphql/schema.py,sha256=D_dDusogaI55tmGx_dr1owsgzXS5fd2S-kPZ7xcXxs8,
31
31
  netbox_dns/graphql/view.py,sha256=S_61hYlQCtPQen1lI1UQs38UBWKQTaWfUxzlbpO07zA,467
32
32
  netbox_dns/graphql/zone.py,sha256=QDBxocezhLSHBGDV4RJnmarBfOsiUTeE9KzBGJ3gJi8,467
33
33
  netbox_dns/management/commands/cleanup_database.py,sha256=eggyMZrRg--cXDQJ-boofHboG1gJTs8j-oldhn6EuCo,6050
34
+ netbox_dns/management/commands/cleanup_rrset_ttl.py,sha256=gCvwH6hGw32YJtcy_pEWIP29PSRMW23-qzB8SHEdixQ,2077
34
35
  netbox_dns/management/commands/setup_coupling.py,sha256=Yn1nffPR7fBgB6WWBdDqdnp3k8z1QK8k6qi9xbx4U6Y,4580
35
36
  netbox_dns/management/commands/update_soa.py,sha256=qvlApMngTVpauj0CU0yeOy9r3lxxDciKorMxFsyvQhs,661
36
37
  netbox_dns/migrations/0001_initial.py,sha256=R9FbIQ7nO1ROb12NL8YQDkFQuP1r6-TtMcPwg4rwjus,4153
@@ -66,10 +67,10 @@ netbox_dns/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
66
67
  netbox_dns/models/__init__.py,sha256=Q7UIEe2vGh18AZN4er6CykciwXPQGgUq0L-9718wZqU,182
67
68
  netbox_dns/models/contact.py,sha256=Mzj4SR3fczOE96etWjObJElk0RVQetNuXZVtm8sS1h8,2849
68
69
  netbox_dns/models/nameserver.py,sha256=SjKUbMRNE3TSDzmxbKFR9b4AfYPS0UPj0QeO7XpwNRk,2941
69
- netbox_dns/models/record.py,sha256=sWyWkB0qVJyN8I5oliaRkaVEz7FTqehlkpPBXvyIsAE,19608
70
+ netbox_dns/models/record.py,sha256=nOKjaoVuyMVIfwNw_V4hYYd71fcE4UyXThU983KrXaY,23629
70
71
  netbox_dns/models/registrar.py,sha256=ByKHeqH5KfswqOLjya8DZExJ1omSKFHMfCjIIYfnwTo,1416
71
72
  netbox_dns/models/view.py,sha256=ljs3Q2xQR63ZOSyja5H7DEdFbm7MX2ZjlR6uNVrAsVo,920
72
- netbox_dns/models/zone.py,sha256=AwVOImYf55vHHeTWaVnupI2uPSaq9lD48XrR-u1eK7A,23843
73
+ netbox_dns/models/zone.py,sha256=Ek7Jui3AldNHVAHowbQhC47QoXv0IgkhLzFziN14UUI,26333
73
74
  netbox_dns/navigation.py,sha256=IutEr_TcPgDGzqTT1ZzV4IUABLSOFU9v364BfpCqbro,4587
74
75
  netbox_dns/signals/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
76
  netbox_dns/signals/ipam_coupling.py,sha256=xbb37_77ZqtgT3mKXZp18QRduqZuiAuYGPk8R9d7SVc,5727
@@ -86,7 +87,7 @@ netbox_dns/templates/netbox_dns/nameserver.html,sha256=3AJVbsuhKg4Jy74rlvwrGSHd_
86
87
  netbox_dns/templates/netbox_dns/record/managed.html,sha256=G6LPG1koUGuzUiwYdv1okdVa4sKaofiQegDBnsFL0kA,89
87
88
  netbox_dns/templates/netbox_dns/record/related.html,sha256=Aqor8uGcuHQTHjlX-Xmni2Yp4N7lOBrMOqQiszrQOC0,742
88
89
  netbox_dns/templates/netbox_dns/record.html,sha256=1XOp9Rf7KiV736RKGh7A8LAYkrfI7veG5SsEhaeMghw,6613
89
- netbox_dns/templates/netbox_dns/registrar.html,sha256=Ijy8w0P3UPiyVFkIjk92d4n3GCqsm-0kQjk-QJ-2BCw,1956
90
+ netbox_dns/templates/netbox_dns/registrar.html,sha256=rSShbH68nP0r8EUHn0-TZOsUj6pg7hmfvM7h2tqouUA,2130
90
91
  netbox_dns/templates/netbox_dns/related_dns_objects.html,sha256=KSzlnw1cStrJa3poKkwrt_ycIH0oH0STWIHRNy3ks4g,806
91
92
  netbox_dns/templates/netbox_dns/view.html,sha256=_lDjd2xY3upfGpNpemXXMzgsaKZlX3-PzPPAdYkIjvs,1350
92
93
  netbox_dns/templates/netbox_dns/zone/base.html,sha256=pucf_b7iGg4hCXqwIbR0_WtEQdeH2LQtTCRTSoeBhFc,460
@@ -95,7 +96,7 @@ netbox_dns/templates/netbox_dns/zone/managed_record.html,sha256=5P85eJuQOB7omih2
95
96
  netbox_dns/templates/netbox_dns/zone/record.html,sha256=1oIRGXOZAjwmTMkTgArfKyVrmL54Sh_IN7IAF3qYEKM,2218
96
97
  netbox_dns/templates/netbox_dns/zone/registration.html,sha256=3R5uqLXZxIJkp9_LVnTTgTV61mITfDPDDNofV7s4d1k,1283
97
98
  netbox_dns/templates/netbox_dns/zone/rfc2317_child_zone.html,sha256=kxIrhn0gghb4l1eZhpsICdfkSYegqSXR9zFIWiZLypA,527
98
- netbox_dns/templates/netbox_dns/zone.html,sha256=mB-oyl7vGgtr-DksgoiU7WSx-88_oD52s4YQBrOIajc,6895
99
+ netbox_dns/templates/netbox_dns/zone.html,sha256=_lQhr00_V8ThadlTuTm5zPkcvyjYQxxYGHQsfTOgDCs,7187
99
100
  netbox_dns/urls.py,sha256=NxqvnRxLM6VwUBTY6KA8u-PRjNklmBai7gy6NXc-1xo,9172
100
101
  netbox_dns/utilities/__init__.py,sha256=8DqDx-99kejykwaIt_29W5KgY8RvWBOk6526vrf22UQ,1967
101
102
  netbox_dns/utilities/ipam_coupling.py,sha256=LhdFafF4OAOU8rgROOqY5Dt7XDT_SUZP2JuIqCQYVqw,3573
@@ -109,7 +110,7 @@ netbox_dns/views/record.py,sha256=2xzZnCWmCZIyEVjQfYo0rByhBNItPxNFiehOwDl8U6w,25
109
110
  netbox_dns/views/registrar.py,sha256=aznSKt1L5tILMLGgcZiBR7u7B8rNl-jM1B2-N0fTeK8,2072
110
111
  netbox_dns/views/view.py,sha256=uUvtlNEh5MYoEALvWWaCOqj_Zj8dpGOL2PUyg-UPfEA,1895
111
112
  netbox_dns/views/zone.py,sha256=SyttTAgrPPzf1jIT1B4RexCLdXYjSmPIZsefO_zog1Q,4587
112
- netbox_plugin_dns-0.22.4.dist-info/LICENSE,sha256=tziMJKpkMbySr09L6bIwsu7Ca9ICoqpMO3yAXgEMQA4,1076
113
- netbox_plugin_dns-0.22.4.dist-info/METADATA,sha256=o5FB0XTzm8-eqyN2NwnsmaDVI-qY3fCOH5B3kJgBPxo,4228
114
- netbox_plugin_dns-0.22.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
115
- netbox_plugin_dns-0.22.4.dist-info/RECORD,,
113
+ netbox_plugin_dns-0.22.5.dist-info/LICENSE,sha256=tziMJKpkMbySr09L6bIwsu7Ca9ICoqpMO3yAXgEMQA4,1076
114
+ netbox_plugin_dns-0.22.5.dist-info/METADATA,sha256=U6WLJ-pstL29t54fKW1LAJMkq8r-rtSR8AtutZqxnHg,4572
115
+ netbox_plugin_dns-0.22.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
116
+ netbox_plugin_dns-0.22.5.dist-info/RECORD,,