netbox-ddns 1.4.0__tar.gz → 1.7.0__tar.gz

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.
Files changed (67) hide show
  1. {netbox_ddns-1.4.0/netbox_ddns.egg-info → netbox_ddns-1.7.0}/PKG-INFO +7 -5
  2. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/__init__.py +3 -3
  3. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/api/serializers.py +20 -2
  4. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/api/views.py +1 -0
  5. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/background_tasks.py +28 -5
  6. netbox_ddns-1.7.0/netbox_ddns/filtersets.py +30 -0
  7. netbox_ddns-1.7.0/netbox_ddns/forms.py +60 -0
  8. netbox_ddns-1.7.0/netbox_ddns/migrations/0011_server_created_server_custom_field_data_and_more.py +36 -0
  9. netbox_ddns-1.7.0/netbox_ddns/migrations/0012_zone_created_zone_custom_field_data_and_more.py +36 -0
  10. netbox_ddns-1.7.0/netbox_ddns/migrations/0013_reversezone_created_reversezone_custom_field_data_and_more.py +36 -0
  11. netbox_ddns-1.7.0/netbox_ddns/migrations/0014_alter_extradnsname_name.py +19 -0
  12. netbox_ddns-1.7.0/netbox_ddns/migrations/0015_server_protocol.py +18 -0
  13. netbox_ddns-1.7.0/netbox_ddns/migrations/__init__.py +0 -0
  14. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/models.py +61 -9
  15. netbox_ddns-1.7.0/netbox_ddns/navigation.py +62 -0
  16. netbox_ddns-1.7.0/netbox_ddns/tables.py +96 -0
  17. netbox_ddns-1.7.0/netbox_ddns/template_content.py +74 -0
  18. netbox_ddns-1.7.0/netbox_ddns/templates/netbox_ddns/extradnsname.html +61 -0
  19. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/templates/netbox_ddns/ipaddress/dns_extra.html +1 -1
  20. netbox_ddns-1.7.0/netbox_ddns/templates/netbox_ddns/managed_dns_names.html +17 -0
  21. netbox_ddns-1.7.0/netbox_ddns/templates/netbox_ddns/reversezone.html +51 -0
  22. netbox_ddns-1.7.0/netbox_ddns/templates/netbox_ddns/server.html +74 -0
  23. netbox_ddns-1.7.0/netbox_ddns/templates/netbox_ddns/update_reverse_zone.html +11 -0
  24. netbox_ddns-1.7.0/netbox_ddns/templates/netbox_ddns/update_zone.html +11 -0
  25. netbox_ddns-1.7.0/netbox_ddns/templates/netbox_ddns/zone.html +53 -0
  26. netbox_ddns-1.7.0/netbox_ddns/urls.py +40 -0
  27. netbox_ddns-1.7.0/netbox_ddns/utils.py +123 -0
  28. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/validators.py +6 -0
  29. netbox_ddns-1.7.0/netbox_ddns/views.py +339 -0
  30. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0/netbox_ddns.egg-info}/PKG-INFO +7 -5
  31. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns.egg-info/SOURCES.txt +12 -0
  32. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/setup.cfg +4 -3
  33. netbox_ddns-1.4.0/netbox_ddns/admin.py +0 -156
  34. netbox_ddns-1.4.0/netbox_ddns/filtersets.py +0 -8
  35. netbox_ddns-1.4.0/netbox_ddns/forms.py +0 -9
  36. netbox_ddns-1.4.0/netbox_ddns/tables.py +0 -50
  37. netbox_ddns-1.4.0/netbox_ddns/template_content.py +0 -37
  38. netbox_ddns-1.4.0/netbox_ddns/templates/netbox_ddns/extradnsname.html +0 -169
  39. netbox_ddns-1.4.0/netbox_ddns/urls.py +0 -22
  40. netbox_ddns-1.4.0/netbox_ddns/utils.py +0 -28
  41. netbox_ddns-1.4.0/netbox_ddns/views.py +0 -155
  42. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/LICENSE.txt +0 -0
  43. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/MANIFEST.in +0 -0
  44. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/README.md +0 -0
  45. /netbox_ddns-1.4.0/netbox_ddns/api/__init__.py → /netbox_ddns-1.7.0/netbox_ddns/admin.py +0 -0
  46. {netbox_ddns-1.4.0/netbox_ddns/migrations → netbox_ddns-1.7.0/netbox_ddns/api}/__init__.py +0 -0
  47. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/api/urls.py +0 -0
  48. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0001_initial.py +0 -0
  49. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0002_add_ttl.py +0 -0
  50. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0003_dnsstatus.py +0 -0
  51. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0004_ensure_trailing_dot.py +0 -0
  52. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0005_extradnsname.py +0 -0
  53. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0006_extradns_cname.py +0 -0
  54. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0007_zone_meta.py +0 -0
  55. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0008_server_server_port.py +0 -0
  56. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0009_alter_dnsstatus_id_alter_extradnsname_id_and_more.py +0 -0
  57. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/migrations/0010_extradnsname_created_extradnsname_custom_field_data_and_more.py +0 -0
  58. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/search.py +0 -0
  59. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/signals.py +0 -0
  60. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/templates/netbox_ddns/ipaddress/dns_info.html +0 -0
  61. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns/templates/netbox_ddns/ipaddress/dns_refresh_button.html +0 -0
  62. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns.egg-info/dependency_links.txt +0 -0
  63. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns.egg-info/not-zip-safe +0 -0
  64. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns.egg-info/requires.txt +0 -0
  65. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/netbox_ddns.egg-info/top_level.txt +0 -0
  66. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/pyproject.toml +0 -0
  67. {netbox_ddns-1.4.0 → netbox_ddns-1.7.0}/setup.py +0 -0
