use zones/nameserver IPs/update keys from DB, logging (thanks to asmaps)

remove unneeded stuff from settings
(we still need some in conftest.py for the tests, though)
init DB for tests via conftest.py
more update algorithm choices
give origin zone (if we already know it) to dnstools functions
new views: DomainOverview, DeleteDomain
unify deletion templates using delete_object.html
add django-extensions
This commit is contained in:
Thomas Waldmann 2013-10-18 15:30:17 -07:00
parent 5f272aa7c4
commit 49693121ea
12 changed files with 191 additions and 27 deletions

22
conftest.py Normal file
View File

@ -0,0 +1,22 @@
import pytest
from django.utils.translation import activate
# Note: fixture must be "function" scope (default), see https://github.com/pelme/pytest_django/issues/33
@pytest.fixture(autouse=True)
def db_init(db):
"""
Init the database contents for testing, so we have a service domain, ...
"""
from nsupdate.main.models import Domain
from django.db import IntegrityError
Domain.objects.create(domain='nsupdate.info',
nameserver_ip='85.10.192.104',
nameserver_update_algorithm='HMAC_SHA512',
nameserver_update_key='YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==',
available_for_everyone=True)
def pytest_runtest_setup(item):
activate('en')

View File

@ -3,6 +3,7 @@ Tests for dnstools module.
""" """
import pytest import pytest
pytestmark = pytest.mark.django_db
from dns.resolver import NXDOMAIN from dns.resolver import NXDOMAIN

View File

@ -4,6 +4,9 @@ Misc. DNS related code: query, dynamic update, etc.
Usually, higher level code wants to call the add/update/delete functions. Usually, higher level code wants to call the add/update/delete functions.
""" """
import logging
logger = logging.getLogger(__name__)
import dns.inet import dns.inet
import dns.name import dns.name
import dns.resolver import dns.resolver
@ -167,11 +170,10 @@ def get_ns_info(origin):
:param origin: zone we are dealing with, must be with trailing dot :param origin: zone we are dealing with, must be with trailing dot
:return: master nameserver, update key, update algo :return: master nameserver, update key, update algo
""" """
# later look this up from Domain model: domain+'.': nameserver_ip, nameserver_update_key from .models import Domain
ns_info = { d = Domain.objects.get(domain=origin.rstrip('.'))
settings.BASEDOMAIN + '.': (settings.SERVER, settings.UPDATE_KEY, settings.UPDATE_ALGO), algorithm = getattr(dns.tsig, d.nameserver_update_algorithm)
} return d.nameserver_ip, d.nameserver_update_key, algorithm
return ns_info[origin]
def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60): def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60):
@ -201,5 +203,7 @@ def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60):
elif action == 'upd': elif action == 'upd':
assert ipaddr is not None assert ipaddr is not None
upd.replace(name, ttl, rdtype, ipaddr) upd.replace(name, ttl, rdtype, ipaddr)
logger.debug("performing %s for name %s and origin %s with rdtype %s and ipaddr %s" % (
action, name, origin, rdtype, ipaddr))
response = dns.query.tcp(upd, nameserver) response = dns.query.tcp(upd, nameserver)
return response return response

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django import forms from django import forms
from .models import Host from .models import Host, Domain
class CreateHostForm(forms.ModelForm): class CreateHostForm(forms.ModelForm):
@ -14,3 +14,9 @@ class EditHostForm(forms.ModelForm):
class Meta(object): class Meta(object):
model = Host model = Host
fields = ['comment'] fields = ['comment']
class CreateDomainForm(forms.ModelForm):
class Meta(object):
model = Domain
exclude = ['created_by']

View File

