Major restructure and add support for extra DNS names
This commit is contained in:
parent
89e31f1f9f
commit
0ff9d62ba6
|
@ -23,10 +23,5 @@
|
|||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/netbox_ddns/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
|
|
|
@ -10,8 +10,10 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from ipam.models import IPAddress
|
||||
from netbox.admin import admin_site
|
||||
from .background_tasks import update_dns
|
||||
from netbox_ddns.models import DNSStatus, ExtraDNSName
|
||||
from .background_tasks import dns_create
|
||||
from .models import ReverseZone, Server, Zone
|
||||
from .utils import normalize_fqdn
|
||||
|
||||
logger = logging.getLogger('netbox_ddns')
|
||||
|
||||
|
@ -65,20 +67,46 @@ class ZoneAdmin(admin.ModelAdmin):
|
|||
more_specifics = Zone.objects.filter(name__endswith=zone.name).exclude(pk=zone.pk)
|
||||
|
||||
# Find all IPAddress objects in this zone but not in the more-specifics
|
||||
addresses = IPAddress.objects.filter(Q(dns_name__endswith=zone.name) |
|
||||
Q(dns_name__endswith=zone.name.rstrip('.')))
|
||||
ip_addresses = IPAddress.objects.filter(Q(dns_name__endswith=zone.name) |
|
||||
Q(dns_name__endswith=zone.name.rstrip('.')))
|
||||
for more_specific in more_specifics:
|
||||
addresses = addresses.exclude(Q(dns_name__endswith=more_specific.name) |
|
||||
Q(dns_name__endswith=more_specific.name.rstrip('.')))
|
||||
ip_addresses = ip_addresses.exclude(Q(dns_name__endswith=more_specific.name) |
|
||||
Q(dns_name__endswith=more_specific.name.rstrip('.')))
|
||||
|
||||
for address in addresses:
|
||||
if address.dns_name:
|
||||
update_dns.delay(
|
||||
new_record=address,
|
||||
skip_reverse=True
|
||||
for ip_address in ip_addresses:
|
||||
new_address = ip_address.address.ip
|
||||
new_dns_name = normalize_fqdn(ip_address.dns_name)
|
||||
|
||||
if new_dns_name:
|
||||
status, created = DNSStatus.objects.get_or_create(ip_address=ip_address)
|
||||
|
||||
dns_create.delay(
|
||||
dns_name=new_dns_name,
|
||||
address=new_address,
|
||||
status=status,
|
||||
reverse=False,
|
||||
)
|
||||
|
||||
counter += 1
|
||||
|
||||
# Find all ExtraDNSName objects in this zone but not in the more-specifics
|
||||
extra_names = ExtraDNSName.objects.filter(name__endswith=zone.name)
|
||||
for more_specific in more_specifics:
|
||||
extra_names = extra_names.exclude(name__endswith=more_specific.name)
|
||||
|
||||
for extra in extra_names:
|
||||
new_address = extra.ip_address.address.ip
|
||||
new_dns_name = extra.name
|
||||
|
||||
dns_create.delay(
|
||||
dns_name=new_dns_name,
|
||||
address=new_address,
|
||||
status=extra,
|
||||
reverse=False,
|
||||
)
|
||||
|
||||
counter += 1
|
||||
|
||||
messages.info(request, _("Updating {count} forward records in {name}").format(count=counter,
|
||||
name=zone.name))
|
||||
|
||||
|
@ -99,17 +127,30 @@ class ReverseZoneAdmin(admin.ModelAdmin):
|
|||
more_specifics = ReverseZone.objects.filter(prefix__net_contained=zone.prefix).exclude(pk=zone.pk)
|
||||
|
||||
# Find all IPAddress objects in this zone but not in the more-specifics
|
||||
addresses = IPAddress.objects.filter(address__net_contained=zone.prefix)
|
||||
ip_addresses = IPAddress.objects.filter(address__net_contained=zone.prefix)
|
||||
for more_specific in more_specifics:
|
||||
addresses = addresses.exclude(address__net_contained=more_specific.prefix)
|
||||
ip_addresses = ip_addresses.exclude(address__net_contained=more_specific.prefix)
|
||||
|
||||
for address in addresses:
|
||||
if address.dns_name:
|
||||
update_dns.delay(
|
||||
new_record=address,
|
||||
skip_forward=True
|
||||
for ip_address in ip_addresses:
|
||||
new_address = ip_address.address.ip
|
||||
new_dns_name = normalize_fqdn(ip_address.dns_name)
|
||||
|
||||
if new_dns_name:
|
||||
status, created = DNSStatus.objects.get_or_create(ip_address=ip_address)
|
||||
|
||||
dns_create.delay(
|
||||
dns_name=new_dns_name,
|
||||
address=new_address,
|
||||
status=status,
|
||||
forward=False,
|
||||
)
|
||||
|
||||
counter += 1
|
||||
|
||||
messages.info(request, _("Updating {count} reverse records in {name}").format(count=counter,
|
||||
name=zone.name))
|
||||
|
||||
|
||||
@admin.register(ExtraDNSName, site=admin_site)
|
||||
class ExtraDNSNameAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import dns.query
|
||||
import dns.rdatatype
|
||||
import dns.resolver
|
||||
from django.db import IntegrityError
|
||||
from django.db.models.functions import Length
|
||||
from django_rq import job
|
||||
from dns import rcode
|
||||
from netaddr import ip
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, ReverseZone, Zone
|
||||
from netbox_ddns.utils import normalize_fqdn
|
||||
|
||||
logger = logging.getLogger('netbox_ddns')
|
||||
|
||||
|
@ -29,7 +30,7 @@ def get_zone(dns_name: str) -> Optional[Zone]:
|
|||
def get_soa(dns_name: str) -> str:
|
||||
parts = dns_name.rstrip('.').split('.')
|
||||
for i in range(len(parts)):
|
||||
zone_name = '.'.join(parts[i:]) + '.'
|
||||
zone_name = normalize_fqdn('.'.join(parts[i:]))
|
||||
|
||||
try:
|
||||
dns.resolver.query(zone_name, dns.rdatatype.SOA)
|
||||
|
@ -55,7 +56,7 @@ def get_reverse_zone(address: ip.IPAddress) -> Optional[ReverseZone]:
|
|||
return zones[-1]
|
||||
|
||||
|
||||
def status_update(output: list, operation: str, response) -> None:
|
||||
def status_update(output: List[str], operation: str, response) -> None:
|
||||
code = response.rcode()
|
||||
|
||||
if code == dns.rcode.NOERROR:
|
||||
|
@ -68,130 +69,161 @@ def status_update(output: list, operation: str, response) -> None:
|
|||
output.append(message)
|
||||
|
||||
|
||||
def create_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_zone(dns_name)
|
||||
if zone:
|
||||
logger.debug(f"Found zone {zone.name} for {dns_name}")
|
||||
if status:
|
||||
status.forward_action = ACTION_CREATE
|
||||
|
||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||
soa = get_soa(dns_name)
|
||||
if soa == zone.name:
|
||||
record_type = 'A' if address.version == 4 else 'AAAA'
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.add(
|
||||
dns_name,
|
||||
zone.ttl,
|
||||
record_type,
|
||||
str(address)
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Adding {dns_name} {record_type} {address}', response)
|
||||
if status:
|
||||
status.forward_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} for {dns_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
if status:
|
||||
status.forward_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {dns_name}")
|
||||
|
||||
|
||||
def delete_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_zone(dns_name)
|
||||
if zone:
|
||||
logger.debug(f"Found zone {zone.name} for {dns_name}")
|
||||
if status:
|
||||
status.forward_action = ACTION_DELETE
|
||||
|
||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||
soa = get_soa(dns_name)
|
||||
if soa == zone.name:
|
||||
record_type = 'A' if address.version == 4 else 'AAAA'
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.delete(
|
||||
dns_name,
|
||||
record_type,
|
||||
str(address)
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Deleting {dns_name} {record_type} {address}', response)
|
||||
if status:
|
||||
status.forward_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} {dns_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
if status:
|
||||
status.forward_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {dns_name}")
|
||||
|
||||
|
||||
def create_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_reverse_zone(address)
|
||||
if zone and dns_name:
|
||||
record_name = zone.record_name(address)
|
||||
logger.debug(f"Found zone {zone.name} for {record_name}")
|
||||
if status:
|
||||
status.reverse_action = ACTION_CREATE
|
||||
|
||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||
soa = get_soa(record_name)
|
||||
if soa == zone.name:
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.add(
|
||||
record_name,
|
||||
zone.ttl,
|
||||
'ptr',
|
||||
dns_name
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Adding {record_name} PTR {dns_name}', response)
|
||||
if status:
|
||||
status.reverse_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
if status:
|
||||
status.reverse_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {address}")
|
||||
|
||||
|
||||
def delete_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_reverse_zone(address)
|
||||
if zone and dns_name:
|
||||
record_name = zone.record_name(address)
|
||||
logger.debug(f"Found zone {zone.name} for {record_name}")
|
||||
if status:
|
||||
status.reverse_action = ACTION_DELETE
|
||||
|
||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||
soa = get_soa(record_name)
|
||||
if soa == zone.name:
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.delete(
|
||||
record_name,
|
||||
'ptr',
|
||||
dns_name
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Deleting {record_name} PTR {dns_name}', response)
|
||||
if status:
|
||||
status.reverse_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
if status:
|
||||
status.reverse_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {address}")
|
||||
|
||||
|
||||
@job
|
||||
def update_dns(old_record: IPAddress = None, new_record: IPAddress = None,
|
||||
skip_forward=False, skip_reverse=False):
|
||||
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 ''
|
||||
|
||||
def dns_create(dns_name: str, address: ip.IPAddress, forward=True, reverse=True, status: DNSStatus = None):
|
||||
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
|
||||
if old_dns_name and old_address and (old_dns_name != new_dns_name or old_address != new_address):
|
||||
# Delete old forward record
|
||||
if not skip_forward:
|
||||
zone = get_zone(old_dns_name)
|
||||
if zone:
|
||||
logger.debug(f"Found zone {zone.name} for {old_dns_name}")
|
||||
status.forward_action = ACTION_DELETE
|
||||
if forward:
|
||||
create_forward(dns_name, address, status, output)
|
||||
if reverse:
|
||||
create_reverse(dns_name, address, status, output)
|
||||
|
||||
# Check the SOA, we don't want to write to a parent zone if it has delegated authority
|
||||
soa = get_soa(old_dns_name)
|
||||
if soa == zone.name:
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.delete(
|
||||
old_dns_name,
|
||||
'a' if old_address.version == 4 else 'aaaa',
|
||||
str(old_address)
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Deleting {old_dns_name} {old_address}', response)
|
||||
status.forward_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} for {old_dns_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
status.forward_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {old_dns_name}")
|
||||
|
||||
# Delete old reverse record
|
||||
if not skip_reverse:
|
||||
zone = get_reverse_zone(old_address)
|
||||
if zone and old_dns_name:
|
||||
record_name = zone.record_name(old_address)
|
||||
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
|
||||
soa = get_soa(record_name)
|
||||
if soa == zone.name:
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.delete(
|
||||
record_name,
|
||||
'ptr',
|
||||
old_dns_name
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Deleting {record_name} {old_dns_name}', response)
|
||||
status.reverse_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
status.reverse_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {old_address}")
|
||||
|
||||
# Always try to add, just in case a previous update failed
|
||||
if new_dns_name and new_address:
|
||||
# Add new forward record
|
||||
if not skip_forward:
|
||||
zone = get_zone(new_dns_name)
|
||||
if zone:
|
||||
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
|
||||
soa = get_soa(new_dns_name)
|
||||
if soa == zone.name:
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.add(
|
||||
new_dns_name,
|
||||
zone.ttl,
|
||||
'a' if new_address.version == 4 else 'aaaa',
|
||||
str(new_address)
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Adding {new_dns_name} {new_address}', response)
|
||||
status.forward_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} for {old_dns_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
status.forward_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {new_dns_name}")
|
||||
|
||||
# Add new reverse record
|
||||
if not skip_reverse:
|
||||
zone = get_reverse_zone(new_address)
|
||||
if zone and new_dns_name:
|
||||
record_name = zone.record_name(new_address)
|
||||
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
|
||||
soa = get_soa(record_name)
|
||||
if soa == zone.name:
|
||||
update = zone.server.create_update(zone.name)
|
||||
update.add(
|
||||
record_name,
|
||||
zone.ttl,
|
||||
'ptr',
|
||||
new_dns_name
|
||||
)
|
||||
response = dns.query.udp(update, zone.server.address)
|
||||
status_update(output, f'Adding {record_name} {new_dns_name}', response)
|
||||
status.reverse_rcode = response.rcode()
|
||||
else:
|
||||
logger.warning(f"Can't update zone {zone.name} for {record_name}, "
|
||||
f"it has delegated authority for {soa}")
|
||||
status.reverse_rcode = rcode.NOTAUTH
|
||||
else:
|
||||
logger.debug(f"No zone found for {new_address}")
|
||||
|
||||
# Store the status
|
||||
status.save()
|
||||
if status:
|
||||
try:
|
||||
status.save()
|
||||
except IntegrityError:
|
||||
# Race condition when creating?
|
||||
status.save(force_update=True)
|
||||
|
||||
return ', '.join(output)
|
||||
|
||||
|
||||
@job
|
||||
def dns_delete(dns_name: str, address: ip.IPAddress, forward=True, reverse=True, status: DNSStatus = None):
|
||||
output = []
|
||||
|
||||
if forward:
|
||||
delete_forward(dns_name, address, status, output)
|
||||
if reverse:
|
||||
delete_reverse(dns_name, address, status, output)
|
||||
|
||||
if status:
|
||||
try:
|
||||
status.save()
|
||||
except IntegrityError:
|
||||
# Race condition when creating?
|
||||
status.save(force_update=True)
|
||||
|
||||
return ', '.join(output)
|
||||
|
|
10
netbox_ddns/forms.py
Normal file
10
netbox_ddns/forms.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django import forms
|
||||
|
||||
from netbox_ddns.models import ExtraDNSName
|
||||
from utilities.forms import BootstrapMixin
|
||||
|
||||
|
||||
class ExtraDNSNameEditForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ExtraDNSName
|
||||
fields = ['name']
|
34
netbox_ddns/migrations/0005_extradnsname.py
Normal file
34
netbox_ddns/migrations/0005_extradnsname.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 3.0.5 on 2020-04-18 22:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import netbox_ddns.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0036_standardize_description'),
|
||||
('netbox_ddns', '0004_ensure_trailing_dot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExtraDNSName',
|
||||
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)),
|
||||
('name', models.CharField(max_length=255, validators=[netbox_ddns.validators.HostnameValidator()])),
|
||||
('ip_address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ipam.IPAddress')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'extra DNS name',
|
||||
'verbose_name_plural': 'extra DNS names',
|
||||
'unique_together': {('ip_address', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
21
netbox_ddns/migrations/0006_extradns_cname.py
Normal file
21
netbox_ddns/migrations/0006_extradns_cname.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.0.5 on 2020-04-19 16:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_ddns', '0005_extradnsname'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='extradnsname',
|
||||
name='reverse_action',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='extradnsname',
|
||||
name='reverse_rcode',
|
||||
),
|
||||
]
|
17
netbox_ddns/migrations/0007_zone_meta.py
Normal file
17
netbox_ddns/migrations/0007_zone_meta.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.0.5 on 2020-04-19 19:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_ddns', '0006_extradns_cname'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='zone',
|
||||
options={'ordering': ('name',), 'verbose_name': 'forward zone', 'verbose_name_plural': 'forward zones'},
|
||||
),
|
||||
]
|
|
@ -6,6 +6,7 @@ import dns.tsigkeyring
|
|||
import dns.update
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.html import format_html
|
||||
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
|
||||
|
@ -13,6 +14,7 @@ from netaddr import ip
|
|||
|
||||
from ipam.fields import IPNetworkField
|
||||
from ipam.models import IPAddress
|
||||
from .utils import normalize_fqdn
|
||||
from .validators import HostnameAddressValidator, HostnameValidator, validate_base64
|
||||
|
||||
logger = logging.getLogger('netbox_ddns')
|
||||
|
@ -93,7 +95,7 @@ class Server(models.Model):
|
|||
self.server = self.server.lower().rstrip('.')
|
||||
|
||||
# Ensure trailing dots from domain-style fields
|
||||
self.tsig_key_name = self.tsig_key_name.lower().rstrip('.') + '.'
|
||||
self.tsig_key_name = normalize_fqdn(self.tsig_key_name.lower().rstrip('.'))
|
||||
|
||||
@property
|
||||
def address(self) -> Optional[str]:
|
||||
|
@ -104,7 +106,7 @@ class Server(models.Model):
|
|||
|
||||
def create_update(self, zone: str) -> dns.update.Update:
|
||||
return dns.update.Update(
|
||||
zone=zone.rstrip('.') + '.',
|
||||
zone=normalize_fqdn(zone),
|
||||
keyring=dns.tsigkeyring.from_text({
|
||||
self.tsig_key_name: self.tsig_key
|
||||
}),
|
||||
|
@ -131,15 +133,15 @@ class Zone(models.Model):
|
|||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('zone')
|
||||
verbose_name_plural = _('zones')
|
||||
verbose_name = _('forward zone')
|
||||
verbose_name_plural = _('forward zones')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
# Ensure trailing dots from domain-style fields
|
||||
self.name = self.name.lower().rstrip('.') + '.'
|
||||
self.name = normalize_fqdn(self.name)
|
||||
|
||||
def get_updater(self):
|
||||
return self.server.create_update(self.name)
|
||||
|
@ -221,7 +223,7 @@ class ReverseZone(models.Model):
|
|||
self.name = f'{nibble}.{self.name}'
|
||||
|
||||
# Ensure trailing dots from domain-style fields
|
||||
self.name = self.name.lower().rstrip('.') + '.'
|
||||
self.name = normalize_fqdn(self.name)
|
||||
|
||||
|
||||
class DNSStatus(models.Model):
|
||||
|
@ -230,6 +232,7 @@ class DNSStatus(models.Model):
|
|||
verbose_name=_('IP address'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
last_update = models.DateTimeField(
|
||||
verbose_name=_('last update'),
|
||||
auto_now=True,
|
||||
|
@ -266,5 +269,69 @@ class DNSStatus(models.Model):
|
|||
def get_forward_rcode_display(self) -> Optional[str]:
|
||||
return get_rcode_display(self.forward_rcode)
|
||||
|
||||
def get_forward_rcode_html_display(self) -> Optional[str]:
|
||||
output = get_rcode_display(self.forward_rcode)
|
||||
colour = 'green' if self.forward_rcode == rcode.NOERROR else 'red'
|
||||
return format_html('<span style="color:{colour}">{output}</span', colour=colour, output=output)
|
||||
|
||||
def get_reverse_rcode_display(self) -> Optional[str]:
|
||||
return get_rcode_display(self.reverse_rcode)
|
||||
|
||||
def get_reverse_rcode_html_display(self) -> Optional[str]:
|
||||
output = get_rcode_display(self.reverse_rcode)
|
||||
colour = 'green' if self.reverse_rcode == rcode.NOERROR else 'red'
|
||||
return format_html('<span style="color:{colour}">{output}</span', colour=colour, output=output)
|
||||
|
||||
|
||||
class ExtraDNSName(models.Model):
|
||||
ip_address = models.ForeignKey(
|
||||
to=IPAddress,
|
||||
verbose_name=_('IP address'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_('DNS name'),
|
||||
max_length=255,
|
||||
validators=[HostnameValidator()],
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
before_save = None
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
('ip_address', 'name'),
|
||||
)
|
||||
verbose_name = _('extra DNS name')
|
||||
verbose_name_plural = _('extra DNS names')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
# Ensure trailing dots from domain-style fields
|
||||
self.name = normalize_fqdn(self.name)
|
||||
|
||||
def get_forward_rcode_display(self) -> Optional[str]:
|
||||
return get_rcode_display(self.forward_rcode)
|
||||
|
||||
def get_forward_rcode_html_display(self) -> Optional[str]:
|
||||
output = get_rcode_display(self.forward_rcode)
|
||||
colour = 'green' if self.forward_rcode == rcode.NOERROR else 'red'
|
||||
return format_html('<span style="color:{colour}">{output}</span', colour=colour, output=output)
|
||||
|
|
|
@ -4,7 +4,9 @@ from django.db.models.signals import post_delete, post_save, pre_save
|
|||
from django.dispatch import receiver
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from netbox_ddns.background_tasks import update_dns
|
||||
from netbox_ddns.background_tasks import dns_create, dns_delete
|
||||
from netbox_ddns.models import DNSStatus, ExtraDNSName
|
||||
from netbox_ddns.utils import normalize_fqdn
|
||||
|
||||
logger = logging.getLogger('netbox_ddns')
|
||||
|
||||
|
@ -16,19 +18,114 @@ def store_original_ipaddress(instance: IPAddress, **_kwargs):
|
|||
|
||||
@receiver(post_save, sender=IPAddress)
|
||||
def trigger_ddns_update(instance: IPAddress, **_kwargs):
|
||||
old_address = instance.before_save.address if instance.before_save else None
|
||||
old_dns_name = instance.before_save.dns_name if instance.before_save else ''
|
||||
old_address = instance.before_save.address.ip if instance.before_save else None
|
||||
old_dns_name = normalize_fqdn(instance.before_save.dns_name) if instance.before_save else ''
|
||||
|
||||
if instance.address != old_address or instance.dns_name != old_dns_name:
|
||||
# IP address or DNS name has changed
|
||||
update_dns.delay(
|
||||
old_record=instance.before_save,
|
||||
new_record=instance,
|
||||
)
|
||||
new_address = instance.address.ip
|
||||
new_dns_name = normalize_fqdn(instance.dns_name)
|
||||
|
||||
extra_dns_names = {normalize_fqdn(extra.name): extra for extra in instance.extradnsname_set.all()}
|
||||
|
||||
if new_address != old_address or new_dns_name != old_dns_name:
|
||||
status, created = DNSStatus.objects.get_or_create(ip_address=instance)
|
||||
|
||||
if old_address and old_dns_name and old_dns_name not in extra_dns_names:
|
||||
delete = dns_delete.delay(
|
||||
dns_name=old_dns_name,
|
||||
address=old_address,
|
||||
status=status,
|
||||
)
|
||||
else:
|
||||
delete = None
|
||||
|
||||
if new_address and new_dns_name:
|
||||
dns_create.delay(
|
||||
dns_name=new_dns_name,
|
||||
address=new_address,
|
||||
status=status,
|
||||
depends_on=delete,
|
||||
)
|
||||
|
||||
if old_address != new_address:
|
||||
# This affects extra names
|
||||
for dns_name, status in extra_dns_names.items():
|
||||
# Don't touch the main dns_name
|
||||
if dns_name == old_dns_name or dns_name == new_dns_name:
|
||||
continue
|
||||
|
||||
if old_dns_name:
|
||||
delete = dns_delete.delay(
|
||||
dns_name=dns_name,
|
||||
address=old_address,
|
||||
reverse=False,
|
||||
status=status,
|
||||
)
|
||||
else:
|
||||
delete = None
|
||||
|
||||
if new_dns_name:
|
||||
dns_create.delay(
|
||||
dns_name=dns_name,
|
||||
address=new_address,
|
||||
reverse=False,
|
||||
status=status,
|
||||
depends_on=delete,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=IPAddress)
|
||||
def trigger_ddns_delete(instance: IPAddress, **_kwargs):
|
||||
update_dns.delay(
|
||||
old_record=instance,
|
||||
old_address = instance.address.ip
|
||||
old_dns_name = normalize_fqdn(instance.dns_name)
|
||||
|
||||
if old_address and old_dns_name:
|
||||
dns_delete.delay(
|
||||
address=old_address,
|
||||
dns_name=old_dns_name,
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=ExtraDNSName)
|
||||
def store_original_extra(instance: ExtraDNSName, **_kwargs):
|
||||
instance.before_save = ExtraDNSName.objects.filter(pk=instance.pk).first()
|
||||
|
||||
|
||||
@receiver(post_save, sender=ExtraDNSName)
|
||||
def trigger_extra_ddns_update(instance: ExtraDNSName, **_kwargs):
|
||||
address = instance.ip_address.address.ip
|
||||
old_dns_name = instance.before_save.name if instance.before_save else ''
|
||||
new_dns_name = instance.name
|
||||
|
||||
if new_dns_name != old_dns_name:
|
||||
if old_dns_name:
|
||||
delete = dns_delete.delay(
|
||||
dns_name=old_dns_name,
|
||||
address=address,
|
||||
reverse=False,
|
||||
status=instance,
|
||||
)
|
||||
else:
|
||||
delete = None
|
||||
|
||||
dns_create.delay(
|
||||
dns_name=new_dns_name,
|
||||
address=address,
|
||||
status=instance,
|
||||
reverse=False,
|
||||
depends_on=delete,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=ExtraDNSName)
|
||||
def trigger_extra_ddns_delete(instance: ExtraDNSName, **_kwargs):
|
||||
address = instance.ip_address.address.ip
|
||||
old_dns_name = instance.name
|
||||
|
||||
if old_dns_name == normalize_fqdn(instance.ip_address.dns_name):
|
||||
return
|
||||
|
||||
dns_delete.delay(
|
||||
dns_name=old_dns_name,
|
||||
address=address,
|
||||
reverse=False,
|
||||
)
|
||||
|
|
44
netbox_ddns/tables.py
Normal file
44
netbox_ddns/tables.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import django_tables2 as tables
|
||||
|
||||
from netbox_ddns.models import ExtraDNSName
|
||||
from utilities.tables import BaseTable, ToggleColumn
|
||||
|
||||
FORWARD_DNS = """
|
||||
{% if record.forward_action is not None %}
|
||||
{{ record.get_forward_action_display }}:
|
||||
{{ record.get_forward_rcode_html_display }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not created</span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
ACTIONS = """
|
||||
{% if perms.dcim.change_extradnsname %}
|
||||
<a href="{% url 'plugins:netbox_ddns:extradnsname_edit' ipaddress_pk=record.ip_address.pk pk=record.pk %}"
|
||||
class="btn btn-xs btn-warning">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_extradnsname %}
|
||||
<a href="{% url 'plugins:netbox_ddns:extradnsname_delete' ipaddress_pk=record.ip_address.pk pk=record.pk %}"
|
||||
class="btn btn-xs btn-danger">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
class PrefixTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
name = tables.Column()
|
||||
last_update = tables.Column()
|
||||
forward_dns = tables.TemplateColumn(template_code=FORWARD_DNS)
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=ACTIONS,
|
||||
attrs={'td': {'class': 'text-right text-nowrap noprint'}},
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ExtraDNSName
|
||||
fields = ('pk', 'name', 'last_update', 'forward_dns', 'actions')
|
|
@ -1,6 +1,7 @@
|
|||
from django.contrib.auth.context_processors import PermWrapper
|
||||
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
from . import tables
|
||||
|
||||
|
||||
# noinspection PyAbstractClass
|
||||
|
@ -9,9 +10,17 @@ class DNSInfo(PluginTemplateExtension):
|
|||
|
||||
def left_page(self):
|
||||
"""
|
||||
An info-box with edit button for the vCenter settings
|
||||
An info-box with the status of the DNS modifications and records
|
||||
"""
|
||||
return self.render('netbox_ddns/ipaddress/dns_info.html')
|
||||
extra_dns_name_table = tables.PrefixTable(list(self.context['object'].extradnsname_set.all()), orderable=False)
|
||||
|
||||
return (
|
||||
self.render('netbox_ddns/ipaddress/dns_info.html') +
|
||||
self.render('netbox_ddns/ipaddress/dns_extra.html', {
|
||||
'perms': PermWrapper(self.context['request'].user),
|
||||
'extra_dns_name_table': extra_dns_name_table,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
template_extensions = [DNSInfo]
|
||||
|
|
20
netbox_ddns/templates/netbox_ddns/ipaddress/dns_extra.html
Normal file
20
netbox_ddns/templates/netbox_ddns/ipaddress/dns_extra.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% if perms.netbox_ddns.view_extradnsname %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Extra DNS Names</strong>
|
||||
</div>
|
||||
|
||||
{% render_table extra_dns_name_table 'inc/table.html' %}
|
||||
|
||||
{% if perms.netbox_ddns.add_extradnsname %}
|
||||
<div class="panel-footer text-right noprint">
|
||||
<a href="{% url 'plugins:netbox_ddns:extradnsname_create' ipaddress_pk=object.pk %}"
|
||||
class="btn btn-xs btn-primary">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add extra DNS name
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
|
@ -16,7 +16,7 @@
|
|||
<td>
|
||||
{% if object.dnsstatus.forward_action is not None %}
|
||||
{{ object.dnsstatus.get_forward_action_display }}:
|
||||
{{ object.dnsstatus.get_forward_rcode_display }}
|
||||
{{ object.dnsstatus.get_forward_rcode_html_display }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not created</span>
|
||||
{% endif %}
|
||||
|
@ -27,7 +27,7 @@
|
|||
<td>
|
||||
{% if object.dnsstatus.reverse_action is not None %}
|
||||
{{ object.dnsstatus.get_reverse_action_display }}:
|
||||
{{ object.dnsstatus.get_reverse_rcode_display }}
|
||||
{{ object.dnsstatus.get_reverse_rcode_html_display }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not created</span>
|
||||
{% endif %}
|
||||
|
|
|
@ -1 +1,15 @@
|
|||
urlpatterns = []
|
||||
from django.urls import path
|
||||
|
||||
from .views import ExtraDNSNameCreateView, ExtraDNSNameDeleteView, ExtraDNSNameEditView
|
||||
|
||||
urlpatterns = [
|
||||
path(route='ip-addresses/<int:ipaddress_pk>/extra/create/',
|
||||
view=ExtraDNSNameCreateView.as_view(),
|
||||
name='extradnsname_create'),
|
||||
path(route='ip-addresses/<int:ipaddress_pk>/extra/<int:pk>/edit/',
|
||||
view=ExtraDNSNameEditView.as_view(),
|
||||
name='extradnsname_edit'),
|
||||
path(route='ip-addresses/<int:ipaddress_pk>/extra/<int:pk>/delete/',
|
||||
view=ExtraDNSNameDeleteView.as_view(),
|
||||
name='extradnsname_delete'),
|
||||
]
|
||||
|
|
5
netbox_ddns/utils.py
Normal file
5
netbox_ddns/utils.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
def normalize_fqdn(dns_name: str) -> str:
|
||||
if not dns_name:
|
||||
return ''
|
||||
|
||||
return dns_name.lower().rstrip('.') + '.'
|
|
@ -0,0 +1,52 @@
|
|||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from netbox_ddns.forms import ExtraDNSNameEditForm
|
||||
from netbox_ddns.models import ExtraDNSName
|
||||
from utilities.views import ObjectDeleteView, ObjectEditView
|
||||
|
||||
|
||||
class ExtraDNSNameObjectMixin:
|
||||
def get_object(self, kwargs):
|
||||
if 'ipaddress_pk' not in kwargs:
|
||||
raise Http404
|
||||
|
||||
ip_address = get_object_or_404(IPAddress, pk=kwargs['ipaddress_pk'])
|
||||
|
||||
if 'pk' in kwargs:
|
||||
return get_object_or_404(ExtraDNSName, ip_address=ip_address, pk=kwargs['pk'])
|
||||
|
||||
return ExtraDNSName(ip_address=ip_address)
|
||||
|
||||
def get_return_url(self, request, obj=None):
|
||||
# First, see if `return_url` was specified as a query parameter or form data. Use this URL only if it's
|
||||
# considered safe.
|
||||
query_param = request.GET.get('return_url') or request.POST.get('return_url')
|
||||
if query_param and is_safe_url(url=query_param, allowed_hosts=request.get_host()):
|
||||
return query_param
|
||||
|
||||
# Otherwise check we have an object and can return to its ip-address
|
||||
elif obj is not None and obj.ip_address is not None:
|
||||
return obj.ip_address.get_absolute_url()
|
||||
|
||||
# If all else fails, return home. Ideally this should never happen.
|
||||
return reverse('home')
|
||||
|
||||
|
||||
class ExtraDNSNameCreateView(PermissionRequiredMixin, ExtraDNSNameObjectMixin, ObjectEditView):
|
||||
permission_required = 'netbox_ddns.add_extradnsname'
|
||||
model = ExtraDNSName
|
||||
model_form = ExtraDNSNameEditForm
|
||||
|
||||
|
||||
class ExtraDNSNameEditView(ExtraDNSNameCreateView):
|
||||
permission_required = 'netbox_ddns.change_extradnsname'
|
||||
|
||||
|
||||
class ExtraDNSNameDeleteView(PermissionRequiredMixin, ExtraDNSNameObjectMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_ddns.delete_extradnsname'
|
||||
model = ExtraDNSName
|
Loading…
Reference in a new issue