@@ -1,22 +1,24 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: netbox-ddns
3
- Version: 1.4.0
3
+ Version: 1.7.0
4
4
  Summary: Dynamic DNS Connector for NetBox
5
5
  Home-page: https://github.com/sjm-steffann/netbox-ddns
6
6
  Author: Sander Steffann
7
7
  Author-email: sander@steffann.nl
8
8
  License: Apache 2.0
9
- Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Framework :: Django
11
11
  Classifier: Framework :: Django :: 3.0
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.6
15
- Requires-Python: >=3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.11
16
17
  Description-Content-Type: text/markdown
17
18
  License-File: LICENSE.txt
18
19
  Requires-Dist: setuptools
19
20
  Requires-Dist: dnspython
21
+ Dynamic: license-file
20
22
 
21
23
  # Dynamic DNS Connector for NetBox
22
24
 
@@ -1,4 +1,4 @@
1
- VERSION = '1.4.0'
1
+ VERSION = '1.7.0'
2
2
 
3
3
  try:
4
4
  from netbox.plugins import PluginConfig
@@ -12,8 +12,8 @@ class NetBoxDDNSConfig(PluginConfig):
12
12
  name = 'netbox_ddns'
13
13
  verbose_name = 'Dynamic DNS'
14
14
  version = VERSION
15
- min_version = '4.0.0'
16
- max_version = '4.1.999'
15
+ min_version = '4.4.0'
16
+ max_version = '4.5.999'
17
17
  author = 'Sander Steffann'
18
18
  author_email = 'sander@steffann.nl'
19
19
  description = 'Dynamic DNS Connector for NetBox'
@@ -2,16 +2,34 @@ from rest_framework import serializers
2
2
  from rest_framework.relations import PrimaryKeyRelatedField
3
3
  from ipam.models import IPAddress
4
4
  from netbox.api.serializers import NetBoxModelSerializer
5
- from ..models import ExtraDNSName
5
+ from ..models import ExtraDNSName, Server, Zone, ReverseZone
6
6
 
7
7
 
