Major restructure and add support for extra DNS names

This commit is contained in:
Sander Steffann 2020-04-19 21:28:06 +02:00
parent 89e31f1f9f
commit 0ff9d62ba6
16 changed files with 628 additions and 170 deletions

View File

@ -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>

View File

@ -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

View File

@ -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
View 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']

View 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')},
},
),
]

View 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',
),
]

View 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'},
),
]

View File

@ -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)

View File

@ -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
View 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')

View File

@ -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]

View 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 %}

View File

@ -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 %}

View File

@ -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
View File

@ -0,0 +1,5 @@
def normalize_fqdn(dns_name: str) -> str:
if not dns_name:
return ''
return dns_name.lower().rstrip('.') + '.'

View File

@ -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