]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Renaming lib/pp_admintools/deploy_zones_from_pdns.py -> lib/pp_admintools/dns_deploy_...
authorFrank Brehm <frank@brehm-online.com>
Wed, 30 Mar 2022 06:59:18 +0000 (08:59 +0200)
committerFrank Brehm <frank@brehm-online.com>
Wed, 30 Mar 2022 06:59:18 +0000 (08:59 +0200)
bin/dns-deploy-zones
lib/pp_admintools/deploy_zones_from_pdns.py [deleted file]
lib/pp_admintools/dns_deploy_zones_app.py [new file with mode: 0644]

index 6d1495eaa8a5692d065ae2e44d866e782895c94f..9c23cc3a14d57b06713274e398a45a1cb5f8596a 100755 (executable)
@@ -43,10 +43,10 @@ module_dir = lib_dir.joinpath('pp_admintools')
 if module_dir.exists():
     sys.path.insert(0, str(lib_dir))
 
-from pp_admintools.deploy_zones_from_pdns import PpDeployZonesApp
+from pp_admintools.dns_deploy_zones_app import PpDeployZonesApp
 
 __author__ = 'Frank Brehm <frank.brehm@pixelpark.com>'
-__copyright__ = '(C) 2021 by Frank Brehm, Pixelpark GmbH, Berlin'
+__copyright__ = '(C) 2022 by Frank Brehm, Pixelpark GmbH, Berlin'
 
 appname = os.path.basename(sys.argv[0])
 