8
8
  class ExtraDNSNameSerializer(NetBoxModelSerializer):
9
9
  url = serializers.HyperlinkedIdentityField(
10
10
  view_name='plugins-api:netbox_ddns-api:extradnsname-detail'
11
11
  )
12
- ip_address = PrimaryKeyRelatedField(queryset=IPAddress.objects.all(),)
12
+ ip_address = PrimaryKeyRelatedField(queryset=IPAddress.objects.all())
13
13
 
14
14
  class Meta:
15
15
  model = ExtraDNSName
16
16
  fields = ('id', 'ip_address', 'name', 'url')
17
17
  read_only_fields = ('id', 'url')
18
+
19
+
20
+ class ServerSerializer(NetBoxModelSerializer):
21
+ class Meta:
22
+ model = Server
23
+ fields = ('server', 'server_port', 'tsig_key_name', 'tsig_algorithm', "tsig_key")
24
+
25
+
26
+ class ZoneSerializer(NetBoxModelSerializer):
27
+ class Meta:
28
+ model = Zone
29
+ fields = ('name', 'ttl', 'server', 'protocol')
30
+
31
+
32
+ class ReverseZoneSerializer(NetBoxModelSerializer):
33
+ class Meta:
34
+ model = ReverseZone
35
+ fields = ('name', 'prefix', 'ttl', 'server')
@@ -8,3 +8,4 @@ class ExtraDNSNameViewSet(NetBoxModelViewSet):
8
8
  queryset = ExtraDNSName.objects.all()
9
9
  serializer_class = ExtraDNSNameSerializer
10
10
  filterset_class = ExtraDNSNameFilterSet
11
+
@@ -9,7 +9,7 @@ from django_rq import job
9
9
  from dns import rcode
10
10
  from netaddr import ip
11
11
 
12
- from netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, RCODE_NO_ZONE, ReverseZone, Zone
12
+ from netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, RCODE_NO_ZONE, ReverseZone, Zone, Protocol
13
13
  from netbox_ddns.utils import get_soa
14
14
 
15
15
  logger = logging.getLogger('netbox_ddns')
@@ -28,6 +28,17 @@ def status_update(output: List[str], operation: str, response) -> None:
28
28
  output.append(message)
29
29
 
30
30
 
31
+ def send_dns_update(update, server, protocol):
32
+ """Send DNS update via TCP or UDP. Returns response or None on unknown protocol."""
33
+ if protocol == Protocol.TCP:
34
+ return dns.query.tcp(update, server.address, port=server.server_port)
35
+ elif protocol == Protocol.UDP:
36
+ return dns.query.udp(update, server.address, port=server.server_port)
37
+ else:
38
+ logger.error(f"Unknown protocol {protocol} for server {server}")
39
+ return None
40
+
41
+
31
42
  def create_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
32
43
  if status:
33
44
  status.forward_action = ACTION_CREATE
@@ -38,6 +49,7 @@ def create_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
38
49
 
39
50
  # Check the SOA, we don't want to write to a parent zone if it has delegated authority
40
51
  soa = get_soa(zone.name)
52
+ protocol = zone.server.protocol
41
53
  if soa == zone.name:
42
54
  record_type = 'A' if address.version == 4 else 'AAAA'
43
55
  update = zone.server.create_update(zone.name)
@@ -47,7 +59,9 @@ def create_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
47
59
  record_type,
48
60
  str(address)
49
61
  )
50
- response = dns.query.udp(update, zone.server.address, port=zone.server.server_port)
62
+ response = send_dns_update(update, zone.server, protocol)
63
+ if response is None:
64
+ return
51
65
  status_update(output, f'Adding {dns_name} {record_type} {address}', response)
52
66
  if status:
53
67
  status.forward_rcode = response.rcode()
@@ -72,6 +86,7 @@ def delete_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
72
86
 
73
87
  # Check the SOA, we don't want to write to a parent zone if it has delegated authority
