Add djbdns backend

Add docs and basic tests
Update config sample file and support matrix
Change-Id: I709cea4e321f6bbee3b0f9f718fa6a9836af3ca5
This commit is contained in:
Federico Ceratto 2016-04-26 16:47:04 +01:00 committed by Federico Ceratto
parent 6ae192335b
commit be7e32dfaa
13 changed files with 921 additions and 1 deletions

110
contrib/djbdns/tinydns.init Executable file
View File

@ -0,0 +1,110 @@
#! /bin/bash
### BEGIN INIT INFO
# Provides: tinydns
# Required-Start: $local_fs $remote_fs $network
# Required-Stop: $local_fs $remote_fs $network
# Should-Start: $syslog
# Should-Stop: $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: tinydns daemon processes
# Description: Start the TinyDNS resolver
### END INIT INFO
# Documentation
# man tinydns
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# Define LSB log_* functions.
. /lib/lsb/init-functions
NAME=tinydns
DAEMON=/usr/bin/$NAME
DAEMON_USER=djbdns
DESC="the tinydns daemon"
ROOTDIR=/var/lib/djbdns
PATH=/sbin:/bin:/usr/sbin:/usr/bin
LAUNCHER=/usr/bin/envuidgid
LAUNCHER_ARGS="$DAEMON_USER envdir ./env softlimit -d300000 $DAEMON"
PIDFILE=/run/$NAME.pid
# Exit if executable is not installed
[ -x "$DAEMON" ] || exit 0
set -x
case "$1" in
start)
if [ ! -d "$ROOTDIR" ]; then
log_action_msg "Not starting $DESC: $ROOTDIR is missing."
exit 0
fi
log_action_begin_msg "Starting $DESC"
if start-stop-daemon --stop --signal 0 --quiet --pidfile $PIDFILE --exec $DAEMON; then
log_action_end_msg 0 "already running"
else
if start-stop-daemon --start --verbose --make-pidfile --chdir $ROOTDIR --pidfile $PIDFILE --exec $LAUNCHER -- $LAUNCHER_ARGS
then
log_action_end_msg 0
else
log_action_end_msg 1
exit 1
fi
fi
;;
stop)
log_action_begin_msg "Stopping $DESC"
pid=$(cat $PIDFILE 2>/dev/null) || true
if test ! -f $PIDFILE -o -z "$pid"; then
log_action_end_msg 0 "not running - there is no $PIDFILE"
exit 0
fi
if start-stop-daemon --stop --signal INT --quiet --pidfile $PIDFILE --exec $DAEMON; then
rm -f $PIDFILE
elif kill -0 $pid 2>/dev/null; then
log_action_end_msg 1 "Is $pid not $NAME? Is $DAEMON a different binary now?"
exit 1
else
log_action_end_msg 1 "$DAEMON died: process $pid not running; or permission denied"
exit 1
fi
;;
reload)
echo "Not implemented, use restart"
exit 1
;;
restart|force-reload)
$0 stop
$0 start
;;
status)
if test ! -r $(dirname $PIDFILE); then
log_failure_msg "cannot read PID file $PIDFILE"
exit 4
fi
pid=$(cat $PIDFILE 2>/dev/null) || true
if test ! -f $PIDFILE -o -z "$pid"; then
log_failure_msg "$NAME is not running"
exit 3
fi
if ps "$pid" >/dev/null 2>&1; then
log_success_msg "$NAME is running"
exit 0
else
log_failure_msg "$NAME is not running"
exit 1
fi
;;
*)
log_action_msg "Usage: $0 {start|stop|restart|force-reload|status}" >&2
exit 1
;;
esac
exit 0

View File

@ -0,0 +1,44 @@
#
# Replace /var/lib/djbdns if needed
#
[Unit]
Description=tinydns DNS resolver
Documentation=man:tinydns
Documentation=https://cr.yp.to/djbdns.html
After=network.target
Requires=network.target
Wants=network.target
ConditionPathExists=/var/lib/djbdns
[Service]
Type=forking
PIDFile=/run/tinydns.pid
Environment="ROOT=/var/lib/djbdns"
ExecStart=/usr/bin/tinydns
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry=TERM/5/KILL/5 --pidfile /run/tinydns.pid
TimeoutStopSec=30
KillMode=mixed
PermissionsStartOnly=true
Restart=on-abnormal
RestartSec=2s
LimitNOFILE=65536
WorkingDirectory=/var/lib/djbdns
User=$ug_name
Group=$ug_name
# Hardening
# CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN CAP_FOWNER
NoNewPrivileges=yes
PrivateDevices=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=full
# TODO: restrict ReadOnlyDirectories
ReadOnlyDirectories=/
ReadWriteDirectories=-/var/lib/djbdns
[Install]
WantedBy=multi-user.target

