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