Compare commits

...

12 Commits
nsd ... master

Author SHA1 Message Date
Eric van der Vlist 97444cb684 Documentation 2020-04-18 09:41:08 +02:00
Eric van der Vlist 7172afa4a2 Support of Docker host names (for NSD) 2020-04-18 09:36:48 +02:00
Eric van der Vlist f063831578 Supporting Django 3. 2020-04-18 09:35:28 +02:00
Eric van der Vlist 676b67e8e1 Changing the order and breaking commands so that the cache can be used when the code is updated (huge Docker build time gain when rebuilding after a code update). 2020-04-18 09:30:31 +02:00
jeffrey forman a158e984de add missing mysql deps! 2019-03-21 20:04:21 -04:00
jeffrey forman e78bb637e9 add django-mysql as requirement since using alpine as the base image no longer includes it by default 2019-03-21 07:56:41 -04:00
jeffrey forman d38aa7fb8f update readme with mentions of NSD 2019-03-18 19:54:57 -04:00
jeffrey forman 77c9ff91e5 bump dnspython 1.11->1.16.0 and add jinja (for nsd config templates) 2019-03-17 20:46:05 -04:00
jeffrey forman ab9ad3e25c statistics_port -> control_port 2019-03-17 20:45:15 -04:00
jeffrey forman 17da7b33a3 make exceptions a bit more module-path happy. fix some iterator references 2019-03-17 20:44:55 -04:00
jeffrey forman 2247f7262e add main functionality for NSD in models.
update the encrypt (save) and decrypt functions to be python3/dnspython friendly
using bytestrings instead of expecting strings.

statistic_port is now control_port.

removed the validations from admin.py because the model fields now provide
for validators of their data.

add creds dir to the settings.py
2019-03-17 20:36:28 -04:00
jeffrey forman 2ee5bc91a6 initial nsd helper library and methods for getting zones, writing config, etc 2019-03-17 20:30:05 -04:00
19 changed files with 258 additions and 86 deletions

View File

@ -3,11 +3,16 @@ FROM python:3-alpine
MAINTAINER Jeffrey Forman <code@jeffreyforman.net> MAINTAINER Jeffrey Forman <code@jeffreyforman.net>
WORKDIR /code WORKDIR /code
RUN apk add --no-cache nsd build-base python3-dev libffi-dev openssl-dev libc-dev libxslt-dev mariadb-connector-c-dev \
&& pip install --upgrade pip
COPY requirements.txt /code/
RUN pip install --no-cache-dir -r requirements.txt
COPY . /code/ COPY . /code/
RUN apk add --no-cache nsd build-base python3-dev libffi-dev openssl-dev libc-dev libxslt-dev \
&& pip install --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
EXPOSE 8000 EXPOSE 8000

View File

@ -5,6 +5,9 @@
A Django web application for viewing and editing BIND DNS zone records. A Django web application for viewing and editing BIND DNS zone records.
It has support for NSD-hosted DNS zones, but as NSD does not support dynamic updates,
that feature is not available.
## Download ## ## Download ##
``` ```
@ -21,7 +24,8 @@ pip install -r requirements.txt
## Running Binder ## ## Running Binder ##
Over the course of developing Binder, it has come to the fore that using a container makes development and runnin Binder much easier. Over the course of developing Binder, it has come to the fore that using a container makes
development and running Binder much easier.
### Local Sqlite database ### ### Local Sqlite database ###
@ -32,6 +36,11 @@ docker run jforman/binder:latest
Default admin user for Binder is 'admin', and password is 'admin' as well. Default admin user for Binder is 'admin', and password is 'admin' as well.
If the default admin user doesn't exist in your database, create one:
```shell script
python manage.py createsuperuser
```
### MySQL database ### ### MySQL database ###
If you wish to use a MySQL database, the following structure works: If you wish to use a MySQL database, the following structure works:
@ -102,6 +111,18 @@ If you wish to use a statically configured encryption/decryption key, one must p
Aside from the Binder application itself, other infrastructure is required Aside from the Binder application itself, other infrastructure is required
to make Binder useful. to make Binder useful.
### NSD DNS Server ###
If you wish to access an NSD DNS server, the credentials are expected to be found in `/creds` creds directory,
where each subdirectory matches the configured hostname.
For example, for NSD host ns1.university.edu, the NSD remote control certificates would be found at the following paths:
```
/creds/ns1.university.edu/nsd_control.key
/creds/ns1.university.edu/nsd_control.pem
```
### BIND DNS Server ### ### BIND DNS Server ###
When Binder accesses your BIND DNS server, it first queries the statistics port to gather zone information. This includes zone name, view, and serial number. When Binder accesses your BIND DNS server, it first queries the statistics port to gather zone information. This includes zone name, view, and serial number.

