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
This commit is contained in:
parent
2ee5bc91a6
commit
2247f7262e
|
@ -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):
|
||||||
|
|
|
@ -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'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
101
binder/models.py
101
binder/models.py
|
@ -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,15 @@ 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
|
||||||
|
|
||||||
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 +27,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 +42,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 +51,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 +80,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 +102,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 +143,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 +163,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 +197,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,
|
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 +216,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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue