217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
# 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_knot2
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
Knot DNS agent backend
|
|
|
|
Create, update, delete zones locally on a Knot DNS resolver using the
|
|
knotc utility.
|
|
|
|
Supported Knot versions: >= 2.1, < 3
|
|
|
|
`User documentation <backends/knot2_agent.html>`_
|
|
|
|
.. WARNING::
|
|
|
|
Untested, do not use in production.
|
|
|
|
.. NOTE::
|
|
|
|
If the backend is killed during a configuration transaction it might be
|
|
required to manually abort the transaction with `sudo knotc conf-abort`
|
|
|
|
Configured in [service:agent:knot2]
|
|
"""
|
|
|
|
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.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:knot2'
|
|
# rootwrap requires a command name instead of full path
|
|
KNOTC_DEFAULT_PATH = 'knotc'
|
|
|
|
# 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)
|
|
|
|
|
|
class Knot2Backend(base.AgentBackend):
|
|
__plugin_name__ = 'knot2'
|
|
__backend_status__ = 'untested'
|
|
_lock_name = 'knot2.lock'
|
|
|
|
@classmethod
|
|
def get_cfg_opts(cls):
|
|
group = cfg.OptGroup(
|
|
name='backend:agent:knot2', title="Configuration for Knot2 backend"
|
|
)
|
|
opts = [
|
|
cfg.StrOpt('knotc-cmd-name',
|
|
help='knotc executable path or rootwrap command name',
|
|
default=KNOTC_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(Knot2Backend, self).__init__(*a, **kw)
|
|
|
|
self._knotc_cmd_name = cfg.CONF[CFG_GROUP].knotc_cmd_name
|
|
|
|
def start(self):
|
|
"""Start the backend"""
|
|
LOG.info(_LI("Started knot2 backend"))
|
|
|
|
def _execute_knotc(self, *knotc_args, **kw):
|
|
"""Run the Knot client and check the output
|
|
|
|
:param expected_output: expected output (default: 'OK')
|
|
:type expected_output: str
|
|
:param expected_error: expected alternative output, will be \
|
|
logged as info(). Default: not set.
|
|
:type expected_error: str
|
|
"""
|
|
# Knotc returns "0" even on failure, we have to check for 'OK'
|
|
# https://gitlab.labs.nic.cz/labs/knot/issues/456
|
|
|
|
LOG.debug("Executing knotc with %r", knotc_args)
|
|
expected = kw.get('expected_output', 'OK')
|
|
expected_alt = kw.get('expected_error', None)
|
|
try:
|
|
out, err = execute(self._knotc_cmd_name, *knotc_args)
|
|
out = out.rstrip()
|
|
LOG.debug("Command output: %r" % out)
|
|
if out != expected:
|
|
if expected_alt is not None and out == expected_alt:
|
|
LOG.info(_LI("Ignoring error: %r"), out)
|
|
else:
|
|
raise ProcessExecutionError(stdout=out, stderr=err)
|
|
|
|
except ProcessExecutionError as e:
|
|
LOG.error(_LE("Command output: %(out)r Stderr: %(err)r"), {
|
|
'out': e.stdout, 'err': e.stderr
|
|
})
|
|
raise exceptions.Backend(e)
|
|
|
|
def _start_minidns_to_knot_axfr(self, zone_name):
|
|
"""Instruct Knot to request an AXFR from MiniDNS. No need to lock
|
|
or enter a configuration transaction.
|
|
"""
|
|
self._execute_knotc('zone-refresh', zone_name)
|
|
|
|
def _modify_zone(self, *knotc_args, **kw):
|
|
"""Create or delete a zone while locking, and within a
|
|
Knot transaction.
|
|
Knot supports only one config transaction at a time.
|
|
|
|
:raises: exceptions.Backend
|
|
"""
|
|
with lockutils.lock(self._lock_name):
|
|
self._execute_knotc('conf-begin')
|
|
try:
|
|
self._execute_knotc(*knotc_args, **kw)
|
|
# conf-diff can be used for debugging
|
|
# self._execute_knotc('conf-diff')
|
|
except Exception as e:
|
|
self._execute_knotc('conf-abort')
|
|
LOG.info(_LI("Zone change aborted: %r"), e)
|
|
raise e
|
|
else:
|
|
self._execute_knotc('conf-commit')
|
|
|
|
def find_zone_serial(self, zone_name):
|
|
"""Get serial from a zone by running knotc
|
|
|
|
:returns: serial (int or None)
|
|
:raises: exceptions.Backend
|
|
"""
|
|
zone_name = zone_name.rstrip('.')
|
|
LOG.debug("Finding %s", zone_name)
|
|
# Output example:
|
|
# [530336536.com.] type: slave | serial: 0 | next-event: idle |
|
|
# auto-dnssec: disabled]
|
|
try:
|
|
out, err = execute(self._knotc_cmd_name, 'zone-status', zone_name)
|
|
except ProcessExecutionError as e:
|
|
if 'no such zone' in e.stdout:
|
|
# Zone not found
|
|
return None
|
|
|
|
LOG.error(_LE("Command output: %(out)r Stderr: %(err)r"), {
|
|
'out': e.stdout, 'err': e.stderr
|
|
})
|
|
raise exceptions.Backend(e)
|
|
|
|
try:
|
|
serial = out.split('|')[1].split()[1]
|
|
return int(serial)
|
|
except Exception as e:
|
|
LOG.error(_LE("Unable to parse knotc output: %r"), out)
|
|
raise exceptions.Backend("Unexpected knotc zone-status output")
|
|
|
|
def create_zone(self, zone):
|
|
"""Create a new Zone by executing knotc
|
|
Do not raise exceptions if the zone already exists.
|
|
|
|
:param zone: zone to be created
|
|
:type zone: raw pythondns Zone
|
|
"""
|
|
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
|
|
self._modify_zone('conf-set', 'zone[%s]' % zone_name,
|
|
expected_error='duplicate identifier')
|
|
|
|
LOG.debug("Triggering initial AXFR from MiniDNS to Knot for %s",
|
|
zone_name)
|
|
self._start_minidns_to_knot_axfr(zone_name)
|
|
|
|
def update_zone(self, zone):
|
|
"""Instruct Knot DNS to perform AXFR from MiniDNS
|
|
|
|
:param zone: zone to be created
|
|
:type zone: raw pythondns Zone
|
|
"""
|
|
zone_name = zone.origin.to_text()
|
|
LOG.debug("Triggering AXFR from MiniDNS to Knot for %s", zone_name)
|
|
self._start_minidns_to_knot_axfr(zone_name)
|
|
|
|
def delete_zone(self, zone_name):
|
|
"""Delete a new Zone by executing knotc
|
|
Do not raise exceptions if the zone does not exist.
|
|
|
|
:param zone_name: zone name
|
|
:type zone_name: str
|
|
"""
|
|
LOG.debug('Delete Zone: %s' % zone_name)
|
|
self._modify_zone('conf-unset', 'zone[%s]' % zone_name,
|
|
expected_error='invalid identifier')
|