netbox-plugin-dns 1.1.0b1__py3-none-any.whl → 1.1b3__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
@@ -5,14 +5,14 @@ from ipam.choices import IPAddressStatusChoices
5
5
 
6
6
  from netbox_dns.choices import RecordTypeChoices
7
7
 
8
- __version__ = "1.1.0-beta1"
8
+ __version__ = "1.1b3"
9
9
 
10
10
 
11
11
  class DNSConfig(PluginConfig):
12
12
  name = "netbox_dns"
13
13
  verbose_name = "NetBox DNS"
14
14
  description = "NetBox plugin for DNS data"
15
- min_version = "4.1.0-beta1"
15
+ min_version = "4.0.0"
16
16
  version = __version__
17
17
  author = "Peter Eckel"
18
18
  author_email = "pete@netbox-dns.org"
@@ -31,6 +31,7 @@ class DNSConfig(PluginConfig):
31
31
  IPAddressStatusChoices.STATUS_DHCP,
32
32
  IPAddressStatusChoices.STATUS_SLAAC,
33
33
  ],
34
+ "autodns_conflict_deactivate": False,
34
35
  "tolerate_characters_in_zone_labels": "",
35
36
  "tolerate_underscores_in_labels": False,
36
37
  "tolerate_underscores_in_hostnames": False, # Deprecated, will be removed in 1.2.0
netbox_dns/forms/view.py CHANGED
@@ -21,12 +21,14 @@ from utilities.forms.rendering import FieldSet
21
21
  from tenancy.models import Tenant
22
22
  from tenancy.forms import TenancyForm, TenancyFilterForm
23
23
  from ipam.models import Prefix
24
+ from netbox.context import current_request
24
25
 
25
26
  from netbox_dns.models import View
26
27
  from netbox_dns.fields import PrefixDynamicModelMultipleChoiceField
27
28
  from netbox_dns.utilities import (
28
- get_ip_addresses_by_prefix,
29
+ check_dns_records,
29
30
  update_dns_records,
31
+ get_ip_addresses_by_prefix,
30
32
  get_views_by_prefix,
31
33
  )
32
34
 
@@ -36,6 +38,7 @@ __all__ = (
36
38
  "ViewFilterForm",
37
39
  "ViewImportForm",
38
40
  "ViewBulkEditForm",
41
+ "ViewPrefixEditForm",
39
42
  )
40
43
 
41
44
 
@@ -52,7 +55,7 @@ class ViewPrefixUpdateMixin:
52
55
  for prefix in prefixes.difference(old_prefixes):
53
56
  for ip_address in get_ip_addresses_by_prefix(prefix, check_view=False):
54
57
  try:
55
- update_dns_records(ip_address, commit=False, view=self.instance)
58
+ check_dns_records(ip_address, view=self.instance)
56
59
  except ValidationError as exc:
57
60
  self.add_error("prefixes", exc.messages)
58
61
 
@@ -74,7 +77,7 @@ class ViewPrefixUpdateMixin:
74
77
  # parent. If that's the case, the IP addresses need to be checked.
75
78
  # -
76
79
  if (parent := check_prefix.get_parents().last()) is None:
77
- return
80
+ continue
78
81
 
79
82
  for view in get_views_by_prefix(parent):
80
83
  if view == self.instance:
@@ -84,7 +87,7 @@ class ViewPrefixUpdateMixin:
84
87
  check_prefix, check_view=False
85
88
  ):
86
89
  try:
87
- update_dns_records(ip_address, commit=False, view=view)
90
+ check_dns_records(ip_address, view=view)
88
91
  except ValidationError as exc:
89
92
  self.add_error("prefixes", exc.messages)
90
93
 
@@ -96,6 +99,21 @@ class ViewForm(ViewPrefixUpdateMixin, TenancyForm, NetBoxModelForm):
96
99
  if settings.PLUGINS_CONFIG["netbox_dns"].get("autodns_disabled"):
97
100
  del self.fields["prefixes"]
98
101
 
102
+ if request := current_request.get():
103
+ if not request.user.has_perm("ipam.view_prefix"):
104
+ self._saved_prefixes = self.initial["prefixes"]
105
+ self.initial["prefixes"] = []
106
+ self.fields["prefixes"].disabled = True
107
+ self.fields["prefixes"].widget.attrs[
108
+ "placeholder"
109
+ ] = "You do not have permission to modify assigned prefixes"
110
+
111
+ def clean_prefixes(self):
112
+ if hasattr(self, "_saved_prefixes"):
113
+ return self._saved_prefixes
114
+
115
+ return self.cleaned_data["prefixes"]
116
+
99
117
  prefixes = PrefixDynamicModelMultipleChoiceField(
100
118
  queryset=Prefix.objects.all(),
101
119
  required=False,
@@ -200,3 +218,74 @@ class ViewBulkEditForm(NetBoxModelBulkEditForm):
200
218
  )
201
219
 
202
220
  nullable_fields = ("description", "tenant")
221
+
222
+
223
+ class ViewPrefixEditForm(forms.ModelForm):
224
+ views = DynamicModelMultipleChoiceField(
225
+ queryset=View.objects.all(),
226
+ required=False,
227
+ label="Assigned DNS Views",
228
+ help_text="Explicitly assigning DNS views overrides all inherited views for this prefix",
229
+ )
230
+
231
+ class Meta:
232
+ model = Prefix
233
+ fields = ("views",)
234
+
235
+ def __init__(self, *args, **kwargs):
236
+ super().__init__(*args, **kwargs)
237
+
238
+ self.initial["views"] = self.instance.netbox_dns_views.all()
239
+ self._permission_denied = False
240
+
241
+ if request := current_request.get():
242
+ if not request.user.has_perm("netbox_dns.change_view"):
243
+ self._permission_denied = True
244
+ self.initial["views"] = []
245
+ self.fields["views"].disabled = True
246
+ self.fields["views"].widget.attrs[
247
+ "placeholder"
248
+ ] = "You do not have permission to modify assigned views"
249
+
250
+ def clean(self, *args, **kwargs):
251
+ if self._permission_denied:
252
+ return
253
+
254
+ prefix = self.instance
255
+
256
+ super().clean(*args, **kwargs)
257
+
258
+ views = self.cleaned_data.get("views")
259
+ old_views = prefix.netbox_dns_views.all()
260
+
261
+ check_views = View.objects.none()
262
+
263
+ if not views.exists():
264
+ if (parent := prefix.get_parents().last()) is not None:
265
+ check_views = parent.netbox_dns_views.all().difference(old_views)
266
+
267
+ else:
268
+ check_views = views.difference(old_views)
269
+
270
+ for view in check_views:
271
+ try:
272
+ for ip_address in get_ip_addresses_by_prefix(prefix, check_view=False):
273
+ check_dns_records(ip_address, view=view)
274
+ except ValidationError as exc:
275
+ self.add_error("views", exc.messages)
276
+
277
+ def save(self, *args, **kwargs):
278
+ prefix = self.instance
279
+
280
+ if self._permission_denied:
281
+ return prefix
282
+
283
+ old_views = prefix.netbox_dns_views.all()
284
+ views = self.cleaned_data.get("views")
285
+
286
+ for view in views.difference(old_views):
287
+ view.prefixes.add(prefix)
288
+ for view in old_views.difference(views):
289
+ view.prefixes.remove(prefix)
290
+
291
+ return prefix
netbox_dns/forms/zone.py CHANGED
@@ -81,6 +81,14 @@ class ZoneTemplateUpdateMixin:
81
81
  else:
82
82
  zone_data = self.cleaned_data.copy()
83
83
 
84
+ custom_fields = dict()
85
+ for key, value in zone_data.copy().items():
86
+ if key.startswith("cf_"):
87
+ custom_fields[key[3:]] = value
88
+ zone_data.pop(key)
89
+ if custom_fields:
90
+ zone_data["custom_field_data"] = custom_fields
91
+
84
92
  zone_data.pop("template", None)
85
93
  zone_data.pop("tenant_group", None)
86
94
  zone_data.pop("_init_time", None)
@@ -29,8 +29,8 @@ class Command(BaseCommand):
29
29
  CustomField.objects.get(
30
30
  name=cf, object_types=ipaddress_object_type
31
31
  ).delete()
32
- if options.get("verbosity") >= 2:
33
- self.stdout.write(f"Custom field '{cf}' removed")
32
+ if options.get("verbosity"):
33
+ self.stdout.write(f"Removed custom field '{cf}'")
34
34
  except CustomField.DoesNotExist:
35
35
  pass
36
36
  return
@@ -38,7 +38,7 @@ class Command(BaseCommand):
38
38
  # +
39
39
  # Remove pre-existing IPAM Coupling custom fields
40
40
  # -
41
- if options.get("verbosity"):
41
+ if options.get("verbosity") >= 2:
42
42
  self.stdout.write(f"Trying to remove obsolete IPAM Coupling custom fields")
43
43
  for cf in (
44
44
  "ipaddress_dns_record_name",
@@ -48,12 +48,12 @@ class Command(BaseCommand):
48
48
  CustomField.objects.get(
49
49
  name=cf, object_types=ipaddress_object_type
50
50
  ).delete()
51
- if options.get("verbosity") >= 2:
51
+ if options.get("verbosity"):
52
52
  self.stdout.write(f"Removed custom field '{cf}'")
53
53
  except CustomField.DoesNotExist:
54
54
  pass
55
55
 
56
- if options.get("verbosity"):
56
+ if options.get("verbosity") >= 2:
57
57
  self.stdout.write(f"Creating IPAM AutoDNS custom fields")
58
58
 
59
59
  if not CustomField.objects.filter(
@@ -61,7 +61,7 @@ class Command(BaseCommand):
61
61
  type=CustomFieldTypeChoices.TYPE_BOOLEAN,
62
62
  object_types=ipaddress_object_type,
63
63
  ).exists():
64
- cf_disable_ptr = CustomField.objects.create(
64
+ cf_autodns_disabled = CustomField.objects.create(
65
65
  name="ipaddress_dns_disabled",
66
66
  label="Disable AutoDNS",
67
67
  description="Disable DNS address and pointer record generation for this address",
@@ -72,15 +72,23 @@ class Command(BaseCommand):
72
72
  is_cloneable=True,
73
73
  weight=100,
74
74
  )
75
- cf_disable_ptr.object_types.set([ipaddress_object_type])
76
- if options.get("verbosity") >= 2:
75
+ cf_autodns_disabled.object_types.set([ipaddress_object_type])
76
+ if options.get("verbosity"):
77
77
  self.stdout.write("Created custom field 'ipaddress_dns_disabled'")
78
78
 
79
- if not CustomField.objects.filter(
80
- name="ipaddress_dns_record_ttl",
81
- type=CustomFieldTypeChoices.TYPE_INTEGER,
82
- object_types=ipaddress_object_type,
83
- ).exists():
79
+ try:
80
+ cf_ttl = CustomField.objects.get(
81
+ name="ipaddress_dns_record_ttl",
82
+ type=CustomFieldTypeChoices.TYPE_INTEGER,
83
+ object_types=ipaddress_object_type,
84
+ )
85
+ if cf_ttl.group_name != "AutoDNS":
86
+ cf_ttl.group_name = "AutoDNS"
87
+ cf_ttl.description = ("TTL for DNS records created for this address",)
88
+ cf_ttl.save()
89
+ if options.get("verbosity"):
90
+ self.stdout.write("Updated custom field 'ipaddress_dns_record_ttl'")
91
+ except CustomField.DoesNotExist:
84
92
  cf_ttl = CustomField.objects.create(
85
93
  name="ipaddress_dns_record_ttl",
86
94
  description="TTL for DNS records created for this address",
@@ -94,14 +102,26 @@ class Command(BaseCommand):
94
102
  weight=200,
95
103
  )
96
104
  cf_ttl.object_types.set([ipaddress_object_type])
97
- if options.get("verbosity") >= 2:
105
+ if options.get("verbosity"):
98
106
  self.stdout.write("Created custom field 'ipaddress_dns_record_ttl'")
99
107
 
100
- if not CustomField.objects.filter(
101
- name="ipaddress_dns_record_disable_ptr",
102
- type=CustomFieldTypeChoices.TYPE_BOOLEAN,
103
- object_types=ipaddress_object_type,
104
- ).exists():
108
+ try:
109
+ cf_disable_ptr = CustomField.objects.get(
110
+ name="ipaddress_dns_record_disable_ptr",
111
+ type=CustomFieldTypeChoices.TYPE_BOOLEAN,
112
+ object_types=ipaddress_object_type,
113
+ )
114
+ if cf_disable_ptr.group_name != "AutoDNS":
115
+ cf_disable_ptr.group_name = "AutoDNS"
116
+ cf_disable_ptr.description = (
117
+ "Disable DNS PTR record generation for this address",
118
+ )
119
+ cf_disable_ptr.save()
120
+ if options.get("verbosity"):
121
+ self.stdout.write(
122
+ "Updated custom field 'ipaddress_dns_record_disable_ptr'"
123
+ )
124
+ except CustomField.DoesNotExist:
105
125
  cf_disable_ptr = CustomField.objects.create(
106
126
  name="ipaddress_dns_record_disable_ptr",
107
127
  description="Disable DNS PTR record generation for this address",
@@ -114,7 +134,7 @@ class Command(BaseCommand):
114
134
  weight=300,
115
135
  )
116
136
  cf_disable_ptr.object_types.set([ipaddress_object_type])
117
- if options.get("verbosity") >= 2:
137
+ if options.get("verbosity"):
118
138
  self.stdout.write(
119
139
  "Created custom field 'ipaddress_dns_record_disable_ptr'"
120
140
  )
@@ -461,8 +461,11 @@ class Record(ObjectModificationMixin, NetBoxModel):
461
461
  self.rfc2317_cname_record.delete(save_zone_serial=save_zone_serial)
462
462
  self.rfc2317_cname_record = None
463
463
 
464
- def update_from_ip_address(self, ip_address):
465
- data = record_data_from_ip_address(ip_address, self.zone)
464
+ def update_from_ip_address(self, ip_address, zone=None):
465
+ if zone is None:
466
+ zone = self.zone
467
+
468
+ data = record_data_from_ip_address(ip_address, zone)
466
469
 
467
470
  if data is None:
468
471
  self.delete()
@@ -490,9 +493,12 @@ class Record(ObjectModificationMixin, NetBoxModel):
490
493
  **data,
491
494
  )
492
495
 
493
- def validate_name(self):
496
+ def validate_name(self, new_zone=None):
497
+ if new_zone is None:
498
+ new_zone = self.zone
499
+
494
500
  try:
495
- _zone = dns_name.from_text(self.zone.name, origin=dns_name.root)
501
+ _zone = dns_name.from_text(new_zone.name, origin=dns_name.root)
496
502
  name = dns_name.from_text(self.name, origin=None)
497
503
  fqdn = dns_name.from_text(self.name, origin=_zone)
498
504
 
@@ -512,7 +518,7 @@ class Record(ObjectModificationMixin, NetBoxModel):
512
518
  if not fqdn.is_subdomain(_zone):
513
519
  raise ValidationError(
514
520
  {
515
- "name": f"{self.name} is not a name in {self.zone.name}",
521
+ "name": f"{self.name} is not a name in {new_zone.name}",
516
522
  }
517
523
  )
518
524
 
@@ -544,15 +550,18 @@ class Record(ObjectModificationMixin, NetBoxModel):
544
550
  except ValidationError as exc:
545
551
  raise ValidationError({"value": exc}) from None
546
552
 
547
- def check_unique_record(self):
553
+ def check_unique_record(self, new_zone=None):
548
554
  if not get_plugin_config("netbox_dns", "enforce_unique_records", False):
549
555
  return
550
556
 
551
557
  if not self.is_active:
552
558
  return
553
559
 
560
+ if new_zone is None:
561
+ new_zone = self.zone
562
+
554
563
  records = Record.objects.filter(
555
- zone=self.zone,
564
+ zone=new_zone,
556
565
  name=self.name,
557
566
  type=self.type,
558
567
  value=self.value,
@@ -562,13 +571,41 @@ class Record(ObjectModificationMixin, NetBoxModel):
562
571
  if self.pk is not None:
563
572
  records = records.exclude(pk=self.pk)
564
573
 
565
- if len(records):
574
+ if records.exists():
575
+ if self.ipam_ip_address is not None:
576
+ if not records.filter(
577
+ ipam_ip_address__isnull=True
578
+ ).exists() or get_plugin_config(
579
+ "netbox_dns", "autodns_conflict_deactivate", False
580
+ ):
581
+ return
582
+
566
583
  raise ValidationError(
567
584
  {
568
585
  "value": f"There is already an active {self.type} record for name {self.name} in zone {self.zone} with value {self.value}."
569
586
  }
570
587
  ) from None
571
588
 
589
+ def handle_conflicting_address_records(self):
590
+ if self.ipam_ip_address is None or not self.is_active:
591
+ return
592
+
593
+ if not get_plugin_config("netbox_dns", "autodns_conflict_deactivate", False):
594
+ return
595
+
596
+ records = Record.objects.filter(
597
+ zone=self.zone,
598
+ name=self.name,
599
+ type=self.type,
600
+ value=self.value,
601
+ status__in=Record.ACTIVE_STATUS_LIST,
602
+ ipam_ip_address__isnull=True,
603
+ )
604
+
605
+ for record in records:
606
+ record.status = RecordStatusChoices.STATUS_INACTIVE
607
+ record.save(update_fields=["status"])
608
+
572
609
  def check_unique_rrset_ttl(self):
573
610
  if self.pk is not None:
574
611
  return
@@ -587,8 +624,12 @@ class Record(ObjectModificationMixin, NetBoxModel):
587
624
  )
588
625
  .exclude(ttl=self.ttl)
589
626
  .exclude(type=RecordTypeChoices.PTR, managed=True)
627
+ .exclude(status=RecordStatusChoices.STATUS_INACTIVE)
590
628
  )