74
88
  soa = get_soa(zone.name)
89
+ protocol = zone.server.protocol
75
90
  if soa == zone.name:
76
91
  record_type = 'A' if address.version == 4 else 'AAAA'
77
92
  update = zone.server.create_update(zone.name)
@@ -80,7 +95,9 @@ def delete_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
80
95
  record_type,
81
96
  str(address)
82
97
  )
83
- response = dns.query.udp(update, zone.server.address, port=zone.server.server_port)
98
+ response = send_dns_update(update, zone.server, protocol)
99
+ if response is None:
100
+ return
84
101
  status_update(output, f'Deleting {dns_name} {record_type} {address}', response)
85
102
  if status:
86
103
  status.forward_rcode = response.rcode()
@@ -106,6 +123,7 @@ def create_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
106
123
 
107
124
  # Check the SOA, we don't want to write to a parent zone if it has delegated authority
108
125
  soa = get_soa(record_name)
126
+ protocol = zone.server.protocol
109
127
  if soa == zone.name:
110
128
  update = zone.server.create_update(zone.name)
111
129
  update.add(
@@ -114,7 +132,9 @@ def create_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
114
132
  'ptr',
115
133
  dns_name
116
134
  )
117
- response = dns.query.udp(update, zone.server.address, port=zone.server.server_port)
135
+ response = send_dns_update(update, zone.server, protocol)
136
+ if response is None:
137
+ return
118
138
  status_update(output, f'Adding {record_name} PTR {dns_name}', response)
119
139
  if status:
120
140
  status.reverse_rcode = response.rcode()
@@ -140,6 +160,7 @@ def delete_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
140
160
 
141
161
  # Check the SOA, we don't want to write to a parent zone if it has delegated authority
142
162
  soa = get_soa(record_name)
163
+ protocol = zone.server.protocol
143
164
  if soa == zone.name:
144
165
  update = zone.server.create_update(zone.name)
145
166
  update.delete(
@@ -147,7 +168,9 @@ def delete_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
147
168
  'ptr',
148
169
  dns_name
149
170
  )
150
- response = dns.query.udp(update, zone.server.address, port=zone.server.server_port)
171
+ response = send_dns_update(update, zone.server, protocol)
172
+ if response is None:
173
+ return
151
174
  status_update(output, f'Deleting {record_name} PTR {dns_name}', response)
152
175
  if status:
153
176
  status.reverse_rcode = response.rcode()
