Ensure trailing dot (fixes #1) and keep track of update status
This commit is contained in:
parent
d962420de7
commit
b447e6816a
|
@ -22,5 +22,10 @@
|
||||||
</component>
|
</component>
|
||||||
<component name="TemplatesService">
|
<component name="TemplatesService">
|
||||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||||
|
<option name="TEMPLATE_FOLDERS">
|
||||||
|
<list>
|
||||||
|
<option value="$MODULE_DIR$/netbox_ddns/templates" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -6,9 +6,11 @@ import dns.rdatatype
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
from django.db.models.functions import Length
|
from django.db.models.functions import Length
|
||||||
from django_rq import job
|
from django_rq import job
|
||||||
from netaddr.ip import IPAddress
|
from dns import rcode
|
||||||
|
from netaddr import ip
|
||||||
|
|
||||||
from netbox_ddns.models import ReverseZone, Zone
|
from ipam.models import IPAddress
|
||||||
|
from netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, ReverseZone, Zone
|
||||||
|
|
||||||
logger = logging.getLogger('netbox_ddns')
|
logger = logging.getLogger('netbox_ddns')
|
||||||
|
|
||||||
|
@ -27,10 +29,10 @@ def get_zone(dns_name: str) -> Optional[Zone]:
|
||||||
def get_soa(dns_name: str) -> str:
|
def get_soa(dns_name: str) -> str:
|
||||||
parts = dns_name.rstrip('.').split('.')
|
parts = dns_name.rstrip('.').split('.')
|
||||||
for i in range(len(parts)):
|
for i in range(len(parts)):
|
||||||
zone_name = '.'.join(parts[i:])
|
zone_name = '.'.join(parts[i:]) + '.'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dns.resolver.query(zone_name + '.', dns.rdatatype.SOA)
|
dns.resolver.query(zone_name, dns.rdatatype.SOA)
|
||||||
return zone_name
|
return zone_name
|
||||||
except dns.resolver.NoAnswer:
|
except dns.resolver.NoAnswer:
|
||||||
# The name exists, but has no SOA. Continue one level further up
|
# The name exists, but has no SOA. Continue one level further up
|
||||||
|
@ -40,10 +42,10 @@ def get_soa(dns_name: str) -> str:
|
||||||
for query, response in e.responses().items():
|
for query, response in e.responses().items():
|
||||||
for rrset in response.authority:
|
for rrset in response.authority:
|
||||||
if rrset.rdtype == dns.rdatatype.SOA:
|
if rrset.rdtype == dns.rdatatype.SOA:
|
||||||
return rrset.name.to_text(omit_final_dot=True)
|
return rrset.name.to_text()
|
||||||
|
|
||||||
|
|
||||||
def get_reverse_zone(address: IPAddress) -> Optional[ReverseZone]:
|
def get_reverse_zone(address: ip.IPAddress) -> Optional[ReverseZone]:
|
||||||
# Find the zone, if any
|
# Find the zone, if any
|
||||||
zones = list(ReverseZone.objects.filter(prefix__net_contains=address))
|
zones = list(ReverseZone.objects.filter(prefix__net_contains=address))
|
||||||
if not zones:
|
if not zones:
|
||||||
|
@ -53,24 +55,29 @@ def get_reverse_zone(address: IPAddress) -> Optional[ReverseZone]:
|
||||||
return zones[-1]
|
return zones[-1]
|
||||||
|
|
||||||
|
|
||||||
def update_status(status: list, operation: str, response) -> None:
|
def status_update(output: list, operation: str, response) -> None:
|
||||||
rcode = response.rcode()
|
code = response.rcode()
|
||||||
|
|
||||||
if rcode == dns.rcode.NOERROR:
|
if code == dns.rcode.NOERROR:
|
||||||
message = f"{operation} successful"
|
message = f"{operation} successful"
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
else:
|
else:
|
||||||
message = f"{operation} failed: {dns.rcode.to_text(rcode)}"
|
message = f"{operation} failed: {dns.rcode.to_text(code)}"
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
|
|
||||||
status.append(message)
|
output.append(message)
|
||||||
|
|
||||||
|
|
||||||
@job
|
@job
|
||||||
def update_dns(old_address: IPAddress = None, new_address: IPAddress = None,
|
def update_dns(old_record: IPAddress = None, new_record: IPAddress = None,
|
||||||
old_dns_name: str = '', new_dns_name: str = '',
|
|
||||||
skip_forward=False, skip_reverse=False):
|
skip_forward=False, skip_reverse=False):
|
||||||
status = []
|
old_address = old_record.address.ip if old_record else None
|
||||||
|
new_address = new_record.address.ip if new_record else None
|
||||||
|
old_dns_name = old_record.dns_name.rstrip('.') + '.' if old_record and old_record.dns_name else ''
|
||||||
|
new_dns_name = new_record.dns_name.rstrip('.') + '.' if new_record and new_record.dns_name else ''
|
||||||
|
|
||||||
|
output = []
|
||||||
|
status, created = DNSStatus.objects.get_or_create(ip_address=new_record or old_record)
|
||||||
|
|
||||||
# Only delete old records when they are provided and not the same as the new records
|
# Only delete old records when they are provided and not the same as the new records
|
||||||
if old_dns_name and old_address and (old_dns_name != new_dns_name or old_address != new_address):
|
if old_dns_name and old_address and (old_dns_name != new_dns_name or old_address != new_address):
|
||||||
|
@ -79,45 +86,51 @@ def update_dns(old_address: IPAddress = None, new_address: IPAddress = None,
|
||||||
zone = get_zone(old_dns_name)
|
zone = get_zone(old_dns_name)
|
||||||
if zone:
|
if zone:
|
||||||
logger.debug(f"Found zone {zone.name} for {old_dns_name}")
|
logger.debug(f"Found zone {zone.name} for {old_dns_name}")
|
||||||
|
status.forward_action = ACTION_DELETE
|
||||||
|
|
||||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||||
soa = get_soa(old_dns_name)
|
soa = get_soa(old_dns_name)
|
||||||
if soa == zone.name:
|
if soa == zone.name:
|
||||||
update = zone.server.create_update(zone.name)
|
update = zone.server.create_update(zone.name)
|
||||||
update.delete(
|
update.delete(
|
||||||
old_dns_name + '.',
|
old_dns_name,
|
||||||
'a' if old_address.version == 4 else 'aaaa',
|
'a' if old_address.version == 4 else 'aaaa',
|
||||||
str(old_address)
|
str(old_address)
|
||||||
)
|
)
|
||||||
response = dns.query.udp(update, zone.server.address)
|
response = dns.query.udp(update, zone.server.address)
|
||||||
update_status(status, f'Deleting {old_dns_name} {old_address}', response)
|
status_update(output, f'Deleting {old_dns_name} {old_address}', response)
|
||||||
|
status.forward_rcode = response.rcode()
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Can't update zone {zone.name} for {old_dns_name}, "
|
logger.warning(f"Can't update zone {zone.name} for {old_dns_name}, "
|
||||||
f"it has delegated authority for {soa}")
|
f"it has delegated authority for {soa}")
|
||||||
|
status.forward_rcode = rcode.NOTAUTH
|
||||||
else:
|
else:
|
||||||
logger.debug(f"No zone found for {old_dns_name}")
|
logger.debug(f"No zone found for {old_dns_name}")
|
||||||
|
|
||||||
# Delete old reverse record
|
# Delete old reverse record
|
||||||
if not skip_reverse:
|
if not skip_reverse:
|
||||||
zone = get_reverse_zone(old_address)
|
zone = get_reverse_zone(old_address)
|
||||||
if zone:
|
if zone and old_dns_name:
|
||||||
record_name = zone.record_name(old_address)
|
record_name = zone.record_name(old_address)
|
||||||
logger.debug(f"Found zone {zone.name} for {record_name}")
|
logger.debug(f"Found zone {zone.name} for {record_name}")
|
||||||
|
status.reverse_action = ACTION_DELETE
|
||||||
|
|
||||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||||
soa = get_soa(record_name)
|
soa = get_soa(record_name)
|
||||||
if soa == zone.name:
|
if soa == zone.name:
|
||||||
update = zone.server.create_update(zone.name)
|
update = zone.server.create_update(zone.name)
|
||||||
update.delete(
|
update.delete(
|
||||||
record_name + '.',
|
record_name,
|
||||||
'ptr',
|
'ptr',
|
||||||
old_dns_name + '.'
|
old_dns_name
|
||||||
)
|
)
|
||||||
response = dns.query.udp(update, zone.server.address)
|
response = dns.query.udp(update, zone.server.address)
|
||||||
update_status(status, f'Deleting {record_name} {old_dns_name}', response)
|
status_update(output, f'Deleting {record_name} {old_dns_name}', response)
|
||||||
|
status.reverse_rcode = response.rcode()
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Can't update zone {zone.name} for {record_name}, "
|
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
||||||
f"it has delegated authority for {soa}")
|
f"it has delegated authority for {soa}")
|
||||||
|
status.reverse_rcode = rcode.NOTAUTH
|
||||||
else:
|
else:
|
||||||
logger.debug(f"No zone found for {old_address}")
|
logger.debug(f"No zone found for {old_address}")
|
||||||
|
|
||||||
|
@ -128,48 +141,57 @@ def update_dns(old_address: IPAddress = None, new_address: IPAddress = None,
|
||||||
zone = get_zone(new_dns_name)
|
zone = get_zone(new_dns_name)
|
||||||
if zone:
|
if zone:
|
||||||
logger.debug(f"Found zone {zone.name} for {new_dns_name}")
|
logger.debug(f"Found zone {zone.name} for {new_dns_name}")
|
||||||
|
status.forward_action = ACTION_CREATE
|
||||||
|
|
||||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||||
soa = get_soa(new_dns_name)
|
soa = get_soa(new_dns_name)
|
||||||
if soa == zone.name:
|
if soa == zone.name:
|
||||||
update = zone.server.create_update(zone.name)
|
update = zone.server.create_update(zone.name)
|
||||||
update.add(
|
update.add(
|
||||||
new_dns_name + '.',
|
new_dns_name,
|
||||||
zone.ttl,
|
zone.ttl,
|
||||||
'a' if new_address.version == 4 else 'aaaa',
|
'a' if new_address.version == 4 else 'aaaa',
|
||||||
str(new_address)
|
str(new_address)
|
||||||
)
|
)
|
||||||
response = dns.query.udp(update, zone.server.address)
|
response = dns.query.udp(update, zone.server.address)
|
||||||
update_status(status, f'Adding {new_dns_name} {new_address}', response)
|
status_update(output, f'Adding {new_dns_name} {new_address}', response)
|
||||||
|
status.forward_rcode = response.rcode()
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Can't update zone {zone.name} for {old_dns_name}, "
|
logger.warning(f"Can't update zone {zone.name} for {old_dns_name}, "
|
||||||
f"it has delegated authority for {soa}")
|
f"it has delegated authority for {soa}")
|
||||||
|
status.forward_rcode = rcode.NOTAUTH
|
||||||
else:
|
else:
|
||||||
logger.debug(f"No zone found for {new_dns_name}")
|
logger.debug(f"No zone found for {new_dns_name}")
|
||||||
|
|
||||||
# Add new reverse record
|
# Add new reverse record
|
||||||
if not skip_reverse:
|
if not skip_reverse:
|
||||||
zone = get_reverse_zone(new_address)
|
zone = get_reverse_zone(new_address)
|
||||||
if zone:
|
if zone and new_dns_name:
|
||||||
record_name = zone.record_name(new_address)
|
record_name = zone.record_name(new_address)
|
||||||
logger.debug(f"Found zone {zone.name} for {record_name}")
|
logger.debug(f"Found zone {zone.name} for {record_name}")
|
||||||
|
status.reverse_action = ACTION_CREATE
|
||||||
|
|
||||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||||
soa = get_soa(record_name)
|
soa = get_soa(record_name)
|
||||||
if soa == zone.name:
|
if soa == zone.name:
|
||||||
update = zone.server.create_update(zone.name)
|
update = zone.server.create_update(zone.name)
|
||||||
update.add(
|
update.add(
|
||||||
record_name + '.',
|
record_name,
|
||||||
zone.ttl,
|
zone.ttl,
|
||||||
'ptr',
|
'ptr',
|
||||||
new_dns_name + '.'
|
new_dns_name
|
||||||
)
|
)
|
||||||
response = dns.query.udp(update, zone.server.address)
|
response = dns.query.udp(update, zone.server.address)
|
||||||
update_status(status, f'Adding {record_name} {old_dns_name}', response)
|
status_update(output, f'Adding {record_name} {new_dns_name}', response)
|
||||||
|
status.reverse_rcode = response.rcode()
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Can't update zone {zone.name} for {record_name}, "
|
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
||||||
f"it has delegated authority for {soa}")
|
f"it has delegated authority for {soa}")
|
||||||
|
status.reverse_rcode = rcode.NOTAUTH
|
||||||
else:
|
else:
|
||||||
logger.debug(f"No zone found for {new_address}")
|
logger.debug(f"No zone found for {new_address}")
|
||||||
|
|
||||||
return ', '.join(status)
|
# Store the status
|
||||||
|
status.save()
|
||||||
|
|
||||||
|
return ', '.join(output)
|
||||||
|
|
31
netbox_ddns/migrations/0003_dnsstatus.py
Normal file
31
netbox_ddns/migrations/0003_dnsstatus.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-15 10:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0036_standardize_description'),
|
||||||
|
('netbox_ddns', '0002_add_ttl'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DNSStatus',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('last_update', models.DateTimeField(auto_now=True)),
|
||||||
|
('forward_action', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||||
|
('forward_rcode', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('reverse_action', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||||
|
('reverse_rcode', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('ip_address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='ipam.IPAddress')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'DNS status',
|
||||||
|
'verbose_name_plural': 'DNS status',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
43
netbox_ddns/migrations/0004_ensure_trailing_dot.py
Normal file
43
netbox_ddns/migrations/0004_ensure_trailing_dot.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-04-15 10:57
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
def add_trailing_dots(apps, schema_editor):
|
||||||
|
update_trailing_dots(apps, trailing_dot='.')
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
def remove_trailing_dots(apps, schema_editor):
|
||||||
|
update_trailing_dots(apps, trailing_dot='')
|
||||||
|
|
||||||
|
|
||||||
|
def update_trailing_dots(apps, trailing_dot):
|
||||||
|
server_model = apps.get_model('netbox_ddns', 'Server')
|
||||||
|
zone_model = apps.get_model('netbox_ddns', 'Zone')
|
||||||
|
reverse_zone_model = apps.get_model('netbox_ddns', 'ReverseZone')
|
||||||
|
|
||||||
|
for server in server_model.objects.all():
|
||||||
|
server.tsig_key_name = server.tsig_key_name.rstrip('.') + trailing_dot
|
||||||
|
server.save()
|
||||||
|
|
||||||
|
for zone in zone_model.objects.all():
|
||||||
|
zone.name = zone.name.rstrip('.') + trailing_dot
|
||||||
|
zone.save()
|
||||||
|
|
||||||
|
for reverse_zone in reverse_zone_model.objects.all():
|
||||||
|
reverse_zone.name = reverse_zone.name.rstrip('.') + trailing_dot
|
||||||
|
reverse_zone.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('netbox_ddns', '0003_dnsstatus'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=add_trailing_dots,
|
||||||
|
reverse_code=remove_trailing_dots),
|
||||||
|
]
|
|
@ -7,10 +7,12 @@ import dns.update
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from dns import rcode
|
||||||
from dns.tsig import HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512
|
from dns.tsig import HMAC_MD5, HMAC_SHA1, HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512
|
||||||
from netaddr.ip import IPAddress
|
from netaddr import ip
|
||||||
|
|
||||||
from ipam.fields import IPNetworkField
|
from ipam.fields import IPNetworkField
|
||||||
|
from ipam.models import IPAddress
|
||||||
from .validators import HostnameAddressValidator, HostnameValidator, validate_base64
|
from .validators import HostnameAddressValidator, HostnameValidator, validate_base64
|
||||||
|
|
||||||
logger = logging.getLogger('netbox_ddns')
|
logger = logging.getLogger('netbox_ddns')
|
||||||
|
@ -24,6 +26,33 @@ TSIG_ALGORITHM_CHOICES = (
|
||||||
(str(HMAC_SHA512), 'HMAC SHA512'),
|
(str(HMAC_SHA512), 'HMAC SHA512'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ACTION_CREATE = 1
|
||||||
|
ACTION_DELETE = 2
|
||||||
|
|
||||||
|
ACTION_CHOICES = (
|
||||||
|
(ACTION_CREATE, 'Create'),
|
||||||
|
(ACTION_DELETE, 'Delete'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rcode_display(code):
|
||||||
|
if code is None:
|
||||||
|
return None
|
||||||
|
elif code == rcode.NOERROR:
|
||||||
|
return _('Success')
|
||||||
|
elif code == rcode.SERVFAIL:
|
||||||
|
return _('Server failure')
|
||||||
|
elif code == rcode.NXDOMAIN:
|
||||||
|
return _('Name does not exist')
|
||||||
|
elif code == rcode.NOTIMP:
|
||||||
|
return _('Not implemented')
|
||||||
|
elif code == rcode.REFUSED:
|
||||||
|
return _('Refused')
|
||||||
|
elif code == rcode.NOTAUTH:
|
||||||
|
return _('Server not authoritative')
|
||||||
|
else:
|
||||||
|
return _('Unknown response: {}').format(code)
|
||||||
|
|
||||||
|
|
||||||
class Server(models.Model):
|
class Server(models.Model):
|
||||||
server = models.CharField(
|
server = models.CharField(
|
||||||
|
@ -60,9 +89,11 @@ class Server(models.Model):
|
||||||
return f'{self.server} ({self.tsig_key_name})'
|
return f'{self.server} ({self.tsig_key_name})'
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# Remove trailing dots from domain-style fields
|
# Remove trailing dots from the server name/address
|
||||||
self.server = self.server.rstrip('.').lower()
|
self.server = self.server.lower().rstrip('.')
|
||||||
self.tsig_key_name = self.tsig_key_name.rstrip('.').lower()
|
|
||||||
|
# Ensure trailing dots from domain-style fields
|
||||||
|
self.tsig_key_name = self.tsig_key_name.lower().rstrip('.') + '.'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self) -> Optional[str]:
|
def address(self) -> Optional[str]:
|
||||||
|
@ -107,8 +138,8 @@ class Zone(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# Remove trailing dots from domain-style fields
|
# Ensure trailing dots from domain-style fields
|
||||||
self.name = self.name.rstrip('.').lower()
|
self.name = self.name.lower().rstrip('.') + '.'
|
||||||
|
|
||||||
def get_updater(self):
|
def get_updater(self):
|
||||||
return self.server.create_update(self.name)
|
return self.server.create_update(self.name)
|
||||||
|
@ -142,7 +173,7 @@ class ReverseZone(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'for {self.prefix}'
|
return f'for {self.prefix}'
|
||||||
|
|
||||||
def record_name(self, address: IPAddress):
|
def record_name(self, address: ip.IPAddress):
|
||||||
record_name = self.name
|
record_name = self.name
|
||||||
if self.prefix.version == 4:
|
if self.prefix.version == 4:
|
||||||
for pos, octet in enumerate(address.words):
|
for pos, octet in enumerate(address.words):
|
||||||
|
@ -161,9 +192,6 @@ class ReverseZone(models.Model):
|
||||||
return record_name
|
return record_name
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# Remove trailing dots from domain-style fields
|
|
||||||
self.name = self.name.rstrip('.')
|
|
||||||
|
|
||||||
if self.prefix.version == 4:
|
if self.prefix.version == 4:
|
||||||
if self.prefix.prefixlen not in [0, 8, 16, 24] and not self.name:
|
if self.prefix.prefixlen not in [0, 8, 16, 24] and not self.name:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
|
@ -192,5 +220,51 @@ class ReverseZone(models.Model):
|
||||||
|
|
||||||
self.name = f'{nibble}.{self.name}'
|
self.name = f'{nibble}.{self.name}'
|
||||||
|
|
||||||
# Store zone names in lowercase
|
# Ensure trailing dots from domain-style fields
|
||||||
self.name = self.name.lower()
|
self.name = self.name.lower().rstrip('.') + '.'
|
||||||
|
|
||||||
|
|
||||||
|
class DNSStatus(models.Model):
|
||||||
|
ip_address = models.OneToOneField(
|
||||||
|
to=IPAddress,
|
||||||
|
verbose_name=_('IP address'),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
last_update = models.DateTimeField(
|
||||||
|
verbose_name=_('last update'),
|
||||||
|
auto_now=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
forward_action = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('forward record action'),
|
||||||
|
choices=ACTION_CHOICES,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
forward_rcode = models.PositiveIntegerField(
|
||||||
|
verbose_name=_('forward record response'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
reverse_action = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('reverse record action'),
|
||||||
|
choices=ACTION_CHOICES,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
reverse_rcode = models.PositiveIntegerField(
|
||||||
|
verbose_name=_('reverse record response'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('DNS status')
|
||||||
|
verbose_name_plural = _('DNS status')
|
||||||
|
|
||||||
|
def get_forward_rcode_display(self) -> Optional[str]:
|
||||||
|
return get_rcode_display(self.forward_rcode)
|
||||||
|
|
||||||
|
def get_reverse_rcode_display(self) -> Optional[str]:
|
||||||
|
return get_rcode_display(self.reverse_rcode)
|
||||||
|
|
|
@ -22,16 +22,13 @@ def trigger_ddns_update(instance: IPAddress, **_kwargs):
|
||||||
if instance.address != old_address or instance.dns_name != old_dns_name:
|
if instance.address != old_address or instance.dns_name != old_dns_name:
|
||||||
# IP address or DNS name has changed
|
# IP address or DNS name has changed
|
||||||
update_dns.delay(
|
update_dns.delay(
|
||||||
old_address=old_address.ip if old_address else None,
|
old_record=instance.before_save,
|
||||||
new_address=instance.address.ip,
|
new_record=instance,
|
||||||
old_dns_name=old_dns_name.rstrip('.'),
|
|
||||||
new_dns_name=instance.dns_name.rstrip('.'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=IPAddress)
|
@receiver(post_delete, sender=IPAddress)
|
||||||
def trigger_ddns_delete(instance: IPAddress, **_kwargs):
|
def trigger_ddns_delete(instance: IPAddress, **_kwargs):
|
||||||
update_dns.delay(
|
update_dns.delay(
|
||||||
old_address=instance.address.ip,
|
old_record=instance,
|
||||||
old_dns_name=instance.dns_name.rstrip('.'),
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1 +1,17 @@
|
||||||
template_extensions = []
|
from django.contrib.auth.context_processors import PermWrapper
|
||||||
|
|
||||||
|
from extras.plugins import PluginTemplateExtension
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyAbstractClass
|
||||||
|
class DNSInfo(PluginTemplateExtension):
|
||||||
|
model = 'ipam.ipaddress'
|
||||||
|
|
||||||
|
def left_page(self):
|
||||||
|
"""
|
||||||
|
An info-box with edit button for the vCenter settings
|
||||||
|
"""
|
||||||
|
return self.render('netbox_ddns/ipaddress/dns_info.html')
|
||||||
|
|
||||||
|
|
||||||
|
template_extensions = [DNSInfo]
|
||||||
|
|
30
netbox_ddns/templates/netbox_ddns/ipaddress/dns_info.html
Normal file
30
netbox_ddns/templates/netbox_ddns/ipaddress/dns_info.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% if object.dnsstatus %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Dynamic DNS Status</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body attr-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Last update</td>
|
||||||
|
<td>
|
||||||
|
{{ object.dnsstatus.last_update }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Forward DNS</td>
|
||||||
|
<td>
|
||||||
|
{{ object.dnsstatus.get_forward_action_display }}:
|
||||||
|
{{ object.dnsstatus.get_forward_rcode_display }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Reverse DNS</td>
|
||||||
|
<td>
|
||||||
|
{{ object.dnsstatus.get_reverse_action_display }}:
|
||||||
|
{{ object.dnsstatus.get_reverse_rcode_display }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
Loading…
Reference in a new issue