Merge "Add djbdns backend"
This commit is contained in:
commit
ab121e66b9
110
contrib/djbdns/tinydns.init
Executable file
110
contrib/djbdns/tinydns.init
Executable 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
|
44
contrib/djbdns/tinydns.service
Normal file
44
contrib/djbdns/tinydns.service
Normal 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
|
@ -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,
|
||||
|
350
designate/backend/agent_backend/impl_djbdns.py
Normal file
350
designate/backend/agent_backend/impl_djbdns.py
Normal 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()
|
127
designate/tests/test_agent/test_backends/test_djbdns.py
Normal file
127
designate/tests/test_agent/test_backends/test_djbdns.py
Normal 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)
|
126
designate/tests/unit/test_agent/test_backends/test_djbdns.py
Normal file
126
designate/tests/unit/test_agent/test_backends/test_djbdns.py
Normal 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'
|
||||
)
|
@ -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:
|
||||
|
132
doc/source/backends/djbdns_agent.rst
Normal file
132
doc/source/backends/djbdns_agent.rst
Normal 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
|
@ -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>
|
||||
|
@ -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
|
||||
|
4
etc/designate/rootwrap.d/djbdns.filters
Normal file
4
etc/designate/rootwrap.d/djbdns.filters
Normal 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
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- An experimental agent backend to support TinyDNS, the DNS resolver
|
||||
from the djbdns tools.
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user