@@ -0,0 +1,30 @@
1
+ import django_filters
2
+
3
+ from netbox.filtersets import NetBoxModelFilterSet
4
+ from .models import ExtraDNSName, Server, Zone, ReverseZone
5
+
6
+
7
+ class ExtraDNSNameFilterSet(NetBoxModelFilterSet):
8
+ class Meta:
9
+ model = ExtraDNSName
10
+ fields = ('id', 'name', 'ip_address', 'forward_rcode')
11
+
12
+
13
+ class ServerFilterSet(NetBoxModelFilterSet):
14
+ class Meta:
15
+ model = Server
16
+ fields = ('id', 'server', 'server_port', 'tsig_key_name', 'tsig_algorithm', "tsig_key")
17
+
18
+
19
+ class ZoneFilterSet(NetBoxModelFilterSet):
20
+ class Meta:
21
+ model = Zone
22
+ fields = ('id', 'name', 'ttl', 'server')
23
+
24
+
25
+ class ReverseZoneFilterSet(NetBoxModelFilterSet):
26
+ prefix = django_filters.CharFilter(lookup_expr='icontains')
27
+
28
+ class Meta:
29
+ model = ReverseZone
30
+ fields = ('id', 'name', 'prefix', 'ttl', 'server')
@@ -0,0 +1,60 @@
1
+ from django.forms import IntegerField
2
+
3
+ from ipam.models import IPAddress
4
+ from netbox.forms import NetBoxModelForm, NetBoxModelBulkEditForm
5
+ from netbox_ddns.models import ExtraDNSName, Server, Zone, ReverseZone
6
+ from utilities.forms.fields import DynamicModelChoiceField
7
+ from utilities.forms.rendering import FieldSet
8
+
9
+
10
+ class ReverseZoneForm(NetBoxModelForm):
11
+ class Meta:
12
+ model = ReverseZone
13
+ fields = ('prefix', 'name', 'ttl', 'server')
14
+
15
+
16
+ class ZoneForm(NetBoxModelForm):
17
+ class Meta:
18
+ model = Zone
19
+ fields = ('name', 'ttl', 'server')
20
+
21
+
22
+ class ZoneBulkEditForm(NetBoxModelBulkEditForm):
23
+ model = Zone
24
+
25
+ ttl = IntegerField(
26
+ min_value=1,
27
+ required=False
28
+ )
29
+ server = DynamicModelChoiceField(
30
+ queryset=Server.objects.all(),
31
+ required=False
32
+ )
33
+
34
+
35
+ class ServerForm(NetBoxModelForm):
36
+ fieldsets = (
37
+ FieldSet('server', 'server_port', 'protocol', name='Server'),
38
+ FieldSet('tsig_key_name', 'tsig_algorithm', "tsig_key", name='Authentication'),
39
+ )
40
+
41
+ class Meta:
42
+ model = Server
43
+ fields = ('server', 'server_port', 'protocol', 'tsig_key_name', 'tsig_algorithm', "tsig_key")
44
+
45
+
46
+ class ExtraDNSNameIPAddressForm(NetBoxModelForm):
47
+ class Meta:
48
+ model = ExtraDNSName
49
+ fields = ['name']
50
+
51
+
52
+ class ExtraDNSNameForm(NetBoxModelForm):
53
+ ip_address = DynamicModelChoiceField(
54
+ queryset=IPAddress.objects.all(),
55
+ required=True
56
+ )
57
+
58
+ class Meta:
59
+ model = ExtraDNSName
60
+ fields = ['name', "ip_address"]
@@ -0,0 +1,36 @@
1
+ # Generated by Django 5.0.9 on 2025-01-08 11:03
2
+
3
+ import taggit.managers
4
+ import utilities.json
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('extras', '0122_charfield_null_choices'),
12
+ ('netbox_ddns', '0010_extradnsname_created_extradnsname_custom_field_data_and_more'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name='server',
18
+ name='created',
19
+ field=models.DateTimeField(auto_now_add=True, null=True),
20
+ ),
21
+ migrations.AddField(
22
+ model_name='server',
23
+ name='custom_field_data',
24
+ field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
25
+ ),
26
+ migrations.AddField(
27
+ model_name='server',
28
+ name='last_updated',
29
+ field=models.DateTimeField(auto_now=True, null=True),
30
+ ),
31
+ migrations.AddField(
32
+ model_name='server',
33
+ name='tags',
34
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
35
+ ),
36
+ ]
@@ -0,0 +1,36 @@
1
+ # Generated by Django 5.0.9 on 2025-01-08 18:46
2
+
3
+ import taggit.managers
4
+ import utilities.json
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('extras', '0122_charfield_null_choices'),
12
+ ('netbox_ddns', '0011_server_created_server_custom_field_data_and_more'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name='zone',
18
+ name='created',
19
+ field=models.DateTimeField(auto_now_add=True, null=True),
20
+ ),
21
+ migrations.AddField(
22
+ model_name='zone',
23
+ name='custom_field_data',
24
+ field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
25
+ ),
26
+ migrations.AddField(
27
+ model_name='zone',
28
+ name='last_updated',
29
+ field=models.DateTimeField(auto_now=True, null=True),
30
+ ),
31
+ migrations.AddField(
32
+ model_name='zone',
33
+ name='tags',
34
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
35
+ ),
36
+ ]
@@ -0,0 +1,36 @@
1
+ # Generated by Django 5.0.9 on 2025-01-08 19:51
2
+
3
+ import taggit.managers
4
+ import utilities.json
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('extras', '0122_charfield_null_choices'),
12
+ ('netbox_ddns', '0012_zone_created_zone_custom_field_data_and_more'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.AddField(
17
+ model_name='reversezone',
18
+ name='created',
19
+ field=models.DateTimeField(auto_now_add=True, null=True),
20
+ ),
21
+ migrations.AddField(
22
+ model_name='reversezone',
23
+ name='custom_field_data',
24
+ field=models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder),
25
+ ),
26
+ migrations.AddField(
27
+ model_name='reversezone',
28
+ name='last_updated',
29
+ field=models.DateTimeField(auto_now=True, null=True),
30
+ ),
31
+ migrations.AddField(
32
+ model_name='reversezone',
33
+ name='tags',
34
+ field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'),
35
+ ),
36
+ ]
@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.2.5 on 2025-09-01 09:17
2
+
3
+ import netbox_ddns.validators
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('netbox_ddns', '0013_reversezone_created_reversezone_custom_field_data_and_more'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='extradnsname',
16
+ name='name',
17
+ field=models.CharField(max_length=255, validators=[netbox_ddns.validators.DNSNameValidator()]),
18
+ ),
19
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.2.6 on 2025-10-01 14:44
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('netbox_ddns', '0014_alter_extradnsname_name'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='server',
15
+ name='protocol',
16
+ field=models.CharField(default='udp', max_length=3),
17
+ ),
18
+ ]
File without changes
@@ -4,6 +4,7 @@ import logging
4
4
  import socket
