netbox-plugin-dns 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of netbox-plugin-dns might be problematic. Click here for more details.

Files changed (91) hide show
  1. netbox_dns/__init__.py +1 -1
  2. netbox_dns/api/nested_serializers.py +46 -1
  3. netbox_dns/api/serializers.py +2 -0
  4. netbox_dns/api/serializers_/contact.py +3 -0
  5. netbox_dns/api/serializers_/nameserver.py +4 -1
  6. netbox_dns/api/serializers_/record.py +5 -4
  7. netbox_dns/api/serializers_/record_template.py +57 -0
  8. netbox_dns/api/serializers_/registrar.py +3 -0
  9. netbox_dns/api/serializers_/view.py +3 -0
  10. netbox_dns/api/serializers_/zone.py +30 -6
  11. netbox_dns/api/serializers_/zone_template.py +129 -0
  12. netbox_dns/api/urls.py +4 -0
  13. netbox_dns/api/views.py +41 -1
  14. netbox_dns/choices/__init__.py +2 -0
  15. netbox_dns/choices/record.py +49 -0
  16. netbox_dns/choices/zone.py +20 -0
  17. netbox_dns/fields/address.py +6 -0
  18. netbox_dns/fields/network.py +3 -0
  19. netbox_dns/fields/rfc2317.py +3 -0
  20. netbox_dns/filtersets/__init__.py +3 -0
  21. netbox_dns/filtersets/contact.py +3 -0
  22. netbox_dns/filtersets/nameserver.py +3 -0
  23. netbox_dns/filtersets/record.py +5 -1
  24. netbox_dns/filtersets/record_template.py +54 -0
  25. netbox_dns/filtersets/registrar.py +3 -0
  26. netbox_dns/filtersets/view.py +3 -0
  27. netbox_dns/filtersets/zone.py +5 -8
  28. netbox_dns/filtersets/zone_template.py +116 -0
  29. netbox_dns/forms/__init__.py +2 -0
  30. netbox_dns/forms/contact.py +8 -0
  31. netbox_dns/forms/nameserver.py +8 -0
  32. netbox_dns/forms/record.py +25 -11
  33. netbox_dns/forms/record_template.py +220 -0
  34. netbox_dns/forms/registrar.py +8 -0
  35. netbox_dns/forms/view.py +10 -0
  36. netbox_dns/forms/zone.py +109 -36
  37. netbox_dns/forms/zone_template.py +298 -0
  38. netbox_dns/graphql/__init__.py +4 -0
  39. netbox_dns/graphql/filters.py +24 -1
  40. netbox_dns/graphql/schema.py +34 -1
  41. netbox_dns/graphql/types.py +73 -5
  42. netbox_dns/management/commands/cleanup_database.py +2 -6
  43. netbox_dns/management/commands/cleanup_rrset_ttl.py +2 -4
  44. netbox_dns/migrations/0001_squashed_netbox_dns_0_22.py +1 -2
  45. netbox_dns/migrations/0006_templating.py +172 -0
  46. netbox_dns/migrations/0021_record_ip_address.py +1 -1
  47. netbox_dns/mixins/object_modification.py +3 -0
  48. netbox_dns/models/__init__.py +7 -0
  49. netbox_dns/models/contact.py +6 -0
  50. netbox_dns/models/nameserver.py +8 -1
  51. netbox_dns/models/record.py +10 -122
  52. netbox_dns/models/record_template.py +180 -0
  53. netbox_dns/models/registrar.py +6 -0
  54. netbox_dns/models/view.py +7 -1
  55. netbox_dns/models/zone.py +57 -66
  56. netbox_dns/models/zone_template.py +149 -0
  57. netbox_dns/navigation.py +47 -0
  58. netbox_dns/tables/__init__.py +2 -0
  59. netbox_dns/tables/contact.py +3 -0
  60. netbox_dns/tables/nameserver.py +3 -2
  61. netbox_dns/tables/record.py +7 -3
  62. netbox_dns/tables/record_template.py +91 -0
  63. netbox_dns/tables/registrar.py +3 -0
  64. netbox_dns/tables/view.py +3 -0
  65. netbox_dns/tables/zone.py +3 -2
  66. netbox_dns/tables/zone_template.py +70 -0
  67. netbox_dns/template_content.py +2 -8
  68. netbox_dns/templates/netbox_dns/recordtemplate.html +84 -0
  69. netbox_dns/templates/netbox_dns/zonetemplate.html +86 -0
  70. netbox_dns/urls/__init__.py +4 -0
  71. netbox_dns/urls/record_template.py +65 -0
  72. netbox_dns/urls/zone_template.py +57 -0
  73. netbox_dns/utilities/ipam_coupling.py +2 -1
  74. netbox_dns/validators/__init__.py +1 -0
  75. netbox_dns/validators/dns_name.py +14 -9
  76. netbox_dns/validators/dns_value.py +83 -0
  77. netbox_dns/validators/rfc2317.py +7 -0
  78. netbox_dns/views/__init__.py +2 -0
  79. netbox_dns/views/contact.py +11 -0
  80. netbox_dns/views/nameserver.py +12 -0
  81. netbox_dns/views/record.py +14 -1
  82. netbox_dns/views/record_template.py +83 -0
  83. netbox_dns/views/registrar.py +12 -0
  84. netbox_dns/views/view.py +12 -0
  85. netbox_dns/views/zone.py +16 -0
  86. netbox_dns/views/zone_template.py +73 -0
  87. {netbox_plugin_dns-1.0.3.dist-info → netbox_plugin_dns-1.0.5.dist-info}/METADATA +2 -1
  88. netbox_plugin_dns-1.0.5.dist-info/RECORD +136 -0
  89. netbox_plugin_dns-1.0.3.dist-info/RECORD +0 -115
  90. {netbox_plugin_dns-1.0.3.dist-info → netbox_plugin_dns-1.0.5.dist-info}/LICENSE +0 -0
  91. {netbox_plugin_dns-1.0.3.dist-info → netbox_plugin_dns-1.0.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,180 @@
1
+ import dns
2
+ from dns import name as dns_name
3
+
4
+ from django.core.exceptions import ValidationError
5
+ from django.db import models
6
+ from django.urls import reverse
7
+
8
+ from netbox.models import NetBoxModel
9
+ from netbox.search import SearchIndex, register_search
10
+ from netbox.plugins.utils import get_plugin_config
11
+
12
+ from netbox_dns.choices import RecordTypeChoices, RecordStatusChoices
13
+ from netbox_dns.validators import validate_generic_name, validate_record_value
14
+
15
+ from .record import Record
16
+
17
+
18
+ __ALL__ = (
19
+ "RecordTemplate",
20
+ "RecordTemplateIndex",
21
+ )
22
+
23
+
24
+ class RecordTemplate(NetBoxModel):
25
+ name = models.CharField(
26
+ verbose_name="Template name",
27
+ unique=True,
28
+ max_length=200,
29
+ )
30
+ record_name = models.CharField(
31
+ verbose_name="Name",
32
+ max_length=255,
33
+ )
34
+ description = models.CharField(
35
+ max_length=200,
36
+ blank=True,
37
+ )
38
+ type = models.CharField(
39
+ choices=RecordTypeChoices,
40
+ )
41
+ value = models.CharField(
42
+ max_length=65535,
43
+ )
44
+ status = models.CharField(
45
+ choices=RecordStatusChoices,
46
+ default=RecordStatusChoices.STATUS_ACTIVE,
47
+ blank=False,
48
+ )
49
+ ttl = models.PositiveIntegerField(
50
+ verbose_name="TTL",
51
+ null=True,
52
+ blank=True,
53
+ )
54
+ disable_ptr = models.BooleanField(
55
+ verbose_name="Disable PTR",
56
+ help_text="Disable PTR record creation",
57
+ default=False,
58
+ )
59
+ tenant = models.ForeignKey(
60
+ to="tenancy.Tenant",
61
+ on_delete=models.PROTECT,
62
+ related_name="+",
63
+ blank=True,
64
+ null=True,
65
+ )
66
+
67
+ clone_fields = (
68
+ "record_name",
69
+ "description",
70
+ "type",
71
+ "value",
72
+ "status",
73
+ "ttl",
74
+ "disable_ptr",
75
+ "tenant",
76
+ )
77
+
78
+ template_fields = (
79
+ "type",
80
+ "value",
81
+ "status",
82
+ "ttl",
83
+ "disable_ptr",
84
+ "tenant",
85
+ )
86
+
87
+ class Meta:
88
+ ordering = ["name"]
89
+
90
+ def __str__(self):
91
+ return str(self.name)
92
+
93
+ def get_status_color(self):
94
+ return RecordStatusChoices.colors.get(self.status)
95
+
96
+ def get_absolute_url(self):
97
+ return reverse("plugins:netbox_dns:recordtemplate", kwargs={"pk": self.pk})
98
+
99
+ def validate_name(self):
100
+ try:
101
+ name = dns_name.from_text(self.record_name, origin=None)
102
+ name.to_unicode()
103
+
104
+ except dns.exception.DNSException as exc:
105
+ raise ValidationError({"record_name": str(exc)})
106
+
107
+ if self.type not in get_plugin_config(
108
+ "netbox_dns", "tolerate_non_rfc1035_types", default=[]
109
+ ):
110
+ try:
111
+ validate_generic_name(
112
+ self.record_name,
113
+ (
114
+ self.type
115
+ in get_plugin_config(
116
+ "netbox_dns",
117
+ "tolerate_leading_underscore_types",
118
+ default=[],
119
+ )
120
+ ),
121
+ )
122
+ except ValidationError as exc:
123
+ raise ValidationError(
124
+ {
125
+ "record_name": exc,
126
+ }
127
+ ) from None
128
+
129
+ def validate_value(self):
130
+ try:
131
+ validate_record_value(self.type, self.value)
132
+ except ValidationError as exc:
133
+ raise ValidationError({"value": exc}) from None
134
+
135
+ def matching_records(self, zone):
136
+ return Record.objects.filter(
137
+ zone=zone, name=self.record_name, type=self.type, value=self.value
138
+ )
139
+
140
+ def create_record(self, zone):
141
+ if self.matching_records(zone).exists():
142
+ return
143
+
144
+ record_data = {
145
+ "zone": zone,
146
+ "name": self.record_name,
147
+ }
148
+ for field in self.template_fields:
149
+ record_data[field] = getattr(self, field)
150
+
151
+ try:
152
+ record = Record.objects.create(**record_data)
153
+ except ValidationError as exc:
154
+ raise ValidationError(
155
+ f"Error while processing record template {self}: {exc.messages[0]}"
156
+ )
157
+
158
+ if tags := self.tags.all():
159
+ record.tags.set(tags)
160
+
161
+ def clean_fields(self, *args, **kwargs):
162
+ self.type = self.type.upper()
163
+ super().clean_fields(*args, **kwargs)
164
+
165
+ def clean(self, *args, **kwargs):
166
+ self.validate_name()
167
+ self.validate_value()
168
+
169
+ super().clean(*args, **kwargs)
170
+
171
+
172
+ @register_search
173
+ class RecordTemplateIndex(SearchIndex):
174
+ model = RecordTemplate
175
+ fields = (
176
+ ("name", 100),
177
+ ("record_name", 120),
178
+ ("value", 150),
179
+ ("type", 200),
180
+ )
@@ -5,6 +5,12 @@ from netbox.models import NetBoxModel
5
5
  from netbox.search import SearchIndex, register_search
6
6
 
7
7
 
8
+ __ALL__ = (
9
+ "Registrar",
10
+ "RegistrarIndex",
11
+ )
12
+
13
+
8
14
  class Registrar(NetBoxModel):
9
15
  # +
10
16
  # Data fields according to https://www.icann.org/resources/pages/rdds-labeling-policy-2017-02-01-en
netbox_dns/models/view.py CHANGED
@@ -10,6 +10,12 @@ from utilities.exceptions import AbortRequest
10
10
  from netbox_dns.mixins import ObjectModificationMixin
11
11
 
12
12
 
13
+ __ALL__ = (
14
+ "View",
15
+ "ViewIndex",
16
+ )
17
+
18
+
13
19
  class View(ObjectModificationMixin, NetBoxModel):
14
20
  name = models.CharField(
15
21
  unique=True,
@@ -54,7 +60,7 @@ class View(ObjectModificationMixin, NetBoxModel):
54
60
 
55
61
  super().delete(*args, **kwargs)
56
62
 
57
- def clean(self, *args, old_state=None, **kwargs):
63
+ def clean(self, *args, **kwargs):
58
64
  if (changed_fields := self.changed_fields) is None:
59
65
  return
60
66
 
netbox_dns/models/zone.py CHANGED
@@ -22,9 +22,9 @@ from netbox.models import NetBoxModel
22
22
  from netbox.search import SearchIndex, register_search
23
23
  from netbox.plugins.utils import get_plugin_config
24
24
  from utilities.querysets import RestrictedQuerySet
25
- from utilities.choices import ChoiceSet
26
25
  from ipam.models import IPAddress
27
26
 
27
+ from netbox_dns.choices import RecordClassChoices, RecordTypeChoices, ZoneStatusChoices
28
28
  from netbox_dns.fields import NetworkField, RFC2317NetworkField
29
29
  from netbox_dns.utilities import (
30
30
  arpa_to_prefix,
@@ -38,12 +38,15 @@ from netbox_dns.validators import (
38
38
  )
39
39
  from netbox_dns.mixins import ObjectModificationMixin
40
40
 
41
- # +
42
- # This is a hack designed to break cyclic imports between View, Record and Zone
43
- # -
44
- import netbox_dns.models.record as record
45
- import netbox_dns.models.view as view
46
- import netbox_dns.models.nameserver as nameserver
41
+ from .record import Record
42
+ from .view import View
43
+ from .nameserver import NameServer
44
+
45
+
46
+ __ALL__ = (
47
+ "Zone",
48
+ "ZoneIndex",
49
+ )
47
50
 
48
51
 
49
52
  class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
@@ -63,24 +66,15 @@ class ZoneManager(models.Manager.from_queryset(RestrictedQuerySet)):
63
66
  )
64
67
 
65
68
 
66
- class ZoneStatusChoices(ChoiceSet):
67
- key = "Zone.status"
68
-
69
- STATUS_ACTIVE = "active"
70
- STATUS_RESERVED = "reserved"
71
- STATUS_DEPRECATED = "deprecated"
72
- STATUS_PARKED = "parked"
69
+ class Zone(ObjectModificationMixin, NetBoxModel):
70
+ ACTIVE_STATUS_LIST = (ZoneStatusChoices.STATUS_ACTIVE,)
73
71
 
74
- CHOICES = [
75
- (STATUS_ACTIVE, "Active", "blue"),
76
- (STATUS_RESERVED, "Reserved", "cyan"),
77
- (STATUS_DEPRECATED, "Deprecated", "red"),
78
- (STATUS_PARKED, "Parked", "gray"),
79
- ]
72
+ def __init__(self, *args, **kwargs):
73
+ kwargs.pop("template", None)
80
74
 
75
+ super().__init__(*args, **kwargs)
81
76
 
82
- class Zone(ObjectModificationMixin, NetBoxModel):
83
- ACTIVE_STATUS_LIST = (ZoneStatusChoices.STATUS_ACTIVE,)
77
+ self._soa_serial_dirty = False
84
78
 
85
79
  view = models.ForeignKey(
86
80
  to="View",
@@ -249,8 +243,6 @@ class Zone(ObjectModificationMixin, NetBoxModel):
249
243
  null=True,
250
244
  )
251
245
 
252
- soa_serial_dirty = False
253
-
254
246
  objects = ZoneManager()
255
247
 
256
248
  clone_fields = [
@@ -288,8 +280,11 @@ class Zone(ObjectModificationMixin, NetBoxModel):
288
280
  except dns_name.IDNAException:
289
281
  name = self.name
290
282
 
291
- if not self.view.default_view:
292
- return f"[{self.view}] {name}"
283
+ try:
284
+ if not self.view.default_view:
285
+ return f"[{self.view}] {name}"
286
+ except ObjectDoesNotExist:
287
+ return f"[<no view assigned>] {name}"
293
288
 
294
289
  return str(name)
295
290
 
@@ -302,6 +297,14 @@ class Zone(ObjectModificationMixin, NetBoxModel):
302
297
  and field not in ("zone_soa_mname", "zone_nameservers")
303
298
  }
304
299
 
300
+ @property
301
+ def soa_serial_dirty(self):
302
+ return self._soa_serial_dirty
303
+
304
+ @soa_serial_dirty.setter
305
+ def soa_serial_dirty(self, soa_serial_dirty):
306
+ self._soa_serial_dirty = soa_serial_dirty
307
+
305
308
  @property
306
309
  def display_name(self):
307
310
  return name_to_unicode(self.name)
@@ -371,7 +374,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
371
374
  return None
372
375
 
373
376
  def record_count(self, managed=False):
374
- return record.Record.objects.filter(zone=self, managed=managed).count()
377
+ return Record.objects.filter(zone=self, managed=managed).count()
375
378
 
376
379
  def rfc2317_child_zone_count(self):
377
380
  return Zone.objects.filter(rfc2317_parent_zone=self).count()
@@ -380,8 +383,8 @@ class Zone(ObjectModificationMixin, NetBoxModel):
380
383
  soa_name = "@"
381
384
  soa_ttl = self.soa_ttl
382
385
  soa_rdata = SOA.SOA(
383
- rdclass=record.RecordClassChoices.IN,
384
- rdtype=record.RecordTypeChoices.SOA,
386
+ rdclass=RecordClassChoices.IN,
387
+ rdtype=RecordTypeChoices.SOA,
385
388
  mname=self.soa_mname.name,
386
389
  rname=self.soa_rname,
387
390
  serial=self.soa_serial,
@@ -392,9 +395,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
392
395
  )
393
396
 
394
397
  try:
395
- soa_record = self.record_set.get(
396
- type=record.RecordTypeChoices.SOA, name=soa_name
397
- )
398
+ soa_record = self.record_set.get(type=RecordTypeChoices.SOA, name=soa_name)
398
399
 
399
400
  if soa_record.ttl != soa_ttl or soa_record.value != soa_rdata.to_text():
400
401
  soa_record.ttl = soa_ttl
@@ -402,10 +403,10 @@ class Zone(ObjectModificationMixin, NetBoxModel):
402
403
  soa_record.managed = True
403
404
  soa_record.save()
404
405
 
405
- except record.Record.DoesNotExist:
406
- record.Record.objects.create(
406
+ except Record.DoesNotExist:
407
+ Record.objects.create(
407
408
  zone_id=self.pk,
408
- type=record.RecordTypeChoices.SOA,
409
+ type=RecordTypeChoices.SOA,
409
410
  name=soa_name,
410
411
  ttl=soa_ttl,
411
412
  value=soa_rdata.to_text(),
@@ -417,14 +418,14 @@ class Zone(ObjectModificationMixin, NetBoxModel):
417
418
 
418
419
  nameservers = [f"{nameserver.name}." for nameserver in self.nameservers.all()]
419
420
 
420
- self.record_set.filter(type=record.RecordTypeChoices.NS, managed=True).exclude(
421
+ self.record_set.filter(type=RecordTypeChoices.NS, managed=True).exclude(
421
422
  value__in=nameservers
422
423
  ).delete()
423
424
 
424
425
  for ns in nameservers:
425
- record.Record.raw_objects.update_or_create(
426
+ Record.raw_objects.update_or_create(
426
427
  zone_id=self.pk,
427
- type=record.RecordTypeChoices.NS,
428
+ type=RecordTypeChoices.NS,
428
429
  name=ns_name,
429
430
  value=ns,
430
431
  managed=True,
@@ -452,14 +453,11 @@ class Zone(ObjectModificationMixin, NetBoxModel):
452
453
  continue
453
454
 
454
455
  relative_name = name.relativize(parent).to_text()
455
- address_records = record.Record.objects.filter(
456
+ address_records = Record.objects.filter(
456
457
  Q(zone=ns_zone),
457
- Q(status__in=record.Record.ACTIVE_STATUS_LIST),
458
+ Q(status__in=Record.ACTIVE_STATUS_LIST),
458
459
  Q(Q(name=f"{_nameserver.name}.") | Q(name=relative_name)),
459
- Q(
460
- Q(type=record.RecordTypeChoices.A)
461
- | Q(type=record.RecordTypeChoices.AAAA)
462
- ),
460
+ Q(Q(type=RecordTypeChoices.A) | Q(type=RecordTypeChoices.AAAA)),
463
461
  )
464
462
 
465
463
  if not address_records:
@@ -482,8 +480,8 @@ class Zone(ObjectModificationMixin, NetBoxModel):
482
480
  )
483
481
 
484
482
  def get_auto_serial(self):
485
- records = record.Record.objects.filter(zone_id=self.pk).exclude(
486
- type=record.RecordTypeChoices.SOA
483
+ records = Record.objects.filter(zone_id=self.pk).exclude(
484
+ type=RecordTypeChoices.SOA
487
485
  )
488
486
  if records:
489
487
  soa_serial = (
@@ -541,7 +539,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
541
539
  self.save(update_fields=["rfc2317_parent_zone"])
542
540
 
543
541
  ptr_records = self.record_set.filter(
544
- type=record.RecordTypeChoices.PTR
542
+ type=RecordTypeChoices.PTR
545
543
  ).prefetch_related("zone", "rfc2317_cname_record")
546
544
  ptr_zones = {ptr_record.zone for ptr_record in ptr_records}
547
545
 
@@ -576,7 +574,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
576
574
  defaults = settings.PLUGINS_CONFIG.get("netbox_dns")
577
575
 
578
576
  if self.view_id is None:
579
- self.view_id = view.View.get_default_view().pk
577
+ self.view_id = View.get_default_view().pk
580
578
 
581
579
  for field, value in self.get_defaults().items():
582
580
  if getattr(self, field) in (None, ""):
@@ -586,10 +584,8 @@ class Zone(ObjectModificationMixin, NetBoxModel):
586
584
  if self.soa_mname_id is None:
587
585
  default_soa_mname = defaults.get("zone_soa_mname")
588
586
  try:
589
- self.soa_mname = nameserver.NameServer.objects.get(
590
- name=default_soa_mname
591
- )
592
- except nameserver.NameServer.DoesNotExist:
587
+ self.soa_mname = NameServer.objects.get(name=default_soa_mname)
588
+ except NameServer.DoesNotExist:
593
589
  raise ValidationError(
594
590
  f"Default soa_mname instance {default_soa_mname} does not exist"
595
591
  )
@@ -715,12 +711,9 @@ class Zone(ObjectModificationMixin, NetBoxModel):
715
711
  view=self.view,
716
712
  arpa_network__net_contains_or_equals=self.arpa_network,
717
713
  )
718
- address_records = record.Record.objects.filter(
714
+ address_records = Record.objects.filter(
719
715
  Q(ptr_record__isnull=True) | Q(ptr_record__zone__in=zones),
720
- type__in=(
721
- record.RecordTypeChoices.A,
722
- record.RecordTypeChoices.AAAA,
723
- ),
716
+ type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA),
724
717
  disable_ptr=False,
725
718
  )
726
719
 
@@ -749,11 +742,11 @@ class Zone(ObjectModificationMixin, NetBoxModel):
749
742
  view=self.view,
750
743
  arpa_network__net_contains=self.rfc2317_prefix,
751
744
  )
752
- address_records = record.Record.objects.filter(
745
+ address_records = Record.objects.filter(
753
746
  Q(ptr_record__isnull=True)
754
747
  | Q(ptr_record__zone__in=zones)
755
748
  | Q(ptr_record__zone=self),
756
- type=record.RecordTypeChoices.A,
749
+ type=RecordTypeChoices.A,
757
750
  disable_ptr=False,
758
751
  )
759
752
 
@@ -771,7 +764,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
771
764
 
772
765
  elif changed_fields is not None and {"name", "view", "status"} & changed_fields:
773
766
  for address_record in self.record_set.filter(
774
- type__in=(record.RecordTypeChoices.A, record.RecordTypeChoices.AAAA)
767
+ type__in=(RecordTypeChoices.A, RecordTypeChoices.AAAA)
775
768
  ):
776
769
  address_record.save(update_fields=["ptr_record"])
777
770
 
@@ -809,9 +802,7 @@ class Zone(ObjectModificationMixin, NetBoxModel):
809
802
  ptr_records = self.record_set.filter(address_record__isnull=False)
810
803
  update_records = [
811
804
  address_record.pk
812
- for address_record in record.Record.objects.filter(
813
- ptr_record__in=ptr_records
814
- )
805
+ for address_record in Record.objects.filter(ptr_record__in=ptr_records)
815
806
  ]
816
807
 
817
808
  cname_records = {
@@ -842,9 +833,9 @@ class Zone(ObjectModificationMixin, NetBoxModel):
842
833
 
843
834
  super().delete(*args, **kwargs)
844
835
 
845
- address_records = record.Record.objects.filter(
846
- pk__in=update_records
847
- ).prefetch_related("zone")
836
+ address_records = Record.objects.filter(pk__in=update_records).prefetch_related(
837
+ "zone"
838
+ )
848
839
 
849
840
  for address_record in address_records:
850
841
  address_record.save(save_zone_serial=False)
@@ -0,0 +1,149 @@
1
+ from django.db import models
2
+ from django.urls import reverse
3
+
4
+ from netbox.models import NetBoxModel
5
+ from netbox.search import SearchIndex, register_search
6
+
7
+
8
+ __ALL__ = (
9
+ "ZoneTemplate",
10
+ "ZoneTemplateIndex",
11
+ )
12
+
13
+
14
+ class ZoneTemplate(NetBoxModel):
15
+ name = models.CharField(
16
+ verbose_name="Template name",
17
+ unique=True,
18
+ max_length=200,
19
+ )
20
+ description = models.CharField(
21
+ max_length=200,
22
+ blank=True,
23
+ )
24
+ nameservers = models.ManyToManyField(
25
+ to="NameServer",
26
+ related_name="+",
27
+ blank=True,
28
+ )
29
+ record_templates = models.ManyToManyField(
30
+ to="RecordTemplate",
31
+ related_name="zone_templates",
32
+ blank=True,
33
+ )
34
+ tenant = models.ForeignKey(
35
+ to="tenancy.Tenant",
36
+ on_delete=models.SET_NULL,
37
+ related_name="+",
38
+ blank=True,
39
+ null=True,
40
+ )
41
+ registrar = models.ForeignKey(
42
+ to="Registrar",
43
+ on_delete=models.SET_NULL,
44
+ related_name="+",
45
+ help_text="The external registrar the domain is registered with",
46
+ blank=True,
47
+ null=True,
48
+ )
49
+ registrant = models.ForeignKey(
50
+ to="Contact",
51
+ on_delete=models.SET_NULL,
52
+ related_name="+",
53
+ help_text="The owner of the domain",
54
+ blank=True,
55
+ null=True,
56
+ )
57
+ admin_c = models.ForeignKey(
58
+ to="Contact",
59
+ on_delete=models.SET_NULL,
60
+ verbose_name="Admin contact",
61
+ related_name="+",
62
+ help_text="The administrative contact for the domain",
63
+ blank=True,
64
+ null=True,
65
+ )
66
+ tech_c = models.ForeignKey(
67
+ to="Contact",
68
+ on_delete=models.SET_NULL,
69
+ verbose_name="Tech contact",
70
+ related_name="+",
71
+ help_text="The technical contact for the domain",
72
+ blank=True,
73
+ null=True,
74
+ )
75
+ billing_c = models.ForeignKey(
76
+ to="Contact",
77
+ on_delete=models.SET_NULL,
78
+ verbose_name="Billing contact",
79
+ related_name="+",
80
+ help_text="The billing contact for the domain",
81
+ blank=True,
82
+ null=True,
83
+ )
84
+
85
+ clone_fields = [
86
+ "description",
87
+ "nameservers",
88
+ "tenant",
89
+ "registrar",
90
+ "registrant",
91
+ "admin_c",
92
+ "tech_c",
93
+ "billing_c",
94
+ ]
95
+
96
+ template_fields = [
97
+ "tenant",
98
+ "registrar",
99
+ "registrant",
100
+ "admin_c",
101
+ "tech_c",
102
+ "billing_c",
103
+ ]
104
+
105
+ class Meta:
106
+ ordering = ["name"]
107
+
108
+ def __str__(self):
109
+ return str(self.name)
110
+
111
+ def get_absolute_url(self):
112
+ return reverse("plugins:netbox_dns:zonetemplate", kwargs={"pk": self.pk})
113
+
114
+ def apply_to_zone(self, zone):
115
+ if not zone.nameservers.all() and self.nameservers.all():
116
+ zone.nameservers.set(self.nameservers.all())
117
+
118
+ if not zone.tags.all() and self.tags.all():
119
+ zone.tags.set(self.tags.all())
120
+
121
+ fields_changed = True
122
+ for field in self.template_fields:
123
+ if getattr(zone, field) is None and getattr(self, field) is not None:
124
+ fields_changed = True
125
+ setattr(zone, field, getattr(self, field))
126
+
127
+ if fields_changed:
128
+ zone.save()
129
+
130
+ self.create_records(zone)
131
+
132
+ def create_records(self, zone):
133
+ for record_template in self.record_templates.all():
134
+ record_template.create_record(zone=zone)
135
+
136
+
137
+ @register_search
138
+ class ZoneTemplateIndex(SearchIndex):
139
+ model = ZoneTemplate
140
+ fields = (
141
+ ("name", 100),
142
+ ("tenant", 300),
143
+ ("registrar", 300),
144
+ ("registrant", 300),
145
+ ("admin_c", 300),
146
+ ("tech_c", 300),
147
+ ("billing_c", 300),
148
+ ("description", 500),
149
+ )