View File

@ -5,27 +5,8 @@ from django.contrib import admin
from django.forms import ModelForm, ValidationError from django.forms import ModelForm, ValidationError
class BindServerAdminForm(ModelForm):
def clean_statistics_port(self):
port = self.cleaned_data["statistics_port"]
if port < 1 or port > 65535:
raise ValidationError("Invalid port number %(port)s. Please enter "
"a valid one between 1 and 65535.",
params={'port': port})
return self.cleaned_data["statistics_port"]
def clean_dns_port(self):
port = self.cleaned_data["dns_port"]
if port < 1 or port > 65535:
raise ValidationError("Invalid port number %(port)s. Please enter "
"a valid one between 1 and 65535.",
params={'port': port})
return self.cleaned_data["dns_port"]
class BindServerAdmin(admin.ModelAdmin): class BindServerAdmin(admin.ModelAdmin):
form = BindServerAdminForm list_display = ['hostname', 'server_type', 'control_port', 'default_transfer_key']
list_display = ['hostname', 'statistics_port', 'default_transfer_key']
class KeyAdminForm(ModelForm): class KeyAdminForm(ModelForm):

View File

70
binder/backends/nsd.py Normal file
View File

@ -0,0 +1,70 @@
# NSD Backend Handler Class
import os
from django.conf import settings
from jinja2 import Template
import re
import subprocess
from binder import helpers
NSD_CONF_TEMPLATE = """
# nsd.conf for {{hostname}}
remote-control:
control-enable: yes
control-key-file: {{creds_dir}}/{{hostname}}/nsd_control.key
control-cert-file: {{creds_dir}}/{{hostname}}/nsd_control.pem
server-cert-file: {{creds_dir}}/{{hostname}}/nsd_server.pem
"""
ZONE_RE = re.compile("""zone:\s+(?P<zone_name>\S+)\s+state:\s+master""")
class NSDServer(object):
"""Class to manage NSD backend server data."""
def __init__(self, hostname, control_port):
self.hostname = hostname
self.control_port = control_port
def get_creds_dir(self):
return os.path.join(settings.CREDS_DIR,
self.hostname)
def get_config_path(self):
return os.path.join(
os.path.join(self.get_creds_dir()),
'nsd.conf')
def write_config(self):
if not os.path.exists(self.get_creds_dir()):
os.makedirs(self.get_creds_dir())
with open(self.get_config_path(), 'w') as f:
template = Template(NSD_CONF_TEMPLATE)
conf = template.render(
creds_dir=settings.CREDS_DIR,
hostname=self.hostname)
f.write(conf)
def get_zone_list(self):
try:
zs_out = subprocess.check_output(
["/usr/sbin/nsd-control",
"-c", self.get_config_path(),
"-s", helpers.ip_address(self.hostname),
"zonestatus"],
stderr=subprocess.STDOUT,
).decode('utf-8')
except subprocess.CalledProcessError:
raise
zones = ZONE_RE.findall(zs_out)
zone_data = {}
zone_data['stats'] = {}
zone_data['stats']['zone_stats'] = {}
for zone in zones:
zone_data['stats']['zone_stats'][zone] = {}
zone_data['stats']['zone_stats'][zone]["no_view"] = {}
zone_data['stats']['zone_stats'][zone]["no_view"]["serial"] = "n/a"
return zone_data