591
629
 
630
+ if self.ipam_ip_address is not None:
631
+ records = records.exclude(ipam_ip_address__isnull=False)
632
+
592
633
  if not records.exists():
593
634
  return
594
635
 
@@ -621,6 +662,7 @@ class Record(ObjectModificationMixin, NetBoxModel):
621
662
  .exclude(pk=self.pk)
622
663
  .exclude(ttl=ttl)
623
664
  .exclude(type=RecordTypeChoices.PTR, managed=True)
665
+ .exclude(status=RecordStatusChoices.STATUS_INACTIVE)
624
666
  )
625
667
 
626
668
  for record in records:
@@ -631,10 +673,10 @@ class Record(ObjectModificationMixin, NetBoxModel):
631
673
  self.type = self.type.upper()
632
674
  super().clean_fields(*args, **kwargs)
633
675
 
634
- def clean(self, *args, **kwargs):
635
- self.validate_name()
676
+ def clean(self, *args, new_zone=None, **kwargs):
677
+ self.validate_name(new_zone=new_zone)
636
678
  self.validate_value()
637
- self.check_unique_record()
679
+ self.check_unique_record(new_zone=new_zone)
638
680
  if self.pk is None:
639
681
  self.check_unique_rrset_ttl()
640
682
 
@@ -740,6 +782,7 @@ class Record(ObjectModificationMixin, NetBoxModel):
740
782
  self.ip_address = None
741
783
 
742
784
  if self.is_address_record:
785
+ self.handle_conflicting_address_records()
743
786
  self.update_ptr_record(
744
787
  update_rfc2317_cname=update_rfc2317_cname,
745
788
  save_zone_serial=save_zone_serial,
netbox_dns/models/zone.py CHANGED
@@ -28,6 +28,7 @@ from netbox_dns.choices import RecordClassChoices, RecordTypeChoices, ZoneStatus
28
28
  from netbox_dns.fields import NetworkField, RFC2317NetworkField
29
29
  from netbox_dns.utilities import (
30
30
  update_dns_records,
31
+ check_dns_records,
31
32
  get_ip_addresses_by_zone,
32
33
  arpa_to_prefix,
33
34
  name_to_unicode,
@@ -652,6 +653,24 @@ class Zone(ObjectModificationMixin, NetBoxModel):
652
653
  }
653
654
  )
654
655
 
656
+ if old_zone.name != self.name or old_zone.view != self.view:
657
+ for ip_address in get_ip_addresses_by_zone(self):
658
+ try:
659
+ check_dns_records(ip_address, zone=self)
660
+ except ValidationError as exc:
661
+ raise ValidationError(exc.messages)
662
+
663
+ ip_addresses = IPAddress.objects.filter(
664
+ netbox_dns_records__in=self.record_set.filter(
665
+ ipam_ip_address__isnull=False
666
+ )
667
+ )
668
+ for ip_address in ip_addresses:
669
+ try:
670
+ check_dns_records(ip_address, zone=self)
671
+ except ValidationError as exc:
672
+ raise ValidationError(exc.messages)
673
+
655
674
  if self.is_reverse_zone:
656
675
  self.arpa_network = self.network_from_name
657
676
 
@@ -1,5 +1,6 @@
1
1
  from netaddr import IPNetwork
2
2
 
3
+ from django.conf import settings
3
4
  from django.dispatch import receiver
4
5
  from django.db.models.signals import pre_delete, pre_save, post_save, m2m_changed
5
6
  from django.core.exceptions import ValidationError
@@ -11,6 +12,8 @@ from utilities.exceptions import AbortRequest
11
12
 
12
13
  from netbox_dns.models import view as _view
