diff --git a/.idea/netbox_ddns.iml b/.idea/netbox_ddns.iml
index 16ea5f7..1282e7b 100644
--- a/.idea/netbox_ddns.iml
+++ b/.idea/netbox_ddns.iml
@@ -23,10 +23,5 @@
-
-
\ No newline at end of file
+
diff --git a/netbox_ddns/admin.py b/netbox_ddns/admin.py
index 9208846..0b17025 100644
--- a/netbox_ddns/admin.py
+++ b/netbox_ddns/admin.py
@@ -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
diff --git a/netbox_ddns/background_tasks.py b/netbox_ddns/background_tasks.py
index 66b92db..690ceea 100644
--- a/netbox_ddns/background_tasks.py
+++ b/netbox_ddns/background_tasks.py
@@ -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)
diff --git a/netbox_ddns/forms.py b/netbox_ddns/forms.py
new file mode 100644
index 0000000..c4b6f09
--- /dev/null
+++ b/netbox_ddns/forms.py
@@ -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']
diff --git a/netbox_ddns/migrations/0005_extradnsname.py b/netbox_ddns/migrations/0005_extradnsname.py
new file mode 100644
index 0000000..bf1819a
--- /dev/null
+++ b/netbox_ddns/migrations/0005_extradnsname.py
@@ -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')},
+ },
+ ),
+ ]
diff --git a/netbox_ddns/migrations/0006_extradns_cname.py b/netbox_ddns/migrations/0006_extradns_cname.py
new file mode 100644
index 0000000..725046d
--- /dev/null
+++ b/netbox_ddns/migrations/0006_extradns_cname.py
@@ -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',
+ ),
+ ]
diff --git a/netbox_ddns/migrations/0007_zone_meta.py b/netbox_ddns/migrations/0007_zone_meta.py
new file mode 100644
index 0000000..8b0631d
--- /dev/null
+++ b/netbox_ddns/migrations/0007_zone_meta.py
@@ -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'},
+ ),
+ ]
diff --git a/netbox_ddns/models.py b/netbox_ddns/models.py
index 072bff5..7cc2e12 100644
--- a/netbox_ddns/models.py
+++ b/netbox_ddns/models.py
@@ -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('{output} 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('{output} 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('{output}Not created
+ {% endif %}
+"""
+
+ACTIONS = """
+ {% if perms.dcim.change_extradnsname %}
+
+
+
+ {% endif %}
+ {% if perms.dcim.delete_extradnsname %}
+
+
+
+ {% 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')
diff --git a/netbox_ddns/template_content.py b/netbox_ddns/template_content.py
index 190d806..ca439f7 100644
--- a/netbox_ddns/template_content.py
+++ b/netbox_ddns/template_content.py
@@ -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]
diff --git a/netbox_ddns/templates/netbox_ddns/ipaddress/dns_extra.html b/netbox_ddns/templates/netbox_ddns/ipaddress/dns_extra.html
new file mode 100644
index 0000000..17671ba
--- /dev/null
+++ b/netbox_ddns/templates/netbox_ddns/ipaddress/dns_extra.html
@@ -0,0 +1,20 @@
+{% load render_table from django_tables2 %}
+
+{% if perms.netbox_ddns.view_extradnsname %}
+
+
+ Extra DNS Names
+
+
+ {% render_table extra_dns_name_table 'inc/table.html' %}
+
+ {% if perms.netbox_ddns.add_extradnsname %}
+
+ {% endif %}
+
+{% endif %}
diff --git a/netbox_ddns/templates/netbox_ddns/ipaddress/dns_info.html b/netbox_ddns/templates/netbox_ddns/ipaddress/dns_info.html
index 55062f6..75f5620 100644
--- a/netbox_ddns/templates/netbox_ddns/ipaddress/dns_info.html
+++ b/netbox_ddns/templates/netbox_ddns/ipaddress/dns_info.html
@@ -16,7 +16,7 @@
{% 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 %}
Not created
{% endif %}
@@ -27,7 +27,7 @@
|
{% 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 %}
Not created
{% endif %}
diff --git a/netbox_ddns/urls.py b/netbox_ddns/urls.py
index 637600f..8d90a62 100644
--- a/netbox_ddns/urls.py
+++ b/netbox_ddns/urls.py
@@ -1 +1,15 @@
-urlpatterns = []
+from django.urls import path
+
+from .views import ExtraDNSNameCreateView, ExtraDNSNameDeleteView, ExtraDNSNameEditView
+
+urlpatterns = [
+ path(route='ip-addresses//extra/create/',
+ view=ExtraDNSNameCreateView.as_view(),
+ name='extradnsname_create'),
+ path(route='ip-addresses//extra//edit/',
+ view=ExtraDNSNameEditView.as_view(),
+ name='extradnsname_edit'),
+ path(route='ip-addresses//extra//delete/',
+ view=ExtraDNSNameDeleteView.as_view(),
+ name='extradnsname_delete'),
+]
diff --git a/netbox_ddns/utils.py b/netbox_ddns/utils.py
new file mode 100644
index 0000000..e88a306
--- /dev/null
+++ b/netbox_ddns/utils.py
@@ -0,0 +1,5 @@
+def normalize_fqdn(dns_name: str) -> str:
+ if not dns_name:
+ return ''
+
+ return dns_name.lower().rstrip('.') + '.'
diff --git a/netbox_ddns/views.py b/netbox_ddns/views.py
index e69de29..5324a11 100644
--- a/netbox_ddns/views.py
+++ b/netbox_ddns/views.py
@@ -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
|