Commits

George Notaras  committed 5c8faeb

Added support for Dynamic IP updating over HTTP.

  • Participants
  • Parent commits cc1d1e3

Comments (0)

Files changed (6)

 - Automatic zone-rectify support using native python code.
 - Zone file import through web form (experimental)
 - Configurable enabled RR types
-
+- Dynamic IP updating over HTTP
 
 TODO: Add more info
 

File src/powerdns_manager/admin.py

 from django.contrib import messages
 from django.contrib.admin import SimpleListFilter
 from django.utils.translation import ugettext_lazy as _
-
+from django.utils.crypto import get_random_string
 
 from powerdns_manager import settings
-
 from powerdns_manager.forms import SoaRecordModelForm
 from powerdns_manager.forms import NsRecordModelForm
 from powerdns_manager.forms import MxRecordModelForm
     verbose_name = 'SOA Resource Record'
     verbose_name_plural = 'SOA Resource Record' # Only one SOA RR per zone
     # The ``name`` field is not available for editing. It is always set to the
-    # name of the domain in ``forms.SoaRecordModelForm.save()`` callback.
+    # name of the domain in ``forms.SoaRecordModelForm.save()`` method.
     fields = ('ttl', 'primary', 'hostmaster', 'serial', 'refresh', 'retry', 'expire', 'default_ttl', 'date_modified')
     readonly_fields = ('date_modified', )
     can_delete = False
     
 admin.site.register(cache.get_model('powerdns_manager', 'SuperMaster'), SuperMasterAdmin)
 