13
14
  from netbox_dns.utilities import (
15
+ check_dns_records,
16
+ check_record_permission,
14
17
  update_dns_records,
15
18
  delete_dns_records,
16
19
  get_views_by_prefix,
@@ -18,22 +21,89 @@ from netbox_dns.utilities import (
18
21
  get_ip_addresses_by_view,
19
22
  )
20
23
 
24
+ AUTODNS_CUSTOM_FIELDS = {
25
+ "ipaddress_dns_disabled": False,
26
+ "ipaddress_dns_record_ttl": None,
27
+ "ipaddress_dns_record_disable_ptr": False,
28
+ }
29
+
30
+ IPADDRESS_ACTIVE_STATUS = settings.PLUGINS_CONFIG["netbox_dns"][
31
+ "autodns_ipaddress_active_status"
32
+ ]
33
+ ENFORCE_UNIQUE_RECORDS = settings.PLUGINS_CONFIG["netbox_dns"]["enforce_unique_records"]
34
+
21
35
 
22
36
  @receiver(post_clean, sender=IPAddress)
23
37
  def ipam_autodns_ipaddress_post_clean(instance, **kwargs):
24
38
  if not isinstance(instance.address, IPNetwork):
25
39
  return
26
40
 
41
+ if instance.custom_field_data.get("ipaddress_dns_disabled"):
42
+ return
43
+
44
+ # +
45
+ # Check for uniqueness of IP address and dns_name. If unique records are
46
+ # enforced, report an error when trying to create the same IP address with
47
+ # the same dns_name. Ignore existing IP addresses that have their CF
48
+ # "ipaddress_dns_disabled" set to "True".
49
+ # -
50
+ duplicate_addresses = IPAddress.objects.filter(
51
+ address=instance.address,
52
+ vrf=instance.vrf,
53
+ dns_name=instance.dns_name,
54
+ status__in=IPADDRESS_ACTIVE_STATUS,
55
+ )
56
+ if instance.pk is not None:
57
+ duplicate_addresses = duplicate_addresses.exclude(pk=instance.pk)
58
+
59
+ if ENFORCE_UNIQUE_RECORDS and instance.status in IPADDRESS_ACTIVE_STATUS:
60
+ for ip_address in duplicate_addresses.only("custom_field_data"):
61
+ if not ip_address.custom_field_data.get("ipaddress_dns_disabled"):
62
+ raise ValidationError(
63
+ {
64
+ "dns_name": "Unique DNS records are enforced and there is already "
65
+ f"an active IP address {instance.address} with DNS name {instance.dns_name}. "
66
+ "Plesase choose a different name or disable record creation for this "
67
+ "IP address."
68
+ }
69
+ )
70
+
71
+ # +
72
+ # Check NetBox DNS record permission for changes to IPAddress custom fields
73
+ #
74
+ # Normally, as the modfication of DNS fields
75
+ if (request := current_request.get()) is not None:
76
+ cf_data = instance.custom_field_data
77
+ if (
78
+ instance.pk is not None
79
+ and any(
80
+ (
81
+ cf_data.get(cf, cf_default)
82
+ != IPAddress.objects.get(pk=instance.pk).custom_field_data.get(
83
+ cf, cf_default
84
+ )
85
+ for cf, cf_default in AUTODNS_CUSTOM_FIELDS.items()
86
+ )
87
+ )
88
+ and not check_record_permission()
89
+ ) or (
90
+ instance.pk is None
91
+ and any(
92
+ (
93
+ cf_data.get(cf, cf_default) != cf_default
94
+ for cf, cf_default in AUTODNS_CUSTOM_FIELDS.items()
95
+ )
96
+ )
97
+ and not check_record_permission(change=False, delete=False)
98
+ ):
99
+ raise ValidationError(
100
+ f"User '{request.user}' is not allowed to alter AutoDNS custom fields"
101
+ )
102
+
27
103
  try:
28
- update_dns_records(instance, commit=False)
104
+ check_dns_records(instance)
29
105
  except ValidationError as exc:
30
- if hasattr(exc, "error_dict"):
31
- for field in ("name", "ttl", "value", "type"):
32
- value = exc.error_dict.pop(field, None)
33
- if value is not None:
34
- raise ValidationError({"dns_name": value})
35
-
36
- raise exc
106
+ raise ValidationError({"dns_name": exc.messages})
37
107
 
38
108
 
39
109
  @receiver(pre_delete, sender=IPAddress)
@@ -42,8 +112,8 @@ def ipam_autodns_ipaddress_pre_delete(instance, **kwargs):
42
112
 
43
113
 
44
114
  @receiver(pre_save, sender=IPAddress)
45
- def ipam_autodns_ipaddress_post_save(instance, **kwargs):
46
- update_dns_records(instance, commit=False)
115
+ def ipam_autodns_ipaddress_pre_save(instance, **kwargs):
116
+ check_dns_records(instance)
47
117
 
48
118
 
49
119
  @receiver(post_save, sender=IPAddress)
@@ -55,16 +125,29 @@ def ipam_autodns_ipaddress_post_save(instance, **kwargs):
55
125
  def ipam_autodns_prefix_pre_save(instance, **kwargs):
56
126
  """
57
127
  Changes that modify the prefix hierarchy cannot be validated properly before
58
- commiting them. So the solution in this case is to remove a prefix whose
59
- VRF or network has changed from all views it currently is assigned to.
128
+ commiting them. So the solution in this case is to ask the user to deassign
129
+ the prefix from any views it is assigned to and retry.
60
130
  """
131
+ request = current_request.get()
132
+
61
133
  if instance.pk is None or not instance.netbox_dns_views.exists():
62
134
  return
63
135
 
64
- saved_prefix = Prefix.objects.get(pk=instance.pk)
136
+ saved_prefix = Prefix.objects.prefetch_related("netbox_dns_views").get(
137
+ pk=instance.pk
138
+ )
65
139
  if saved_prefix.prefix != instance.prefix or saved_prefix.vrf != instance.vrf:
66
- for view in saved_prefix.netbox_dns_views.all():
67
- view.prefixes.remove(saved_prefix)
140
+ dns_views = ", ".join([view.name for view in instance.netbox_dns_views.all()])
141
+ if request is not None:
142
+ raise AbortRequest(
143
+ f"This prefix is currently assigned to the following DNS views: {dns_views}"
144
+ f"Please deassign it from these views before making changes to the prefix "
145
+ f"or VRF."
146
+ )
147
+
148
+ raise ValidationError(
149
+ f"Prefix is assigned to DNS views {dns_views}. Prefix and VRF must not be changed"
150
+ )
68
151
 
69
152
 
70
153
  @receiver(pre_delete, sender=Prefix)
@@ -80,7 +163,7 @@ def ipam_autodns_prefix_pre_delete(instance, **kwargs):
80
163
  _depth=instance.depth + 1, netbox_dns_views__isnull=True
81
164
  ):
82
165
  for ip_address in get_ip_addresses_by_prefix(prefix):
83
- update_dns_records(ip_address, commit=False)
166
+ check_dns_records(ip_address)
84
167
  except ValidationError as exc:
85
168
  if request is not None:
86
169
  raise AbortRequest(
@@ -1,4 +1,5 @@
1
1
  from django.conf import settings
2
+ from django.urls import reverse
2
3
 
3
4
  from netbox.plugins.utils import get_plugin_config
4
5
  from netbox.plugins import PluginTemplateExtension
@@ -63,6 +64,17 @@ class RelatedDNSViews(PluginTemplateExtension):
63
64
  extra_context=context,
64
65
  )