diff --git a/lib/pp_admintools/deploy_zones_from_pdns.py b/lib/pp_admintools/deploy_zones_from_pdns.py
deleted file mode 100644 (file)
index 2f27d76..0000000
+++ /dev/null
@@ -1,999 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-@author: Frank Brehm
-@contact: frank.brehm@pixelpark.com
-@copyright: © 2021 by Frank Brehm, Berlin
-@summary: A module for the application class for configuring named
-"""
-from __future__ import absolute_import
-
-import os
-import logging
-import logging.config
-import textwrap
-import re
-import shlex
-import copy
-import datetime
-import socket
-import tempfile
-import time
-import shutil
-import pipes
-
-from subprocess import Popen, TimeoutExpired, PIPE
-
-# Third party modules
-import six
-from pytz import timezone, UnknownTimeZoneError
-
-# Own modules
-from fb_tools.common import pp, to_str, to_bool
-
-from fb_tools.app import BaseApplication
-
-from .pdns_app import PpPDNSAppError, PpPDNSApplication
-
-from .pidfile import PidFileError, PidFile
-
-from .xlate import XLATOR
-
-__version__ = '0.7.4'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-
-
-# =============================================================================
-class PpDeployZonesError(PpPDNSAppError):
-    pass
-
-
-# =============================================================================
-class PpDeployZonesApp(PpPDNSApplication):
-    """
-    Class for a application 'dns-deploy-zones' for configuring slaves
-    of the BIND named daemon.
-    """
-
-    default_pidfile = '/run/dns-deploy-zones.pid'
-
-    default_named_conf_dir = '/etc'
-    default_named_zones_cfg_file = 'named.zones.conf'
-    default_named_basedir = '/var/named'
-    default_named_slavedir = 'slaves'
-
-    zone_masters_local = [
-        '217.66.53.87',
-    ]
-
-    zone_masters_public = [
-        '217.66.53.97',
-    ]
-
-    default_cmd_checkconf = '/usr/sbin/named-checkconf'
-    default_cmd_reload = '/usr/sbin/rndc reload'
-    default_cmd_status = '/usr/bin/systemctl status named.service'
-    default_cmd_start = '/usr/bin/systemctl start named.service'
-    default_cmd_restart = '/usr/bin/systemctl restart named.service'
-
-    re_ipv4_zone = re.compile(r'^((?:\d+\.)+)in-addr\.arpa\.$')
-    re_ipv6_zone = re.compile(r'^((?:[\da-f]\.)+)ip6\.arpa\.$')
-
-    re_block_comment = re.compile(r'/\*.*?\*/', re.MULTILINE | re.DOTALL)
-    re_line_comment = re.compile(r'(?://|#).*$', re.MULTILINE)
-
-    re_split_addresses = re.compile(r'[,;\s]+')
-    re_integer = re.compile(r'^\s*(\d+)\s*$')
-
-    re_rev = re.compile(r'^rev\.', re.IGNORECASE)
-    re_trail_dot = re.compile(r'\.+$')
-
-    open_args = {}
-    if six.PY3:
-        open_args = {
-            'encoding': 'utf-8',
-            'errors': 'surrogateescape',
-        }
-
-    # -------------------------------------------------------------------------
-    def __init__(self, appname=None, base_dir=None, version=__version__):
-
-        self.zones = {}
-        self.pidfile = None
-
-        self._show_simulate_opt = True
-
-        self.is_internal = False
-        self.named_listen_on_v6 = False
-        self.pidfile_name = self.default_pidfile
-
-        # Configuration files and directories
-        self.named_conf_dir = self.default_named_conf_dir
-        self._named_zones_cfg_file = self.default_named_zones_cfg_file
-        self.named_basedir = self.default_named_basedir
-        self._named_slavedir = self.default_named_slavedir
-
-        self.zone_masters = copy.copy(self.zone_masters_public)
-        self.masters_configured = False
-
-        self.tempdir = None
-        self.temp_zones_cfg_file = None
-        self.keep_tempdir = False
-        self.keep_backup = False
-
-        self.backup_suffix = (
-            '.' + datetime.datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S') + '.bak')
-
-        self.reload_necessary = False
-        self.restart_necessary = False
-
-        self.cmd_checkconf = self.default_cmd_checkconf
-        self.cmd_reload = self.default_cmd_reload
-        self.cmd_status = self.default_cmd_status
-        self.cmd_start = self.default_cmd_start
-        self.cmd_restart = self.default_cmd_restart
-
-        self.named_keys = {}
-        self.servers = {}
-
-        self.zone_tsig_key = None
-
-        self.files2replace = {}
-        self.moved_files = {}
-
-        description = _('Generation of the BIND9 configuration file for slave zones.')
-
-        super(PpDeployZonesApp, self).__init__(
-            appname=appname, version=version, description=description,
-            base_dir=base_dir, cfg_stems='dns-deploy-zones', environment="public",
-        )
-
-        self.post_init()
-
-    # -------------------------------------------
-    @property
-    def named_zones_cfg_file(self):
-        """The file for configuration of all own zones."""
-        return os.path.join(self.named_conf_dir, self._named_zones_cfg_file)
-
-    # -------------------------------------------
-    @property
-    def named_slavedir_rel(self):
-        """The directory for zone files of slave zones."""
-        return self._named_slavedir
-
-    # -------------------------------------------
-    @property
-    def named_slavedir_abs(self):
-        """The directory for zone files of slave zones."""
-        return os.path.join(self.named_basedir, self._named_slavedir)
-
-    # -------------------------------------------------------------------------
-    def init_arg_parser(self):
-
-        super(PpDeployZonesApp, self).init_arg_parser()
-
-        self.arg_parser.add_argument(
-            '-B', '--backup', dest="keep_backup", action='store_true',
-            help=_("Keep a backup file for each changed configuration file."),
-        )
-
-        self.arg_parser.add_argument(
-            '-K', '--keep-tempdir', dest='keep_tempdir', action='store_true',
-            help=_(
-                "Keeping the temporary directory instead of removing it at the end "
-                "(e.g. for debugging purposes)"),
-        )
-
-    # -------------------------------------------------------------------------
-    def perform_arg_parser(self):
-        """
-        Public available method to execute some actions after parsing
-        the command line parameters.
-        """
-
-        super(PpDeployZonesApp, self).perform_arg_parser()
-
-        if self.args.keep_tempdir:
-            self.keep_tempdir = True
-
-        if self.args.keep_backup:
-            self.keep_backup = True
-
-    # -------------------------------------------------------------------------
-    def perform_config(self):
-
-        super(PpDeployZonesApp, self).perform_config()
-
-        for section_name in self.cfg.keys():
-
-            if self.verbose > 3:
-                LOG.debug(_("Checking config section {!r} ...").format(section_name))
-
-            section = self.cfg[section_name]
-
-            if section_name.lower() == 'app':
-                self._check_path_config(section, section_name, 'pidfile', 'pidfile_name', True)
-                if 'keep-backup' in section:
-                    self.keep_backup = to_bool(section['keep-backup'])
-                if 'keep_backup' in section:
-                    self.keep_backup = to_bool(section['keep_backup'])
-
-            if section_name.lower() == 'named':
-                self.set_named_options(section, section_name)
-
-        if not self.masters_configured:
-            if self.environment == 'local':
-                self.zone_masters = copy.copy(self.zone_masters_local)
-            else:
-                self.zone_masters = copy.copy(self.zone_masters_public)
-
-    # -------------------------------------------------------------------------
-    def set_named_options(self, section, section_name):
-
-        if self.verbose > 2:
-            LOG.debug(
-                _("Evaluating config section {!r}:").format(section_name) + '\n' + pp(section))
-
-        # Configuration files and directories
-        self._check_path_config(
-            section, section_name, 'config_dir', 'named_conf_dir', True)
-        self._check_path_config(
-            section, section_name, 'zones_cfg_file', '_named_zones_cfg_file', False)
-        self._check_path_config(section, section_name, 'base_dir', 'named_basedir', True)
-        self._check_path_config(section, section_name, 'slave_dir', '_named_slavedir', False)
-
-        if 'listen_on_v6' in section and section['listen_on_v6'] is not None:
-            self.named_listen_on_v6 = to_bool(section['listen_on_v6'])
-
-        if 'masters' in section:
-            self._get_masters_from_cfg(section['masters'], section_name)
-
-        for item in (
-                'cmd_checkconf', 'cmd_reload', 'cmd_status', 'cmd_start',
-                'cmd_restart', 'zone_tsig_key'):
-            if item in section and section[item].strip():
-                setattr(self, item, section[item].strip())
-
-    # -------------------------------------------------------------------------
-    def _get_masters_from_cfg(self, value, section_name):
-
-        value = value.strip()
-        if not value:
-            msg = _("No masters given in [{}]/masters.").format(section_name)
-            LOG.error(msg)
-            self.config_has_errors = True
-            return
-
-        masters = []
-
-        for m in self.re_split_addresses.split(value):
-            if m:
-                m = m.strip().lower()
-                LOG.debug(_("Checking given master address {!r} ...").format(m))
-                try:
-                    addr_infos = socket.getaddrinfo(
-                        m, 53, proto=socket.IPPROTO_TCP)
-                    for addr_info in addr_infos:
-                        addr = addr_info[4][0]
-                        if not self.named_listen_on_v6 and addr_info[0] == socket.AF_INET6:
-                            msg = _(
-                                "Not using {!r} as a master IP address, because "
-                                "we are not using IPv6.").format(addr)
-                            LOG.debug(msg)
-                            continue
-                        if addr in masters:
-                            LOG.debug(_("Address {!r} are already in masters yet.").format(addr))
-                        else:
-                            LOG.debug(_("Address {!r} are not in masters yet.").format(addr))
-                            masters.append(addr)
-
-                except socket.gaierror as e:
-                    msg = _(
-                        "Invalid hostname or address {a!r} found in [{s}]/masters: {e}").format(
-                            a=m, s=section_name, e=e)
-                    LOG.error(msg)
-                    self.config_has_errors = True
-                    m = None
-        if masters:
-            if self.verbose > 2:
-                LOG.debug(_("Using configured masters: {}").format(pp(masters)))
-            self.zone_masters = masters
-            self.masters_configured = True
-        else:
-            LOG.warn(_("No valid masters found in configuration."))
-
-    # -------------------------------------------------------------------------
-    def post_init(self):
-
-        super(PpDeployZonesApp, self).post_init()
-        self.initialized = False
-
-        if not self.quiet:
-            print('')
-
-        LOG.debug(_("Post init phase."))
-
-        LOG.debug(_("Checking for masters, which are local addresses ..."))
-        ext_masters = []
-        for addr in self.zone_masters:
-            if addr in self.local_addresses:
-                LOG.debug(
-                    _("Address {!r} is in list of local addresses.").format(addr))
-            else:
-                LOG.debug(
-                    _("Address {!r} is not in list of local addresses.").format(addr))
-                ext_masters.append(addr)
-        self.zone_masters = ext_masters
-        LOG.info(_("Using masters for slave zones: {}").format(
-            ', '.join(map(lambda x: '{!r}'.format(x), self.zone_masters))))
-
-        self.pidfile = PidFile(
-            filename=self.pidfile_name, appname=self.appname, verbose=self.verbose,
-            base_dir=self.base_dir, simulate=self.simulate)
-
-        self.initialized = True
-
-    # -------------------------------------------------------------------------
-    def pre_run(self):
-        """
-        Dummy function to run before the main routine.
-        Could be overwritten by descendant classes.
-
-        """
-
-        my_uid = os.geteuid()
-        if my_uid:
-            msg = _("You must be root to execute this script.")
-            if self.simulate:
-                LOG.warn(msg)
-                time.sleep(1)
-            else:
-                LOG.error(msg)
-                self.exit(1)
-
-        super(PpDeployZonesApp, self).pre_run()
-
-        if self.environment == 'global':
-            LOG.error(_(
-                "Using the global DNS master is not supported, "
-                "please use 'local' or 'public'"))
-            self.exit(1)
-
-        cmd_namedcheckconf = self.get_command('named-checkconf')
-        if not cmd_namedcheckconf:
-            self.exit(1)
-        self.cmd_checkconf = cmd_namedcheckconf
-
-    # -------------------------------------------------------------------------
-    def _run(self):
-
-        local_tz_name = 'Europe/Berlin'
-        if 'TZ' in os.environ and os.environ['TZ']:
-            local_tz_name = os.environ['TZ']
-        try:
-            local_tz = timezone(local_tz_name)
-        except UnknownTimeZoneError:
-            LOG.error(_("Unknown time zone: {!r}.").format(local_tz_name))
-            self.exit(6)
-
-        LOG.info(_("Starting: {}").format(
-            datetime.datetime.now(local_tz).strftime('%Y-%m-%d %H:%M:%S %Z')))
-
-        self.get_named_keys()
-
-        try:
-            self.pidfile.create()
-        except PidFileError as e:
-            LOG.error(_("Could not occupy pidfile: {}").format(e))
-            self.exit(7)
-            return
-
-        try:
-
-            self.zones = self.get_api_zones()
-
-            self.init_temp_objects()
-            self.generate_slave_cfg_file()
-            self.compare_files()
-
-            try:
-                self.replace_configfiles()
-                if not self.check_namedconf():
-                    self.restore_configfiles()
-                    self.exit(99)
-                self.apply_config()
-            except Exception:
-                self.restore_configfiles()
-                raise
-
-        finally:
-            self.cleanup()
-            self.pidfile = None
-            LOG.info(_("Ending: {}").format(
-                datetime.datetime.now(local_tz).strftime('%Y-%m-%d %H:%M:%S %Z')))
-
-    # -------------------------------------------------------------------------
-    def cleanup(self):
-
-        LOG.info(_("Cleaning up ..."))
-
-        for tgt_file in self.moved_files.keys():
-            backup_file = self.moved_files[tgt_file]
-            LOG.debug(_("Searching for {!r}.").format(backup_file))
-            if os.path.exists(backup_file):
-                if self.keep_backup:
-                    LOG.info(_("Keep existing backup file {!r}.").format(backup_file))
-                else:
-                    LOG.info(_("Removing {!r} ...").format(backup_file))
-                    if not self.simulate:
-                        os.remove(backup_file)
-
-        # -----------------------
-        def emit_rm_err(function, path, excinfo):
-            LOG.error(_("Error removing {p!r} - {c}: {e}").format(
-                p=path, c=excinfo[1].__class__.__name__, e=excinfo[1]))
-
-        if self.tempdir:
-            if self.keep_tempdir:
-                msg = _(
-                    "Temporary directory {!r} will not be removed. "
-                    "It's on yours to remove it manually.").format(self.tempdir)
-                LOG.warn(msg)
-            else:
-                LOG.debug(_("Destroying temporary directory {!r} ...").format(self.tempdir))
-                shutil.rmtree(self.tempdir, False, emit_rm_err)
-                self.tempdir = None
-
-    # -------------------------------------------------------------------------
-    def init_temp_objects(self):
-        """Init temporary objects and properties."""
-
-        self.tempdir = tempfile.mkdtemp(
-            prefix=(self.appname + '.'), suffix='.tmp.d'
-        )
-        LOG.debug(_("Temporary directory: {!r}.").format(self.tempdir))
-
-        self.temp_zones_cfg_file = os.path.join(
-            self.tempdir, self.default_named_zones_cfg_file)
-
-        if self.verbose > 1:
-            LOG.debug(_("Temporary zones conf: {!r}").format(self.temp_zones_cfg_file))
-
-    # -------------------------------------------------------------------------
-    def get_named_keys(self):
-
-        LOG.info(_("Trying to get all keys from named.conf ..."))
-
-        cmd = shlex.split(str(self.cmd_checkconf))
-        cmd.append('-p')
-
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
-        LOG.debug(_("Executing: {}").format(cmd_str))
-
-        result = super(BaseApplication, self).run(
-            cmd, stdout=PIPE, stderr=PIPE, timeout=10, check=True, may_simulate=False)
-
-        if self.verbose > 3:
-            LOG.debug(_("Result:") + '\n' + str(result))
-
-        config = result.stdout
-
-        key_pattern = r'^\s*key\s+("[^"]+"|\S+)\s+\{([^\}]+)\}\s*;'
-        re_quotes = re.compile(r'^\s*"([^"]+)"\s*$')
-        re_key = re.compile(key_pattern, re.IGNORECASE | re.MULTILINE | re.DOTALL)
-        re_algo = re.compile(r'^\s*algorithm\s+"([^"]+)"\s*;', re.IGNORECASE)
-        re_secret = re.compile(r'^\s*secret\s+"([^"]+)"\s*;', re.IGNORECASE)
-
-        for match in re_key.finditer(config):
-            match_quotes = re_quotes.match(match[1])
-            if match_quotes:
-                key_name = match_quotes[1]
-            else:
-                key_name = match[1]
-            key_data = match[2].strip()
-            if self.verbose > 2:
-                LOG.debug("Found key {!r}:".format(key_name) + '\n' + key_data)
-
-            algorithm = None
-            secret = None
-
-            for line in key_data.splitlines():
-                # Searching for algorithm
-                match_algo = re_algo.search(line)
-                if match_algo:
-                    algorithm = match_algo[1]
-                # Searching for secret
-                match_secret = re_secret.search(line)
-                if match_secret:
-                    secret = match_secret[1]
-
-            if algorithm and secret:
-                self.named_keys[key_name] = {
-                    'algorithm': algorithm,
-                    'secret': secret,
-                }
-
-        if self.verbose > 1:
-            if self.named_keys:
-                LOG.debug(_("Found named keys:") + '\n' + pp(self.named_keys))
-            else:
-                LOG.debug(_("Found named keys:") + ' ' + _('None'))
-
-    # -------------------------------------------------------------------------
-    def generate_slave_cfg_file(self):
-
-        LOG.info(_("Generating {} ...").format(self.default_named_zones_cfg_file))
-
-        cur_date = datetime.datetime.now().isoformat(' ')
-
-        lines = []
-        lines.append('###############################################################')
-        lines.append('')
-        lines.append(' Bind9 configuration file for slave sones')
-        lines.append(' {}'.format(self.named_zones_cfg_file))
-        lines.append('')
-        lines.append(' Generated at: {}'.format(cur_date))
-        lines.append('')
-        lines.append('###############################################################')
-        header = textwrap.indent('\n'.join(lines), '//', lambda line: True) + '\n'
-
-        content = header
-
-        for zone_name in self.zones.keys():
-
-            zone_config = self.generate_zone_config(zone_name)
-            if zone_config:
-                content += '\n' + zone_config
-
-        if self.servers:
-            LOG.debug(_("Collected server configuration:") + '\n' + pp(self.servers))
-        else:
-            LOG.debug(_("Collected server configuration:") + ' ' + _('None'))
-
-        if self.servers:
-            for server in sorted(self.servers.keys()):
-                lines = []
-                lines.append('')
-                lines.append('server {} {{'.format(server))
-                lines.append('\tkeys {')
-                for key_id in sorted(self.servers[server]['keys']):
-                    lines.append('\t\t"{}";'.format(key_id))
-                lines.append('\t};')
-                lines.append('};')
-                content += '\n'.join(lines) + '\n'
-
-        content += '\n// vim: ts=8 filetype=named noet noai\n'
-
-        with open(self.temp_zones_cfg_file, 'w', **self.open_args) as fh:
-            fh.write(content)
-
-        if self.verbose > 2:
-            LOG.debug(
-                _("Generated file {!r}:").format(
-                    self.temp_zones_cfg_file) + '\n' + content.strip())
-
-    # -------------------------------------------------------------------------
-    def generate_zone_config(self, zone_name):
-
-        zone = self.zones[zone_name]
-        zone.update()
-
-        canonical_name = zone.name_unicode
-        match = self.re_ipv4_zone.search(zone.name)
-
-        if match:
-            prefix = self._get_ipv4_prefix(match.group(1))
-            if prefix:
-                if prefix == '127.0.0':
-                    LOG.debug(_("Pure local zone {!r} will not be considered.").format(prefix))
-                    return ''
-                canonical_name = 'rev.' + prefix
-        else:
-            match = self.re_ipv6_zone.search(zone.name)
-            if match:
-                prefix = self._get_ipv6_prefix(match.group(1))
-                if prefix:
-                    canonical_name = 'rev.' + prefix
-
-        show_name = canonical_name
-        show_name = self.re_rev.sub('Reverse ', show_name)
-        show_name = self.re_trail_dot.sub('', show_name)
-        zname = self.re_trail_dot.sub('', zone.name)
-
-        zfile = os.path.join(
-            self.named_slavedir_rel, self.re_trail_dot.sub('', canonical_name) + '.zone')
-
-        lines = []
-        lines.append('')
-        lines.append('// {}'.format(show_name))
-        lines.append('zone "{}" in {{'.format(zname))
-        lines.append('\tmasters {')
-        for master in self.zone_masters:
-            lines.append('\t\t{};'.format(master))
-        lines.append('\t};')
-        lines.append('\ttype slave;')
-        lines.append('\tfile "{}";'.format(zfile))
-
-        if zone.master_tsig_key_ids:
-
-            for key_id in zone.master_tsig_key_ids:
-                if key_id not in self.named_keys:
-                    msg = _("Key {k!r} for zone {z!r} not found in named configuration.").format(
-                        k=key_id, z=show_name)
-                    raise PpDeployZonesError(msg)
-
-            allow_line = '\tallow-transfer {'
-            for key_id in zone.master_tsig_key_ids:
-                allow_line += ' key "{}";'.format(key_id)
-            allow_line += ' };'
-            lines.append(allow_line)
-
-            for master in self.zone_masters:
-                if master not in self.servers:
-                    self.servers[master] = {}
-                if 'keys' not in self.servers[master]:
-                    self.servers[master]['keys'] = set()
-                for key_id in zone.master_tsig_key_ids:
-                    self.servers[master]['keys'].add(key_id)
-
-        lines.append('};')
-
-        return '\n'.join(lines) + '\n'
-
-    # -------------------------------------------------------------------------
-    def _get_ipv4_prefix(self, match):
-
-        tuples = []
-        for t in match.split('.'):
-            if t:
-                tuples.insert(0, t)
-        if self.verbose > 2:
-            LOG.debug(_("Got IPv4 tuples: {}").format(pp(tuples)))
-        return '.'.join(tuples)
-
-    # -------------------------------------------------------------------------
-    def _get_ipv6_prefix(self, match):
-
-        tuples = []
-        for t in match.split('.'):
-            if t:
-                tuples.insert(0, t)
-
-        tokens = []
-        while len(tuples):
-            token = ''.join(tuples[0:4]).ljust(4, '0')
-            if token.startswith('000'):
-                token = token[3:]
-            elif token.startswith('00'):
-                token = token[2:]
-            elif token.startswith('0'):
-                token = token[1:]
-            tokens.append(token)
-            del tuples[0:4]
-
-        if self.verbose > 2:
-            LOG.debug(_("Got IPv6 tokens: {}").format(pp(tokens)))
-
-        return ':'.join(tokens)
-
-    # -------------------------------------------------------------------------
-    def compare_files(self):
-
-        LOG.info(_("Comparing generated files with existing ones."))
-
-        if not self.files_equal_content(self.temp_zones_cfg_file, self.named_zones_cfg_file):
-            self.reload_necessary = True
-            self.files2replace[self.named_zones_cfg_file] = self.temp_zones_cfg_file
-
-        if self.verbose > 1:
-            LOG.debug(_("Files to replace:") + '\n' + pp(self.files2replace))
-
-    # -------------------------------------------------------------------------
-    def files_equal_content(self, file_src, file_tgt):
-
-        if not file_src:
-            raise PpDeployZonesError(_("Source file not defined."))
-        if not file_tgt:
-            raise PpDeployZonesError(_("Target file not defined."))
-
-        LOG.debug(_("Comparing {one!r} with {two!r} ...").format(
-            one=file_src, two=file_tgt))
-
-        if not os.path.exists(file_src):
-            msg = _("{what} {f!r} does not exists.").format(
-                what=_("Source file"), f=file_src)
-            raise PpDeployZonesError(msg)
-        if not os.path.isfile(file_src):
-            msg = _("{what} {f!r} is not a regular file.").format(
-                what=_("Source file"), f=file_src)
-            raise PpDeployZonesError(msg)
-
-        if not os.path.exists(file_tgt):
-            msg = _("{what} {f!r} does not exists.").format(
-                what=_("Target file"), f=file_tgt)
-            LOG.debug(msg)
-            return False
-        if not os.path.isfile(file_tgt):
-            msg = _("{what} {f!r} is not a regular file.").format(
-                what=_("Target file"), f=file_tgt)
-            raise PpDeployZonesError(msg)
-
-        # Reading source file
-        content_src = ''
-        if self.verbose > 2:
-            LOG.debug(_("Reading {!r} ...").format(file_src))
-        with open(file_src, 'r', **self.open_args) as fh:
-            content_src = fh.read()
-        lines_str_src = self.re_block_comment.sub('', content_src)
-        lines_str_src = self.re_line_comment.sub('', lines_str_src)
-        lines_src = []
-        for line in lines_str_src.splitlines():
-            line = line.strip()
-            if line:
-                lines_src.append(line)
-        if self.verbose > 3:
-            LOG.debug(_("Cleaned version of {!r}:").format(file_src) + '\n' + '\n'.join(lines_src))
-
-        # Reading target file
-        content_tgt = ''
-        if self.verbose > 2:
-            LOG.debug(_("Reading {!r} ...").format(file_tgt))
-        with open(file_tgt, 'r', **self.open_args) as fh:
-            content_tgt = fh.read()
-        lines_str_tgt = self.re_block_comment.sub('', content_tgt)
-        lines_str_tgt = self.re_line_comment.sub('', lines_str_tgt)
-        lines_tgt = []
-        for line in lines_str_tgt.splitlines():
-            line = line.strip()
-            if line:
-                lines_tgt.append(line)
-        if self.verbose > 3:
-            LOG.debug(_("Cleaned version of {!r}:").format(file_tgt) + '\n' + '\n'.join(lines_tgt))
-
-        if len(lines_src) != len(lines_tgt):
-            LOG.debug(_(
-                "Source file {sf!r} has different number essential lines ({sl}) than "
-                "the target file {tf!r} ({tl} lines).").format(
-                sf=file_src, sl=len(lines_src), tf=file_tgt, tl=len(lines_tgt)))
-            return False
-
-        i = 0
-        while i < len(lines_src):
-            if lines_src[i] != lines_tgt[i]:
-                LOG.debug(_(
-                    "Source file {sf!r} has a different content than "
-                    "the target file {tf!r}.").format(sf=file_src, tf=lines_tgt))
-                return False
-            i += 1
-
-        return True
-
-    # -------------------------------------------------------------------------
-    def replace_configfiles(self):
-
-        if not self.files2replace:
-            LOG.debug(_("No replacement of any config files necessary."))
-            return
-
-        LOG.debug(_("Start replacing of config files ..."))
-
-        for tgt_file in self.files2replace.keys():
-
-            backup_file = tgt_file + self.backup_suffix
-
-            if os.path.exists(tgt_file):
-                self.moved_files[tgt_file] = backup_file
-                LOG.info(_("Copying {frm!r} => {to!r} ...").format(frm=tgt_file, to=backup_file))
-                if not self.simulate:
-                    shutil.copy2(tgt_file, backup_file)
-
-        if self.verbose > 1:
-            LOG.debug(_("All backuped config files:") + '\n' + pp(self.moved_files))
-
-        for tgt_file in self.files2replace.keys():
-            src_file = self.files2replace[tgt_file]
-            LOG.info(_("Copying {frm!r} => {to!r} ...").format(frm=src_file, to=tgt_file))
-            if not self.simulate:
-                shutil.copy2(src_file, tgt_file)
-
-    # -------------------------------------------------------------------------
-    def restore_configfiles(self):
-
-        LOG.error(_("Restoring of original config files because of an exception."))
-
-        for tgt_file in self.moved_files.keys():
-            backup_file = self.moved_files[tgt_file]
-            LOG.info(_("Moving {frm!r} => {to!r} ...").format(frm=backup_file, to=tgt_file))
-            if not self.simulate:
-                if os.path.exists(backup_file):
-                    os.rename(backup_file, tgt_file)
-                else:
-                    LOG.error(_("Could not find backup file {!r}.").format(backup_file))
-
-    # -------------------------------------------------------------------------
-    def check_namedconf(self):
-
-        LOG.info(_("Checking syntax correctness of named.conf ..."))
-        cmd = shlex.split(str(self.cmd_checkconf))
-        if self.verbose > 2:
-            cmd.append('-p')
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
-        LOG.debug(_("Executing: {}").format(cmd_str))
-
-        result = super(BaseApplication, self).run(
-            cmd, stdout=PIPE, stderr=PIPE, timeout=10, check=False, may_simulate=False)
-
-        if self.verbose > 2:
-            LOG.debug(_("Result:") + '\n' + str(result))
-
-        if result.returncode:
-            return False
-        return True
-
-    # -------------------------------------------------------------------------
-    def apply_config(self):
-
-        if not self.reload_necessary and not self.restart_necessary:
-            LOG.info(_("Reload or restart of named is not necessary."))
-            return
-
-        running = self.named_running()
-        if not running:
-            LOG.warn(_("Named is not running, please start it manually."))
-            return
-
-        if self.restart_necessary:
-            self.restart_named()
-        else:
-            self.reload_named()
-
-    # -------------------------------------------------------------------------
-    def named_running(self):
-
-        LOG.debug(_("Checking, whether named is running ..."))
-
-        cmd = shlex.split(self.cmd_status)
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
-        LOG.debug(_("Executing: {}").format(cmd_str))
-
-        std_out = None
-        std_err = None
-        ret_val = None
-
-        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
-            try:
-                std_out, std_err = proc.communicate(timeout=10)
-            except TimeoutExpired:
-                proc.kill()
-                std_out, std_err = proc.communicate()
-            ret_val = proc.wait()
-
-        LOG.debug(_("Return value: {!r}").format(ret_val))
-        if std_out and std_out.strip():
-            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
-        if std_err and std_err.strip():
-            LOG.warn(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
-
-        if ret_val:
-            return False
-
-        return True
-
-    # -------------------------------------------------------------------------
-    def start_named(self):
-
-        LOG.info(_("Starting {} ...").format('named'))
-
-        cmd = shlex.split(self.cmd_start)
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
-        LOG.debug(_("Executing: {}").format(cmd_str))
-
-        if self.simulate:
-            return
-
-        std_out = None
-        std_err = None
-        ret_val = None
-
-        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
-            try:
-                std_out, std_err = proc.communicate(timeout=30)
-            except TimeoutExpired:
-                proc.kill()
-                std_out, std_err = proc.communicate()
-            ret_val = proc.wait()
-
-        LOG.debug(_("Return value: {!r}").format(ret_val))
-        if std_out and std_out.strip():
-            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
-        if std_err and std_err.strip():
-            LOG.error(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
-
-        if ret_val:
-            return False
-
-        return True
-
-    # -------------------------------------------------------------------------
-    def restart_named(self):
-
-        LOG.info(_("Restarting {} ...").format('named'))
-
-        cmd = shlex.split(self.cmd_restart)
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
-        LOG.debug(_("Executing: {}").format(cmd_str))
-
-        if self.simulate:
-            return
-
-        std_out = None
-        std_err = None
-        ret_val = None
-
-        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
-            try:
-                std_out, std_err = proc.communicate(timeout=30)
-            except TimeoutExpired:
-                proc.kill()
-                std_out, std_err = proc.communicate()
-            ret_val = proc.wait()
-
-        LOG.debug(_("Return value: {!r}").format(ret_val))
-        if std_out and std_out.strip():
-            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
-        if std_err and std_err.strip():
-            LOG.error(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
-
-        if ret_val:
-            return False
-
-        return True
-
-    # -------------------------------------------------------------------------
-    def reload_named(self):
-
-        LOG.info(_("Reloading {} ...").format('named'))
-
-        cmd = shlex.split(self.cmd_reload)
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
-        LOG.debug(_("Executing: {}").format(cmd_str))
-
-        if self.simulate:
-            return
-
-        std_out = None
-        std_err = None
-        ret_val = None
-
-        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
-            try:
-                std_out, std_err = proc.communicate(timeout=30)
-            except TimeoutExpired:
-                proc.kill()
-                std_out, std_err = proc.communicate()
-            ret_val = proc.wait()
-
-        LOG.debug(_("Return value: {!r}").format(ret_val))
-        if std_out and std_out.strip():
-            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
-        if std_err and std_err.strip():
-            LOG.error(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
-
-        if ret_val:
-            return False
-
-        return True
-
-
-# =============================================================================
-
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/dns_deploy_zones_app.py b/lib/pp_admintools/dns_deploy_zones_app.py
new file mode 100644 (file)
index 0000000..634bb0d
--- /dev/null
@@ -0,0 +1,999 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2022 by Frank Brehm, Berlin
+@summary: A module for the application class for configuring named
+"""
+from __future__ import absolute_import
+
+import os
+import logging
+import logging.config
+import textwrap
+import re
+import shlex
+import copy
+import datetime
+import socket
+import tempfile
+import time
+import shutil
+import pipes
+
+from subprocess import Popen, TimeoutExpired, PIPE
+
+# Third party modules
+import six
+from pytz import timezone, UnknownTimeZoneError
+
+# Own modules
+from fb_tools.common import pp, to_str, to_bool
+
+from fb_tools.app import BaseApplication
+
+from .pdns_app import PpPDNSAppError, PpPDNSApplication
+
+from .pidfile import PidFileError, PidFile
+
+from .xlate import XLATOR
+
+__version__ = '0.7.4'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+
+
+# =============================================================================
+class PpDeployZonesError(PpPDNSAppError):
+    pass
+
+
+# =============================================================================
+class PpDeployZonesApp(PpPDNSApplication):
+    """
+    Class for a application 'dns-deploy-zones' for configuring slaves
+    of the BIND named daemon.
+    """
+
+    default_pidfile = '/run/dns-deploy-zones.pid'
+
+    default_named_conf_dir = '/etc'
+    default_named_zones_cfg_file = 'named.zones.conf'
+    default_named_basedir = '/var/named'
+    default_named_slavedir = 'slaves'
+
+    zone_masters_local = [
+        '217.66.53.87',
+    ]
+
+    zone_masters_public = [
+        '217.66.53.97',
+    ]
+
+    default_cmd_checkconf = '/usr/sbin/named-checkconf'
+    default_cmd_reload = '/usr/sbin/rndc reload'
+    default_cmd_status = '/usr/bin/systemctl status named.service'
+    default_cmd_start = '/usr/bin/systemctl start named.service'
+    default_cmd_restart = '/usr/bin/systemctl restart named.service'
+
+    re_ipv4_zone = re.compile(r'^((?:\d+\.)+)in-addr\.arpa\.$')
+    re_ipv6_zone = re.compile(r'^((?:[\da-f]\.)+)ip6\.arpa\.$')
+
+    re_block_comment = re.compile(r'/\*.*?\*/', re.MULTILINE | re.DOTALL)
+    re_line_comment = re.compile(r'(?://|#).*$', re.MULTILINE)
+
+    re_split_addresses = re.compile(r'[,;\s]+')
+    re_integer = re.compile(r'^\s*(\d+)\s*$')
+
+    re_rev = re.compile(r'^rev\.', re.IGNORECASE)
+    re_trail_dot = re.compile(r'\.+$')
+
+    open_args = {}
+    if six.PY3:
+        open_args = {
+            'encoding': 'utf-8',
+            'errors': 'surrogateescape',
+        }
+
+    # -------------------------------------------------------------------------
+    def __init__(self, appname=None, base_dir=None, version=__version__):
+
+        self.zones = {}
+        self.pidfile = None
+
+        self._show_simulate_opt = True
+
+        self.is_internal = False
+        self.named_listen_on_v6 = False
+        self.pidfile_name = self.default_pidfile
+
+        # Configuration files and directories
+        self.named_conf_dir = self.default_named_conf_dir
+        self._named_zones_cfg_file = self.default_named_zones_cfg_file
+        self.named_basedir = self.default_named_basedir
+        self._named_slavedir = self.default_named_slavedir
+
+        self.zone_masters = copy.copy(self.zone_masters_public)
+        self.masters_configured = False
+
+        self.tempdir = None
+        self.temp_zones_cfg_file = None
+        self.keep_tempdir = False
+        self.keep_backup = False
+
+        self.backup_suffix = (
+            '.' + datetime.datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S') + '.bak')
+
+        self.reload_necessary = False
+        self.restart_necessary = False
+
+        self.cmd_checkconf = self.default_cmd_checkconf
+        self.cmd_reload = self.default_cmd_reload
+        self.cmd_status = self.default_cmd_status
+        self.cmd_start = self.default_cmd_start
+        self.cmd_restart = self.default_cmd_restart
+
+        self.named_keys = {}
+        self.servers = {}
+
+        self.zone_tsig_key = None
+
+        self.files2replace = {}
+        self.moved_files = {}
+
+        description = _('Generation of the BIND9 configuration file for slave zones.')
+
+        super(PpDeployZonesApp, self).__init__(
+            appname=appname, version=version, description=description,
+            base_dir=base_dir, cfg_stems='dns-deploy-zones', environment="public",
+        )
+
+        self.post_init()
+
+    # -------------------------------------------
+    @property
+    def named_zones_cfg_file(self):
+        """The file for configuration of all own zones."""
+        return os.path.join(self.named_conf_dir, self._named_zones_cfg_file)
+
+    # -------------------------------------------
+    @property
+    def named_slavedir_rel(self):
+        """The directory for zone files of slave zones."""
+        return self._named_slavedir
+
+    # -------------------------------------------
+    @property
+    def named_slavedir_abs(self):
+        """The directory for zone files of slave zones."""
+        return os.path.join(self.named_basedir, self._named_slavedir)
+
+    # -------------------------------------------------------------------------
+    def init_arg_parser(self):
+
+        super(PpDeployZonesApp, self).init_arg_parser()
+
+        self.arg_parser.add_argument(
+            '-B', '--backup', dest="keep_backup", action='store_true',
+            help=_("Keep a backup file for each changed configuration file."),
+        )
+
+        self.arg_parser.add_argument(
+            '-K', '--keep-tempdir', dest='keep_tempdir', action='store_true',
+            help=_(
+                "Keeping the temporary directory instead of removing it at the end "
+                "(e.g. for debugging purposes)"),
+        )
+
+    # -------------------------------------------------------------------------
+    def perform_arg_parser(self):
+        """
+        Public available method to execute some actions after parsing
+        the command line parameters.
+        """
+
+        super(PpDeployZonesApp, self).perform_arg_parser()
+
+        if self.args.keep_tempdir:
+            self.keep_tempdir = True
+
+        if self.args.keep_backup:
+            self.keep_backup = True
+
+    # -------------------------------------------------------------------------
+    def perform_config(self):
+
+        super(PpDeployZonesApp, self).perform_config()
+
+        for section_name in self.cfg.keys():
+
+            if self.verbose > 3:
+                LOG.debug(_("Checking config section {!r} ...").format(section_name))
+
+            section = self.cfg[section_name]
+
+            if section_name.lower() == 'app':
+                self._check_path_config(section, section_name, 'pidfile', 'pidfile_name', True)
+                if 'keep-backup' in section:
+                    self.keep_backup = to_bool(section['keep-backup'])
+                if 'keep_backup' in section:
+                    self.keep_backup = to_bool(section['keep_backup'])
+
+            if section_name.lower() == 'named':
+                self.set_named_options(section, section_name)
+
+        if not self.masters_configured:
+            if self.environment == 'local':
+                self.zone_masters = copy.copy(self.zone_masters_local)
+            else:
+                self.zone_masters = copy.copy(self.zone_masters_public)
+
+    # -------------------------------------------------------------------------
+    def set_named_options(self, section, section_name):
+
+        if self.verbose > 2:
+            LOG.debug(
+                _("Evaluating config section {!r}:").format(section_name) + '\n' + pp(section))
+
+        # Configuration files and directories
+        self._check_path_config(
+            section, section_name, 'config_dir', 'named_conf_dir', True)
+        self._check_path_config(
+            section, section_name, 'zones_cfg_file', '_named_zones_cfg_file', False)
+        self._check_path_config(section, section_name, 'base_dir', 'named_basedir', True)
+        self._check_path_config(section, section_name, 'slave_dir', '_named_slavedir', False)
+
+        if 'listen_on_v6' in section and section['listen_on_v6'] is not None:
+            self.named_listen_on_v6 = to_bool(section['listen_on_v6'])
+
+        if 'masters' in section:
+            self._get_masters_from_cfg(section['masters'], section_name)
+
+        for item in (
+                'cmd_checkconf', 'cmd_reload', 'cmd_status', 'cmd_start',
+                'cmd_restart', 'zone_tsig_key'):
+            if item in section and section[item].strip():
+                setattr(self, item, section[item].strip())
+
+    # -------------------------------------------------------------------------
+    def _get_masters_from_cfg(self, value, section_name):
+
+        value = value.strip()
+        if not value:
+            msg = _("No masters given in [{}]/masters.").format(section_name)
+            LOG.error(msg)
+            self.config_has_errors = True
+            return
+
+        masters = []
+
+        for m in self.re_split_addresses.split(value):
+            if m:
+                m = m.strip().lower()
+                LOG.debug(_("Checking given master address {!r} ...").format(m))
+                try:
+                    addr_infos = socket.getaddrinfo(
+                        m, 53, proto=socket.IPPROTO_TCP)
+                    for addr_info in addr_infos:
+                        addr = addr_info[4][0]
+                        if not self.named_listen_on_v6 and addr_info[0] == socket.AF_INET6:
+                            msg = _(
+                                "Not using {!r} as a master IP address, because "
+                                "we are not using IPv6.").format(addr)
+                            LOG.debug(msg)
+                            continue
+                        if addr in masters:
+                            LOG.debug(_("Address {!r} are already in masters yet.").format(addr))
+                        else:
+                            LOG.debug(_("Address {!r} are not in masters yet.").format(addr))
+                            masters.append(addr)
+
+                except socket.gaierror as e:
+                    msg = _(
+                        "Invalid hostname or address {a!r} found in [{s}]/masters: {e}").format(
+                            a=m, s=section_name, e=e)
+                    LOG.error(msg)
+                    self.config_has_errors = True
+                    m = None
+        if masters:
+            if self.verbose > 2:
+                LOG.debug(_("Using configured masters: {}").format(pp(masters)))
+            self.zone_masters = masters
+            self.masters_configured = True
+        else:
+            LOG.warn(_("No valid masters found in configuration."))
+
+    # -------------------------------------------------------------------------
+    def post_init(self):
+
+        super(PpDeployZonesApp, self).post_init()
+        self.initialized = False
+
+        if not self.quiet:
+            print('')
+
+        LOG.debug(_("Post init phase."))
+
+        LOG.debug(_("Checking for masters, which are local addresses ..."))
+        ext_masters = []
+        for addr in self.zone_masters:
+            if addr in self.local_addresses:
+                LOG.debug(
+                    _("Address {!r} is in list of local addresses.").format(addr))
+            else:
+                LOG.debug(
+                    _("Address {!r} is not in list of local addresses.").format(addr))
+                ext_masters.append(addr)
+        self.zone_masters = ext_masters
+        LOG.info(_("Using masters for slave zones: {}").format(
+            ', '.join(map(lambda x: '{!r}'.format(x), self.zone_masters))))
+
+        self.pidfile = PidFile(
+            filename=self.pidfile_name, appname=self.appname, verbose=self.verbose,
+            base_dir=self.base_dir, simulate=self.simulate)
+
+        self.initialized = True
+
+    # -------------------------------------------------------------------------
+    def pre_run(self):
+        """
+        Dummy function to run before the main routine.
+        Could be overwritten by descendant classes.
+
+        """
+
+        my_uid = os.geteuid()
+        if my_uid:
+            msg = _("You must be root to execute this script.")
+            if self.simulate:
+                LOG.warn(msg)
+                time.sleep(1)
+            else:
+                LOG.error(msg)
+                self.exit(1)
+
+        super(PpDeployZonesApp, self).pre_run()
+
+        if self.environment == 'global':
+            LOG.error(_(
+                "Using the global DNS master is not supported, "
+                "please use 'local' or 'public'"))
+            self.exit(1)
+
+        cmd_namedcheckconf = self.get_command('named-checkconf')
+        if not cmd_namedcheckconf:
+            self.exit(1)
+        self.cmd_checkconf = cmd_namedcheckconf
+
+    # -------------------------------------------------------------------------
+    def _run(self):
+
+        local_tz_name = 'Europe/Berlin'
+        if 'TZ' in os.environ and os.environ['TZ']:
+            local_tz_name = os.environ['TZ']
+        try:
+            local_tz = timezone(local_tz_name)
+        except UnknownTimeZoneError:
+            LOG.error(_("Unknown time zone: {!r}.").format(local_tz_name))
+            self.exit(6)
+
+        LOG.info(_("Starting: {}").format(
+            datetime.datetime.now(local_tz).strftime('%Y-%m-%d %H:%M:%S %Z')))
+
+        self.get_named_keys()
+
+        try:
+            self.pidfile.create()
+        except PidFileError as e:
+            LOG.error(_("Could not occupy pidfile: {}").format(e))
+            self.exit(7)
+            return
+
+        try:
+
+            self.zones = self.get_api_zones()
+
+            self.init_temp_objects()
+            self.generate_slave_cfg_file()
+            self.compare_files()
+
+            try:
+                self.replace_configfiles()
+                if not self.check_namedconf():
+                    self.restore_configfiles()
+                    self.exit(99)
+                self.apply_config()
+            except Exception:
+                self.restore_configfiles()
+                raise
+
+        finally:
+            self.cleanup()
+            self.pidfile = None
+            LOG.info(_("Ending: {}").format(
+                datetime.datetime.now(local_tz).strftime('%Y-%m-%d %H:%M:%S %Z')))
+
+    # -------------------------------------------------------------------------
+    def cleanup(self):
+
+        LOG.info(_("Cleaning up ..."))
+
+        for tgt_file in self.moved_files.keys():
+            backup_file = self.moved_files[tgt_file]
+            LOG.debug(_("Searching for {!r}.").format(backup_file))
+            if os.path.exists(backup_file):
+                if self.keep_backup:
+                    LOG.info(_("Keep existing backup file {!r}.").format(backup_file))
+                else:
+                    LOG.info(_("Removing {!r} ...").format(backup_file))
+                    if not self.simulate:
+                        os.remove(backup_file)
+
+        # -----------------------
+        def emit_rm_err(function, path, excinfo):
+            LOG.error(_("Error removing {p!r} - {c}: {e}").format(
+                p=path, c=excinfo[1].__class__.__name__, e=excinfo[1]))
+
+        if self.tempdir:
+            if self.keep_tempdir:
+                msg = _(
+                    "Temporary directory {!r} will not be removed. "
+                    "It's on yours to remove it manually.").format(self.tempdir)
+                LOG.warn(msg)
+            else:
+                LOG.debug(_("Destroying temporary directory {!r} ...").format(self.tempdir))
+                shutil.rmtree(self.tempdir, False, emit_rm_err)
+                self.tempdir = None
+
+    # -------------------------------------------------------------------------
+    def init_temp_objects(self):
+        """Init temporary objects and properties."""
+
+        self.tempdir = tempfile.mkdtemp(
+            prefix=(self.appname + '.'), suffix='.tmp.d'
+        )
+        LOG.debug(_("Temporary directory: {!r}.").format(self.tempdir))
+
+        self.temp_zones_cfg_file = os.path.join(
+            self.tempdir, self.default_named_zones_cfg_file)
+
+        if self.verbose > 1:
+            LOG.debug(_("Temporary zones conf: {!r}").format(self.temp_zones_cfg_file))
+
+    # -------------------------------------------------------------------------
+    def get_named_keys(self):
+
+        LOG.info(_("Trying to get all keys from named.conf ..."))
+
+        cmd = shlex.split(str(self.cmd_checkconf))
+        cmd.append('-p')
+
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
+        LOG.debug(_("Executing: {}").format(cmd_str))
+
+        result = super(BaseApplication, self).run(
+            cmd, stdout=PIPE, stderr=PIPE, timeout=10, check=True, may_simulate=False)
+
+        if self.verbose > 3:
+            LOG.debug(_("Result:") + '\n' + str(result))
+
+        config = result.stdout
+
+        key_pattern = r'^\s*key\s+("[^"]+"|\S+)\s+\{([^\}]+)\}\s*;'
+        re_quotes = re.compile(r'^\s*"([^"]+)"\s*$')
+        re_key = re.compile(key_pattern, re.IGNORECASE | re.MULTILINE | re.DOTALL)
+        re_algo = re.compile(r'^\s*algorithm\s+"([^"]+)"\s*;', re.IGNORECASE)
+        re_secret = re.compile(r'^\s*secret\s+"([^"]+)"\s*;', re.IGNORECASE)
+
+        for match in re_key.finditer(config):
+            match_quotes = re_quotes.match(match[1])
+            if match_quotes:
+                key_name = match_quotes[1]
+            else:
+                key_name = match[1]
+            key_data = match[2].strip()
+            if self.verbose > 2:
+                LOG.debug("Found key {!r}:".format(key_name) + '\n' + key_data)
+
+            algorithm = None
+            secret = None
+
+            for line in key_data.splitlines():
+                # Searching for algorithm
+                match_algo = re_algo.search(line)
+                if match_algo:
+                    algorithm = match_algo[1]
+                # Searching for secret
+                match_secret = re_secret.search(line)
+                if match_secret:
+                    secret = match_secret[1]
+
+            if algorithm and secret:
+                self.named_keys[key_name] = {
+                    'algorithm': algorithm,
+                    'secret': secret,
+                }
+
+        if self.verbose > 1:
+            if self.named_keys:
+                LOG.debug(_("Found named keys:") + '\n' + pp(self.named_keys))
+            else:
+                LOG.debug(_("Found named keys:") + ' ' + _('None'))
+
+    # -------------------------------------------------------------------------
+    def generate_slave_cfg_file(self):
+
+        LOG.info(_("Generating {} ...").format(self.default_named_zones_cfg_file))
+
+        cur_date = datetime.datetime.now().isoformat(' ')
+
+        lines = []
+        lines.append('###############################################################')
+        lines.append('')
+        lines.append(' Bind9 configuration file for slave sones')
+        lines.append(' {}'.format(self.named_zones_cfg_file))
+        lines.append('')
+        lines.append(' Generated at: {}'.format(cur_date))
+        lines.append('')
+        lines.append('###############################################################')
+        header = textwrap.indent('\n'.join(lines), '//', lambda line: True) + '\n'
+
+        content = header
+
+        for zone_name in self.zones.keys():
+
+            zone_config = self.generate_zone_config(zone_name)
+            if zone_config:
+                content += '\n' + zone_config
+
+        if self.servers:
+            LOG.debug(_("Collected server configuration:") + '\n' + pp(self.servers))
+        else:
+            LOG.debug(_("Collected server configuration:") + ' ' + _('None'))
+
+        if self.servers:
+            for server in sorted(self.servers.keys()):
+                lines = []
+                lines.append('')
+                lines.append('server {} {{'.format(server))
+                lines.append('\tkeys {')
+                for key_id in sorted(self.servers[server]['keys']):
+                    lines.append('\t\t"{}";'.format(key_id))
+                lines.append('\t};')
+                lines.append('};')
+                content += '\n'.join(lines) + '\n'
+
+        content += '\n// vim: ts=8 filetype=named noet noai\n'
+
+        with open(self.temp_zones_cfg_file, 'w', **self.open_args) as fh:
+            fh.write(content)
+
+        if self.verbose > 2:
+            LOG.debug(
+                _("Generated file {!r}:").format(
+                    self.temp_zones_cfg_file) + '\n' + content.strip())
+
+    # -------------------------------------------------------------------------
+    def generate_zone_config(self, zone_name):
+
+        zone = self.zones[zone_name]
+        zone.update()
+
+        canonical_name = zone.name_unicode
+        match = self.re_ipv4_zone.search(zone.name)
+
+        if match:
+            prefix = self._get_ipv4_prefix(match.group(1))
+            if prefix:
+                if prefix == '127.0.0':
+                    LOG.debug(_("Pure local zone {!r} will not be considered.").format(prefix))
+                    return ''
+                canonical_name = 'rev.' + prefix
+        else:
+            match = self.re_ipv6_zone.search(zone.name)
+            if match:
+                prefix = self._get_ipv6_prefix(match.group(1))
+                if prefix:
+                    canonical_name = 'rev.' + prefix
+
+        show_name = canonical_name
+        show_name = self.re_rev.sub('Reverse ', show_name)
+        show_name = self.re_trail_dot.sub('', show_name)
+        zname = self.re_trail_dot.sub('', zone.name)
+
+        zfile = os.path.join(
+            self.named_slavedir_rel, self.re_trail_dot.sub('', canonical_name) + '.zone')
+
+        lines = []
+        lines.append('')
+        lines.append('// {}'.format(show_name))
+        lines.append('zone "{}" in {{'.format(zname))
+        lines.append('\tmasters {')
+        for master in self.zone_masters:
+            lines.append('\t\t{};'.format(master))
+        lines.append('\t};')
+        lines.append('\ttype slave;')
+        lines.append('\tfile "{}";'.format(zfile))
+
+        if zone.master_tsig_key_ids:
+
+            for key_id in zone.master_tsig_key_ids:
+                if key_id not in self.named_keys:
+                    msg = _("Key {k!r} for zone {z!r} not found in named configuration.").format(
+                        k=key_id, z=show_name)
+                    raise PpDeployZonesError(msg)
+
+            allow_line = '\tallow-transfer {'
+            for key_id in zone.master_tsig_key_ids:
+                allow_line += ' key "{}";'.format(key_id)
+            allow_line += ' };'
+            lines.append(allow_line)
+
+            for master in self.zone_masters:
+                if master not in self.servers:
+                    self.servers[master] = {}
+                if 'keys' not in self.servers[master]:
+                    self.servers[master]['keys'] = set()
+                for key_id in zone.master_tsig_key_ids:
+                    self.servers[master]['keys'].add(key_id)
+
+        lines.append('};')
+
+        return '\n'.join(lines) + '\n'
+
+    # -------------------------------------------------------------------------
+    def _get_ipv4_prefix(self, match):
+
+        tuples = []
+        for t in match.split('.'):
+            if t:
+                tuples.insert(0, t)
+        if self.verbose > 2:
+            LOG.debug(_("Got IPv4 tuples: {}").format(pp(tuples)))
+        return '.'.join(tuples)
+
+    # -------------------------------------------------------------------------
+    def _get_ipv6_prefix(self, match):
+
+        tuples = []
+        for t in match.split('.'):
+            if t:
+                tuples.insert(0, t)
+
+        tokens = []
+        while len(tuples):
+            token = ''.join(tuples[0:4]).ljust(4, '0')
+            if token.startswith('000'):
+                token = token[3:]
+            elif token.startswith('00'):
+                token = token[2:]
+            elif token.startswith('0'):
+                token = token[1:]
+            tokens.append(token)
+            del tuples[0:4]
+
+        if self.verbose > 2:
+            LOG.debug(_("Got IPv6 tokens: {}").format(pp(tokens)))
+
+        return ':'.join(tokens)
+
+    # -------------------------------------------------------------------------
+    def compare_files(self):
+
+        LOG.info(_("Comparing generated files with existing ones."))
+
+        if not self.files_equal_content(self.temp_zones_cfg_file, self.named_zones_cfg_file):
+            self.reload_necessary = True
+            self.files2replace[self.named_zones_cfg_file] = self.temp_zones_cfg_file
+
+        if self.verbose > 1:
+            LOG.debug(_("Files to replace:") + '\n' + pp(self.files2replace))
+
+    # -------------------------------------------------------------------------
+    def files_equal_content(self, file_src, file_tgt):
+
+        if not file_src:
+            raise PpDeployZonesError(_("Source file not defined."))
+        if not file_tgt:
+            raise PpDeployZonesError(_("Target file not defined."))
+
+        LOG.debug(_("Comparing {one!r} with {two!r} ...").format(
+            one=file_src, two=file_tgt))
+
+        if not os.path.exists(file_src):
+            msg = _("{what} {f!r} does not exists.").format(
+                what=_("Source file"), f=file_src)
+            raise PpDeployZonesError(msg)
+        if not os.path.isfile(file_src):
+            msg = _("{what} {f!r} is not a regular file.").format(
+                what=_("Source file"), f=file_src)
+            raise PpDeployZonesError(msg)
+
+        if not os.path.exists(file_tgt):
+            msg = _("{what} {f!r} does not exists.").format(
+                what=_("Target file"), f=file_tgt)
+            LOG.debug(msg)
+            return False
+        if not os.path.isfile(file_tgt):
+            msg = _("{what} {f!r} is not a regular file.").format(
+                what=_("Target file"), f=file_tgt)
+            raise PpDeployZonesError(msg)
+
+        # Reading source file
+        content_src = ''
+        if self.verbose > 2:
+            LOG.debug(_("Reading {!r} ...").format(file_src))
+        with open(file_src, 'r', **self.open_args) as fh:
+            content_src = fh.read()
+        lines_str_src = self.re_block_comment.sub('', content_src)
+        lines_str_src = self.re_line_comment.sub('', lines_str_src)
+        lines_src = []
+        for line in lines_str_src.splitlines():
+            line = line.strip()
+            if line:
+                lines_src.append(line)
+        if self.verbose > 3:
+            LOG.debug(_("Cleaned version of {!r}:").format(file_src) + '\n' + '\n'.join(lines_src))
+
+        # Reading target file
+        content_tgt = ''
+        if self.verbose > 2:
+            LOG.debug(_("Reading {!r} ...").format(file_tgt))
+        with open(file_tgt, 'r', **self.open_args) as fh:
+            content_tgt = fh.read()
+        lines_str_tgt = self.re_block_comment.sub('', content_tgt)
+        lines_str_tgt = self.re_line_comment.sub('', lines_str_tgt)
+        lines_tgt = []
+        for line in lines_str_tgt.splitlines():
+            line = line.strip()
+            if line:
+                lines_tgt.append(line)
+        if self.verbose > 3:
+            LOG.debug(_("Cleaned version of {!r}:").format(file_tgt) + '\n' + '\n'.join(lines_tgt))
+
+        if len(lines_src) != len(lines_tgt):
+            LOG.debug(_(
+                "Source file {sf!r} has different number essential lines ({sl}) than "
+                "the target file {tf!r} ({tl} lines).").format(
+                sf=file_src, sl=len(lines_src), tf=file_tgt, tl=len(lines_tgt)))
+            return False
+
+        i = 0
+        while i < len(lines_src):
+            if lines_src[i] != lines_tgt[i]:
+                LOG.debug(_(
+                    "Source file {sf!r} has a different content than "
+                    "the target file {tf!r}.").format(sf=file_src, tf=lines_tgt))
+                return False
+            i += 1
+
+        return True
+
+    # -------------------------------------------------------------------------
+    def replace_configfiles(self):
+
+        if not self.files2replace:
+            LOG.debug(_("No replacement of any config files necessary."))
+            return
+
+        LOG.debug(_("Start replacing of config files ..."))
+
+        for tgt_file in self.files2replace.keys():
+
+            backup_file = tgt_file + self.backup_suffix
+
+            if os.path.exists(tgt_file):
+                self.moved_files[tgt_file] = backup_file
+                LOG.info(_("Copying {frm!r} => {to!r} ...").format(frm=tgt_file, to=backup_file))
+                if not self.simulate:
+                    shutil.copy2(tgt_file, backup_file)
+
+        if self.verbose > 1:
+            LOG.debug(_("All backuped config files:") + '\n' + pp(self.moved_files))
+
+        for tgt_file in self.files2replace.keys():
+            src_file = self.files2replace[tgt_file]
+            LOG.info(_("Copying {frm!r} => {to!r} ...").format(frm=src_file, to=tgt_file))
+            if not self.simulate:
+                shutil.copy2(src_file, tgt_file)
+
+    # -------------------------------------------------------------------------
+    def restore_configfiles(self):
+
+        LOG.error(_("Restoring of original config files because of an exception."))
+
+        for tgt_file in self.moved_files.keys():
+            backup_file = self.moved_files[tgt_file]
+            LOG.info(_("Moving {frm!r} => {to!r} ...").format(frm=backup_file, to=tgt_file))
+            if not self.simulate:
+                if os.path.exists(backup_file):
+                    os.rename(backup_file, tgt_file)
+                else:
+                    LOG.error(_("Could not find backup file {!r}.").format(backup_file))
+
+    # -------------------------------------------------------------------------
+    def check_namedconf(self):
+
+        LOG.info(_("Checking syntax correctness of named.conf ..."))
+        cmd = shlex.split(str(self.cmd_checkconf))
+        if self.verbose > 2:
+            cmd.append('-p')
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
+        LOG.debug(_("Executing: {}").format(cmd_str))
+
+        result = super(BaseApplication, self).run(
+            cmd, stdout=PIPE, stderr=PIPE, timeout=10, check=False, may_simulate=False)
+
+        if self.verbose > 2:
+            LOG.debug(_("Result:") + '\n' + str(result))
+
+        if result.returncode:
+            return False
+        return True
+
+    # -------------------------------------------------------------------------
+    def apply_config(self):
+
+        if not self.reload_necessary and not self.restart_necessary:
+            LOG.info(_("Reload or restart of named is not necessary."))
+            return
+
+        running = self.named_running()
+        if not running:
+            LOG.warn(_("Named is not running, please start it manually."))
+            return
+
+        if self.restart_necessary:
+            self.restart_named()
+        else:
+            self.reload_named()
+
+    # -------------------------------------------------------------------------
+    def named_running(self):
+
+        LOG.debug(_("Checking, whether named is running ..."))
+
+        cmd = shlex.split(self.cmd_status)
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
+        LOG.debug(_("Executing: {}").format(cmd_str))
+
+        std_out = None
+        std_err = None
+        ret_val = None
+
+        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
+            try:
+                std_out, std_err = proc.communicate(timeout=10)
+            except TimeoutExpired:
+                proc.kill()
+                std_out, std_err = proc.communicate()
+            ret_val = proc.wait()
+
+        LOG.debug(_("Return value: {!r}").format(ret_val))
+        if std_out and std_out.strip():
+            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
+        if std_err and std_err.strip():
+            LOG.warn(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
+
+        if ret_val:
+            return False
+
+        return True
+
+    # -------------------------------------------------------------------------
+    def start_named(self):
+
+        LOG.info(_("Starting {} ...").format('named'))
+
+        cmd = shlex.split(self.cmd_start)
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
+        LOG.debug(_("Executing: {}").format(cmd_str))
+
+        if self.simulate:
+            return
+
+        std_out = None
+        std_err = None
+        ret_val = None
+
+        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
+            try:
+                std_out, std_err = proc.communicate(timeout=30)
+            except TimeoutExpired:
+                proc.kill()
+                std_out, std_err = proc.communicate()
+            ret_val = proc.wait()
+
+        LOG.debug(_("Return value: {!r}").format(ret_val))
+        if std_out and std_out.strip():
+            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
+        if std_err and std_err.strip():
+            LOG.error(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
+
+        if ret_val:
+            return False
+
+        return True
+
+    # -------------------------------------------------------------------------
+    def restart_named(self):
+
+        LOG.info(_("Restarting {} ...").format('named'))
+
+        cmd = shlex.split(self.cmd_restart)
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
+        LOG.debug(_("Executing: {}").format(cmd_str))
+
+        if self.simulate:
+            return
+
+        std_out = None
+        std_err = None
+        ret_val = None
+
+        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
+            try:
+                std_out, std_err = proc.communicate(timeout=30)
+            except TimeoutExpired:
+                proc.kill()
+                std_out, std_err = proc.communicate()
+            ret_val = proc.wait()
+
+        LOG.debug(_("Return value: {!r}").format(ret_val))
+        if std_out and std_out.strip():
+            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
+        if std_err and std_err.strip():
+            LOG.error(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
+
+        if ret_val:
+            return False
+
+        return True
+
+    # -------------------------------------------------------------------------
+    def reload_named(self):
+
+        LOG.info(_("Reloading {} ...").format('named'))
+
+        cmd = shlex.split(self.cmd_reload)
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
+        LOG.debug(_("Executing: {}").format(cmd_str))
+
+        if self.simulate:
+            return
+
+        std_out = None
+        std_err = None
+        ret_val = None
+
+        with Popen(cmd, stdout=PIPE, stderr=PIPE) as proc:
+            try:
+                std_out, std_err = proc.communicate(timeout=30)
+            except TimeoutExpired:
+                proc.kill()
+                std_out, std_err = proc.communicate()
+            ret_val = proc.wait()
+
+        LOG.debug(_("Return value: {!r}").format(ret_val))
+        if std_out and std_out.strip():
+            LOG.debug(_("Output on {}").format('STDOUT') + '\n' + to_str(std_out.strip()))
+        if std_err and std_err.strip():
+            LOG.error(_("Output on {}").format('STDERR') + ' ' + to_str(std_err.strip()))
+
+        if ret_val:
+            return False
+
+        return True
+
+
+# =============================================================================
+
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list