@ -34,12 +34,25 @@ def domain_blacklist_validator(value):
raise ValidationError(u'This domain is not allowed') raise ValidationError(u'This domain is not allowed')
UPDATE_ALGORITHMS = (
('HMAC_SHA512', 'HMAC_SHA512'),
('HMAC_SHA384', 'HMAC_SHA384'),
('HMAC_SHA256', 'HMAC_SHA256'),
('HMAC_SHA224', 'HMAC_SHA224'),
('HMAC_SHA1', 'HMAC_SHA1'),
('HMAC_MD5', 'HMAC_MD5'),
)
class Domain(models.Model): class Domain(models.Model):
domain = models.CharField(max_length=256, unique=True) domain = models.CharField(max_length=256, unique=True)
nameserver_ip = models.GenericIPAddressField( nameserver_ip = models.GenericIPAddressField(
max_length=256, max_length=256,
help_text="An IP where the nsupdates for this domain will be sent to") help_text="IP where the dynamic updates for this domain will be sent to")
nameserver_update_key = models.CharField(max_length=256) nameserver_update_key = models.CharField(max_length=256)
nameserver_update_algorithm = models.CharField(
max_length=256, default='HMAC_SHA512', choices=UPDATE_ALGORITHMS)
available_for_everyone = models.BooleanField(default=False)
last_update = models.DateTimeField(auto_now=True) last_update = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
@ -88,14 +101,14 @@ class Host(models.Model):
def getIPv4(self): def getIPv4(self):
try: try:
return dnstools.query_ns(self.get_fqdn(), 'A') return dnstools.query_ns(self.get_fqdn(), 'A', origin=self.domain.domain)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.resolver.Timeout):
return '' return ''
def getIPv6(self): def getIPv6(self):
try: try:
return dnstools.query_ns(self.get_fqdn(), 'AAAA') return dnstools.query_ns(self.get_fqdn(), 'AAAA', origin=self.domain.domain)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.resolver.Timeout):
return '' return ''
def poke(self): def poke(self):
@ -118,6 +131,6 @@ class Host(models.Model):
def post_delete_host(sender, **kwargs): def post_delete_host(sender, **kwargs):
obj = kwargs['instance'] obj = kwargs['instance']
dnstools.delete(obj.get_fqdn()) dnstools.delete(obj.get_fqdn(), origin=obj.domain.domain)
post_delete.connect(post_delete_host, sender=Host) post_delete.connect(post_delete_host, sender=Host)

View File

