netbox-plugin-dns 1.1.4__py3-none-any.whl → 1.1.6__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 (41) hide show
  1. netbox_dns/__init__.py +18 -4
  2. netbox_dns/api/views.py +2 -2
  3. netbox_dns/filtersets/record.py +0 -1
  4. netbox_dns/filtersets/zone.py +1 -2
  5. netbox_dns/forms/record.py +12 -8
  6. netbox_dns/forms/view.py +2 -3
  7. netbox_dns/forms/zone.py +8 -9
  8. netbox_dns/forms/zone_template.py +5 -5
  9. netbox_dns/graphql/types.py +37 -0
  10. netbox_dns/locale/de/LC_MESSAGES/django.mo +0 -0
  11. netbox_dns/management/commands/cleanup_database.py +4 -6
  12. netbox_dns/management/commands/rebuild_dnssync.py +3 -1
  13. netbox_dns/migrations/0011_rename_related_fields.py +63 -0
  14. netbox_dns/mixins/object_modification.py +4 -0
  15. netbox_dns/models/nameserver.py +5 -9
  16. netbox_dns/models/record.py +23 -38
  17. netbox_dns/models/record_template.py +4 -4
  18. netbox_dns/models/registration_contact.py +1 -1
  19. netbox_dns/models/view.py +2 -3
  20. netbox_dns/models/zone.py +96 -48
  21. netbox_dns/tables/record.py +14 -2
  22. netbox_dns/tables/zone.py +1 -2
  23. netbox_dns/template_content.py +16 -0
  24. netbox_dns/templates/netbox_dns/record.html +12 -0
  25. netbox_dns/templates/netbox_dns/view.html +1 -1
  26. netbox_dns/templates/netbox_dns/zone/delegation_record.html +18 -0
  27. netbox_dns/templates/netbox_dns/zone.html +1 -1
  28. netbox_dns/utilities/__init__.py +1 -0
  29. netbox_dns/utilities/dns.py +12 -0
  30. netbox_dns/utilities/ipam_dnssync.py +10 -13
  31. netbox_dns/views/nameserver.py +5 -5
  32. netbox_dns/views/record.py +44 -11
  33. netbox_dns/views/registrar.py +3 -3
  34. netbox_dns/views/registration_contact.py +2 -2
  35. netbox_dns/views/view.py +4 -4
  36. netbox_dns/views/zone.py +50 -21
  37. {netbox_plugin_dns-1.1.4.dist-info → netbox_plugin_dns-1.1.6.dist-info}/METADATA +2 -1
  38. {netbox_plugin_dns-1.1.4.dist-info → netbox_plugin_dns-1.1.6.dist-info}/RECORD +41 -38
  39. {netbox_plugin_dns-1.1.4.dist-info → netbox_plugin_dns-1.1.6.dist-info}/WHEEL +1 -1
  40. {netbox_plugin_dns-1.1.4.dist-info → netbox_plugin_dns-1.1.6.dist-info}/LICENSE +0 -0
  41. {netbox_plugin_dns-1.1.4.dist-info → netbox_plugin_dns-1.1.6.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@
5
5
  <div class="row">
6
6
  <div class="col col-md-6">
7
7
  <div class="card">
8
- <h5 class="card-header">{% trans "View" context "DNS" %}</h5>
8
+ <h5 class="card-header">{% trans "View" %}</h5>
9
9
  <table class="table table-hover attr-table">
10
10
  <tr>
11
11
  <th scope="row">{% trans "Name" %}</th>
@@ -0,0 +1,18 @@
1
+ {% extends 'netbox_dns/zone/base.html' %}
2
+ {% load helpers %}
3
+ {% load render_table from django_tables2 %}
4
+ {% load perms %}
5
+
6
+ {% block content %}
7
+ {% include 'inc/table_controls_htmx.html' with table_modal="DelegationRecordTable_config" %}
8
+ <div class="card">
9
+ <div class="htmx-container table-responsive" id="object_list">
10
+ {% include 'htmx/table.html' %}
11
+ </div>
12
+ </div>
13
+ {% endblock %}
14
+
15
+ {% block modals %}
16
+ {{ block.super }}
17
+ {% table_config_form table %}
18
+ {% endblock modals %}
@@ -25,7 +25,7 @@
25
25
  </tr>
26
26
  {% endif %}
27
27
  <tr>
28
- <th scope="row">{% trans "View" context "DNS" %}</th>
28
+ <th scope="row">{% trans "View" %}</th>
29
29
  <td>{{ object.view|linkify }}</td>
30
30
  </tr>
31
31
  {% if object.description %}
@@ -1,2 +1,3 @@
1
+ from .dns import *
1
2
  from .conversions import *
2
3
  from .ipam_dnssync import *
@@ -0,0 +1,12 @@
1
+ from dns import name as dns_name
2
+
3
+
4
+ __all__ = ("get_parent_zone_names",)
5
+
6
+
7
+ def get_parent_zone_names(name, min_labels=1, include_self=False):
8
+ fqdn = dns_name.from_text(name)
9
+ return [
10
+ fqdn.split(i)[1].to_text().rstrip(".")
11
+ for i in range(min_labels + 1, len(fqdn.labels) + include_self)
12
+ ]
@@ -12,6 +12,8 @@ from ipam.models import IPAddress, Prefix
12
12
 
13
13
  from netbox_dns.choices import RecordStatusChoices
14
14
 
15
+ from .dns import get_parent_zone_names
16
+
15
17
 
16
18
  __all__ = (
17
19
  "get_zones",
@@ -85,15 +87,12 @@ def get_zones(ip_address, view=None, old_zone=None):
85
87
  min_labels = settings.PLUGINS_CONFIG["netbox_dns"].get(
86
88
  "dnssync_minimum_zone_labels", 2
87
89
  )
88
- fqdn = dns_name.from_text(ip_address.dns_name)
89
- zone_name_candidates = [
90
- fqdn.split(i)[1].to_text().rstrip(".")
91
- for i in range(min_labels + 1, len(fqdn.labels) + 1)
92
- ]
93
90
 
94
91
  zones = Zone.objects.filter(
95
92
  view__in=views,
96
- name__in=zone_name_candidates,
93
+ name__in=get_parent_zone_names(
94
+ ip_address.dns_name, min_labels=min_labels, include_self=True
95
+ ),
97
96
  active=True,
98
97
  )
99
98
 
@@ -194,9 +193,7 @@ def update_dns_records(ip_address, view=None, force=False):
194
193
  updated = True
195
194
 
196
195
  zones = Zone.objects.filter(pk__in=[zone.pk for zone in zones]).exclude(
197
- pk__in=set(
198
- ip_address.netbox_dns_records.all().values_list("zone", flat=True)
199
- )
196
+ pk__in=set(ip_address.netbox_dns_records.values_list("zone", flat=True))
200
197
  )
201
198
 
202
199
  for zone in zones:
@@ -213,18 +210,18 @@ def update_dns_records(ip_address, view=None, force=False):
213
210
 
214
211
 
215
212
  def delete_dns_records(ip_address, view=None):
216
- deleted = False
217
-
218
213
  if view is None:
219
214
  address_records = ip_address.netbox_dns_records.all()
220
215
  else:
221
216
  address_records = ip_address.netbox_dns_records.filter(zone__view=view)
222
217
 
218
+ if not address_records.exists():
219
+ return False
220
+
223
221
  for record in address_records:
224
222
  record.delete()
225
- deleted = True
226
223
 
227
- return deleted
224
+ return True
228
225
 
229
226
 
230
227
  def get_views_by_prefix(prefix):
@@ -36,7 +36,7 @@ class NameServerListView(generic.ObjectListView):
36
36
 
37
37
 
38
38
  class NameServerView(generic.ObjectView):
39
- queryset = NameServer.objects.all().prefetch_related("zones")
39
+ queryset = NameServer.objects.prefetch_related("zones")
40
40
 
41
41
  def get_extra_context(self, request, instance):
42
42
  name = dns_name.from_text(instance.name)
@@ -85,7 +85,7 @@ class NameServerContactsView(ObjectContactsView):
85
85
 
86
86
  @register_model_view(NameServer, "zones")
87
87
  class NameServerZoneListView(generic.ObjectChildrenView):
88
- queryset = NameServer.objects.all().prefetch_related("zones")
88
+ queryset = NameServer.objects.prefetch_related("zones")
89
89
  child_model = Zone
90
90
  table = ZoneTable
91
91
  filterset = ZoneFilterSet
@@ -105,7 +105,7 @@ class NameServerZoneListView(generic.ObjectChildrenView):
105
105
 
106
106
  @register_model_view(NameServer, "soa_zones")
107
107
  class NameServerSOAZoneListView(generic.ObjectChildrenView):
108
- queryset = NameServer.objects.all().prefetch_related("zones_soa")
108
+ queryset = NameServer.objects.prefetch_related("soa_zones")
109
109
  child_model = Zone
110
110
  table = ZoneTable
111
111
  filterset = ZoneFilterSet
@@ -115,9 +115,9 @@ class NameServerSOAZoneListView(generic.ObjectChildrenView):
115
115
  tab = ViewTab(
116
116
  label=_("SOA Zones"),
117
117
  permission="netbox_dns.view_zone",
118
- badge=lambda obj: obj.zones_soa.count(),
118
+ badge=lambda obj: obj.soa_zones.count(),
119
119
  hide_if_empty=True,
120
120
  )
121
121
 
122
122
  def get_children(self, request, parent):
123
- return parent.zones_soa
123
+ return parent.soa_zones
@@ -1,5 +1,7 @@
1
1
  from dns import name as dns_name
2
2
 
3
+ from django.utils.translation import gettext_lazy as _
4
+
3
5
  from netbox.views import generic
4
6
  from utilities.views import register_model_view
5
7
  from tenancy.views import ObjectContactsView
@@ -14,7 +16,7 @@ from netbox_dns.forms import (
14
16
  from netbox_dns.models import Record, Zone
15
17
  from netbox_dns.choices import RecordTypeChoices
16
18
  from netbox_dns.tables import RecordTable, ManagedRecordTable, RelatedRecordTable
17
- from netbox_dns.utilities import value_to_unicode
19
+ from netbox_dns.utilities import value_to_unicode, get_parent_zone_names
18
20
 
19
21
 
20
22
  __all__ = (
@@ -29,6 +31,10 @@ __all__ = (
29
31
  )
30
32
 
31
33
 
34
+ class CNAMEWarning(Exception):
35
+ pass
36
+
37
+
32
38
  class RecordListView(generic.ObjectListView):
33
39
  queryset = Record.objects.filter(managed=False).prefetch_related(
34
40
  "zone", "ptr_record"
@@ -50,7 +56,7 @@ class ManagedRecordListView(generic.ObjectListView):
50
56
 
51
57
 
52
58
  class RecordView(generic.ObjectView):
53
- queryset = Record.objects.all().prefetch_related("zone", "ptr_record")
59
+ queryset = Record.objects.prefetch_related("zone", "ptr_record")
54
60
 
55
61
  def get_value_records(self, instance):
56
62
  value_fqdn = dns_name.from_text(instance.value_fqdn)
@@ -64,6 +70,18 @@ class RecordView(generic.ObjectView):
64
70
  data=cname_targets,
65
71
  )
66
72
 
73
+ if instance.zone.view.zones.filter(
74
+ name__in=get_parent_zone_names(instance.value_fqdn, min_labels=1),
75
+ active=True,
76
+ ).exists():
77
+ raise (
78
+ CNAMEWarning(
79
+ _(
80
+ "There is no matching target record for CNAME value {value}"
81
+ ).format(value=instance.value_fqdn)
82
+ )
83
+ )
84
+
67
85
  return None
68
86
 
69
87
  def get_cname_records(self, instance):
@@ -75,14 +93,8 @@ class RecordView(generic.ObjectView):
75
93
  )
76
94
  )
77
95
 
78
- fqdn = dns_name.from_text(instance.fqdn)
79
- parent_zone_names = [
80
- fqdn.split(length)[1].to_text().rstrip(".")
81
- for length in range(1, len(fqdn) + 1)
82
- ]
83
-
84
- parent_zones = Zone.objects.filter(
85
- view=instance.zone.view, name__in=parent_zone_names
96
+ parent_zones = instance.zone.view.zones.filter(
97
+ name__in=get_parent_zone_names(instance.fqdn, include_self=True),
86
98
  )
87
99
 
88
100
  for parent_zone in parent_zones:
@@ -118,10 +130,31 @@ class RecordView(generic.ObjectView):
118
130
  context["unicode_value"] = unicode_value
119
131
 
120
132
  if instance.type == RecordTypeChoices.CNAME:
121
- context["cname_target_table"] = self.get_value_records(instance)
133
+ try:
134
+ context["cname_target_table"] = self.get_value_records(instance)
135
+ except CNAMEWarning as exc:
136
+ context["cname_warning"] = str(exc)
122
137
  else:
123
138
  context["cname_table"] = self.get_cname_records(instance)
124
139
 
140
+ if not instance.managed:
141
+ name = dns_name.from_text(instance.name, origin=None)
142
+
143
+ if not instance.is_delegation_record:
144
+ fqdn = dns_name.from_text(instance.fqdn)
145
+
146
+ if Zone.objects.filter(
147
+ active=True,
148
+ name__in=get_parent_zone_names(
149
+ instance.fqdn,
150
+ min_labels=len(fqdn) - len(name),
151
+ include_self=True,
152
+ ),
153
+ ).exists():
154
+ context["mask_warning"] = _(
155
+ "Record is masked by a child zone and may not be visible in DNS"
156
+ )
157
+
125
158
  return context
126
159
 
127
160
 
@@ -69,7 +69,7 @@ class RegistrarBulkDeleteView(generic.BulkDeleteView):
69
69
 
70
70
  @register_model_view(Registrar, "zones")
71
71
  class RegistrarZoneListView(generic.ObjectChildrenView):
72
- queryset = Registrar.objects.all().prefetch_related("zone_set")
72
+ queryset = Registrar.objects.prefetch_related("zones")
73
73
  child_model = Zone
74
74
  table = ZoneTable
75
75
  filterset = ZoneFilterSet
@@ -79,9 +79,9 @@ class RegistrarZoneListView(generic.ObjectChildrenView):
79
79
  tab = ViewTab(
80
80
  label=_("Zones"),
81
81
  permission="netbox_dns.view_zone",
82
- badge=lambda obj: obj.zone_set.count(),
82
+ badge=lambda obj: obj.zones.count(),
83
83
  hide_if_empty=True,
84
84
  )
85
85
 
86
86
  def get_children(self, request, parent):
87
- return parent.zone_set
87
+ return parent.zones
@@ -70,8 +70,8 @@ class RegistrationContactBulkDeleteView(generic.BulkDeleteView):
70
70
 
71
71
  @register_model_view(RegistrationContact, "zones")
72
72
  class RegistrationContactZoneListView(generic.ObjectChildrenView):
73
- queryset = RegistrationContact.objects.all().prefetch_related(
74
- "zone_set", "admin_c_zones", "tech_c_zones", "billing_c_zones"
73
+ queryset = RegistrationContact.objects.prefetch_related(
74
+ "registrant_zones", "admin_c_zones", "tech_c_zones", "billing_c_zones"
75
75
  )
76
76
  child_model = Zone
77
77
  table = ZoneTable
netbox_dns/views/view.py CHANGED
@@ -31,7 +31,7 @@ __all__ = (
31
31
 
32
32
 
33
33
  class ViewView(generic.ObjectView):
34
- queryset = View.objects.all().prefetch_related("zone_set")
34
+ queryset = View.objects.prefetch_related("zones")
35
35
 
36
36
 
37
37
  class ViewListView(generic.ObjectListView):
@@ -89,7 +89,7 @@ class ViewPrefixEditView(generic.ObjectEditView):
89
89
 
90
90
  @register_model_view(View, "zones")
91
91
  class ViewZoneListView(generic.ObjectChildrenView):
92
- queryset = View.objects.all().prefetch_related("zone_set")
92
+ queryset = View.objects.prefetch_related("zones")
93
93
  child_model = Zone
94
94
  table = ZoneTable
95
95
  filterset = ZoneFilterSet
@@ -99,12 +99,12 @@ class ViewZoneListView(generic.ObjectChildrenView):
99
99
  tab = ViewTab(
100
100
  label=_("Zones"),
101
101
  permission="netbox_dns.view_zone",
102
- badge=lambda obj: obj.zone_set.count(),
102
+ badge=lambda obj: obj.zones.count(),
103
103
  hide_if_empty=True,
104
104
  )
105
105
 
106
106
  def get_children(self, request, parent):
107
- return parent.zone_set
107
+ return parent.zones
108
108
 
109
109
 
110
110
  @register_model_view(View, "contacts")
netbox_dns/views/zone.py CHANGED
@@ -18,6 +18,7 @@ from netbox_dns.tables import (
18
18
  ZoneTable,
19
19
  RecordTable,
20
20
  ManagedRecordTable,
21
+ DelegationRecordTable,
21
22
  )
22
23
 
23
24
 
@@ -33,19 +34,19 @@ __all__ = (
33
34
 
34
35
 
35
36
  class ZoneListView(generic.ObjectListView):
36
- queryset = Zone.objects.all().prefetch_related("view", "tags")
37
+ queryset = Zone.objects.prefetch_related("view", "tags")
37
38
  filterset = ZoneFilterSet
38
39
  filterset_form = ZoneFilterForm
39
40
  table = ZoneTable
40
41
 
41
42
 
42
43
  class ZoneView(generic.ObjectView):
43
- queryset = Zone.objects.all().prefetch_related(
44
+ queryset = Zone.objects.prefetch_related(
44
45
  "view",
45
46
  "tags",
46
47
  "nameservers",
47
48
  "soa_mname",
48
- "record_set",
49
+ "records",
49
50
  )
50
51
 
51
52
  def get_extra_context(self, request, instance):
@@ -65,9 +66,7 @@ class ZoneView(generic.ObjectView):
65
66
 
66
67
 
67
68
  class ZoneEditView(generic.ObjectEditView):
68
- queryset = Zone.objects.all().prefetch_related(
69
- "view", "tags", "nameservers", "soa_mname"
70
- )
69
+ queryset = Zone.objects.prefetch_related("view", "tags", "nameservers", "soa_mname")
71
70
  form = ZoneForm
72
71
  default_return_url = "plugins:netbox_dns:zone_list"
73
72
 
@@ -78,18 +77,14 @@ class ZoneDeleteView(generic.ObjectDeleteView):
78
77
 
79
78
 
80
79
  class ZoneBulkImportView(generic.BulkImportView):
81
- queryset = Zone.objects.all().prefetch_related(
82
- "view", "tags", "nameservers", "soa_mname"
83
- )
80
+ queryset = Zone.objects.prefetch_related("view", "tags", "nameservers", "soa_mname")
84
81
  model_form = ZoneImportForm
85
82
  table = ZoneTable
86
83
  default_return_url = "plugins:netbox_dns:zone_list"
87
84
 
88
85
 
89
86
  class ZoneBulkEditView(generic.BulkEditView):
90
- queryset = Zone.objects.all().prefetch_related(
91
- "view", "tags", "nameservers", "soa_mname"
92
- )
87
+ queryset = Zone.objects.prefetch_related("view", "tags", "nameservers", "soa_mname")
93
88
  filterset = ZoneFilterSet
94
89
  table = ZoneTable
95
90
  form = ZoneBulkEditForm
@@ -131,14 +126,12 @@ class ZoneRecordListView(generic.ObjectChildrenView):
131
126
  tab = ViewTab(
132
127
  label=_("Records"),
133
128
  permission="netbox_dns.view_record",
134
- badge=lambda obj: obj.record_count(managed=False),
129
+ badge=lambda obj: obj.records.filter(managed=False).count(),
135
130
  hide_if_empty=True,
136
131
  )
137
132
 
138
133
  def get_children(self, request, parent):
139
- return Record.objects.restrict(request.user, "view").filter(
140
- zone=parent, managed=False
141
- )
134
+ return parent.records.restrict(request.user, "view").filter(managed=False)
142
135
 
143
136
 
144
137
  @register_model_view(Zone, "managed_records")
@@ -153,14 +146,50 @@ class ZoneManagedRecordListView(generic.ObjectChildrenView):
153
146
  tab = ViewTab(
154
147
  label=_("Managed Records"),
155
148
  permission="netbox_dns.view_record",
156
- badge=lambda obj: obj.record_count(managed=True),
149
+ badge=lambda obj: obj.records.filter(managed=True).count(),
150
+ hide_if_empty=True,
151
+ )
152
+
153
+ def get_children(self, request, parent):
154
+ return parent.records.restrict(request.user, "view").filter(managed=True)
155
+
156
+
157
+ @register_model_view(Zone, "delegation_records")
158
+ class ZoneDelegationRecordListView(generic.ObjectChildrenView):
159
+ queryset = Zone.objects.all()
160
+ child_model = Record
161
+ table = DelegationRecordTable
162
+ filterset = RecordFilterSet
163
+ template_name = "netbox_dns/zone/delegation_record.html"
164
+
165
+ tab = ViewTab(
166
+ label=_("Delegation Records"),
167
+ permission="netbox_dns.view_record",
168
+ badge=lambda obj: obj.delegation_records.count(),
169
+ hide_if_empty=True,
170
+ )
171
+
172
+ def get_children(self, request, parent):
173
+ return parent.delegation_records.restrict(request.user, "view")
174
+
175
+
176
+ @register_model_view(Zone, "parent_delegation_records")
177
+ class ZoneParentDelegationRecordListView(generic.ObjectChildrenView):
178
+ queryset = Zone.objects.all()
179
+ child_model = Record
180
+ table = DelegationRecordTable
181
+ filterset = RecordFilterSet
182
+ template_name = "netbox_dns/zone/delegation_record.html"
183
+
184
+ tab = ViewTab(
185
+ label=_("Parent Delegation Records"),
186
+ permission="netbox_dns.view_record",
187
+ badge=lambda obj: obj.ancestor_delegation_records.count(),
157
188
  hide_if_empty=True,
158
189
  )
159
190
 
160
191
  def get_children(self, request, parent):
161
- return Record.objects.restrict(request.user, "view").filter(
162
- zone=parent, managed=True
163
- )
192
+ return parent.ancestor_delegation_records.restrict(request.user, "view")
164
193
 
165
194
 
166
195
  @register_model_view(Zone, "rfc2317_child_zones")
@@ -174,7 +203,7 @@ class ZoneRFC2317ChildZoneListView(generic.ObjectChildrenView):
174
203
  tab = ViewTab(
175
204
  label=_("RFC2317 Child Zones"),
176
205
  permission="netbox_dns.view_zone",
177
- badge=lambda obj: obj.rfc2317_child_zone_count(),
206
+ badge=lambda obj: obj.rfc2317_child_zones.count(),
178
207
  hide_if_empty=True,
179
208
  )
180
209
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: netbox-plugin-dns
3
- Version: 1.1.4
3
+ Version: 1.1.6
4
4
  Summary: NetBox DNS is a NetBox plugin for managing DNS data.
5
5
  Author-email: Peter Eckel <pete@netbox-dns.org>
6
6
  Project-URL: Homepage, https://github.com/peteeckel/netbox-plugin-dns
@@ -25,6 +25,7 @@ The NetBox DNS plugin enables NetBox to manage operational DNS data such as name
25
25
  <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>
26
26
  <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>
27
27
  <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>
28
+ <a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code Style Black"/></a>
28
29
  <a href="https://pepy.tech/project/netbox-plugin-dns"><img alt="Downloads" src="https://static.pepy.tech/badge/netbox-plugin-dns"></a>
29
30
  <a href="https://pepy.tech/project/netbox-plugin-dns"><img alt="Downloads/Week" src="https://static.pepy.tech/badge/netbox-plugin-dns/month"></a>
30
31
  <a href="https://pepy.tech/project/netbox-plugin-dns"><img alt="Downloads/Month" src="https://static.pepy.tech/badge/netbox-plugin-dns/week"></a>