Major restructure and add support for extra DNS names
This commit is contained in:
parent
89e31f1f9f
commit
0ff9d62ba6
|
@ -23,10 +23,5 @@
|
||||||
</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>
|
||||||
|
|
|
@ -10,8 +10,10 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
from netbox.admin import admin_site
|
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 .models import ReverseZone, Server, Zone
|
||||||
|
from .utils import normalize_fqdn
|
||||||
|
|
||||||
logger = logging.getLogger('netbox_ddns')
|
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)
|
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
|
# Find all IPAddress objects in this zone but not in the more-specifics
|
||||||
addresses = IPAddress.objects.filter(Q(dns_name__endswith=zone.name) |
|
ip_addresses = IPAddress.objects.filter(Q(dns_name__endswith=zone.name) |
|
||||||
Q(dns_name__endswith=zone.name.rstrip('.')))
|
Q(dns_name__endswith=zone.name.rstrip('.')))
|
||||||
for more_specific in more_specifics:
|
for more_specific in more_specifics:
|
||||||
addresses = addresses.exclude(Q(dns_name__endswith=more_specific.name) |
|
ip_addresses = ip_addresses.exclude(Q(dns_name__endswith=more_specific.name) |
|
||||||
Q(dns_name__endswith=more_specific.name.rstrip('.')))
|
Q(dns_name__endswith=more_specific.name.rstrip('.')))
|
||||||
|
|
||||||
for address in addresses:
|
for ip_address in ip_addresses:
|
||||||
if address.dns_name:
|
new_address = ip_address.address.ip
|
||||||
update_dns.delay(
|
new_dns_name = normalize_fqdn(ip_address.dns_name)
|
||||||
new_record=address,
|
|
||||||
skip_reverse=True
|
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
|
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,
|
messages.info(request, _("Updating {count} forward records in {name}").format(count=counter,
|
||||||
name=zone.name))
|
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)
|
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
|
# 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:
|
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:
|
for ip_address in ip_addresses:
|
||||||
if address.dns_name:
|
new_address = ip_address.address.ip
|
||||||
update_dns.delay(
|
new_dns_name = normalize_fqdn(ip_address.dns_name)
|
||||||
new_record=address,
|
|
||||||
skip_forward=True
|
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
|
counter += 1
|
||||||
|
|
||||||
messages.info(request, _("Updating {count} reverse records in {name}").format(count=counter,
|
messages.info(request, _("Updating {count} reverse records in {name}").format(count=counter,
|
||||||
name=zone.name))
|
name=zone.name))
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ExtraDNSName, site=admin_site)
|
||||||
|
class ExtraDNSNameAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import dns.query
|
import dns.query
|
||||||
import dns.rdatatype
|
import dns.rdatatype
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
|
from django.db import IntegrityError
|
||||||
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 dns import rcode
|
from dns import rcode
|
||||||
from netaddr import ip
|
from netaddr import ip
|
||||||
|
|
||||||
from ipam.models import IPAddress
|
|
||||||
from netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, ReverseZone, Zone
|
from netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, ReverseZone, Zone
|
||||||
|
from netbox_ddns.utils import normalize_fqdn
|
||||||
|
|
||||||
logger = logging.getLogger('netbox_ddns')
|
logger = logging.getLogger('netbox_ddns')
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ 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 = normalize_fqdn('.'.join(parts[i:]))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dns.resolver.query(zone_name, dns.rdatatype.SOA)
|
dns.resolver.query(zone_name, dns.rdatatype.SOA)
|
||||||
|
@ -55,7 +56,7 @@ def get_reverse_zone(address: ip.IPAddress) -> Optional[ReverseZone]:
|
||||||
return zones[-1]
|
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()
|
code = response.rcode()
|
||||||
|
|
||||||
if code == dns.rcode.NOERROR:
|
if code == dns.rcode.NOERROR:
|
||||||
|
@ -68,130 +69,161 @@ def status_update(output: list, operation: str, response) -> None:
|
||||||
output.append(message)
|
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
|
@job
|
||||||
def update_dns(old_record: IPAddress = None, new_record: IPAddress = None,
|
def dns_create(dns_name: str, address: ip.IPAddress, forward=True, reverse=True, status: DNSStatus = 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 ''
|
|
||||||
|
|
||||||
output = []
|
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 forward:
|
||||||
if old_dns_name and old_address and (old_dns_name != new_dns_name or old_address != new_address):
|
create_forward(dns_name, address, status, output)
|
||||||
# Delete old forward record
|
if reverse:
|
||||||
if not skip_forward:
|
create_reverse(dns_name, address, status, output)
|
||||||
zone = get_zone(old_dns_name)
|
|
||||||
if zone:
|
|
||||||
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
|
if status:
|
||||||
soa = get_soa(old_dns_name)
|
try:
|
||||||
if soa == zone.name:
|
status.save()
|
||||||
update = zone.server.create_update(zone.name)
|
except IntegrityError:
|
||||||
update.delete(
|
# Race condition when creating?
|
||||||
old_dns_name,
|
status.save(force_update=True)
|
||||||
'a' if old_address.version == 4 else 'aaaa',
|
|
||||||
str(old_address)
|
return ', '.join(output)
|
||||||
)
|
|
||||||
response = dns.query.udp(update, zone.server.address)
|
|
||||||
status_update(output, f'Deleting {old_dns_name} {old_address}', response)
|
@job
|
||||||
status.forward_rcode = response.rcode()
|
def dns_delete(dns_name: str, address: ip.IPAddress, forward=True, reverse=True, status: DNSStatus = None):
|
||||||
else:
|
output = []
|
||||||
logger.warning(f"Can't update zone {zone.name} for {old_dns_name}, "
|
|
||||||
f"it has delegated authority for {soa}")
|
if forward:
|
||||||
status.forward_rcode = rcode.NOTAUTH
|
delete_forward(dns_name, address, status, output)
|
||||||
else:
|
if reverse:
|
||||||
logger.debug(f"No zone found for {old_dns_name}")
|
delete_reverse(dns_name, address, status, output)
|
||||||
|
|
||||||
# Delete old reverse record
|
if status:
|
||||||
if not skip_reverse:
|
try:
|
||||||
zone = get_reverse_zone(old_address)
|
status.save()
|
||||||
if zone and old_dns_name:
|
except IntegrityError:
|
||||||
record_name = zone.record_name(old_address)
|
# Race condition when creating?
|
||||||
logger.debug(f"Found zone {zone.name} for {record_name}")
|
status.save(force_update=True)
|
||||||
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()
|
|
||||||
|
|
||||||
return ', '.join(output)
|
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
|
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.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from dns import rcode
|
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
|
||||||
|
@ -13,6 +14,7 @@ from netaddr import ip
|
||||||
|
|
||||||
from ipam.fields import IPNetworkField
|
from ipam.fields import IPNetworkField
|
||||||
from ipam.models import IPAddress
|
from ipam.models import IPAddress
|
||||||
|
from .utils import normalize_fqdn
|
||||||
from .validators import HostnameAddressValidator, HostnameValidator, validate_base64
|
from .validators import HostnameAddressValidator, HostnameValidator, validate_base64
|
||||||
|
|
||||||
logger = logging.getLogger('netbox_ddns')
|
logger = logging.getLogger('netbox_ddns')
|
||||||
|
@ -93,7 +95,7 @@ class Server(models.Model):
|
||||||
self.server = self.server.lower().rstrip('.')
|
self.server = self.server.lower().rstrip('.')
|
||||||
|
|
||||||
# Ensure trailing dots from domain-style fields
|
# 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
|
@property
|
||||||
def address(self) -> Optional[str]:
|
def address(self) -> Optional[str]:
|
||||||
|
@ -104,7 +106,7 @@ class Server(models.Model):
|
||||||
|
|
||||||
def create_update(self, zone: str) -> dns.update.Update:
|
def create_update(self, zone: str) -> dns.update.Update:
|
||||||
return dns.update.Update(
|
return dns.update.Update(
|
||||||
zone=zone.rstrip('.') + '.',
|
zone=normalize_fqdn(zone),
|
||||||
keyring=dns.tsigkeyring.from_text({
|
keyring=dns.tsigkeyring.from_text({
|
||||||
self.tsig_key_name: self.tsig_key
|
self.tsig_key_name: self.tsig_key
|
||||||
}),
|
}),
|
||||||
|
@ -131,15 +133,15 @@ class Zone(models.Model):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
verbose_name = _('zone')
|
verbose_name = _('forward zone')
|
||||||
verbose_name_plural = _('zones')
|
verbose_name_plural = _('forward zones')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# Ensure trailing dots from domain-style fields
|
# Ensure trailing dots from domain-style fields
|
||||||
self.name = self.name.lower().rstrip('.') + '.'
|
self.name = normalize_fqdn(self.name)
|
||||||
|
|
||||||
def get_updater(self):
|
def get_updater(self):
|
||||||
return self.server.create_update(self.name)
|
return self.server.create_update(self.name)
|
||||||
|
@ -221,7 +223,7 @@ class ReverseZone(models.Model):
|
||||||
self.name = f'{nibble}.{self.name}'
|
self.name = f'{nibble}.{self.name}'
|
||||||
|
|
||||||
# Ensure trailing dots from domain-style fields
|
# Ensure trailing dots from domain-style fields
|
||||||
self.name = self.name.lower().rstrip('.') + '.'
|
self.name = normalize_fqdn(self.name)
|
||||||
|
|
||||||
|
|
||||||
class DNSStatus(models.Model):
|
class DNSStatus(models.Model):
|
||||||
|
@ -230,6 +232,7 @@ class DNSStatus(models.Model):
|
||||||
verbose_name=_('IP address'),
|
verbose_name=_('IP address'),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
last_update = models.DateTimeField(
|
last_update = models.DateTimeField(
|
||||||
verbose_name=_('last update'),
|
verbose_name=_('last update'),
|
||||||
auto_now=True,
|
auto_now=True,
|
||||||
|
@ -266,5 +269,69 @@ class DNSStatus(models.Model):
|
||||||
def get_forward_rcode_display(self) -> Optional[str]:
|
def get_forward_rcode_display(self) -> Optional[str]:
|
||||||
return get_rcode_display(self.forward_rcode)
|
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]:
|
def get_reverse_rcode_display(self) -> Optional[str]:
|
||||||
return get_rcode_display(self.reverse_rcode)
|
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 django.dispatch import receiver
|
||||||
|
|
||||||
from ipam.models import IPAddress
|
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')
|
logger = logging.getLogger('netbox_ddns')
|
||||||
|
|
||||||
|
@ -16,19 +18,114 @@ def store_original_ipaddress(instance: IPAddress, **_kwargs):
|
||||||
|
|
||||||
@receiver(post_save, sender=IPAddress)
|
@receiver(post_save, sender=IPAddress)
|
||||||
def trigger_ddns_update(instance: IPAddress, **_kwargs):
|
def trigger_ddns_update(instance: IPAddress, **_kwargs):
|
||||||
old_address = instance.before_save.address if instance.before_save else None
|
old_address = instance.before_save.address.ip if instance.before_save else None
|
||||||
old_dns_name = instance.before_save.dns_name if instance.before_save else ''
|
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:
|
new_address = instance.address.ip
|
||||||
# IP address or DNS name has changed
|
new_dns_name = normalize_fqdn(instance.dns_name)
|
||||||
update_dns.delay(
|
|
||||||
old_record=instance.before_save,
|
extra_dns_names = {normalize_fqdn(extra.name): extra for extra in instance.extradnsname_set.all()}
|
||||||
new_record=instance,
|
|
||||||
)
|
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)
|
@receiver(post_delete, sender=IPAddress)
|
||||||
def trigger_ddns_delete(instance: IPAddress, **_kwargs):
|
def trigger_ddns_delete(instance: IPAddress, **_kwargs):
|
||||||
update_dns.delay(
|
old_address = instance.address.ip
|
||||||
old_record=instance,
|
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 django.contrib.auth.context_processors import PermWrapper
|
||||||
|
|
||||||
from extras.plugins import PluginTemplateExtension
|
from extras.plugins import PluginTemplateExtension
|
||||||
|
from . import tables
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyAbstractClass
|
# noinspection PyAbstractClass
|
||||||
|
@ -9,9 +10,17 @@ class DNSInfo(PluginTemplateExtension):
|
||||||
|
|
||||||
def left_page(self):
|
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]
|
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>
|
<td>
|
||||||
{% if object.dnsstatus.forward_action is not None %}
|
{% if object.dnsstatus.forward_action is not None %}
|
||||||
{{ object.dnsstatus.get_forward_action_display }}:
|
{{ object.dnsstatus.get_forward_action_display }}:
|
||||||
{{ object.dnsstatus.get_forward_rcode_display }}
|
{{ object.dnsstatus.get_forward_rcode_html_display }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not created</span>
|
<span class="text-muted">Not created</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<td>
|
<td>
|
||||||
{% if object.dnsstatus.reverse_action is not None %}
|
{% if object.dnsstatus.reverse_action is not None %}
|
||||||
{{ object.dnsstatus.get_reverse_action_display }}:
|
{{ object.dnsstatus.get_reverse_action_display }}:
|
||||||
{{ object.dnsstatus.get_reverse_rcode_display }}
|
{{ object.dnsstatus.get_reverse_rcode_html_display }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not created</span>
|
<span class="text-muted">Not created</span>
|
||||||
{% endif %}
|
{% 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