@ -2,10 +2,9 @@
{% load bootstrap %} {% load bootstrap %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">
<h3>Delete Host <small><a href="{% url 'overview' %}"><i class="icon-double-angle-left"></i> back to overview</a></small></h3> <h3>Delete {{ object }}</h3>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p> <p>Are you sure you want to delete "{{ object }}"?</p>
@ -13,6 +12,4 @@
</form> </form>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "base.html" %}
{% load bootstrap %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<h3>Your Domains</h3>
<table class="table">
<thead>
<tr>
<th>Domain</th>
<th>Nameserver IP</th>
<th>Update key</th>
<th>Algorithm</th>
<th>Public?</th>
<th>Action</th>
</tr>
</thead>
{% for domain in domains %}
<tr>
<td><b>{{ domain.domain }}</b></td>
<td>{{ domain.nameserver_ip }}</td>
<td>{{ domain.nameserver_update_key }}</td>
<td>{{ domain.get_nameserver_update_algorithm_display }}</td>
<td>{{ domain.available_for_everyone|yesno }}</td>
<td>
<a href="{% url 'delete_domain' domain.pk %}"><i class="icon icon-remove"></i> delete</a>
</td>
</tr>
{% empty %}
<tr><td colspan="6">No domains yet.</td></tr>
{% endfor %}
</table>
</div>
</div>
<hr>
<div class="row">
<div class="col-lg-4">
<div class="well well-sm">
<h3>Add a new Domain</h3>
<form method="post" action="">
{% csrf_token %}
{{ form|bootstrap }}
<button type="submit" class="btn btn-primary">Add</button>
</form>
</div>
</div>
<div class="col-lg-8">
<div class="well well-sm">
<h3>Help</h3>
<p>Here you can add new domains (zones) which you control (and this is only useful if you
have some own zone which you can update automatically).</p>
<p>You need to configure the primary master nameserver of the zone so it accepts dynamic updates
if the correct update key is presented (which is just a shared secret between the nameserver
and this service).
</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,7 +3,7 @@ from django.views.generic import TemplateView
from .views import ( from .views import (
HomeView, OverviewView, HostView, DeleteHostView, AboutView, HelpView, GenerateSecretView, HomeView, OverviewView, HostView, DeleteHostView, AboutView, HelpView, GenerateSecretView,
RobotsTxtView, ) RobotsTxtView, DomainOverwievView, DeleteDomainView)
from ..api.views import ( from ..api.views import (
MyIpView, DetectIpView, NicUpdateView, AuthorizedNicUpdateView) MyIpView, DetectIpView, NicUpdateView, AuthorizedNicUpdateView)
@ -20,6 +20,10 @@ urlpatterns = patterns(
url(r'^generate_secret/(?P<pk>\d+)/$', GenerateSecretView.as_view(), name='generate_secret_view'), url(r'^generate_secret/(?P<pk>\d+)/$', GenerateSecretView.as_view(), name='generate_secret_view'),
url(r'^host/(?P<pk>\d+)/delete/$', url(r'^host/(?P<pk>\d+)/delete/$',
DeleteHostView.as_view(), name='delete_host'), DeleteHostView.as_view(), name='delete_host'),
url(r'^domain_overview/$',
DomainOverwievView.as_view(), name='domain_overview'),
url(r'^domain/(?P<pk>\d+)/delete/$',
DeleteDomainView.as_view(), name='delete_domain'),
url(r'^myip$', MyIpView), url(r'^myip$', MyIpView),
url(r'^detectip/$', DetectIpView), url(r'^detectip/$', DetectIpView),
url(r'^detectip/(?P<secret>\w+)/$', DetectIpView), url(r'^detectip/(?P<secret>\w+)/$', DetectIpView),

View File

@ -2,6 +2,7 @@
import dns.inet import dns.inet
from django.db.models import Q
from django.views.generic import TemplateView, CreateView from django.views.generic import TemplateView, CreateView
from django.views.generic.edit import UpdateView, DeleteView from django.views.generic.edit import UpdateView, DeleteView
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
@ -13,8 +14,8 @@ from django.core.exceptions import PermissionDenied
import dnstools import dnstools
from .forms import CreateHostForm, EditHostForm from .forms import CreateHostForm, EditHostForm, CreateDomainForm
from .models import Host from .models import Host, Domain
class GenerateSecretView(UpdateView): class GenerateSecretView(UpdateView):
@ -87,11 +88,17 @@ class OverviewView(CreateView):
def get_success_url(self): def get_success_url(self):
return reverse('generate_secret_view', args=(self.object.pk,)) return reverse('generate_secret_view', args=(self.object.pk,))
def get_form(self, form_class):
form = super(OverviewView, self).get_form(form_class)
form.fields['domain'].queryset = Domain.objects.filter(
Q(created_by=self.request.user) | Q(available_for_everyone=True))
return form
def form_valid(self, form): def form_valid(self, form):
self.object = form.save(commit=False) self.object = form.save(commit=False)
self.object.created_by = self.request.user self.object.created_by = self.request.user
self.object.save() self.object.save()
dnstools.add(self.object.get_fqdn(), self.request.META['REMOTE_ADDR']) dnstools.add(self.object.get_fqdn(), self.request.META['REMOTE_ADDR'], origin=self.object.domain.domain)
messages.add_message(self.request, messages.SUCCESS, 'Host added.') messages.add_message(self.request, messages.SUCCESS, 'Host added.')
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
@ -136,8 +143,7 @@ class HostView(UpdateView):
class DeleteHostView(DeleteView): class DeleteHostView(DeleteView):
model = Host model = Host
template_name = "main/delete_host.html" template_name = "main/delete_object.html"
form_class = EditHostForm
@method_decorator(login_required) @method_decorator(login_required)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
@ -159,6 +165,52 @@ class DeleteHostView(DeleteView):
return context return context
class DomainOverwievView(CreateView):
model = Domain
template_name = "main/domain_overview.html"
form_class = CreateDomainForm
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(DomainOverwievView, self).dispatch(*args, **kwargs)
def get_success_url(self):
return reverse('domain_overview')
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
self.object.save()
messages.add_message(self.request, messages.SUCCESS, 'Domain added.')
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, *args, **kwargs):
context = super(
DomainOverwievView, self).get_context_data(*args, **kwargs)
context['nav_domains'] = True
context['domains'] = Domain.objects.filter(
created_by=self.request.user)
return context
class DeleteDomainView(DeleteView):
model = Domain
template_name = "main/delete_object.html"
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(DeleteDomainView, self).dispatch(*args, **kwargs)
def get_object(self, *args, **kwargs):
obj = super(DeleteDomainView, self).get_object(*args, **kwargs)
if obj.created_by != self.request.user:
raise PermissionDenied() # or Http404
return obj
def get_success_url(self):
return reverse('domain_overview')
def RobotsTxtView(request): def RobotsTxtView(request):
""" """
Dynamically serve robots.txt content. Dynamically serve robots.txt content.

View File

@ -27,7 +27,6 @@ DATABASES = {
} }
} }
SERVER = '85.10.192.104' # ns1.thinkmo.de (master / dynamic upd server for nsupdate.info)
BASEDOMAIN = 'nsupdate.info' BASEDOMAIN = 'nsupdate.info'
NONEXISTING_HOST = 'nonexisting.' + BASEDOMAIN NONEXISTING_HOST = 'nonexisting.' + BASEDOMAIN
@ -39,9 +38,6 @@ WWW_IPV6_IP = '2001:41d0:8:e00e::1'
BAD_AGENTS = set() # useragent blacklist for /nic/update service BAD_AGENTS = set() # useragent blacklist for /nic/update service
UPDATE_ALGO = dns.tsig.HMAC_SHA512
UPDATE_KEY = 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=='
# Hosts/domain names that are valid for this site; required if DEBUG is False # Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = [WWW_HOST, WWW_IPV4_HOST, WWW_IPV6_HOST] ALLOWED_HOSTS = [WWW_HOST, WWW_IPV4_HOST, WWW_IPV6_HOST]
@ -156,6 +152,7 @@ INSTALLED_APPS = (
'nsupdate.main', 'nsupdate.main',
'bootstrapform', 'bootstrapform',
'registration', 'registration',
'django_extensions',
) )
# A sample logging configuration. The only tangible logging # A sample logging configuration. The only tangible logging
@ -189,6 +186,11 @@ LOGGING = {
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': True, 'propagate': True,
}, },
'nsupdate.main.dnstools': {
'handlers': ['stderr', ],
'level': 'DEBUG',
'propagate': True,
},
'django.request': { 'django.request': {
'handlers': ['mail_admins', ], 'handlers': ['mail_admins', ],
'level': 'ERROR', 'level': 'ERROR',

View File

@ -52,6 +52,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'account_profile' %}">Profile</a></li> <li><a href="{% url 'account_profile' %}">Profile</a></li>
<li><a href="{% url 'password_change' %}">Change password</a></li> <li><a href="{% url 'password_change' %}">Change password</a></li>
<li><a href="{% url 'domain_overview' %}">Own Domains</a></li>
{% if request.user.is_staff %} {% if request.user.is_staff %}
<li><a href="/admin/">Admin</a></li> <li><a href="/admin/">Admin</a></li>
{% endif %} {% endif %}

View File

@ -43,6 +43,7 @@ setup(
'south', 'south',
'django-bootstrap-form', 'django-bootstrap-form',
'django-registration', 'django-registration',
'django-extensions',
# packages only needed for development: # packages only needed for development:
'django-debug-toolbar', 'django-debug-toolbar',
'pytest', 'pytest',