View File

@ -175,6 +175,20 @@ def ip_info(host_name):
return info return info
def ip_address(host_name):
"""Find an IP address for a host.
"""
try:
for s_family, s_type, s_proto, s_cannoname, s_sockaddr in socket.getaddrinfo(host_name, None):
if s_family == 2 and s_type == 1:
return s_sockaddr[0]
if s_family == 10 and s_type == 1:
return s_sockaddr[0]
except (socket.gaierror, err):
return None
return None
def send_dns_update(dns_message, dns_server, port, key_name): def send_dns_update(dns_message, dns_server, port, key_name):
"""Send DNS message to server and return response. """Send DNS message to server and return response.

View File

@ -1,5 +1,6 @@
# Generated by Django 2.1.7 on 2019-02-14 01:37 # Generated by Django 2.1.7 on 2019-03-18 00:13
import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -17,8 +18,9 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hostname', models.CharField(help_text='Host name or IP address of the BIND server.', max_length=255, unique=True)), ('hostname', models.CharField(help_text='Host name or IP address of the BIND server.', max_length=255, unique=True)),
('dns_port', models.IntegerField(default=53, help_text='The port where the BIND server is listening for DNS requests. binder especially uses that port for the dynamic zone updates. In most cases you should always leave it at the default port 53.', verbose_name='DNS port')), ('dns_port', models.IntegerField(default=53, help_text='The port where the DNS server is listening for DNS requests. binder especially uses that port for the dynamic zone updates. In most cases you should always leave it at the default port 53.', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)], verbose_name='DNS port')),
('statistics_port', models.IntegerField(help_text='Port where the BIND server is serving statistics on.')), ('server_type', models.CharField(choices=[('BIND', 'Bind'), ('NSD', 'NSD')], default='BIND', help_text='DNS Server Type.', max_length=20)),
('control_port', models.IntegerField(default=0, help_text='Port where the DNS server accepts remote statistic or control commands. 8053 for BIND, 8952 for NSD.', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(65535)])),
], ],
options={ options={
'ordering': ['hostname'], 'ordering': ['hostname'],
@ -29,7 +31,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='A human readable name for the key to store, used for further references to the key.', max_length=255, unique=True)), ('name', models.CharField(help_text='A human readable name for the key to store, used for further references to the key.', max_length=255, unique=True)),
('data', models.CharField(help_text='The private part of the TSIG key.', max_length=255)), ('data', models.CharField(help_text='The TSIG key.', max_length=255)),
('algorithm', models.CharField(choices=[('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'), ('hmac-sha1', 'SHA1'), ('hmac-sha256', 'SHA256'), ('hmac-sha384', 'SHA384'), ('hmac-sha512', 'SHA512')], help_text='The algorithm which has been used for the key.', max_length=255)), ('algorithm', models.CharField(choices=[('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'), ('hmac-sha1', 'SHA1'), ('hmac-sha256', 'SHA256'), ('hmac-sha384', 'SHA384'), ('hmac-sha512', 'SHA512')], help_text='The algorithm which has been used for the key.', max_length=255)),
], ],
options={ options={
@ -39,6 +41,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='bindserver', model_name='bindserver',
name='default_transfer_key', name='default_transfer_key',
field=models.ForeignKey(blank=True, help_text='The default key to use for all actions with this DNS server as long as no other key is specified explicitly.', null=True, on_delete=django.db.models.deletion.CASCADE, to='binder.Key'), field=models.ForeignKey(help_text='The default key to use for all actions with this DNS server.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='binder.Key'),
), ),
] ]

View File