65
66
 
67
+ def buttons(self):
68
+ return self.render(
69
+ "netbox_dns/view/button.html",
70
+ extra_context={
71
+ "url": reverse(
72
+ "plugins:netbox_dns:prefix_views",
73
+ kwargs={"pk": self.context.get("object").pk},
74
+ ),
75
+ },
76
+ )
77
+
66
78
 
67
79
  class IPRelatedDNSRecords(PluginTemplateExtension):
68
80
  model = "ipam.ipaddress"
@@ -0,0 +1,9 @@
1
+ {% load perms %}
2
+
3
+ {% if perms.netbox_dns.change_view %}
4
+ <a href="{{ url }}?return_url={{ object.get_absolute_url }}">
5
+ <button type="submit" class="btn btn-primary" name="assign-view">
6
+ <i class="mdi mdi-eye-outline" aria-hidden="true"></i> DNS Views
7
+ </button>
8
+ </a>
9
+ {% endif %}
@@ -0,0 +1,41 @@
1
+ {% extends 'generic/_base.html' %}
2
+
3
+ {% block title %}
4
+ Configure DNS views for {{ object|meta:"verbose_name" }} {{ object }} {% if object.vrf %}[{{ object.vrf }}]{% endif %}
5
+ {% endblock title %}
6
+
7
+ {% block content %}
8
+ <div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
9
+
10
+ <form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
11
+ {% csrf_token %}
12
+
13
+ <div id="form_fields" hx-disinherit="hx-select hx-swap">
14
+ {% if inherited_from %}
15
+ <div class="card">
16
+ <table class="table table-hover attr-table">
17
+ <th>Views inherited from prefix {{ inherited_from }} {% if inherited_from.vrf %}[{{ inherited_from.vrf }}] {% endif %}</th>
18
+ {% for view in inherited_views %}
19
+ <tr><td>{{ view|linkify }}</td><td>{{ view.description }}</td></tr>
20
+ {% endfor %}
21
+ </table>
22
+ </div>
23
+ {% endif %}
24
+ {% block form %}
25
+ {% include 'htmx/form.html' %}
26
+ {% endblock form %}
27
+ </div>
28
+
29
+ <div class="text-end my-3">
30
+ {% block buttons %}
31
+ <a href="{{ return_url }}" class="btn btn-outline-secondary">Cancel</a>
32
+ <button type="submit" name="_update" class="btn btn-primary">Save</button>
33
+ {% endblock buttons %}
34
+ </div>
35
+ </form>
36
+ </div>
37
+ {% endblock content %}
38
+
39
+ {% block modals %}
40
+ {% include 'inc/htmx_modal.html' with size='lg' %}
41
+ {% endblock %}
@@ -45,15 +45,11 @@
45
45
  {% for prefix in object.prefixes.all %}
46
46
  <tr>
47
47
  <td>
48
- <a href="{% url 'ipam:prefix' pk=prefix.pk %}">
49
- {{ prefix }}
50
- </a>
48
+ {{ prefix|linkify }}
51
49
  </td>
52
50
  {% if prefix.vrf %}
53
51
  <td>
54
- <a href="{% url 'ipam:vrf' pk=prefix.vrf.pk %}">
55
- {{ prefix.vrf }}
56
- </a>
52
+ {{ prefix.vrf|linkify }}
57
53
  </td>
58
54
  {% else %}
59
55
  <td>Global</td>
netbox_dns/urls/view.py CHANGED
@@ -12,6 +12,7 @@ from netbox_dns.views import (
12
12
  ViewBulkEditView,
13
13
  ViewBulkDeleteView,
14
14
  ViewZoneListView,
15
+ ViewPrefixEditView,
15
16
  )
16
17
 
17
18
  view_urlpatterns = [
@@ -36,4 +37,9 @@ view_urlpatterns = [
36
37
  name="view_changelog",
37
38
  kwargs={"model": View},
38
39
  ),
40
+ path(
41
+ "prefixes/<int:pk>/assign-views/",
42
+ ViewPrefixEditView.as_view(),
43
+ name="prefix_views",
44
+ ),
39
45
  ]
@@ -7,6 +7,7 @@ from dns import name as dns_name
7
7
  from django.conf import settings
8
8
  from django.db.models import Q
9
9
 
10
+ from netbox.context import current_request
10
11
  from ipam.models import IPAddress, Prefix
11
12
 
12
13
  from netbox_dns.models import zone as _zone
@@ -16,12 +17,14 @@ from netbox_dns.models import view as _view
16
17
 
17
18
  __all__ = (
18
19
  "get_zones",
20
+ "check_dns_records",
19
21
  "update_dns_records",
20
22
  "delete_dns_records",
21
23
  "get_views_by_prefix",
22
24
  "get_ip_addresses_by_prefix",
23
25
  "get_ip_addresses_by_view",
24
26
  "get_ip_addresses_by_zone",
27
+ "check_record_permission",
25
28
  )
26
29
 
27
30
 
@@ -49,7 +52,13 @@ def _get_record_status(ip_address):
49
52
  )
50
53
 
51
54
 
52
- def get_zones(ip_address, view=None):
55
+ def _valid_entry(ip_address, zone):
56
+ return zone.view in _get_assigned_views(ip_address) and dns_name.from_text(
57
+ ip_address.dns_name
58
+ ).is_subdomain(dns_name.from_text(zone.name))
59
+
60
+
61
+ def get_zones(ip_address, view=None, old_zone=None):
53
62
  if view is None:
54
63
  views = _get_assigned_views(ip_address)
55
64
  if not views:
@@ -69,10 +78,13 @@ def get_zones(ip_address, view=None):
69
78
  active=True,
70
79
  )
71
80
 
72
- if not zones:
73
- return []
74
-
75
81
  zone_map = defaultdict(list)
82
+
83
+ if old_zone is not None:
84
+ zones = zones.exclude(pk=old_zone.pk)
85
+ if _valid_entry(ip_address, old_zone):
86
+ zone_map[old_zone.view].append(old_zone)
87
+
76
88
  for zone in zones:
77
89
  zone_map[zone.view].append(zone)
78
90
 
@@ -82,19 +94,68 @@ def get_zones(ip_address, view=None):
82
94
  ]
83
95
 
84
96
 
85
- def update_dns_records(ip_address, commit=True, view=None):
97
+ def check_dns_records(ip_address, zone=None, view=None):
86
98
  if ip_address.dns_name == "":
87
- if commit:
88
- delete_dns_records(ip_address)
89
99
  return
90
100
 
