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
pytestmark = pytest.mark.django_db
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.
"""
import logging
logger = logging.getLogger(__name__)
import dns.inet
import dns.name
import dns.resolver
@ -167,11 +170,10 @@ def get_ns_info(origin):
:param origin: zone we are dealing with, must be with trailing dot
:return: master nameserver, update key, update algo
"""
# later look this up from Domain model: domain+'.': nameserver_ip, nameserver_update_key
ns_info = {
settings.BASEDOMAIN + '.': (settings.SERVER, settings.UPDATE_KEY, settings.UPDATE_ALGO),
}
return ns_info[origin]
from .models import Domain
d = Domain.objects.get(domain=origin.rstrip('.'))
algorithm = getattr(dns.tsig, d.nameserver_update_algorithm)
return d.nameserver_ip, d.nameserver_update_key, algorithm
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':
assert ipaddr is not None
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)
return response

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from django import forms
from .models import Host
from .models import Host, Domain
class CreateHostForm(forms.ModelForm):
@ -14,3 +14,9 @@ class EditHostForm(forms.ModelForm):
class Meta(object):
model = Host
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')
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):
domain = models.CharField(max_length=256, unique=True)
nameserver_ip = models.GenericIPAddressField(
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_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)
created = models.DateTimeField(auto_now_add=True)
@ -88,14 +101,14 @@ class Host(models.Model):
def getIPv4(self):
try:
return dnstools.query_ns(self.get_fqdn(), 'A')
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return dnstools.query_ns(self.get_fqdn(), 'A', origin=self.domain.domain)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.resolver.Timeout):
return ''
def getIPv6(self):
try:
return dnstools.query_ns(self.get_fqdn(), 'AAAA')
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return dnstools.query_ns(self.get_fqdn(), 'AAAA', origin=self.domain.domain)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.resolver.Timeout):
return ''
def poke(self):
@ -118,6 +131,6 @@ class Host(models.Model):
def post_delete_host(sender, **kwargs):
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)

View File

@ -2,10 +2,9 @@
{% load bootstrap %}
{% block content %}
<div class="row">
<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">
{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
@ -13,6 +12,4 @@
</form>
</div>
</div>
{% 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 (
HomeView, OverviewView, HostView, DeleteHostView, AboutView, HelpView, GenerateSecretView,
RobotsTxtView, )
RobotsTxtView, DomainOverwievView, DeleteDomainView)
from ..api.views import (
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'^host/(?P<pk>\d+)/delete/$',
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'^detectip/$', DetectIpView),
url(r'^detectip/(?P<secret>\w+)/$', DetectIpView),

View File

@ -2,6 +2,7 @@
import dns.inet
from django.db.models import Q
from django.views.generic import TemplateView, CreateView
from django.views.generic.edit import UpdateView, DeleteView
from django.http import HttpResponse, HttpResponseRedirect
@ -13,8 +14,8 @@ from django.core.exceptions import PermissionDenied
import dnstools
from .forms import CreateHostForm, EditHostForm
from .models import Host
from .forms import CreateHostForm, EditHostForm, CreateDomainForm
from .models import Host, Domain
class GenerateSecretView(UpdateView):
@ -87,11 +88,17 @@ class OverviewView(CreateView):
def get_success_url(self):
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):
self.object = form.save(commit=False)
self.object.created_by = self.request.user
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.')
return HttpResponseRedirect(self.get_success_url())
@ -136,8 +143,7 @@ class HostView(UpdateView):
class DeleteHostView(DeleteView):
model = Host
template_name = "main/delete_host.html"
form_class = EditHostForm
template_name = "main/delete_object.html"
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
@ -159,6 +165,52 @@ class DeleteHostView(DeleteView):
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):
"""
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'
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
UPDATE_ALGO = dns.tsig.HMAC_SHA512
UPDATE_KEY = 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=='
# 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
ALLOWED_HOSTS = [WWW_HOST, WWW_IPV4_HOST, WWW_IPV6_HOST]
@ -156,6 +152,7 @@ INSTALLED_APPS = (
'nsupdate.main',
'bootstrapform',
'registration',
'django_extensions',
)
# A sample logging configuration. The only tangible logging
@ -189,6 +186,11 @@ LOGGING = {
'level': 'DEBUG',
'propagate': True,
},
'nsupdate.main.dnstools': {
'handlers': ['stderr', ],
'level': 'DEBUG',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins', ],
'level': 'ERROR',

View File

@ -52,6 +52,7 @@
<ul class="dropdown-menu">
<li><a href="{% url 'account_profile' %}">Profile</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 %}
<li><a href="/admin/">Admin</a></li>
{% endif %}

View File

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