@ -1,6 +1,7 @@
### Binder Models ### Binder Models
# Standard Imports # Standard Imports
import base64
import binascii import binascii
import socket import socket
@ -10,12 +11,17 @@ from pybindxml import reader as bindreader
import dns.exception import dns.exception
import dns.query import dns.query
import dns.tsig import dns.tsig
import dns.tsigkeyring
import dns.zone import dns.zone
# App Imports # App Imports
from binder import exceptions from binder import exceptions
from binder.backends import nsd
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator
from binder import helpers
TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'), TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'),
('hmac-sha1', 'SHA1'), ('hmac-sha1', 'SHA1'),
@ -23,6 +29,11 @@ TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'),
('hmac-sha384', 'SHA384'), ('hmac-sha384', 'SHA384'),
('hmac-sha512', 'SHA512')) ('hmac-sha512', 'SHA512'))
DNS_BACKENDS = (
('BIND', 'Bind'),
('NSD', 'NSD'),
)
class Key(models.Model): class Key(models.Model):
@ -33,7 +44,7 @@ class Key(models.Model):
help_text="A human readable name for the key to " help_text="A human readable name for the key to "
"store, used for further references to the key.") "store, used for further references to the key.")
data = models.CharField(max_length=255, data = models.CharField(max_length=255,
help_text="The private part of the TSIG key.") help_text="The TSIG key.")
algorithm = models.CharField(max_length=255, algorithm = models.CharField(max_length=255,
choices=TSIG_ALGORITHMS, choices=TSIG_ALGORITHMS,
help_text="The algorithm which has been used " help_text="The algorithm which has been used "
@ -42,23 +53,26 @@ class Key(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
def __str__(self):
return self.name
class Meta: class Meta:
ordering = ["name"] ordering = ["name"]
app_label = 'binder'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
f = Fernet(settings.FERNET_KEY) f = Fernet(settings.FERNET_KEY)
crypted_key = f.encrypt(bytes(self.data, encoding="utf8")) encrypted_text = f.encrypt(bytes(self.data, 'utf-8'))
self.data = crypted_key self.data = encrypted_text
super(Key, self).save(*args, **kwargs) super(Key, self).save(*args, **kwargs)
def create_keyring(self): def create_keyring(self):
if self.name is None: if self.name is None:
return None return None
try: try:
key_data = self.decrypt_keydata() key_data = self.decrypt_keydata()
keyring = dns.tsigkeyring.from_text({self.name: key_data}) keyring = dns.tsigkeyring.from_text({self.name: key_data})
except (binascii.Error, err): except binascii.Error as err:
raise exceptions.KeyringException("Incorrect key data. Verify key: %s. Reason: %s" % (self.name, err)) raise exceptions.KeyringException("Incorrect key data. Verify key: %s. Reason: %s" % (self.name, err))
return keyring return keyring
@ -68,18 +82,21 @@ class Key(models.Model):
fernet_key=key fernet_key=key
else: else:
fernet_key=settings.FERNET_KEY fernet_key=settings.FERNET_KEY
try: try:
f = Fernet(fernet_key) f = Fernet(fernet_key)
decrypted_key = f.decrypt(bytes(self.data)) # self.data is returned as a string. so we need to re-convert it
except InvalidToken: # to a byte string with just the key, then decrypt it.
decrypted_key = f.decrypt(bytes(self.data[2:-1], 'utf-8'))
except InvalidToken as err:
raise exceptions.KeyringException() raise exceptions.KeyringException()
return decrypted_key return str(decrypted_key)[2:-1]
class BindServer(models.Model): class BindServer(models.Model):
"""Store DNS servers and attributes for referencing their statistics ports. """Store DNS servers and attributes for referencing their control ports.
Also reference FK for TSIG transfer keys, if required. Also reference FK for TSIG transfer keys, if required.
""" """
@ -87,21 +104,38 @@ class BindServer(models.Model):
hostname = models.CharField(max_length=255, hostname = models.CharField(max_length=255,
unique=True, unique=True,
help_text="Host name or IP address of the BIND server.") help_text="Host name or IP address of the BIND server.")
dns_port = models.IntegerField(default=53, dns_port = models.IntegerField(default=53,
verbose_name="DNS port", verbose_name="DNS port",
help_text="The port where the BIND server is listening for DNS " validators=[
MinValueValidator(1),
MaxValueValidator(65535),
],
help_text="The port where the DNS server is listening for DNS "
"requests. binder especially uses that port for the dynamic " "requests. binder especially uses that port for the dynamic "
"zone updates. In most cases you should always leave it at the " "zone updates. In most cases you should always leave it at the "
"default port 53.") "default port 53.")
statistics_port = models.IntegerField(help_text="Port where the BIND server is serving "
"statistics on.") server_type = models.CharField(
help_text="DNS Server Type.",
choices=DNS_BACKENDS,
default='BIND',
max_length=20)
control_port = models.IntegerField(
default=0,
validators=[
MinValueValidator(1),
MaxValueValidator(65535),
],
help_text="Port where the DNS server accepts remote statistic or control commands. "
"8053 for BIND, 8952 for NSD.")
default_transfer_key = models.ForeignKey(Key, default_transfer_key = models.ForeignKey(Key,
on_delete=models.SET_NULL,
null=True, null=True,
blank=True,
on_delete=models.CASCADE,
help_text="The default key to use for all actions " help_text="The default key to use for all actions "
"with this DNS server as long as no other key is " "with this DNS server.")
"specified explicitly.")
def __unicode__(self): def __unicode__(self):
return self.hostname return self.hostname
@ -111,6 +145,14 @@ class BindServer(models.Model):
class Meta: class Meta:
ordering = ["hostname"] ordering = ["hostname"]
app_label = 'binder'
def save(self, *args, **kwargs):
if self.server_type == 'NSD':
server = nsd.NSDServer(hostname=self.hostname, control_port=self.control_port)
server.write_config()
super().save(*args, **kwargs)
def list_zones(self): def list_zones(self):
"""List the DNS zones and attributes. """List the DNS zones and attributes.
@ -123,9 +165,18 @@ class BindServer(models.Model):
String zone_class, String zone_class,
String zone_serial } String zone_serial }
""" """
zone_data = bindreader.BindXmlReader(host=self.hostname, port=self.statistics_port) if self.server_type == "BIND":
zone_data.get_stats() # TODO: just return stats from get_stats call. This is probably not used
return zone_data # anywhere else.
zone_data = bindreader.BindXmlReader(host=self.hostname, port=self.control_port)
zone_data.get_stats()
return zone_data
elif self.server_type == "NSD":
zone_data = nsd.NSDServer(hostname=self.hostname,
control_port=self.control_port)
zone_dict = zone_data.get_zone_list()
return zone_dict
def list_zone_records(self, zone_name): def list_zone_records(self, zone_name):
"""List all records in a specific zone. """List all records in a specific zone.
@ -148,11 +199,13 @@ class BindServer(models.Model):
algorithm = transfer_key.algorithm algorithm = transfer_key.algorithm
try: try:
zone = dns.zone.from_xfr(dns.query.xfr(self.hostname, xfr = dns.query.xfr(
zone_name, helpers.ip_address(self.hostname),
port=self.dns_port, zone_name,
keyring=keyring, port=self.dns_port,
keyalgorithm=algorithm)) keyring=keyring,
keyalgorithm=algorithm)
zone = dns.zone.from_xfr(xfr)
except dns.tsig.PeerBadKey: except dns.tsig.PeerBadKey:
# The incorrect TSIG key was selected for transfers. # The incorrect TSIG key was selected for transfers.
raise exceptions.TransferException("Unable to list zone records because of a TSIG key mismatch.") raise exceptions.TransferException("Unable to list zone records because of a TSIG key mismatch.")
@ -165,7 +218,7 @@ class BindServer(models.Model):
raise exceptions.TransferException("Unable to perform AXFR to list zone records. Did you forget to specify a default transfer key?") raise exceptions.TransferException("Unable to perform AXFR to list zone records. Did you forget to specify a default transfer key?")
names = zone.nodes.keys() names = zone.nodes.keys()
names.sort() sorted(names)
record_array = [] record_array = []
for current_name in names: for current_name in names:
current_record = zone[current_name].to_text(current_name) current_record = zone[current_name].to_text(current_name)

View File

@ -180,3 +180,9 @@ try:
from local_settings import * from local_settings import *
except ImportError: except ImportError:
pass pass
# Base directory where credentials are to be stored.
# For NSD, a subdirectory under CREDS_DIR should be created with th e
# appropriate certificates for nsd-control to execute.
CREDS_DIR = "/creds"

View File

@ -1,4 +1,4 @@
{% load static from staticfiles %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
{% block header %} {% block header %}

View File

@ -9,8 +9,8 @@
<th>View</th> <th>View</th>
<th>Serial Number</th> <th>Serial Number</th>
</tr> </tr>
{% for current_zone, cz_data in zone_array.stats.zone_stats.iteritems %} {% for current_zone, cz_data in zone_array.stats.zone_stats.items %}
{% for current_view, cv_data in cz_data.iteritems %} {% for current_view, cv_data in cz_data.items %}
<tr> <tr>
<td> <td>
<a href="{% url "zone_list" dns_server=dns_server.hostname zone_name=current_zone %}"> {{ current_zone }}</a> <a href="{% url "zone_list" dns_server=dns_server.hostname zone_name=current_zone %}"> {{ current_zone }}</a>

View File

@ -7,6 +7,7 @@
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
<th>IP Address</th> <th>IP Address</th>
<th>Server Type</th>
</tr> </tr>
{% for current_server in server_info %} {% for current_server in server_info %}
<tr> <tr>
@ -15,6 +16,7 @@
{{type}}: {{data}}<br> {{type}}: {{data}}<br>
{% endfor %} {% endfor %}
</td> </td>
<td> {{ current_server.server_type}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View File

@ -27,6 +27,7 @@
<td>{{ current_record.rr_data }}</td> <td>{{ current_record.rr_data }}</td>
<td> <td>
<div class="btn-toolbar" style="margin: 0;"> <div class="btn-toolbar" style="margin: 0;">
{% if dynamic_dns_available %}
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Record Actions <span class="caret"></span> Record Actions <span class="caret"></span>
@ -38,6 +39,9 @@
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
{% else %}
Actions not available.
{% endif %}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -1,4 +1,4 @@
{% load static from staticfiles %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>

View File

@ -9,11 +9,11 @@ class Form_Tests(TestCase):
def setUp(self): def setUp(self):
self.ns1_key = Key.objects.create(name='test1', data='testdata1234', algorithm='MD5') self.ns1_key = Key.objects.create(name='test1', data='testdata1234', algorithm='MD5')
self.ns1 = BindServer.objects.create(hostname='ns1.test.net', self.ns1 = BindServer.objects.create(hostname='ns1.test.net',
statistics_port=8053, control_port=8053,
default_transfer_key=self.ns1_key) default_transfer_key=self.ns1_key)
self.ns2_key = Key.objects.create(name='test2', data='testdata1234', algorithm='MD5') self.ns2_key = Key.objects.create(name='test2', data='testdata1234', algorithm='MD5')
self.ns2 = BindServer.objects.create(hostname='ns2.test.net', self.ns2 = BindServer.objects.create(hostname='ns2.test.net',
statistics_port=8053, control_port=8053,
default_transfer_key=self.ns2_key) default_transfer_key=self.ns2_key)
def test_Valid_FormAddRecordWithoutReverseRecord(self): def test_Valid_FormAddRecordWithoutReverseRecord(self):

View File

@ -12,24 +12,17 @@ class Model_BindServer_Tests(TestCase):
"""Test that adding a well-formed BindServer works.""" """Test that adding a well-formed BindServer works."""
self.assertEqual(models.BindServer.objects.count(), 0) self.assertEqual(models.BindServer.objects.count(), 0)
bindserver_1 = models.BindServer(hostname="test1", bindserver_1 = models.BindServer(hostname="test1",
statistics_port=1234) control_port=1234)
bindserver_1.save() bindserver_1.save()
self.assertEqual(models.BindServer.objects.count(), 1) self.assertEqual(models.BindServer.objects.count(), 1)
def test_BindServerMissingStatisticsPort(self): def test_BindServerNonIntControlPort(self):
"""Attempt to add a BindServer without a statistics port.""" """Attempt to add a Bindserver with a non-integer control port."""
bindserver_1 = models.BindServer(hostname="badtest1")
with self.assertRaises(IntegrityError):
bindserver_1.save()
def test_BindServerNonIntStatisticsPort(self):
"""Attempt to add a Bindserver with a non-integer statistics port."""
bindserver_1 = models.BindServer(hostname="foo", bindserver_1 = models.BindServer(hostname="foo",
statistics_port="bar1") control_port="bar1")
with self.assertRaisesMessage(ValueError, "invalid literal for int() with base 10: 'bar1'"): with self.assertRaisesMessage(ValueError, "invalid literal for int() with base 10: 'bar1'"):
bindserver_1.save() bindserver_1.save()
class Model_Key_Tests(TestCase): class Model_Key_Tests(TestCase):
def test_KeyModel(self): def test_KeyModel(self):
"""Test that adding a well-formed Key works.""" """Test that adding a well-formed Key works."""

View File

@ -42,7 +42,7 @@ class PostTests(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
models.BindServer(hostname="testserver.test.net", models.BindServer(hostname="testserver.test.net",
statistics_port=1234).save() control_port=1234).save()
user = User.objects.create_user('testuser', user = User.objects.create_user('testuser',
'testuser@example.com', 'testuser@example.com',

View File

@ -1,12 +1,15 @@
# Binder VIews # Binder Views
import subprocess
# 3rd Party # 3rd Party
import dns.query
from django.contrib import messages from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
# App Imports # App Imports
from binder import forms, helpers, models from binder import forms, helpers, models
from binder.exceptions import KeyringException, RecordException, TransferException, ZoneException from binder import exceptions
def home_index(request): def home_index(request):
"""List the main index page for Binder.""" """List the main index page for Binder."""
@ -19,7 +22,8 @@ def view_server_list(request):
server_info = [] server_info = []
for current in server_list: for current in server_list:
server_info.append({"host_name": current, server_info.append({"host_name": current,
"ip_address": helpers.ip_info(current.hostname)}) "ip_address": helpers.ip_info(current.hostname),
"server_type": current.server_type})
return render(request, "bcommon/list_servers.html", return render(request, "bcommon/list_servers.html",
{"server_info": server_info}) {"server_info": server_info})
@ -33,8 +37,10 @@ def view_server_zones(request, dns_server):
try: try:
zone_array = this_server.list_zones() zone_array = this_server.list_zones()
except ZoneException as exc: except exceptions.ZoneException as exc:
messages.error(request, "Unable to list server zones. Error: %s" % exc) messages.error(request, "Unable to list server zones. Error: %s" % exc)
except subprocess.CalledProcessError as err:
messages.error(request, "Error in retrieving zones: %s." % str(err.output))
return render(request, "bcommon/list_server_zones.html", return render(request, "bcommon/list_server_zones.html",
{"dns_server": this_server, {"dns_server": this_server,
@ -49,22 +55,31 @@ def view_zone_records(request, dns_server, zone_name):
try: try:
zone_array = this_server.list_zone_records(zone_name) zone_array = this_server.list_zone_records(zone_name)
except TransferException as exc: except exceptions.TransferException as exc:
messages.error(request, "TransferException: %s." % exc)
return render(request, "bcommon/list_zone.html", return render(request, "bcommon/list_zone.html",
{"zone_name": zone_name, {"zone_name": zone_name,
"dns_server": this_server}) "dns_server": this_server})
except KeyringException: except exceptions.KeyringException:
messages.error(request, "Unable to get zone list. A problem was encountered " messages.error(request, "Unable to get zone list. A problem was encountered "
"decrypting your TSIG key. Ensure the key is correctly " "decrypting your TSIG key. Ensure the key is correctly "
"specified in the Binder Database.") "specified in the Binder Database.")
return render(request, "bcommon/list_zone.html", return render(request, "bcommon/list_zone.html",
{ "dns_server": this_server, { "dns_server": this_server,
"zone_name" :zone_name }) "zone_name" :zone_name })
except dns.query.TransferError as err:
messages.error(request, "TransferError: %s." % err)
return render(request, "bcommon/list_zone.html",
{"zone_name": zone_name,
"dns_server": this_server})
return render(request, "bcommon/list_zone.html", return render(request, "bcommon/list_zone.html",
{"zone_array": zone_array, {"zone_array": zone_array,
"dns_server": this_server, "dns_server": this_server,
"zone_name": zone_name}) "zone_name": zone_name,
# NOTE: A hack because NSD doesn't support dynamic updates
# so merely display the zone.
"dynamic_dns_available": this_server.server_type in ['BIND']})
def view_add_record(request, dns_server, zone_name): def view_add_record(request, dns_server, zone_name):
@ -86,7 +101,8 @@ def view_add_record(request, dns_server, zone_name):
form_cleaned["ttl"], form_cleaned["ttl"],
form_cleaned["key_name"], form_cleaned["key_name"],
form_cleaned["create_reverse"]) form_cleaned["create_reverse"])
except (KeyringException, RecordException) as exc: except (exceptions.KeyringException,
exceptions.RecordException) as exc:
messages.error(request, "Adding %s.%s failed: %s" % messages.error(request, "Adding %s.%s failed: %s" %
(form_cleaned["record_name"], zone_name, exc)) (form_cleaned["record_name"], zone_name, exc))
else: else:
@ -130,7 +146,8 @@ def view_edit_record(request, dns_server, zone_name, record_name=None,
form_cleaned["ttl"], form_cleaned["ttl"],
form_cleaned["key_name"], form_cleaned["key_name"],
form_cleaned["create_reverse"]) form_cleaned["create_reverse"])
except (KeyringException, RecordException) as exc: except (exceptions.KeyringException,
exceptions.RecordException) as exc:
messages.error(request, "Modifying %s.%s failed: %s" % messages.error(request, "Modifying %s.%s failed: %s" %
(form_cleaned["record_name"], zone_name, exc)) (form_cleaned["record_name"], zone_name, exc))
else: else:
@ -173,7 +190,8 @@ def view_add_cname_record(request, dns_server, zone_name, record_name):
str(form_cleaned["zone_name"])), str(form_cleaned["zone_name"])),
form_cleaned["ttl"], form_cleaned["ttl"],
form_cleaned["key_name"]) form_cleaned["key_name"])
except (KeyringException, RecordException) as exc: except (exceptions.KeyringException,
exceptions.RecordException) as exc:
messages.error(request, "Adding %s.%s failed: %s" % messages.error(request, "Adding %s.%s failed: %s" %
(form_cleaned["cname"], zone_name, exc)) (form_cleaned["cname"], zone_name, exc))
else: else:
@ -215,7 +233,7 @@ def view_delete_record(request, dns_server, zone_name):
response = helpers.delete_record(form_cleaned["dns_server"], response = helpers.delete_record(form_cleaned["dns_server"],
rr_list, rr_list,
form_cleaned["key_name"]) form_cleaned["key_name"])
except KeyringException as exc: except exceptions.KeyringException as exc:
for record in rr_list: for record in rr_list:
messages.error(request, "Deleting %s.%s failed: %s" % messages.error(request, "Deleting %s.%s failed: %s" %
(record, zone_name, exc)) (record, zone_name, exc))

View File

@ -1,5 +1,8 @@
Django
cryptography cryptography
dnspython>=1.11 Django
pybindxml>=0.7 django-mysql
dnspython>=1.16.0
Jinja2
lxml lxml
pybindxml>=0.7
mysqlclient