91
- zones = get_zones(ip_address, view=view)
101
+ if zone is None:
102
+ zones = get_zones(ip_address, view=view)
103
+
104
+ if ip_address.pk is not None:
105
+ for record in ip_address.netbox_dns_records.filter(zone__in=zones):
106
+ if (
107
+ record.fqdn != ip_address.dns_name
108
+ or record.value != ip_address.address.ip
109
+ or record.status != _get_record_status(ip_address)
110
+ ):
111
+ record.update_from_ip_address(ip_address)
112
+
113
+ if record is not None:
114
+ record.clean()
115
+
116
+ zones = _zone.Zone.objects.filter(
117
+ pk__in=[zone.pk for zone in zones]
118
+ ).exclude(
119
+ pk__in=set(
120
+ ip_address.netbox_dns_records.all().values_list("zone", flat=True)
121
+ )
122
+ )
123
+
124
+ for zone in zones:
125
+ record = _record.Record.create_from_ip_address(
126
+ ip_address,
127
+ zone,
128
+ )
129
+
130
+ if record is not None:
131
+ record.clean()
132
+
133
+ if ip_address.pk is None:
134
+ return
135
+
136
+ try:
137
+ new_zone = get_zones(ip_address, old_zone=zone)[0]
138
+ except IndexError:
139
+ return
140
+
141
+ for record in ip_address.netbox_dns_records.filter(zone=zone):
142
+ record.update_from_ip_address(ip_address, new_zone)
143
+
144
+ if record is not None:
145
+ record.clean(new_zone=new_zone)
146
+
147
+
148
+ def update_dns_records(ip_address):
149
+ if ip_address.dns_name == "":
150
+ delete_dns_records(ip_address)
151
+ return
152
+
153
+ zones = get_zones(ip_address)
92
154
 
93
155
  if ip_address.pk is not None:
94
156
  for record in ip_address.netbox_dns_records.all():
95
157
  if record.zone not in zones:
96
- if commit:
97
- record.delete()
158
+ record.delete()
98
159
  continue
99
160
 
