summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFederico Ceratto <federico.ceratto@hpe.com>2016-04-26 16:47:04 +0100
committerFederico Ceratto <federico.ceratto@gmail.com>2016-05-20 10:46:36 +0000
commitbe7e32dfaa8f2884ac89bf7335da9b309fcdc861 (patch)
tree2b8e24bf7b08cae819418bfe26a7099de99a3111
parent6ae192335bdef42b9b55cba06f68c936eeccb665 (diff)
downloaddesignate-be7e32dfaa8f2884ac89bf7335da9b309fcdc861.tar.gz
Add djbdns backend
Add docs and basic tests Update config sample file and support matrix Change-Id: I709cea4e321f6bbee3b0f9f718fa6a9836af3ca5
-rwxr-xr-xcontrib/djbdns/tinydns.init110
-rw-r--r--contrib/djbdns/tinydns.service44
-rw-r--r--designate/agent/__init__.py2
-rw-r--r--designate/backend/agent_backend/impl_djbdns.py350
-rw-r--r--designate/tests/test_agent/test_backends/test_djbdns.py127
-rw-r--r--designate/tests/unit/test_agent/test_backends/test_djbdns.py126
-rw-r--r--doc/source/backend.rst9
-rw-r--r--doc/source/backends/djbdns_agent.rst132
-rw-r--r--doc/source/support-matrix.ini4
-rw-r--r--etc/designate/designate.conf.sample9
-rw-r--r--etc/designate/rootwrap.d/djbdns.filters4
-rw-r--r--releasenotes/notes/djbdns-agent-backend-c84e9eeab48d2e01.yaml4
-rw-r--r--setup.cfg1
13 files changed, 921 insertions, 1 deletions
diff --git a/contrib/djbdns/tinydns.init b/contrib/djbdns/tinydns.init
new file mode 100755
index 00000000..7bcbb477
--- /dev/null
+++ b/contrib/djbdns/tinydns.init
@@ -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
diff --git a/contrib/djbdns/tinydns.service b/contrib/djbdns/tinydns.service
new file mode 100644
index 00000000..2fcf9d2a
--- /dev/null
+++ b/contrib/djbdns/tinydns.service
@@ -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
diff --git a/designate/agent/__init__.py b/designate/agent/__init__.py
index 5dacb1d1..3dbb8d08 100644
--- a/designate/agent/__init__.py
+++ b/designate/agent/__init__.py
@@ -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,
diff --git a/designate/backend/agent_backend/impl_djbdns.py b/designate/backend/agent_backend/impl_djbdns.py
new file mode 100644
index 00000000..bf33bf5d
--- /dev/null
+++ b/designate/backend/agent_backend/impl_djbdns.py
@@ -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()
diff --git a/designate/tests/test_agent/test_backends/test_djbdns.py b/designate/tests/test_agent/test_backends/test_djbdns.py
new file mode 100644
index 00000000..f955ef41
--- /dev/null
+++ b/designate/tests/test_agent/test_backends/test_djbdns.py
@@ -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)
diff --git a/designate/tests/unit/test_agent/test_backends/test_djbdns.py b/designate/tests/unit/test_agent/test_backends/test_djbdns.py
new file mode 100644
index 00000000..a6979430
--- /dev/null
+++ b/designate/tests/unit/test_agent/test_backends/test_djbdns.py
@@ -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'
+ )
diff --git a/doc/source/backend.rst b/doc/source/backend.rst
index f3499ed8..59945c34 100644
--- a/doc/source/backend.rst
+++ b/doc/source/backend.rst
@@ -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:
diff --git a/doc/source/backends/djbdns_agent.rst b/doc/source/backends/djbdns_agent.rst
new file mode 100644
index 00000000..983f59b4
--- /dev/null
+++ b/doc/source/backends/djbdns_agent.rst
@@ -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
diff --git a/doc/source/support-matrix.ini b/doc/source/support-matrix.ini
index 29202f00..a9698b5f 100644
--- a/doc/source/support-matrix.ini
+++ b/doc/source/support-matrix.ini
@@ -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>
diff --git a/etc/designate/designate.conf.sample b/etc/designate/designate.conf.sample
index e1032355..3062f86f 100644
--- a/etc/designate/designate.conf.sample
+++ b/etc/designate/designate.conf.sample
@@ -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
diff --git a/etc/designate/rootwrap.d/djbdns.filters b/etc/designate/rootwrap.d/djbdns.filters
new file mode 100644
index 00000000..1471c932
--- /dev/null
+++ b/etc/designate/rootwrap.d/djbdns.filters
@@ -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
diff --git a/releasenotes/notes/djbdns-agent-backend-c84e9eeab48d2e01.yaml b/releasenotes/notes/djbdns-agent-backend-c84e9eeab48d2e01.yaml
new file mode 100644
index 00000000..4105a6c0
--- /dev/null
+++ b/releasenotes/notes/djbdns-agent-backend-c84e9eeab48d2e01.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - An experimental agent backend to support TinyDNS, the DNS resolver
+ from the djbdns tools.
diff --git a/setup.cfg b/setup.cfg
index 96413b78..1b4f3fe4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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