5
5
  from django.core.exceptions import ValidationError
6
6
  from django.db import models
7
+ from django.db.models import Q
7
8
  from django.db.models.functions import Length
8
9
  from django.utils.html import format_html
9
10
  from django.utils.translation import gettext_lazy as _
@@ -15,8 +16,10 @@ from typing import Optional
15
16
  from netbox.models import NetBoxModel
16
17
  from ipam.fields import IPNetworkField
17
18
  from ipam.models import IPAddress
19
+ from utilities.querysets import RestrictedQuerySet
18
20
  from .utils import normalize_fqdn
19
- from .validators import HostnameAddressValidator, HostnameValidator, validate_base64, MinValueValidator, MaxValueValidator
21
+ from .validators import DNSNameValidator, HostnameAddressValidator, HostnameValidator, validate_base64, MinValueValidator, \
22
+ MaxValueValidator
20
23
 
21
24
  logger = logging.getLogger('netbox_ddns')
22
25
 
@@ -61,8 +64,11 @@ def get_rcode_display(code):
61
64
  else:
62
65
  return _('Unknown response: {}').format(code)
63
66
 
67
+ class Protocol(models.TextChoices):
68
+ UDP = 'udp', _('UDP')
69
+ TCP = 'tcp', _('TCP')
64
70
 
65
- class Server(models.Model):
71
+ class Server(NetBoxModel):
66
72
  server = models.CharField(
67
73
  verbose_name=_('DDNS Server'),
68
74
  max_length=255,
@@ -92,6 +98,12 @@ class Server(models.Model):
92
98
  validators=[validate_base64],
93
99
  help_text=_('in base64 notation'),
94
100
  )
101
+ protocol = models.CharField(
102
+ verbose_name=_('Protocol'),
103
+ max_length=3,
104
+ choices=Protocol.choices,
105
+ default=Protocol.UDP,
106
+ )
95
107
 
96
108
  class Meta:
97
109
  unique_together = (
@@ -111,6 +123,9 @@ class Server(models.Model):
111
123
  # Ensure trailing dots from domain-style fields
112
124
  self.tsig_key_name = normalize_fqdn(self.tsig_key_name.lower().rstrip('.'))
113
125
 
126
+ def get_absolute_url(self):
127
+ return reverse('plugins:netbox_ddns:server', args=[self.pk])
128
+
114
129
  @property
115
130
  def address(self) -> Optional[str]:
116
131
  addrinfo = socket.getaddrinfo(self.server, self.server_port, proto=socket.IPPROTO_UDP)
@@ -129,7 +144,7 @@ class Server(models.Model):
129
144
  )