100
161
  if (
@@ -105,13 +166,12 @@ def update_dns_records(ip_address, commit=True, view=None):
105
166
  record.update_from_ip_address(ip_address)
106
167
 
107
168
  if record is not None:
108
- if commit:
109
- record.save()
110
- else:
111
- record.clean()
169
+ record.save()
112
170
 
113
- zones = set(zones).difference(
114
- {record.zone for record in ip_address.netbox_dns_records.all()}
171
+ zones = _zone.Zone.objects.filter(pk__in=[zone.pk for zone in zones]).exclude(
172
+ pk__in=set(
173
+ ip_address.netbox_dns_records.all().values_list("zone", flat=True)
174
+ )
115
175
  )
116
176
 
117
177
  for zone in zones:
@@ -121,10 +181,7 @@ def update_dns_records(ip_address, commit=True, view=None):
121
181
  )
122
182
 
123
183
  if record is not None:
124
- if commit:
125
- record.save()
126
- else:
127
- record.clean()
184
+ record.save()
128
185
 
129
186
 
130
187
  def delete_dns_records(ip_address):
@@ -203,3 +260,20 @@ def get_ip_addresses_by_zone(zone):
203
260
  queryset = get_ip_addresses_by_view(zone.view)
204
261
 
205
262
  return queryset.filter(dns_name__regex=rf"\.{re.escape(zone.name)}\.?$")
263
+
264
+
265
+ def check_record_permission(add=True, change=True, delete=True):
266
+ checks = locals().copy()
267
+
268
+ request = current_request.get()
269
+
270
+ if request is None:
271
+ return True
272
+
273
+ return all(
274
+ (
275
+ request.user.has_perm(f"netbox_dns.{perm}_record")
276
+ for perm, check in checks.items()
277
+ if check
278
+ )
279
+ )
netbox_dns/views/view.py CHANGED
@@ -1,11 +1,19 @@
1
1
  from utilities.views import ViewTab, register_model_view
2
2
 
3
3
  from netbox.views import generic
4
+ from ipam.models import Prefix
4
5
 
5
6
  from netbox_dns.models import View, Zone
6
7
  from netbox_dns.filtersets import ViewFilterSet, ZoneFilterSet
7
- from netbox_dns.forms import ViewForm, ViewFilterForm, ViewImportForm, ViewBulkEditForm
8
+ from netbox_dns.forms import (
9
+ ViewForm,
10
+ ViewFilterForm,
11
+ ViewImportForm,
12
+ ViewBulkEditForm,
13
+ ViewPrefixEditForm,
14
+ )
8
15
  from netbox_dns.tables import ViewTable, ZoneTable
16
+ from netbox_dns.utilities import get_views_by_prefix
9
17
 
10
18
 
11
19
  __all__ = (
@@ -17,6 +25,7 @@ __all__ = (
17
25
  "ViewBulkEditView",
18
26
  "ViewBulkDeleteView",
19
27
  "ViewZoneListView",
28
+ "ViewPrefixEditView",
20
29
  )
21
30
 
22
31
 
@@ -61,6 +70,22 @@ class ViewBulkDeleteView(generic.BulkDeleteView):
61
70
  table = ViewTable
62
71
 
63
72
 
73
+ class ViewPrefixEditView(generic.ObjectEditView):
74
+ queryset = Prefix.objects.all()
75
+ form = ViewPrefixEditForm
76
+ template_name = "netbox_dns/view/prefix.html"
77
+
78
+ def get_extra_context(self, request, instance):
79
+ parents = instance.get_parents()
80
+ if parents:
81
+ return {
82
+ "inherited_views": get_views_by_prefix(parents.last()),
83
+ "inherited_from": parents.filter(netbox_dns_views__isnull=False).last(),
84
+ }
85
+
86
+ return {}
87
+
88
+
64
89
  @register_model_view(View, "zones")
65
90
  class ViewZoneListView(generic.ObjectChildrenView):
66
91
  queryset = View.objects.all().prefetch_related("zone_set")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: netbox-plugin-dns
3
- Version: 1.1.0b1
3
+ Version: 1.1b3
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,7 +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.10,<4.0
11
- Classifier: Development Status :: 4 - Beta
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.10
@@ -1,4 +1,4 @@
1
- netbox_dns/__init__.py,sha256=vipRMjUAjOubMr_a7qFS6wiK0igQpR5FOGKlSrFjDy0,1799
1
+ netbox_dns/__init__.py,sha256=Ck45UmyPbGnlkricEwMjOpBDT-BBjVBZfmKi3AbwsxM,1833
2
2
  netbox_dns/api/nested_serializers.py,sha256=-ZhAiyf-8UHlkcBomBp1J7ci1dSwrxWRbbfskD-D_yQ,3172
3
3
  netbox_dns/api/serializers.py,sha256=u-kQurUftGkUGAMh-VkMgXPebLYeZq9WDz9uKzkk2No,370
4
4
  netbox_dns/api/serializers_/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -37,8 +37,8 @@ netbox_dns/forms/nameserver.py,sha256=LHomCHmFcASobaD3Z7yhAyA24h-LrYImVMz-EUXbwK
37
37
  netbox_dns/forms/record.py,sha256=svBVAFy-egDEPLcRWkxNi_1bkabKmWgJ87pmdNt6dh4,7155
38
38
  netbox_dns/forms/record_template.py,sha256=Q77p9sExJ8Xbl-Co2Px2R0At5O3naQJwx4pnino6i2o,5573
39
39
  netbox_dns/forms/registrar.py,sha256=FMnvrcq62R3wNp_2ZUEk3v_PIav0KrWPATaJ7_9KFAo,3758
40
- netbox_dns/forms/view.py,sha256=txD8BpM-d1YUUm6tYPdEIG7CD30ubE_w6EF1G3PQKi0,6149
41
- netbox_dns/forms/zone.py,sha256=ZbsYcWX-t1luqBsLj4vec0IZG0lmCwGW5nzVh77qJrw,23164
40
+ netbox_dns/forms/view.py,sha256=AMTBIw8uNTjDdq8QZTxyw-e8KfQ43SVkO2tc3og4vt8,9069
41
+ netbox_dns/forms/zone.py,sha256=17Ii2csnYquuz7HGgaK36ZgOzWnFXFeh1IQYvWnBKC0,23537
42
42
  netbox_dns/forms/zone_template.py,sha256=UNykid5pRB_ydy40j2DzRlBXp3_QAOqdqxdUojKYTd4,8161
43
43
  netbox_dns/graphql/__init__.py,sha256=ZZSsx-VM108tB_FrcVy3uGGhtmePpkXnY5U1ytnoTvE,490
44
44
  netbox_dns/graphql/filters.py,sha256=6Ot_d1e7h5lVXVVBB3hyWUql94K3zsK9Tjb3RVJqluw,1706
@@ -46,7 +46,7 @@ netbox_dns/graphql/schema.py,sha256=P-oQ8ei3sC6XLhgCa_riRbRTrMkPCVTJXkGv0U2rPYw,
46
46
  netbox_dns/graphql/types.py,sha256=4ewWOqEbWtCBiU9bdIm_6CIm6MKAM6szCAXSvokpqWg,6108
47
47
  netbox_dns/management/commands/cleanup_database.py,sha256=kfnyybudwKGigjJmrOwafPWSUasZr9jQsxN4eWAgMvY,5969
48
48
  netbox_dns/management/commands/cleanup_rrset_ttl.py,sha256=UFRURLBcFeGHUS2lrYFv7UWIebjI72aG1EUQJt0XsXw,2046
49
- netbox_dns/management/commands/setup_autodns.py,sha256=cyxHjkoVSxSPiUp1OXa6avZ3_jOH6PLVA53lTheuPkk,4786
49
+ netbox_dns/management/commands/setup_autodns.py,sha256=8ipEDyvZ0MIzkcj9gASModRbPHKyKCnyfke2SYOl61g,5733
50
50
  netbox_dns/management/commands/update_soa.py,sha256=Rj_Xk-qpwkAVRubVnM5OqSTwgzi93E0PqjwGb3rYjf0,660
51
51
  netbox_dns/migrations/0001_squashed_netbox_dns_0_15.py,sha256=3U0810NWSHPu2dTSHpfzlleDgwMS04FhJ_CkO76SDaw,10283
52
52
  netbox_dns/migrations/0001_squashed_netbox_dns_0_22.py,sha256=ML6Hp17lrXiaG0eUlBjKMm6HUNhw0AHPnKrb9AN-F6E,20279
@@ -72,15 +72,15 @@ netbox_dns/mixins/object_modification.py,sha256=JbGi8a52wkZ3fFBlfat590CfqRJcEWxB
72
72
  netbox_dns/models/__init__.py,sha256=wjwNsRttUVYQHZODZi806a_iUDoq_o7mdKObqh1N7N4,300
73
73
  netbox_dns/models/contact.py,sha256=oNLyD_6TOTNQQTcCvv6TAC7OkzPTMIRy2NP5nwNKaNg,3009
74
74
  netbox_dns/models/nameserver.py,sha256=yKo4Fwqnv5VtTndU2px7tRS3voF3Cal7OWQ6AImLwl0,3208
75
- netbox_dns/models/record.py,sha256=j8sAeExtFFknMv2-dDhFvDOxOhXYgiKU63zd9sgpEcc,24453
75
+ netbox_dns/models/record.py,sha256=ArQp7gB94FZH9MeihfIbx9pN3Y90gSj4VsoSF6y1348,25966
76
76
  netbox_dns/models/record_template.py,sha256=3t9VceviX3kNIo5o0VPVFupLFDqPxpHIVLp5U3pBKB4,4661
77
77
  netbox_dns/models/registrar.py,sha256=T_oMUlTWTDixOVlIbEZGvOBdvUrKxRkkS41xgM2Oee8,1557
78
78
  netbox_dns/models/view.py,sha256=SYmhNYyRCv0rSCK5jrHtug4QgfWCBbjsAjZEEHk02QU,2873
79
- netbox_dns/models/zone.py,sha256=Vx9yC6YHzk3AViyMPy473oS9A6fxuPttK8O7jcG4cHE,28231
79
+ netbox_dns/models/zone.py,sha256=C1f6uGKGeD_FKtFhWXiUO7gKF19pzu-9-pj0txP8R1E,29063
80
80
  netbox_dns/models/zone_template.py,sha256=lkiSIfx8KM0Cs3Mb3dLBxKbSpcssVUzQiSmD5W46was,3753
81
81
  netbox_dns/navigation.py,sha256=EITDZkbpu4KCC9u4Noj7OORWnkL3EYT2RIRvYlTw34Q,5961
82
82
  netbox_dns/signals/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
- netbox_dns/signals/ipam_autodns.py,sha256=OQIqAue4pA851rn-ZpVJYwtQdXAqEIaGwJepbqNOqig,4771
83
+ netbox_dns/signals/ipam_autodns.py,sha256=XkngO_hbfi_kzb7pnplQbOYp8TcUvpP1j3R49rWy7Go,7908
84
84
  netbox_dns/tables/__init__.py,sha256=s41w4o77tIwmhnLjsOsg08R9m3wrlomkkfCLTVQuPzc,196
85
85
  netbox_dns/tables/contact.py,sha256=sPs7d1ZhVC5dOS37dPYFqebNd7WGvsV_eYzX_TMcbzY,804
86
86
  netbox_dns/tables/nameserver.py,sha256=fFiE-yH-_GyRDaV4SVw094r6uH58Kx56NSWDGaMR58g,764
@@ -90,7 +90,7 @@ netbox_dns/tables/registrar.py,sha256=M-ckyQUs6dqjTCPf7bAr6UuLEA-q9f9CxKW7yp3rGo
90
90
  netbox_dns/tables/view.py,sha256=jf2S4TiOdMq6-wWk0ndR1uBJpkOx_f3pqAuM1nSXTBo,1178
91
91
  netbox_dns/tables/zone.py,sha256=IeCiflrQBn1INV_PxoTySWQrDalykY4mDSG76VXC5WM,1877
92
92
  netbox_dns/tables/zone_template.py,sha256=70hvS-xpeaLkcM6y0R9xsUMQVKgTgZJaWWNd99BfmzI,1479
93
- netbox_dns/template_content.py,sha256=Lhrse5_Aw6L5xaemTD8-ZlTyFqJ-fVqp97TSRT6E7ek,3390
93
+ netbox_dns/template_content.py,sha256=a2TAwrw0TeDICv8vSiAuzeeuBEqPbahm_OfFkYGrG-s,3742
94
94
  netbox_dns/templates/netbox_dns/contact.html,sha256=fMHAQyLXIxohKoCTxFEnKetl9UVXeQgjasfpv_JONaw,2855
95
95
  netbox_dns/templates/netbox_dns/nameserver.html,sha256=DpTdetQVV_jKThDbi62LvbhiCay-1QxR-yiJEiPFm4w,1554
96
96
  netbox_dns/templates/netbox_dns/record/managed.html,sha256=G6LPG1koUGuzUiwYdv1okdVa4sKaofiQegDBnsFL0kA,89
@@ -98,8 +98,10 @@ netbox_dns/templates/netbox_dns/record/related.html,sha256=Aqor8uGcuHQTHjlX-Xmni
98
98
  netbox_dns/templates/netbox_dns/record.html,sha256=o3z_D6Fqqn7nx1IwPXKQ75ZaPhU6kae0WpaWa3UMcxQ,5211
99
99
  netbox_dns/templates/netbox_dns/recordtemplate.html,sha256=9tkXtKqa5p3LdOU9REm99WSFwGJaH8OczpIqXZuXMcg,3099
100
100
  netbox_dns/templates/netbox_dns/registrar.html,sha256=O5veGmW59Pf5yN25ihPLvRIkA2P7xmSGv0G3NrRG8vI,2152
101
+ netbox_dns/templates/netbox_dns/view/button.html,sha256=oXKNyPtY8XIu2sxtZWpFRXKXv862407ESyUQ4YsWCGE,292
102
+ netbox_dns/templates/netbox_dns/view/prefix.html,sha256=HD8f4mnbzFOXDj3Y_yq8yEeDpz_yFud8ZMpqbxzCEnA,1445
101
103
  netbox_dns/templates/netbox_dns/view/related.html,sha256=W9Ie2aOsFkWyYtBnZn38seQDBmyJkV9dqFDG-Dq3yMk,736
102
- netbox_dns/templates/netbox_dns/view.html,sha256=8Ox9J-BWSutwOBbnwQL4BMydDMBMM2l4bbMwySkiP4k,2706
104
+ netbox_dns/templates/netbox_dns/view.html,sha256=NSEfPSHPLw5yjUSat9N_KYKF5FezmTlCXqPC6FYiK9E,2479
103
105
  netbox_dns/templates/netbox_dns/zone/base.html,sha256=n_E4aVYdGeZZl-ARE8sb4DgAAgPs92X1UEFepX3xIlM,495
104
106
  netbox_dns/templates/netbox_dns/zone/child.html,sha256=kH56PJFBGCjiRdIh7zCtClnZdfOChqN_sYslsyoz5gU,2147
105
107
  netbox_dns/templates/netbox_dns/zone/child_zone.html,sha256=b9CSGWEfWT7hLQ80gApMnu7mXM8w2LT-3UaOYe6HIRQ,510
@@ -115,12 +117,12 @@ netbox_dns/urls/nameserver.py,sha256=BBbY-wqPqCquvLLv1_JhqToj7oDHhPNGCWHt0IfjBNM
115
117
  netbox_dns/urls/record.py,sha256=bDprohTso1N0GtPXH4X3TNHnkxopiOSQFXWItifEZ_k,1432
116
118
  netbox_dns/urls/record_template.py,sha256=Z-7aA-rPIxRBCmXNUiQcHIgjYfai28Tf_sLtkl2ihDk,1827
117
119
  netbox_dns/urls/registrar.py,sha256=u6B0zGGYNUJIKTo9uGiUeZLPD0QMGaQOAPShGEy4NaA,1728
118
- netbox_dns/urls/view.py,sha256=8AeBnOHWusXXQs4JXpNfMSHqszXAY1GDXGWmNsMulQ8,1327
120
+ netbox_dns/urls/view.py,sha256=jz5ANOOLCMAcWermTZYGq9BvnP02jpKGL6hCm33C47Q,1478
119
121
  netbox_dns/urls/zone.py,sha256=rmB1BkzmWNG06ILUf-39Aj6-SBFkwQouyixMQiamqPc,2005
120
122
  netbox_dns/urls/zone_template.py,sha256=w3Gu8qfLCWyHofeLkGZd1HpYSlcslomVlBQJZyqh8kk,1690
121
123
  netbox_dns/utilities/__init__.py,sha256=M9T8PUFlGddtENzEznHAPbEsz1VFrPcmbD-BGLCsvB4,55
122
124
  netbox_dns/utilities/conversions.py,sha256=NS37SoMqXc13wNWRkKnLfyQbVi6QKD33fu5ovTKRo74,1979
123
- netbox_dns/utilities/ipam_autodns.py,sha256=akS_4o0jtuPYhJVe0mXn75loU_opFFTYQR35MqYPfOI,5993
125
+ netbox_dns/utilities/ipam_autodns.py,sha256=QMCMZkY8YxZL3VeH5xHCQWWd3Cji9Nx0GTUcsjtXsQY,8137
124
126
  netbox_dns/validators/__init__.py,sha256=Mr8TvmcJTa8Pubj8TzbFBKfbHhEmGcr5JdQvczEJ39A,72
125
127
  netbox_dns/validators/dns_name.py,sha256=B4A0BOW5pKDjjukvksriRtnLzkYTx_pFjh7eqKo6PBE,3069
126
128
  netbox_dns/validators/dns_value.py,sha256=y2Zga4hmywqDrTBXcMC-sWaFbw4eoY8pySq7cWnMP8Y,2822
@@ -131,10 +133,10 @@ netbox_dns/views/nameserver.py,sha256=DFr0eybMshc1FW06g4cy9Nk4VRMxRqakI5KtHFiAVR
131
133
  netbox_dns/views/record.py,sha256=fHMafCC14C7d6oXbXc2vN-T70OAOaTY77_m3Dct-oiQ,4590
132
134
  netbox_dns/views/record_template.py,sha256=BkemTBEramLhYqB6HrA80sNgtduW1ZOJwbYs3i7srik,2510
133
135
  netbox_dns/views/registrar.py,sha256=yRQgFm3vgBD21ZQex9asjs0QWegvSHlcyHXLnjvc5xs,2324
134
- netbox_dns/views/view.py,sha256=I_hVZYFJF8GTnlUKPrTgBk_x9UDCbZXM8R7U5Bhizjs,2107
136
+ netbox_dns/views/view.py,sha256=iXBJTc3JD5cD5z0RTcHVTtYV-KNIJGneeoxymXChdUE,2759
135
137
  netbox_dns/views/zone.py,sha256=SKhf_WHcFVpKqFTuUMf-Dmxu1AwFHBeo_DtD8UGFrJ8,5483
136
138
  netbox_dns/views/zone_template.py,sha256=qvXl-bpc1fMc1WFngynj4-Q3-JJDgKdT-r54s4M1D0s,2118
137
- netbox_plugin_dns-1.1.0b1.dist-info/LICENSE,sha256=I3tDu11bZfhFm3EkV4zOD5TmWgLjnUNLEFwrdjniZYs,1112
138
- netbox_plugin_dns-1.1.0b1.dist-info/METADATA,sha256=LNhAMiqES0BIvNc1JrGeHLss0EoAqnOM6TNr8IlYdrU,6393
139
- netbox_plugin_dns-1.1.0b1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
140
- netbox_plugin_dns-1.1.0b1.dist-info/RECORD,,
139
+ netbox_plugin_dns-1.1b3.dist-info/LICENSE,sha256=I3tDu11bZfhFm3EkV4zOD5TmWgLjnUNLEFwrdjniZYs,1112
140
+ netbox_plugin_dns-1.1b3.dist-info/METADATA,sha256=TZ6wKgEaZXVAH0j4IZsRrlfmQZ_kQjhy72ATDEvQmag,6404
141
+ netbox_plugin_dns-1.1b3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
142
+ netbox_plugin_dns-1.1b3.dist-info/RECORD,,