Add a DNS refresh button to IPAddress view
This commit is contained in:
parent
0ff9d62ba6
commit
2bc5f9f204
|
@ -5,57 +5,16 @@ 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 netbox_ddns.models import ACTION_CREATE, ACTION_DELETE, DNSStatus, ReverseZone, Zone
|
||||
from netbox_ddns.utils import normalize_fqdn
|
||||
from netbox_ddns.utils import get_soa
|
||||
|
||||
logger = logging.getLogger('netbox_ddns')
|
||||
|
||||
|
||||
def get_zone(dns_name: str) -> Optional[Zone]:
|
||||
# Generate all possible zones
|
||||
zones = []
|
||||
parts = dns_name.lower().split('.')
|
||||
for i in range(len(parts)):
|
||||
zones.append('.'.join(parts[-i - 1:]))
|
||||
|
||||
# Find the zone, if any
|
||||
return Zone.objects.filter(name__in=zones).order_by(Length('name').desc()).first()
|
||||
|
||||
|
||||
def get_soa(dns_name: str) -> str:
|
||||
parts = dns_name.rstrip('.').split('.')
|
||||
for i in range(len(parts)):
|
||||
zone_name = normalize_fqdn('.'.join(parts[i:]))
|
||||
|
||||
try:
|
||||
dns.resolver.query(zone_name, dns.rdatatype.SOA)
|
||||
return zone_name
|
||||
except dns.resolver.NoAnswer:
|
||||
# The name exists, but has no SOA. Continue one level further up
|
||||
continue
|
||||
except dns.resolver.NXDOMAIN as e:
|
||||
# Look for a SOA record in the authority section
|
||||
for query, response in e.responses().items():
|
||||
for rrset in response.authority:
|
||||
if rrset.rdtype == dns.rdatatype.SOA:
|
||||
return rrset.name.to_text()
|
||||
|
||||
|
||||
def get_reverse_zone(address: ip.IPAddress) -> Optional[ReverseZone]:
|
||||
# Find the zone, if any
|
||||
zones = list(ReverseZone.objects.filter(prefix__net_contains=address))
|
||||
if not zones:
|
||||
return None
|
||||
|
||||
zones.sort(key=lambda zone: zone.prefix.prefixlen)
|
||||
return zones[-1]
|
||||
|
||||
|
||||
def status_update(output: List[str], operation: str, response) -> None:
|
||||
code = response.rcode()
|
||||
|
||||
|
@ -70,7 +29,7 @@ def status_update(output: List[str], operation: str, response) -> None:
|
|||
|
||||
|
||||
def create_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_zone(dns_name)
|
||||
zone = Zone.objects.find_for_dns_name(dns_name)
|
||||
if zone:
|
||||
logger.debug(f"Found zone {zone.name} for {dns_name}")
|
||||
if status:
|
||||
|
@ -101,7 +60,7 @@ def create_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
|
|||
|
||||
|
||||
def delete_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_zone(dns_name)
|
||||
zone = Zone.objects.find_for_dns_name(dns_name)
|
||||
if zone:
|
||||
logger.debug(f"Found zone {zone.name} for {dns_name}")
|
||||
if status:
|
||||
|
@ -131,7 +90,7 @@ def delete_forward(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
|
|||
|
||||
|
||||
def create_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_reverse_zone(address)
|
||||
zone = ReverseZone.objects.find_for_address(address)
|
||||
if zone and dns_name:
|
||||
record_name = zone.record_name(address)
|
||||
logger.debug(f"Found zone {zone.name} for {record_name}")
|
||||
|
@ -162,7 +121,7 @@ def create_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSSta
|
|||
|
||||
|
||||
def delete_reverse(dns_name: str, address: ip.IPAddress, status: Optional[DNSStatus], output: List[str]):
|
||||
zone = get_reverse_zone(address)
|
||||
zone = ReverseZone.objects.find_for_address(address)
|
||||
if zone and dns_name:
|
||||
record_name = zone.record_name(address)
|
||||
logger.debug(f"Found zone {zone.name} for {record_name}")
|
||||
|
|
|
@ -6,6 +6,7 @@ import dns.tsigkeyring
|
|||
import dns.update
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.functions import Length
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dns import rcode
|
||||
|
@ -115,6 +116,18 @@ class Server(models.Model):
|
|||
)
|
||||
|
||||
|
||||
class ZoneQuerySet(models.QuerySet):
|
||||
def find_for_dns_name(self, dns_name: str) -> Optional['Zone']:
|
||||
# Generate all possible zones
|
||||
zones = []
|
||||
parts = dns_name.lower().split('.')
|
||||
for i in range(len(parts)):
|
||||
zones.append('.'.join(parts[-i - 1:]))
|
||||
|
||||
# Find the zone, if any
|
||||
return self.filter(name__in=zones).order_by(Length('name').desc()).first()
|
||||
|
||||
|
||||
class Zone(models.Model):
|
||||
name = models.CharField(
|
||||
verbose_name=_('zone name'),
|
||||
|
@ -131,6 +144,8 @@ class Zone(models.Model):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
objects = ZoneQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = _('forward zone')
|
||||
|
@ -147,6 +162,17 @@ class Zone(models.Model):
|
|||
return self.server.create_update(self.name)
|
||||
|
||||
|
||||
class ReverseZoneQuerySet(models.QuerySet):
|
||||
def find_for_address(self, address: ip.IPAddress) -> Optional['ReverseZone']:
|
||||
# Find the zone, if any
|
||||
zones = list(ReverseZone.objects.filter(prefix__net_contains=address))
|
||||
if not zones:
|
||||
return None
|
||||
|
||||
zones.sort(key=lambda zone: zone.prefix.prefixlen)
|
||||
return zones[-1]
|
||||
|
||||
|
||||
class ReverseZone(models.Model):
|
||||
prefix = IPNetworkField(
|
||||
verbose_name=_('prefix'),
|
||||
|
@ -167,6 +193,8 @@ class ReverseZone(models.Model):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
objects = ReverseZoneQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('prefix',)
|
||||
verbose_name = _('reverse zone')
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.contrib.auth.context_processors import PermWrapper
|
||||
from django.template.context_processors import csrf
|
||||
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
from . import tables
|
||||
|
@ -8,6 +9,16 @@ from . import tables
|
|||
class DNSInfo(PluginTemplateExtension):
|
||||
model = 'ipam.ipaddress'
|
||||
|
||||
def buttons(self):
|
||||
"""
|
||||
A button to force DNS re-provisioning
|
||||
"""
|
||||
context = {
|
||||
'perms': PermWrapper(self.context['request'].user),
|
||||
}
|
||||
context.update(csrf(self.context['request']))
|
||||
return self.render('netbox_ddns/ipaddress/dns_refresh_button.html', context)
|
||||
|
||||
def left_page(self):
|
||||
"""
|
||||
An info-box with the status of the DNS modifications and records
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{% if perms.ipam.change_ipaddress %}
|
||||
<form method="post" style="display: inline-block">
|
||||
{% csrf_token %}
|
||||
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url 'plugins:netbox_ddns:ipaddress_dnsname_recreate' ipaddress_pk=object.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Recreate DNS
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
|
@ -1,8 +1,11 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import ExtraDNSNameCreateView, ExtraDNSNameDeleteView, ExtraDNSNameEditView
|
||||
from .views import ExtraDNSNameCreateView, ExtraDNSNameDeleteView, ExtraDNSNameEditView, IPAddressDNSNameRecreateView
|
||||
|
||||
urlpatterns = [
|
||||
path(route='ip-addresses/<int:ipaddress_pk>/recreate/',
|
||||
view=IPAddressDNSNameRecreateView.as_view(),
|
||||
name='ipaddress_dnsname_recreate'),
|
||||
path(route='ip-addresses/<int:ipaddress_pk>/extra/create/',
|
||||
view=ExtraDNSNameCreateView.as_view(),
|
||||
name='extradnsname_create'),
|
||||
|
|
|
@ -1,5 +1,28 @@
|
|||
import dns.rdatatype
|
||||
import dns.resolver
|
||||
|
||||
|
||||
def normalize_fqdn(dns_name: str) -> str:
|
||||
if not dns_name:
|
||||
return ''
|
||||
|
||||
return dns_name.lower().rstrip('.') + '.'
|
||||
|
||||
|
||||
def get_soa(dns_name: str) -> str:
|
||||
parts = dns_name.rstrip('.').split('.')
|
||||
for i in range(len(parts)):
|
||||
zone_name = normalize_fqdn('.'.join(parts[i:]))
|
||||
|
||||
try:
|
||||
dns.resolver.query(zone_name, dns.rdatatype.SOA)
|
||||
return zone_name
|
||||
except dns.resolver.NoAnswer:
|
||||
# The name exists, but has no SOA. Continue one level further up
|
||||
continue
|
||||
except dns.resolver.NXDOMAIN as e:
|
||||
# Look for a SOA record in the authority section
|
||||
for query, response in e.responses().items():
|
||||
for rrset in response.authority:
|
||||
if rrset.rdtype == dns.rdatatype.SOA:
|
||||
return rrset.name.to_text()
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
|
||||
from ipam.models import IPAddress
|
||||
from netbox_ddns.background_tasks import dns_create
|
||||
from netbox_ddns.forms import ExtraDNSNameEditForm
|
||||
from netbox_ddns.models import ExtraDNSName
|
||||
from netbox_ddns.models import DNSStatus, ExtraDNSName, Zone
|
||||
from netbox_ddns.utils import normalize_fqdn
|
||||
from utilities.views import ObjectDeleteView, ObjectEditView
|
||||
|
||||
|
||||
|
@ -50,3 +55,52 @@ class ExtraDNSNameEditView(ExtraDNSNameCreateView):
|
|||
class ExtraDNSNameDeleteView(PermissionRequiredMixin, ExtraDNSNameObjectMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_ddns.delete_extradnsname'
|
||||
model = ExtraDNSName
|
||||
|
||||
|
||||
class IPAddressDNSNameRecreateView(PermissionRequiredMixin, View):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
|
||||
def post(self, request, ipaddress_pk):
|
||||
ip_address = get_object_or_404(IPAddress, pk=ipaddress_pk)
|
||||
|
||||
new_address = ip_address.address.ip
|
||||
new_dns_name = normalize_fqdn(ip_address.dns_name)
|
||||
|
||||
updated_names = []
|
||||
zoneless_names = []
|
||||
|
||||
if new_dns_name and Zone.objects.find_for_dns_name(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,
|
||||
)
|
||||
|
||||
updated_names.append(new_dns_name)
|
||||
else:
|
||||
zoneless_names.append(new_dns_name)
|
||||
|
||||
for extra in ip_address.extradnsname_set.all():
|
||||
new_address = extra.ip_address.address.ip
|
||||
new_dns_name = extra.name
|
||||
|
||||
if Zone.objects.find_for_dns_name(new_dns_name):
|
||||
dns_create.delay(
|
||||
dns_name=new_dns_name,
|
||||
address=new_address,
|
||||
status=extra,
|
||||
reverse=False,
|
||||
)
|
||||
|
||||
updated_names.append(new_dns_name)
|
||||
else:
|
||||
zoneless_names.append(new_dns_name)
|
||||
|
||||
if updated_names:
|
||||
messages.info(request, _("Updating DNS for {names}").format(names=', '.join(updated_names)))
|
||||
if zoneless_names:
|
||||
messages.warning(request, _("No DNS zone configured for {names}").format(names=', '.join(zoneless_names)))
|
||||
|
||||
return redirect('ipam:ipaddress', pk=ip_address.pk)
|
||||
|
|
Loading…
Reference in a new issue