130
145
 
131
146
 
132
- class ZoneQuerySet(models.QuerySet):
147
+ class ZoneQuerySet(RestrictedQuerySet):
133
148
  def find_for_dns_name(self, dns_name: str) -> Optional['Zone']:
134
149
  # Generate all possible zones
135
150
  zones = []
@@ -141,7 +156,7 @@ class ZoneQuerySet(models.QuerySet):
141
156
  return self.filter(name__in=zones).order_by(Length('name').desc()).first()
142
157
 
143
158
 
144
- class Zone(models.Model):
159
+ class Zone(NetBoxModel):
145
160
  name = models.CharField(
146
161
  verbose_name=_('zone name'),
147
162
  max_length=255,
@@ -167,6 +182,28 @@ class Zone(models.Model):
167
182
  def __str__(self):
168
183
  return self.name
169
184
 
185
+ def get_managed_ip_address(self):
186
+ # Find all more-specific zones
187
+ more_specifics = Zone.objects.filter(name__endswith=self.name).exclude(pk=self.pk)
188
+
189
+ # Find all IPAddress objects in this self but not in the more-specifics
190
+ ip_addresses = IPAddress.objects.filter(Q(dns_name__endswith=self.name) |
191
+ Q(dns_name__endswith=self.name.rstrip('.')))
192
+ for more_specific in more_specifics:
193
+ ip_addresses = ip_addresses.exclude(Q(dns_name__endswith=more_specific.name) |
194
+ Q(dns_name__endswith=more_specific.name.rstrip('.')))
195
+ return ip_addresses
196
+
197
+ def get_managed_extra_dns_name(self):
198
+ # Find all more-specific zones
199
+ more_specifics = Zone.objects.filter(name__endswith=self.name).exclude(pk=self.pk)
200
+
201
+ # Find all ExtraDNSName objects in this zone but not in the more-specifics
202
+ extra_names = ExtraDNSName.objects.filter(name__endswith=self.name)
203
+ for more_specific in more_specifics:
204
+ extra_names = extra_names.exclude(name__endswith=more_specific.name)
205
+ return extra_names
206
+
170
207
  def clean(self):
171
208
  # Ensure trailing dots from domain-style fields
172
209
  self.name = normalize_fqdn(self.name)
@@ -174,8 +211,11 @@ class Zone(models.Model):
174
211
  def get_updater(self):
175
212
  return self.server.create_update(self.name)
176
213
 
214
+ def get_absolute_url(self):
215
+ return reverse('plugins:netbox_ddns:zone', args=[self.pk])
216
+
177
217
 
178
- class ReverseZoneQuerySet(models.QuerySet):
218
+ class ReverseZoneQuerySet(RestrictedQuerySet):
179
219
  def find_for_address(self, address: ip.IPAddress) -> Optional['ReverseZone']:
180
220
  # Find the zone, if any
181
221
  zones = list(ReverseZone.objects.filter(prefix__net_contains=address))
@@ -186,7 +226,7 @@ class ReverseZoneQuerySet(models.QuerySet):
186
226
  return zones[-1]
187
227
 
188
228
 
189
- class ReverseZone(models.Model):
229
+ class ReverseZone(NetBoxModel):
190
230
  prefix = IPNetworkField(
191
231
  verbose_name=_('prefix'),
192
232
  unique=True,
@@ -214,7 +254,19 @@ class ReverseZone(models.Model):
214
254
  verbose_name_plural = _('reverse zones')
215
255
 
216
256
  def __str__(self):
217
- return f'for {self.prefix}'
257
+ return f'{self.prefix}'
258
+
259
+ def get_managed_ip_address(self):
260
+ # Find all more-specific zones
261
+ more_specifics = ReverseZone.objects.filter(prefix__net_contained=self.prefix).exclude(pk=self.pk)
262
+ # Find all IPAddress objects in this zone but not in the more-specifics
263
+ ip_addresses = IPAddress.objects.filter(address__net_contained_or_equal=self.prefix)
264
+ for more_specific in more_specifics:
265
+ ip_addresses = ip_addresses.exclude(address__net_contained_or_equal=more_specific.prefix)
266
+ return ip_addresses
267
+
268
+ def get_absolute_url(self):
269
+ return reverse('plugins:netbox_ddns:reversezone', args=[self.pk])
218
270
 
219
271
  def record_name(self, address: ip.IPAddress):
220
272
  record_name = self.name
@@ -334,7 +386,7 @@ class ExtraDNSName(NetBoxModel):
334
386
  name = models.CharField(
335
387
  verbose_name=_('DNS name'),
336
388
  max_length=255,
337
- validators=[HostnameValidator()],
389
+ validators=[DNSNameValidator()],
338
390
  )
339
391
 
340
392
  last_update = models.DateTimeField(
@@ -367,7 +419,7 @@ class ExtraDNSName(NetBoxModel):
367
419
  return self.name
368
420
 
369
421
  def get_absolute_url(self):
370
- return reverse('plugins:netbox_ddns:extradnsname', args=[self.ip_address.pk, self.pk])
422
+ return reverse('plugins:netbox_ddns:extradnsname', args=[self.pk])
371
423
 
372
424
  def clean(self):
373
425
  # Ensure trailing dots from domain-style fields
@@ -0,0 +1,62 @@
1
+ from netbox.plugins import PluginMenuItem, PluginMenuButton, PluginMenu
2
+
3
+ menu = PluginMenu(
4
+ label='DDNS',
5
+ groups=(
6
+ ('Configuration', (
7
+ PluginMenuItem(
8
+ link='plugins:netbox_ddns:managed_dns_name_list',
9
+ link_text='DNS Names',
10
+ ),
11
+ PluginMenuItem(
12
+ link='plugins:netbox_ddns:server_list',
13
+ link_text='DDNS Servers',
14
+ buttons=[
15
+ PluginMenuButton(
16
+ link='plugins:netbox_ddns:server_add',
17
+ title='Add',
18
+ icon_class='mdi mdi-plus-thick',
19
+ )
20
+ ]
21
+ ),
22
+ PluginMenuItem(
23
+ link='plugins:netbox_ddns:zone_list',
24
+ link_text='Forward Zones',
25
+ buttons=[
26
+ PluginMenuButton(
27
+ link='plugins:netbox_ddns:zone_add',
28
+ title='Add',
29
+ icon_class='mdi mdi-plus-thick',
30
+ )
31
+ ]
32
+ ),
33
+ PluginMenuItem(
34
+ link='plugins:netbox_ddns:reversezone_list',
35
+ link_text='Reverse Zones',
36
+ buttons=[
37
+ PluginMenuButton(
38
+ link='plugins:netbox_ddns:reversezone_add',
39
+ title='Add',
40
+ icon_class='mdi mdi-plus-thick',
41
+ )
42
+ ]
43
+ ),
44
+ ),
45
+ ),
46
+ ('Extra DNS Names', (
47
+ PluginMenuItem(
48
+ link='plugins:netbox_ddns:extradnsname_list',
49
+ link_text='Extra DNS names',
50
+ buttons=[
51
+ PluginMenuButton(
52
+ link='plugins:netbox_ddns:extradnsname_add',
53
+ title='Add',
54
+ icon_class='mdi mdi-plus-thick',
55
+ )
56
+ ]
57
+ ),
58
+ ),
59
+ ),
60
+ ),
61
+ icon_class='mdi mdi-router'
62
+ )