View File

@ -47,7 +47,7 @@ OPTS = [
cfg.ListOpt('masters', default=[],
help='List of masters for the Agent, format ip:port'),
cfg.StrOpt('backend-driver', default='bind9',
help='The backend driver to use: bind9 or knot2'),
help='The backend driver to use, e.g. bind9, djbdns, knot2'),
cfg.StrOpt('transfer-source',
help='An IP address to be used to fetch zones transferred in'),
cfg.FloatOpt('notify-delay', default=0.0,

View File

@ -0,0 +1,350 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Author: Federico Ceratto <federico.ceratto@hpe.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
backend.agent_backend.impl_djbdns
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Djbdns DNS agent backend
Create, update, delete zones locally on a Djbdns DNS resolver using the
axfr-get utility.
`User documentation <backends/djbdns_agent.html>`_
.. WARNING::
Untested, do not use in production.
Configured in [service:agent:djbdns]
Requires rootwrap (or equivalent sudo privileges) to execute:
- tcpclient
- axfr-get
- tinydns-data
"""
import glob
import os
import random
import tempfile
import dns
import dns.resolver
from oslo_concurrency import lockutils
from oslo_concurrency.processutils import ProcessExecutionError
from oslo_config import cfg
from oslo_log import log as logging
from designate import exceptions
from designate import utils
from designate.backend.agent_backend import base
from designate.i18n import _LI
from designate.i18n import _LE
from designate.utils import execute
LOG = logging.getLogger(__name__)
CFG_GROUP = 'backend:agent:djbdns'
# rootwrap requires a command name instead of full path
TCPCLIENT_DEFAULT_PATH = 'tcpclient'
AXFR_GET_DEFAULT_PATH = 'axfr-get'
TINYDNS_DATA_DEFAULT_PATH = 'tinydns-data'
TINYDNS_DATADIR_DEFAULT_PATH = '/var/lib/djbdns'
SOA_QUERY_TIMEOUT = 1
# TODO(Federico) on zone creation and update, agent.handler unnecessarily
# perfors AXFR from MiniDNS to the Agent to populate the `zone` argument
# (needed by the Bind backend)
def filter_exceptions(fn):
# Let Backend() exceptions pass through, log out every other exception
# and re-raise it as Backend()
def wrapper(*a, **kw):
try:
return fn(*a, **kw)
except exceptions.Backend as e:
raise e
except Exception as e:
LOG.error(_LE("Unhandled exception %s"), str(e), exc_info=True)
raise exceptions.Backend(str(e))
return wrapper
class DjbdnsBackend(base.AgentBackend):
__plugin_name__ = 'djbdns'
__backend_status__ = 'experimental'
@classmethod
def get_cfg_opts(cls):
group = cfg.OptGroup(
name='backend:agent:djbdns',
title="Configuration for Djbdns backend"
)
opts = [
cfg.StrOpt(
'tcpclient-cmd-name',
help='tcpclient executable path or rootwrap command name',
default=TCPCLIENT_DEFAULT_PATH
),
cfg.StrOpt(
'axfr-get-cmd-name',
help='axfr-get executable path or rootwrap command name',
default=AXFR_GET_DEFAULT_PATH
),
cfg.StrOpt(
'tinydns-data-cmd-name',
help='tinydns-data executable path or rootwrap command name',
default=TINYDNS_DATA_DEFAULT_PATH
),
cfg.StrOpt(
'tinydns-datadir',
help='TinyDNS data directory',
default=TINYDNS_DATADIR_DEFAULT_PATH
),
cfg.StrOpt('query-destination', default='127.0.0.1',
help='Host to query when finding zones')
]
return [(group, opts)]
def __init__(self, *a, **kw):
"""Configure the backend"""
super(DjbdnsBackend, self).__init__(*a, **kw)
self._resolver = dns.resolver.Resolver(configure=False)
self._resolver.timeout = SOA_QUERY_TIMEOUT
self._resolver.lifetime = SOA_QUERY_TIMEOUT
self._resolver.nameservers = [cfg.CONF[CFG_GROUP].query_destination]
self._masters = [utils.split_host_port(ns)
for ns in cfg.CONF['service:agent'].masters]
LOG.info(_LI("Resolvers: %r"), self._resolver.nameservers)
LOG.info(_LI("AXFR masters: %r"), self._masters)
if not self._masters:
raise exceptions.Backend("Missing agent AXFR masters")
self._tcpclient_cmd_name = cfg.CONF[CFG_GROUP].tcpclient_cmd_name
self._axfr_get_cmd_name = cfg.CONF[CFG_GROUP].axfr_get_cmd_name
# Directory where data.cdb lives, usually /var/lib/djbdns/root
tinydns_root_dir = os.path.join(cfg.CONF[CFG_GROUP].tinydns_datadir,
'root')
# Usually /var/lib/djbdns/root/data.cdb
self._tinydns_cdb_filename = os.path.join(tinydns_root_dir, 'data.cdb')
LOG.info(_LI("data.cdb path: %r"), self._tinydns_cdb_filename)
# Where the agent puts the zone datafiles,
# usually /var/lib/djbdns/datafiles
self._datafiles_dir = datafiles_dir = os.path.join(
cfg.CONF[CFG_GROUP].tinydns_datadir,
'datafiles')
self._datafiles_tmp_path_tpl = os.path.join(datafiles_dir, "%s.ztmp")
self._datafiles_path_tpl = os.path.join(datafiles_dir, "%s.zonedata")
self._datafiles_path_glob = self._datafiles_path_tpl % '*'
self._check_dirs(tinydns_root_dir, datafiles_dir)
@staticmethod
def _check_dirs(*dirnames):
"""Check if directories are writable
"""
for dn in dirnames:
if not os.path.isdir(dn):
raise exceptions.Backend("Missing directory %s" % dn)
if not os.access(dn, os.W_OK):
raise exceptions.Backend("Directory not writable: %s" % dn)
def start(self):
"""Start the backend"""
LOG.info(_LI("Started djbdns backend"))
def find_zone_serial(self, zone_name):
"""Query the local resolver for a zone
Times out after SOA_QUERY_TIMEOUT
"""
LOG.debug("Finding %s", zone_name)
try:
rdata = self._resolver.query(
zone_name, rdtype=dns.rdatatype.SOA)[0]
return rdata.serial
except Exception:
return None
@staticmethod
def _concatenate_zone_datafiles(data_fn, path_glob):
"""Concatenate all zone datafiles into 'data'
"""
with open(data_fn, 'w') as data_f:
zone_cnt = 0
for zone_fn in glob.glob(path_glob):
zone_cnt += 1
with open(zone_fn) as zf:
data_f.write(zf.read())
LOG.info(_LI("Loaded %d zone datafiles."), zone_cnt)
def _rebuild_data_cdb(self):
"""Rebuild data.cdb file from zone datafiles
Requires global lock
On zone creation, axfr-get creates datafiles atomically by doing
rename. On zone deletion, os.remove deletes the file atomically
Globbing and reading the datafiles can be done without locking on
them.
The data and data.cdb files are written into a unique temp directory
"""
tmpdir = tempfile.mkdtemp(dir=self._datafiles_dir)
data_fn = os.path.join(tmpdir, 'data')
tmp_cdb_fn = os.path.join(tmpdir, 'data.cdb')
try:
self._concatenate_zone_datafiles(data_fn,
self._datafiles_path_glob)
# Generate the data.cdb file
LOG.info(_LI("Updating data.cdb"))
LOG.debug("Convert %s to %s", data_fn, tmp_cdb_fn)
try:
out, err = execute(
cfg.CONF[CFG_GROUP].tinydns_data_cmd_name,
cwd=tmpdir
)
except ProcessExecutionError as e:
LOG.error(_LE("Failed to generate data.cdb"))
LOG.error(_LE("Command output: %(out)r Stderr: %(err)r"), {
'out': e.stdout, 'err': e.stderr
})
raise exceptions.Backend("Failed to generate data.cdb")
LOG.debug("Move %s to %s", tmp_cdb_fn, self._tinydns_cdb_filename)
try:
os.rename(tmp_cdb_fn, self._tinydns_cdb_filename)
except OSError:
os.remove(tmp_cdb_fn)
LOG.error(_LE("Unable to move data.cdb to %s"),
self._tinydns_cdb_filename)
raise exceptions.Backend("Unable to move data.cdb")
finally:
try:
os.remove(data_fn)
except OSError:
pass
try:
os.removedirs(tmpdir)
except OSError:
pass
def _perform_axfr_from_minidns(self, zone_name):
"""Instruct axfr-get to request an AXFR from MiniDNS.
:raises: exceptions.Backend on error
"""
zone_fn = self._datafiles_path_tpl % zone_name
zone_tmp_fn = self._datafiles_tmp_path_tpl % zone_name
# Perform AXFR, create or update a zone datafile
# No need to lock globally here.
# Axfr-get creates the datafile atomically by doing rename
mdns_hostname, mdns_port = random.choice(self._masters)
with lockutils.lock("%s.lock" % zone_name):
LOG.debug("writing to %s", zone_fn)
cmd = (
self._tcpclient_cmd_name,
mdns_hostname,
"%d" % mdns_port,
self._axfr_get_cmd_name,
zone_name,
zone_fn,
zone_tmp_fn
)
LOG.debug("Executing AXFR as %r", ' '.join(cmd))
try:
out, err = execute(*cmd)
except ProcessExecutionError as e:
LOG.error(_LE("Error executing AXFR as %r"), ' '.join(cmd))
LOG.error(_LE("Command output: %(out)r Stderr: %(err)r"), {
'out': e.stdout, 'err': e.stderr
})
raise exceptions.Backend(str(e))
finally:
try:
os.remove(zone_tmp_fn)
except OSError:
pass
@filter_exceptions
def create_zone(self, zone):
"""Create a new Zone
Do not raise exceptions if the zone already exists.
:param zone: zone to be created
:type zone: raw pythondns Zone
:raises: exceptions.Backend on error
"""
zone_name = zone.origin.to_text().rstrip('.')
LOG.debug("Creating %s", zone_name)
# The zone might be already in place due to a race condition between
# checking if the zone is there and creating it across different
# greenlets
LOG.debug("Triggering initial AXFR from MiniDNS to Djbdns for %s",
zone_name)
self._perform_axfr_from_minidns(zone_name)
self._rebuild_data_cdb()
@filter_exceptions
def update_zone(self, zone):
"""Instruct Djbdns DNS to perform AXFR from MiniDNS
:param zone: zone to be created
:type zone: raw pythondns Zone
:raises: exceptions.Backend on error
"""
zone_name = zone.origin.to_text().rstrip('.')
LOG.debug("Triggering AXFR from MiniDNS to Djbdns for %s", zone_name)
self._perform_axfr_from_minidns(zone_name)
self._rebuild_data_cdb()
@filter_exceptions
def delete_zone(self, zone_name):
"""Delete a new Zone
Do not raise exceptions if the zone does not exist.
:param zone_name: zone name
:type zone_name: str
:raises: exceptions.Backend on error
"""
zone_name = zone_name.rstrip('.')
LOG.debug('Deleting Zone: %s', zone_name)
zone_fn = self._datafiles_path_tpl % zone_name
try:
os.remove(zone_fn)
LOG.debug('Deleted Zone: %s', zone_name)
except OSError as e:
if os.errno.ENOENT == e.errno:
LOG.info(_LI("Zone datafile %s was already deleted"), zone_fn)
return
raise
self._rebuild_data_cdb()

View File

@ -0,0 +1,127 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Author: Federico Ceratto <federico.ceratto@hpe.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Test the Djbdns agent backend
These tests *do* rely on creating directories and files or running
executables from the djbdns suite
If djbdns is not available some tests are skipped.
"""
import os
import tempfile
import unittest
import fixtures
import mock
from designate import exceptions
from designate.backend.agent_backend.impl_djbdns import DjbdnsBackend
from designate.tests import TestCase
import designate.backend.agent_backend.impl_djbdns
TINYDNSDATA_PATH = '/usr/bin/tinydns-data'
class DjbdnsAgentBackendSimpleTestCase(TestCase):
def test__check_dirs(self):
DjbdnsBackend._check_dirs('/tmp')
def test__check_dirs_not_found(self):
self.assertRaises(
exceptions.Backend,
DjbdnsBackend._check_dirs,
'/nonexistent_dir_name'
)
class DjbdnsAgentBackendTestCase(TestCase):
def setUp(self):
super(DjbdnsAgentBackendTestCase, self).setUp()
self.CONF.set_override('masters', ('127.0.0.1:5354',), 'service:agent')
tmp_datafiles_dir = tempfile.mkdtemp()
os.mkdir(os.path.join(tmp_datafiles_dir, 'datafiles'))
self.CONF.set_override(
'tinydns_datadir',
tmp_datafiles_dir,
designate.backend.agent_backend.impl_djbdns.CFG_GROUP
)
self.useFixture(fixtures.MockPatchObject(
DjbdnsBackend, '_check_dirs'
))
self.backend = DjbdnsBackend('foo')
self.patch_ob(self.backend._resolver, 'query')
def tearDown(self):
super(DjbdnsAgentBackendTestCase, self).tearDown()
def patch_ob(self, *a, **kw):
self.useFixture(fixtures.MockPatchObject(*a, **kw))
@mock.patch('designate.backend.agent_backend.impl_djbdns.os.remove')
@mock.patch('designate.backend.agent_backend.impl_djbdns.execute')
def test__perform_axfr_from_minidns(self, mock_exe, mock_rm):
mock_exe.return_value = (None, None)
self.backend._perform_axfr_from_minidns('foo')
mock_exe.assert_called_once_with(
'tcpclient', '127.0.0.1', '5354', 'axfr-get', 'foo',
os.path.join(self.backend._datafiles_dir, 'foo.zonedata'),
os.path.join(self.backend._datafiles_dir, 'foo.ztmp')
)
def test_delete_zone_no_file(self):
self.patch_ob(self.backend, '_rebuild_data_cdb')
# Should not raise exceptions
self.backend.delete_zone('non_existent_zone_file')
@unittest.skipIf(not os.path.isfile(TINYDNSDATA_PATH),
"tinydns-data not installed")
def test__rebuild_data_cdb_empty(self):
# Check that tinydns-data can be run and the required files are
# generated / renamed as needed
self.CONF.set_override('root_helper', ' ') # disable rootwrap
self.backend._tinydns_cdb_filename = tempfile.mkstemp()[1]
self.backend._rebuild_data_cdb()
assert os.path.isfile(self.backend._tinydns_cdb_filename)
os.remove(self.backend._tinydns_cdb_filename)
@unittest.skipIf(not os.path.isfile(TINYDNSDATA_PATH),
"tinydns-data not installed")
def test__rebuild_data_cdb(self):
# Check that tinydns-data can be run and the required files are
# generated / renamed as needed
self.CONF.set_override('root_helper', ' ') # disable rootwrap
self.backend._tinydns_cdb_filename = tempfile.mkstemp()[1]
fn = os.path.join(self.backend._datafiles_dir, 'example.org.zonedata')
with open(fn, 'w') as f:
f.write(""".example.org::ns1.example.org
+ns1.example.org:127.0.0.1
+www.example.org:127.0.0.1
""")
self.backend._rebuild_data_cdb()
assert os.path.isfile(self.backend._tinydns_cdb_filename)
os.remove(self.backend._tinydns_cdb_filename)

View File

@ -0,0 +1,126 @@
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Author: Federico Ceratto <federico.ceratto@hpe.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Unit-test the Djbdns agent backend
These tests do not rely on creating directories and files or running
executables from the djbdns suite
"""
import dns.zone
import fixtures
import mock
from designate import exceptions
from designate.backend.agent_backend.impl_djbdns import DjbdnsBackend
from designate.tests import TestCase
import designate.backend.agent_backend.impl_djbdns # noqa
class DjbdnsAgentBackendUnitTestCase(TestCase):
def setUp(self):
super(DjbdnsAgentBackendUnitTestCase, self).setUp()
self.CONF.set_override('masters', ('127.0.0.1:5354',), 'service:agent')
self.useFixture(fixtures.MockPatchObject(
DjbdnsBackend, '_check_dirs'
))
self.backend = DjbdnsBackend('foo')
self.patch_ob(self.backend._resolver, 'query')
def tearDown(self):
super(DjbdnsAgentBackendUnitTestCase, self).tearDown()
def _create_dnspy_zone(self, name):
zone_text = (
'$ORIGIN %(name)s\n%(name)s 3600 IN SOA %(ns)s '
'email.email.com. 1421777854 3600 600 86400 3600\n%(name)s '
'3600 IN NS %(ns)s\n') % {'name': name, 'ns': 'ns1.designate.com'}
return dns.zone.from_text(zone_text, check_origin=False)
def patch_ob(self, *a, **kw):
self.useFixture(fixtures.MockPatchObject(*a, **kw))
def test_init(self):
self.assertTrue(hasattr(self.backend, '_resolver'))
self.assertEqual(1, self.backend._resolver.timeout)
self.assertEqual(1, self.backend._resolver.lifetime)
self.assertEqual(['127.0.0.1'], self.backend._resolver.nameservers)
self.assertEqual('/var/lib/djbdns/root/data.cdb',
self.backend._tinydns_cdb_filename)
self.assertEqual('/var/lib/djbdns/datafiles',
self.backend._datafiles_dir)
self.assertEqual('/var/lib/djbdns/datafiles/%s.zonedata',
self.backend._datafiles_path_tpl)
self.assertEqual([('127.0.0.1', 5354)], self.backend._masters)
def test_find_zone_serial(self):
class Data(object):
serial = 3
self.backend._resolver.query.return_value = [Data(), ]
serial = self.backend.find_zone_serial('example.com')
self.assertEqual(3, serial)
def test_find_zone_serial_error(self):
self.backend._resolver.query.side_effect = RuntimeError('foo')
serial = self.backend.find_zone_serial('example.com')
self.assertEqual(None, serial)
@mock.patch('designate.backend.agent_backend.impl_djbdns.execute')
def test_create_zone(self, mock_exe):
self.patch_ob(self.backend, '_perform_axfr_from_minidns')
self.patch_ob(self.backend, '_rebuild_data_cdb')
zone = self._create_dnspy_zone('example.org')
self.backend.create_zone(zone)
def test_update_zone(self):
self.patch_ob(self.backend, '_perform_axfr_from_minidns')
self.patch_ob(self.backend, '_rebuild_data_cdb')
zone = self._create_dnspy_zone('example.org')
self.backend.update_zone(zone)
@mock.patch('designate.backend.agent_backend.impl_djbdns.os.remove')
def test_delete_zone(self, mock_rm):
self.patch_ob(self.backend, '_rebuild_data_cdb')
self.backend.delete_zone('foo')
mock_rm.assert_called_once_with(
'/var/lib/djbdns/datafiles/foo.zonedata'
)
@mock.patch('designate.backend.agent_backend.impl_djbdns.os.remove')
def test_exception_filter(self, *mocks):
self.patch_ob(self.backend, '_rebuild_data_cdb')
self.assertRaises(
exceptions.Backend,
self.backend.delete_zone,
None
)
@mock.patch('designate.backend.agent_backend.impl_djbdns.os.remove')
def test_exception_filter_pass_through(self, mock_rm):
self.patch_ob(self.backend, '_rebuild_data_cdb')
mock_rm.side_effect = exceptions.Backend
self.assertRaises(
exceptions.Backend,
self.backend.delete_zone,
'foo'
)

View File

@ -87,3 +87,12 @@ Agent Backend KnotDNS
:undoc-members:
:show-inheritance:
Agent Backend Djbdns
====================
.. automodule:: designate.backend.agent_backend.impl_djbdns
:members:
:special-members:
:private-members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,132 @@
..
Copyright 2016 Hewlett Packard Enterprise Development Company LP
Author: Federico Ceratto <federico.ceratto@hpe.com>
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Djbdns Agent backend
********************
User documentation
==================
This page documents the Agent backend for `djbdns <https://cr.yp.to/djbdns.html>`_.
The agent runs on the same host as the `tinydns <https://cr.yp.to/djbdns/tinydns.html>`_ resolver.
It receives DNS messages from Mini DNS using private DNS OPCODEs and classes and creates or deletes
zones in the data.cdb file using `axfr-get <https://cr.yp.to/djbdns/axfr-get.html>`_ and
`tinydns-data <https://cr.yp.to/djbdns/tinydns-data.html>`_
Setting up Djbdns on Ubuntu Trusty
------------------------------------
Assuming no DNS resolver is already installed, run as root:
.. code-block:: bash
set -u
datadir=/var/lib/djbdns
ug_name=djbdns
tinydns_ipaddr=127.0.0.1
[[ -d $datadir ]] && echo "$datadir already exists" && exit 1
set -e
apt-get update
apt-get install dbndns daemontools
if ! getent passwd $ug_name >/dev/null; then
adduser --quiet --system --group --no-create-home --home /nonexistent $ug_name
fi
tinydns-conf $ug_name $ug_name $datadir $tinydns_ipaddr
cd $datadir/root
tinydns-data data
chown -Rv $ug_name:$ug_name $datadir
Setup the a Systemd service or, alternatively, an initfile to start TinyDNS.
In the contrib/djbdns directory there are example files for both.
.. code-block:: bash
systemctl daemon-reload
service tinydns start
service tinydns status
If needed, create the rootwrap filters, as root:
.. code-block:: bash
cat > /etc/designate/rootwrap.d/djbdns.filters <<EOF
# cmd-name: filter-name, raw-command, user, args
[Filters]
tcpclient: CommandFilter, /usr/bin/tcpclient, root
axfr-get: CommandFilter, /usr/bin/axfr-get, root
EOF
# Check the filter:
sudo /usr/local/bin/designate-rootwrap /etc/designate/rootwrap.conf tcpclient -h
sudo /usr/local/bin/designate-rootwrap /etc/designate/rootwrap.conf axfr-get -h
Configure the "service.agent" and "backend.agent.djbdns" sections in /etc/designate/designate.conf
Look in designate.conf.example for examples.
Create an agent pool:
.. code-block:: bash
# Fetch the existing pool(s) if needed or start from scratch
designate-manage pool generate_file --file /tmp/pool.yaml
# Edit the file (see below) and reload it as:
designate-manage pool update --file /tmp/pool.yaml
The "targets" section in pool.yaml should look like:
.. code-block:: ini
targets:
- description: gdnsd agent
masters:
- host: <MiniDNS IP addr>
port: 5354
options: {}
options:
- host: <Agent IP addr>
port: 5358
type: agent
Testing
^^^^^^^
Create new zones and records. Monitor the agent logfile and the contents of the
TinyDNS datadir. The data.cdb file should be receiving updates.
.. code-block:: bash
openstack zone create --email example@example.org example.org.
openstack recordset create example.org. --type A foo --records 1.2.3.4
dig example.org @<tinydns_ipaddr> SOA
dig foo.example.org @<tinydns_ipaddr> A
Developer documentation
=======================
Devstack testbed
----------------
Follow "Setting up Djbdns on Ubuntu Trusty"
Configure Tinydns to do AXFR from MiniDNS on 192.168.121.131

View File

@ -54,6 +54,7 @@ backend-impl-agent=Agent
backend-impl-bind9-agent=Bind9 (Agent)
backend-impl-denominator=Denominator
backend-impl-knot2-agent=Knot2 (Agent)
backend-impl-djbdns-agent=Djbdns (Agent)
[backends.backend-impl-bind9]
@ -79,6 +80,9 @@ type=agent
[backends.backend-impl-knot2-agent]
type=agent
[backends.backend-impl-djbdns-agent]
type=agent
[backends.backend-impl-infoblox-xfr]
status=release-compatible
maintainers=Infoblox OpenStack Team <openstack-maintainer@infoblox.com>

View File

@ -465,6 +465,15 @@ debug = False
# knotc command name when rootwrap is used. Location of the knotc executable
# on the resolver host if rootwrap is not used
#knotc_cmd_name = /usr/sbin/knotc
#
[backend:agent:djbdns]
# Command names when rootwrap is used or location of the executables
# on the resolver host when rootwrap is not used
# tcpclient_cmd_name =
# axfr_get_cmd_name =
# tinydns_data_cmd_name =
# tinydns_datadir =
#query_destination = 127.0.0.1
[backend:agent:denominator]
#name = dynect

View File

@ -0,0 +1,4 @@
[Filters]
tcpclient: CommandFilter, /usr/bin/tcpclient, root
axfr-get: CommandFilter, /usr/bin/axfr-get, root
tinydns-data: CommandFilter, /usr/bin/tinydns-data, root

View File

@ -0,0 +1,4 @@
---
features:
- An experimental agent backend to support TinyDNS, the DNS resolver
from the djbdns tools.

View File

@ -92,6 +92,7 @@ designate.backend =
designate.backend.agent_backend =
bind9 = designate.backend.agent_backend.impl_bind9:Bind9Backend
knot2 = designate.backend.agent_backend.impl_knot2:Knot2Backend
djbdns = designate.backend.agent_backend.impl_djbdns:DjbdnsBackend
denominator = designate.backend.agent_backend.impl_denominator:DenominatorBackend
fake = designate.backend.agent_backend.impl_fake:FakeBackend