+
+
+class DynamicZoneAdmin(admin.ModelAdmin):
+    fields = ('domain', 'api_key', 'date_created', 'date_modified')
+    readonly_fields = ('api_key', 'date_created', 'date_modified')
+    list_display = ('domain', 'date_created', 'date_modified')
+    search_fields = ('domain', )
+    verbose_name = 'Dynamic Zone'
+    verbose_name_plural = 'Dynamic Zones'
+    actions = ['reset_api_key']
+
+    def reset_api_key(self, request, queryset):
+        for obj in queryset:
+            obj.api_key = self._get_new_api_key()
+            obj.save()
+    reset_api_key.short_description = "Reset API Key"
+    
+    def queryset(self, request):
+        qs = super(DynamicZoneAdmin, self).queryset(request)
+        if not request.user.is_superuser:
+            # Non-superusers see the dynamic zones they have created
+            qs = qs.filter(created_by=request.user)
+        return qs
+    
+    def formfield_for_foreignkey(self, db_field, request, **kwargs):
+        Domain = cache.get_model('powerdns_manager', 'Domain')
+        if db_field.name == 'domain':
+            if not request.user.is_superuser:    # Superusers see the full choice list
+                kwargs["queryset"] = Domain.objects.filter(
+                    created_by=request.user, powerdns_manager_dynamiczone_domain__isnull=True)
+        return super(DynamicZoneAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
+    
+    def save_model(self, request, obj, form, change):
+        if not change:
+            obj.api_key = self._get_new_api_key()
+            obj.created_by = request.user
+        obj.save()
+    
+    def _get_new_api_key(self):
+        return get_random_string(
+            length=24, allowed_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
+        
+admin.site.register(cache.get_model('powerdns_manager', 'DynamicZone'), DynamicZoneAdmin)
+

File src/powerdns_manager/forms.py

 #
 
 import time
+import re
 
 from django import forms
 from django.db.models.loading import cache
     zonetext = forms.CharField(widget=forms.Textarea, initial='', required=True, label=_('Zone file'), help_text="""Paste the zone file text. (required)""")
     overwrite = forms.BooleanField(required=False, label=_('Overwrite'), help_text="""If checked, existing zone will be replaced by this one. Proceed with caution.""")
 
+
+
+class DynamicIPUpdateForm(forms.Form):
+    """This form is used to validate the supplied data in the POST request.
+    
+    """
+    api_key = forms.CharField(max_length=24, required=True)
+    hostname = forms.CharField(max_length=128, required=False)
+    ipv4 = forms.GenericIPAddressField(protocol='IPv4', required=False)
+    ipv6 = forms.GenericIPAddressField(protocol='IPv6', required=False)
+
+    def clean_api_key(self):
+        """Checks the provided API key.
+        
+        1) The key must contain [A-Z0-9]
+        2) A dynamic zone must be configured with the supplied key
+        
+        """
+        api_key = self.cleaned_data.get('api_key')
+
+        if not re.match('^[A-Z0-9]+$', api_key):
+            raise forms.ValidationError('Invalid API key')
+        
+        DynamicZone = cache.get_model('powerdns_manager', 'DynamicZone')
+        try:
+            DynamicZone.objects.get(api_key__exact=api_key)
+        except DynamicZone.DoesNotExist:
+            raise forms.ValidationError('Invalid API key')
+        else:
+            return api_key
+    
+    def clean_hostname(self):
+        """Checks the provided hostname.
+        
+        Hostname may be empty.
+        
+        Performs sanity checks.
+        
+        """
+        hostname = self.cleaned_data.get('hostname')
+        
+        if not hostname:
+            return hostname
+        
+        if not re.match('^[A-Za-z0-9._\-]+$', hostname):
+            raise forms.ValidationError('Invalid hostname')
+        
+        return hostname
+        
+   

File src/powerdns_manager/models.py

         return self.name
 
 
+
+class DynamicZone(models.Model):
+    """Model for Dynamic Zones.
+    
+    This is a PowerDNS Manager feature to support updating, the A and AAAA
+    resource records of a zone over HTTP.
+    
+    This feature can be used to easily update the IP of hosts whose IP
+    address is dynamic.
+    
+    """
+    domain = models.ForeignKey('powerdns_manager.Domain', unique=True, related_name='%(app_label)s_%(class)s_domain', verbose_name=_('domain'), help_text=_("""Select the domain, the A and AAAA records of which might be updated dynamically over HTTP."""))
+    api_key = models.CharField(max_length=24, verbose_name=_('API Key'), help_text="""The API key is generated automatically. To reset it, use the relevant action in the changelist view.""")
+
+    date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Created on'))
+    date_modified = models.DateTimeField(auto_now=True, verbose_name=_('Last Modified'))
+    created_by = models.ForeignKey('auth.User', related_name='%(app_label)s_%(class)s_created_by', null=True, verbose_name=_('created by'), help_text="""The Django user this zone belongs to.""")
+    
+    class Meta:
+        verbose_name = _('dynamic zone')
+        verbose_name_plural = _('dynamic zones')
+        get_latest_by = 'date_modified'
+        ordering = ['-date_created']
+        
+    def __unicode__(self):
+        return self.domain.name
+

File src/powerdns_manager/urls.py

 
 urlpatterns = patterns('powerdns_manager.views',
     url(r'^import/$', 'import_zone_view', name='import_zone'),
+    url(r'^update/$', 'dynamic_ip_update_view', name='dynamic_ip_update'),
 )
 

File src/powerdns_manager/views.py

 
 from django.contrib.auth.decorators import login_required
 from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.csrf import csrf_exempt
 from django.shortcuts import render_to_response
 from django.template import RequestContext
 from django.utils.translation import ugettext_lazy as _
 from django.http import HttpResponse
+from django.http import HttpResponseNotAllowed
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseNotFound
 from django.db.models.loading import cache
 from django.utils.html import mark_safe
+from django.core.validators import validate_ipv4_address
+from django.core.validators import validate_ipv6_address
+from django.core.exceptions import ValidationError
 
 from powerdns_manager.forms import ZoneImportForm
+from powerdns_manager.forms import DynamicIPUpdateForm
 from powerdns_manager.utils import process_zone_file
 
 
+
 @login_required
 @csrf_protect
 def import_zone_view(request):
     return render_to_response(
         'powerdns_manager/import/zone.html', info_dict, context_instance=RequestContext(request), mimetype='text/html')
 
+
+@csrf_exempt
+def dynamic_ip_update_view(request):
+    """
+    if hostname is missing, the ips of all A and AAAA records of the zone are changed
+    otherwise only the specific record with the name=hostname and provided that the
+    correct ip (v4, v6) has been provided for the type of the record (A, AAAA)
+    
+    curl -k \
+        -F "api_key=UBSE1RJ0J175MRAMJC31JFUH" \
+        -F "hostname=ns1.centos.example.org" \
+        -F "ipv4=128.1.2.3" \
+        -F "ipv6=3ffe:1900:4545:3:200:f8ff:fe21:67cf" \
+        https://centos.example.org/powerdns/update/
+
+    """
+    if request.method != 'POST':
+        return HttpResponseNotAllowed(['POST'])
+    form = DynamicIPUpdateForm(request.POST)
+    
+    if not form.is_valid():
+        return HttpResponseBadRequest(repr(form.errors))
+    
+    # Determine protocol or REMOTE_ADDR
+    remote_ipv4 = None
+    remote_ipv6 = None
+    try:
+        validate_ipv4_address(request.META['REMOTE_ADDR'])
+    except ValidationError:
+        try:
+            validate_ipv6_address(request.META['REMOTE_ADDR'])
+        except ValidationError:
+            return HttpResponseBadRequest('Cannot determine protocol of remote IP address')
+        else:
+            remote_ipv6 = request.META['REMOTE_ADDR']
+    else:
+        remote_ipv4 = request.META['REMOTE_ADDR']
+    
+    # Gather required information
+    
+    api_key = form.cleaned_data['api_key']
+    hostname = form.cleaned_data['hostname']
+    
+    ipv4 = form.cleaned_data['ipv4']
+    if not ipv4:
+        ipv4 = remote_ipv4
+    
+    ipv6 = form.cleaned_data['ipv6']
+    if not ipv6:
+        ipv6 = remote_ipv6
+    
+    # If the hostname is missing, the IP addresses of all A and AAAA records
+    # of the zone are updated.
+    update_all_hosts_in_zone = False
+    if not hostname:
+        update_all_hosts_in_zone = True
+    
+    # All required data is good. Process the request.
+    
+    DynamicZone = cache.get_model('powerdns_manager', 'DynamicZone')
+    Record = cache.get_model('powerdns_manager', 'Record')
+    
+    # Get the relevant dynamic zone instance
+    dyn_zone = DynamicZone.objects.get(api_key__exact=api_key)
+    
+    # Get A and AAAA records
+    dyn_rrs = Record.objects.filter(domain=dyn_zone.domain, type__in=('A', 'AAAA'))
+    if not dyn_rrs:
+        HttpResponseNotFound('A or AAAA resource records not found')
+    
+    # Update the IPs
+    if update_all_hosts_in_zone:    # No hostname supplied
+        for rr in dyn_rrs:
+            
+            # Try to update A records
+            if ipv4 and rr.type == 'A':
+                rr.content = ipv4
+            
+            # Try to update AAAA records
+            elif ipv6 and rr.type == 'AAAA':
+                rr.content = ipv6
+            
+            rr.save()
+        
+    else:    # A hostname is supplied
+        for rr in dyn_rrs:
+            if rr.name == hostname:
+                
+                # Try to update A records
+                if ipv4 and rr.type == 'A':
+                    rr.content = ipv4
+            
+                # Try to update AAAA records
+                elif ipv6 and rr.type == 'AAAA':
+                    rr.content = ipv6
+                
+                rr.save()
+    
+    return HttpResponse('Success')
+