]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Sorting modules into appropriate sub dirs
authorFrank Brehm <frank.brehm@pixelpark.com>
Mon, 5 Sep 2022 15:17:24 +0000 (17:17 +0200)
committerFrank Brehm <frank.brehm@pixelpark.com>
Mon, 5 Sep 2022 15:17:24 +0000 (17:17 +0200)
26 files changed:
bin/dns-deploy-zones
bin/remove-ldap-user
lib/pp_admintools/app/__init__.py [new file with mode: 0644]
lib/pp_admintools/app/dns_deploy_zones.py [new file with mode: 0644]
lib/pp_admintools/app/ldap.py [new file with mode: 0644]
lib/pp_admintools/app/mail.py [new file with mode: 0644]
lib/pp_admintools/app/pdns.py [new file with mode: 0644]
lib/pp_admintools/app/remove_ldap_user.py [new file with mode: 0644]
lib/pp_admintools/apps/__init__.py [deleted file]
lib/pp_admintools/apps/remove_ldap_user.py [deleted file]
lib/pp_admintools/config.py [deleted file]
lib/pp_admintools/config/__init__.py [new file with mode: 0644]
lib/pp_admintools/config/dns_deploy_zones.py [new file with mode: 0644]
lib/pp_admintools/config/ldap.py [new file with mode: 0644]
lib/pp_admintools/config/mail.py [new file with mode: 0644]
lib/pp_admintools/config/pdns.py [new file with mode: 0644]
lib/pp_admintools/dns_deploy_zones_app.py [deleted file]
lib/pp_admintools/dns_deploy_zones_config.py [deleted file]
lib/pp_admintools/ldap_app.py [deleted file]
lib/pp_admintools/ldap_config.py [deleted file]
lib/pp_admintools/mail_app.py [deleted file]
lib/pp_admintools/mail_config.py [deleted file]
lib/pp_admintools/pdns_app.py [deleted file]
lib/pp_admintools/pdns_config.py [deleted file]
test/test_05_mailcfg.py
test/test_06_ldapcfg.py

index 9c23cc3a14d57b06713274e398a45a1cb5f8596a..93f617fe0ca45567f2453151ceec8539f439aa39 100755 (executable)
@@ -43,7 +43,7 @@ module_dir = lib_dir.joinpath('pp_admintools')
 if module_dir.exists():
     sys.path.insert(0, str(lib_dir))
 
-from pp_admintools.dns_deploy_zones_app import PpDeployZonesApp
+from pp_admintools.app.dns_deploy_zones import PpDeployZonesApp
 
 __author__ = 'Frank Brehm <frank.brehm@pixelpark.com>'
 __copyright__ = '(C) 2022 by Frank Brehm, Pixelpark GmbH, Berlin'
index 47ab83f40beef10555bc78fe04b4cba6b220caf1..b85ae36a670fa625c1e21b649945621c870f2ee6 100755 (executable)
@@ -46,7 +46,7 @@ module_dir = lib_dir.joinpath('pp_admintools')
 if module_dir.exists():
     sys.path.insert(0, str(lib_dir))
 
-from pp_admintools.apps.remove_ldap_user import RemoveLdapUserApplication
+from pp_admintools.app.remove_ldap_user import RemoveLdapUserApplication
 
 appname = os.path.basename(sys.argv[0])
 
diff --git a/lib/pp_admintools/app/__init__.py b/lib/pp_admintools/app/__init__.py
new file mode 100644 (file)
index 0000000..892def3
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/env python3
+# -*- coding: utf-8 -*-
+
+__version__ = '0.1.0'
+
+# vim: ts=4 et list
diff --git a/lib/pp_admintools/app/dns_deploy_zones.py b/lib/pp_admintools/app/dns_deploy_zones.py
new file mode 100644 (file)
index 0000000..c2cbcdf
--- /dev/null
@@ -0,0 +1,970 @@
+#!/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 datetime
+import tempfile
+import time
+import shutil
+import pipes
+import ipaddress
+
+from subprocess import Popen, TimeoutExpired, PIPE
+
+from pathlib import Path
+
+# Third party modules
+import six
+from pytz import timezone, UnknownTimeZoneError
+
+# Own modules
+from fb_tools.common import pp, to_str
+
+from fb_tools.app import BaseApplication
+
+from fb_tools.pidfile import PidFileError, PidFile
+
+from .. import __version__ as GLOBAL_VERSION
+
+from .pdns import PpPDNSAppError, PpPDNSApplication
+
+from ..config.dns_deploy_zones import DnsDeployZonesConfig
+
+from ..xlate import XLATOR
+
+__version__ = '0.8.3'
+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.
+    """
+
+    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'\.+$')
+
+    default_local_tz_name = 'Europe/Berlin'
+
+    open_args = {}
+    if six.PY3:
+        open_args = {
+            'encoding': 'utf-8',
+            'errors': 'surrogateescape',
+        }
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, base_dir=None, version=GLOBAL_VERSION,
+            cfg_class=DnsDeployZonesConfig):
+
+        self.zones = {}
+        self.pidfile = None
+
+        self._show_simulate_opt = True
+        self.cfg = None
+
+        # Configuration files and directories
+
+        self.tempdir = None
+        self.temp_zones_cfg_file = None
+        self.keep_tempdir = False
+        self.keep_backup = False
+
+        self.local_tz = None
+        self.local_tz_name = self.default_local_tz_name
+
+        self.backup_suffix = (
+            '.' + datetime.datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S') + '.bak')
+
+        self.reload_necessary = False
+        self.restart_necessary = False
+
+        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_class=cfg_class, initialized=False, instance="public",
+        )
+
+        masters = []
+        for addr in sorted(self.cfg.masters, key=ipaddress.ip_address):
+            if addr not in self.local_addresses:
+                masters.append(addr)
+
+        self.cfg.masters = masters
+
+        self.initialized = True
+
+    # -------------------------------------------
+    @property
+    def cmd_named_checkconf(self):
+        """The OS command for named-checkconf."""
+
+        checkconf = DnsDeployZonesConfig.default_named_checkconf
+        if self.cfg:
+            checkconf = self.cfg.named_checkconf
+        return str(checkconf)
+
+    # -------------------------------------------
+    @property
+    def cmd_named_reload(self):
+        """The OS command to reload the BIND nameserver."""
+
+        rndc = DnsDeployZonesConfig.default_rndc
+        if self.cfg:
+            rndc = self.cfg.rndc
+
+        return "{} reload".format(rndc)
+
+    # -------------------------------------------
+    @property
+    def cmd_named_status(self):
+        """The OS command to show the status of the BIND nameserver service."""
+
+        systemctl = DnsDeployZonesConfig.default_systemctl
+        if self.cfg:
+            systemctl = self.cfg.systemctl
+
+        return "{} status named.service".format(systemctl)
+
+    # -------------------------------------------
+    @property
+    def cmd_named_start(self):
+        """The OS command to start the BIND nameserver service."""
+
+        systemctl = DnsDeployZonesConfig.default_systemctl
+        if self.cfg:
+            systemctl = self.cfg.systemctl
+
+        return "{} start named.service".format(systemctl)
+
+    # -------------------------------------------
+    @property
+    def cmd_named_restart(self):
+        """The OS command to restart the BIND nameserver service."""
+
+        systemctl = DnsDeployZonesConfig.default_systemctl
+        if self.cfg:
+            systemctl = self.cfg.systemctl
+
+        return "{} restart named.service".format(systemctl)
+
+    # -------------------------------------------
+    @property
+    def named_zones_cfg_file(self):
+        """The file for configuration of all own zones."""
+
+        conf_dir = DnsDeployZonesConfig.default_named_conf_dir
+        zones_cfg_file = DnsDeployZonesConfig.default_named_zones_cfg_file
+        if self.cfg:
+            conf_dir = self.cfg.named_conf_dir
+            zones_cfg_file = self.cfg.named_zones_cfg_file
+
+        return (conf_dir / zones_cfg_file).resolve()
+
+    # -------------------------------------------
+    @property
+    def named_slavedir_rel(self):
+        """The directory for zone files of slave zones."""
+
+        if self.cfg:
+            return self.cfg.named_slavedir
+        return DnsDeployZonesConfig.default_named_slavedir
+
+    # -------------------------------------------
+    @property
+    def named_basedir(self):
+        """The base directory of named, where all volatile data are stored."""
+
+        if self.cfg:
+            return self.cfg.named_basedir
+        return DnsDeployZonesConfig.default_named_basedir
+
+    # -------------------------------------------
+    @property
+    def named_slavedir_abs(self):
+        """The directory for zone files of slave zones."""
+
+        return (self.named_basedir / self.named_slavedir_rel).resolve()
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(PpDeployZonesApp, self).as_dict(short=short)
+
+        res['named_slavedir_abs'] = self.named_slavedir_abs
+        res['cmd_named_checkconf'] = self.cmd_named_checkconf
+        res['cmd_named_reload'] = self.cmd_named_reload
+        res['cmd_named_status'] = self.cmd_named_status
+        res['cmd_named_start'] = self.cmd_named_start
+        res['cmd_named_restart'] = self.cmd_named_restart
+        res['named_zones_cfg_file'] = self.named_zones_cfg_file
+        res['named_basedir'] = self.named_basedir
+        res['named_slavedir_rel'] = self.named_slavedir_rel
+        res['named_slavedir_abs'] = self.named_slavedir_abs
+
+        return res
+
+    # -------------------------------------------------------------------------
+    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 post_init(self):
+
+        if not self.quiet:
+            print('')
+
+        LOG.debug(_("Post init phase."))
+
+        super(PpDeployZonesApp, self).post_init()
+
+        LOG.debug(_("My own post init phase."))
+
+        cmd_namedcheckconf = self.get_command('named-checkconf', resolve=True)
+        if not cmd_namedcheckconf:
+            self.exit(1)
+        self.cfg.named_checkconf = cmd_namedcheckconf
+
+        self.pidfile = PidFile(
+            filename=self.cfg.pidfile, appname=self.appname, verbose=self.verbose,
+            base_dir=self.base_dir, simulate=self.simulate)
+
+        if 'TZ' in os.environ and os.environ['TZ']:
+            self.local_tz_name = os.environ['TZ']
+        try:
+            self.local_tz = timezone(self.local_tz_name)
+        except UnknownTimeZoneError:
+            LOG.error(_("Unknown time zone: {!r}.").format(self.local_tz_name))
+            self.exit(6)
+
+    # -------------------------------------------------------------------------
+    def current_timestamp(self):
+
+        if self.local_tz:
+            return datetime.datetime.now(self.local_tz).strftime('%Y-%m-%d %H:%M:%S %Z')
+        return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+    # -------------------------------------------------------------------------
+    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:
+                msg += ' ' + _("But in simulation mode we are continuing nevertheless.")
+                LOG.warn(msg)
+                time.sleep(1)
+            else:
+                LOG.error(msg)
+                self.exit(1)
+
+        super(PpDeployZonesApp, self).pre_run()
+
+        if self.cfg.pdns_instance == 'global':
+            LOG.error(_(
+                "Using the global DNS master is not supported, "
+                "please use 'local' or 'public'"))
+            self.exit(1)
+
+    # -------------------------------------------------------------------------
+    def _run(self):
+
+        LOG.info(_("Starting: {}").format(self.current_timestamp()))
+
+        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(self.current_timestamp()))
+
+    # -------------------------------------------------------------------------
+    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 backup_file.exists():
+                if self.keep_backup:
+                    LOG.info(_("Keep existing backup file {!r}.").format(str(backup_file)))
+                else:
+                    LOG.info(_("Removing {!r} ...").format(str(backup_file)))
+                    if not self.simulate:
+                        backup_file.unlink()
+
+        # -----------------------
+        def emit_rm_err(function, path, excinfo):
+            LOG.error(_("Error removing {p!r} - {c}: {e}").format(
+                p=str(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(str(self.tempdir))
+                LOG.warn(msg)
+            else:
+                LOG.debug(_("Destroying temporary directory {!r} ...").format(str(self.tempdir)))
+                shutil.rmtree(str(self.tempdir), False, emit_rm_err)
+                self.tempdir = None
+
+    # -------------------------------------------------------------------------
+    def init_temp_objects(self):
+        """Init temporary objects and properties."""
+
+        self.tempdir = Path(tempfile.mkdtemp(prefix=(self.appname + '.'), suffix='.tmp.d'))
+        LOG.debug(_("Temporary directory: {!r}.").format(str(self.tempdir)))
+
+        self.temp_zones_cfg_file = self.tempdir / self.cfg.named_zones_cfg_file
+
+        if self.verbose > 1:
+            LOG.debug(_("Temporary zones conf: {!r}").format(str(self.temp_zones_cfg_file)))
+
+    # -------------------------------------------------------------------------
+    def get_named_keys(self):
+
+        LOG.info(_("Trying to get all keys from named.conf ..."))
+
+        cmd = shlex.split(self.cmd_named_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.cfg.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(str(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 self.temp_zones_cfg_file.open('w', **self.open_args) as fh:
+            fh.write(content)
+
+        if self.verbose > 2:
+            LOG.debug(
+                _("Generated file {!r}:").format(
+                    str(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('// {}'.format(show_name))
+        lines.append('zone "{}" in {{'.format(zname))
+        lines.append('\tmasters {')
+        for master in self.cfg.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.cfg.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=str(file_src), two=str(file_tgt)))
+
+        if not file_src.exists():
+            msg = _("{what} {f!r} does not exists.").format(
+                what=_("Source file"), f=str(file_src))
+            raise PpDeployZonesError(msg)
+        if not file_src.is_file():
+            msg = _("{what} {f!r} is not a regular file.").format(
+                what=_("Source file"), f=str(file_src))
+            raise PpDeployZonesError(msg)
+
+        if not file_tgt.exists():
+            msg = _("{what} {f!r} does not exists.").format(
+                what=_("Target file"), f=str(file_tgt))
+            LOG.debug(msg)
+            return False
+        if not file_tgt.is_file():
+            msg = _("{what} {f!r} is not a regular file.").format(
+                what=_("Target file"), f=str(file_tgt))
+            raise PpDeployZonesError(msg)
+
+        # Reading source file
+        content_src = ''
+        if self.verbose > 2:
+            LOG.debug(_("Reading {!r} ...").format(str(file_src)))
+        content_src = file_src.read_text(**self.open_args)
+        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:
+            msg = _("Cleaned version of {!r}:").format(str(file_src))
+            msg += '\n' + '\n'.join(lines_src)
+            LOG.debug(msg)
+
+        # Reading target file
+        content_tgt = ''
+        if self.verbose > 2:
+            LOG.debug(_("Reading {!r} ...").format(str(file_tgt)))
+        content_tgt = file_tgt.read_text(**self.open_args)
+        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:
+            msg = _("Cleaned version of {!r}:").format(str(file_tgt))
+            msg += '\n' + '\n'.join(lines_tgt)
+            LOG.debug(msg)
+
+        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=str(file_src), sl=len(lines_src), tf=str(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=str(file_src), tf=str(file_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 = Path(str(tgt_file) + self.backup_suffix)
+
+            if tgt_file.exists():
+                self.moved_files[tgt_file] = backup_file
+                LOG.info(_("Copying {frm!r} => {to!r} ...").format(
+                    frm=str(tgt_file), to=str(backup_file)))
+                if not self.simulate:
+                    shutil.copy2(str(tgt_file), str(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=str(src_file), to=str(tgt_file)))
+            if not self.simulate:
+                shutil.copy2(str(src_file), str(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=str(backup_file), to=str(tgt_file)))
+            if not self.simulate:
+                if backup_file.exists():
+                    backup_file.rename(tgt_file)
+                else:
+                    LOG.error(_("Could not find backup file {!r}.").format(str(backup_file)))
+
+    # -------------------------------------------------------------------------
+    def check_namedconf(self):
+
+        LOG.info(_("Checking syntax correctness of named.conf ..."))
+        cmd = shlex.split(self.cmd_named_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_named_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_named_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_named_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_named_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/app/ldap.py b/lib/pp_admintools/app/ldap.py
new file mode 100644 (file)
index 0000000..98c7c0a
--- /dev/null
@@ -0,0 +1,368 @@
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2022 by Frank Brehm, Berlin
+@summary: A base module for application classes with LDAP support
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import logging
+import os
+import argparse
+
+try:
+    from pathlib import Path
+except ImportError:
+    from pathlib2 import Path
+
+# Third party modules
+from fb_tools.cfg_app import FbConfigApplication
+
+from fb_tools.errors import FbAppError
+
+# Own modules
+from .. import __version__ as GLOBAL_VERSION
+
+from ..xlate import XLATOR
+
+from .. import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
+
+# from ..argparse_actions import PortOptionAction
+
+# from ..config.ldap import LdapConfigError
+from ..config.ldap import LdapConnectionInfo, LdapConfiguration
+# rom ..config.ldap import DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS
+from ..config.ldap import DEFAULT_TIMEOUT, MAX_TIMEOUT
+
+__version__ = '0.1.4'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+ngettext = XLATOR.ngettext
+
+
+# =============================================================================
+class LdapAppError(FbAppError):
+    """ Base exception class for all exceptions in all LDAP using application classes."""
+    pass
+
+
+# =============================================================================
+class PasswordFileOptionAction(argparse.Action):
+
+    # -------------------------------------------------------------------------
+    def __init__(self, option_strings, must_exists=True, *args, **kwargs):
+
+        self.must_exists = bool(must_exists)
+
+        super(PasswordFileOptionAction, self).__init__(
+            option_strings=option_strings, *args, **kwargs)
+
+    # -------------------------------------------------------------------------
+    def __call__(self, parser, namespace, given_path, option_string=None):
+
+        path = Path(given_path)
+        if not path.is_absolute():
+            msg = _("The path {!r} must be an absolute path.").format(given_path)
+            raise argparse.ArgumentError(self, msg)
+
+        if self.must_exists:
+
+            if not path.exists():
+                msg = _("The file {!r} does not exists.").format(str(path))
+                raise argparse.ArgumentError(self, msg)
+
+            if not path.is_file():
+                msg = _("The given path {!r} exists, but is not a regular file.").format(str(path))
+                raise argparse.ArgumentError(self, msg)
+
+            if not os.access(str(path), os.R_OK):
+                msg = _("The given file {!r} is not readable.").format(str(path))
+                raise argparse.ArgumentError(self, msg)
+
+        setattr(namespace, self.dest, path)
+
+
+# =============================================================================
+class LdapPortOptionAction(argparse.Action):
+
+    # -------------------------------------------------------------------------
+    def __init__(self, option_strings, *args, **kwargs):
+
+        super(LdapPortOptionAction, self).__init__(
+            option_strings=option_strings, *args, **kwargs)
+
+    # -------------------------------------------------------------------------
+    def __call__(self, parser, namespace, given_port, option_string=None):
+
+        try:
+            port = int(given_port)
+            if port <= 0 or port > MAX_PORT_NUMBER:
+                msg = _(
+                    "a port number must be greater than zero and less "
+                    "or equal to {}.").format(MAX_PORT_NUMBER)
+                raise ValueError(msg)
+        except (ValueError, TypeError) as e:
+            msg = _("Wrong port number {!r}:").format(given_port)
+            msg += ' ' + str(e)
+            raise argparse.ArgumentError(self, msg)
+
+        setattr(namespace, self.dest, port)
+
+
+# =============================================================================
+class TimeoutOptionAction(argparse.Action):
+
+    # -------------------------------------------------------------------------
+    def __init__(self, option_strings, *args, **kwargs):
+
+        super(TimeoutOptionAction, self).__init__(
+            option_strings=option_strings, *args, **kwargs)
+
+    # -------------------------------------------------------------------------
+    def __call__(self, parser, namespace, given_timeout, option_string=None):
+
+        try:
+            timeout = int(given_timeout)
+            if timeout <= 0 or timeout > MAX_TIMEOUT:
+                msg = _(
+                    "a timeout must be greater than zero and less "
+                    "or equal to {}.").format(MAX_TIMEOUT)
+                raise ValueError(msg)
+        except (ValueError, TypeError) as e:
+            msg = _("Wrong timeout {!r}:").format(given_timeout)
+            msg += ' ' + str(e)
+            raise argparse.ArgumentError(self, msg)
+
+        setattr(namespace, self.dest, timeout)
+
+
+# =============================================================================
+class BaseLdapApplication(FbConfigApplication):
+    """
+    Base class for all application classes using LDAP.
+    """
+
+    use_default_ldap_connection = True
+    show_cmdline_ldap_timeout = True
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=GLOBAL_VERSION, base_dir=None,
+            cfg_class=LdapConfiguration, initialized=False, usage=None, description=None,
+            argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None,
+            config_dir=DEFAULT_CONFIG_DIR):
+
+        self._password_file = None
+
+        super(BaseLdapApplication, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            description=description, cfg_class=cfg_class, initialized=False,
+            argparse_epilog=argparse_epilog, argparse_prefix_chars=argparse_prefix_chars,
+            env_prefix=env_prefix, config_dir=config_dir
+        )
+
+    # -----------------------------------------------------------
+    @property
+    def password_file(self):
+        """The file containing the password of the Bind DN of the default LDAP connection."""
+        return self._password_file
+
+    @password_file.setter
+    def password_file(self, value):
+
+        path = Path(value)
+        if not path.is_absolute():
+            msg = _("The path {!r} must be an absolute path.").format(value)
+            raise LdapAppError(msg)
+
+        if not path.exists():
+            msg = _("The file {!r} does not exists.").format(str(path))
+            raise LdapAppError(msg)
+
+        if not path.is_file():
+            msg = _("The given path {!r} exists, but is not a regular file.").format(str(path))
+            raise LdapAppError(msg)
+
+        if not os.access(str(path), os.R_OK):
+            msg = _("The given file {!r} is not readable.").format(str(path))
+            raise LdapAppError(msg)
+
+        self._password_file = path
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(BaseLdapApplication, self).as_dict(short=short)
+
+        res['password_file'] = self.password_file
+        res['show_cmdline_ldap_timeout'] = self.show_cmdline_ldap_timeout
+        res['use_default_ldap_connection'] = self.use_default_ldap_connection
+
+        return res
+
+    # -------------------------------------------------------------------------
+    def init_arg_parser(self):
+        """
+        Public available method to initiate the argument parser.
+        """
+
+        super(BaseLdapApplication, self).init_arg_parser()
+
+        ldap_group = self.arg_parser.add_argument_group(_(
+            'Options for the default LDAP connection'))
+
+        if self.use_default_ldap_connection:
+
+            ldap_host = LdapConfiguration.default_ldap_server
+            ldap_ssl = LdapConfiguration.use_ssl_on_default
+            ldap_ssl_str = _('No')
+            if ldap_ssl:
+                ldap_ssl_str = _('Yes')
+            ldap_port = LdapConfiguration.default_ldap_port
+            ldap_base_dn = LdapConfiguration.default_base_dn
+            ldap_bind_dn = LdapConfiguration.default_bind_dn
+
+            ldap_group.add_argument(
+                '-H', '--ldap-host', metavar=_("HOST"), dest="ldap_host",
+                help=_(
+                    "Hostname or address of the LDAP server to use. Default: {!r}").format(
+                    ldap_host),
+            )
+
+            ldap_group.add_argument(
+                '--ssl', '--ldaps', '--ldap-ssl', dest="ldap_ssl", action="store_true",
+                help=_("Use ldaps to connect to the LDAP server. Default: {}").format(
+                    ldap_ssl_str),
+            )
+
+            ldap_group.add_argument(
+                '-p', '--ldap-port', metavar=_("PORT"), type=int, dest="ldap_port",
+                action=LdapPortOptionAction,
+                help=_("The port number to connect to the LDAP server. Default: {}").format(
+                    ldap_port),
+            )
+
+            ldap_group.add_argument(
+                '-b', '--base-dn', metavar="DN", dest="ldap_base_dn",
+                help=_(
+                    "The base DN used as the root for the LDAP searches. "
+                    "Default: {!r}").format(ldap_base_dn),
+            )
+
+            ldap_group.add_argument(
+                '-D', '--bind-dn', metavar="DN", dest="ldap_bind_dn",
+                help=_(
+                    "The Bind DN to use to connect to the LDAP server. Default: {!r}").format(
+                    ldap_bind_dn),
+            )
+
+            pw_group = ldap_group.add_mutually_exclusive_group()
+
+            pw_group.add_argument(
+                '-w', '--bind-pw', '--password', metavar=_("PASSWORD"), dest="ldap_bind_pw",
+                help=_("Use PASSWORD as the password for simple LDAP authentication."),
+            )
+
+            pw_group.add_argument(
+                '-W', '--password-prompt', action="store_true", dest="ldap_pw_prompt",
+                help=_(
+                    "Prompt for simple LDAP authentication. This is used instead of "
+                    "specifying the password on the command line."),
+            )
+
+            pw_group.add_argument(
+                '-y', '--password-file', metavar=_('PASSWORD_FILE'), dest="ldap_pw_file",
+                action=PasswordFileOptionAction,
+                help=_("Use contents of PASSWORD_FILE as the password for simple authentication."),
+            )
+
+        if self.show_cmdline_ldap_timeout:
+            self.arg_parser.add_argument(
+                '-T', '--timeout', metavar=_('SECONDS'), dest="ldap_timeout",
+                action=TimeoutOptionAction,
+                help=_(
+                    "Using the given timeout in seconds for all LDAP operations. "
+                    "Default: {}").format(DEFAULT_TIMEOUT),
+            )
+
+    # -------------------------------------------------------------------------
+    def post_init(self):
+        """
+        Method to execute before calling run(). Here could be done some
+        finishing actions after reading in commandline parameters,
+        configuration a.s.o.
+
+        This method could be overwritten by descendant classes, these
+        methhods should allways include a call to post_init() of the
+        parent class.
+
+        """
+
+        self.initialized = False
+
+        super(BaseLdapApplication, self).post_init()
+
+        if not self.use_default_ldap_connection:
+            return
+
+        if 'default' in self.cfg.ldap_connection:
+            default_connection = self.cfg.ldap_connection['default']
+        else:
+            default_connection = LdapConnectionInfo(
+                appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
+                host=LdapConfiguration.default_ldap_server,
+                use_ldaps=LdapConfiguration.use_ssl_on_default,
+                port=LdapConfiguration.default_ldap_port,
+                base_dn=LdapConfiguration.default_base_dn,
+                bind_dn=LdapConfiguration.default_bind_dn,
+                initialized=False)
+            self.cfg.ldap_connection['default'] = default_connection
+
+        v = getattr(self.args, 'ldap_host', None)
+        if v:
+            default_connection.host = v
+
+        if getattr(self.args, 'ldap_ssl', False):
+            default_connection.use_ldaps = True
+
+        v = getattr(self.args, 'ldap_port', None)
+        if v is not None:
+            default_connection.port = v
+
+        v = getattr(self.args, 'ldap_base_dn', None)
+        if v:
+            default_connection.base_dn = v
+
+        v = getattr(self.args, 'ldap_bind_dn', None)
+        if v:
+            default_connection.bind_dn = v
+
+        v = getattr(self.args, 'ldap_bind_pw', None)
+        if v:
+            default_connection.bind_pw = v
+
+        v = getattr(self.args, 'ldap_timeout', None)
+        if v:
+            self.cfg.ldap_timeout = v
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/app/mail.py b/lib/pp_admintools/app/mail.py
new file mode 100644 (file)
index 0000000..0cbae47
--- /dev/null
@@ -0,0 +1,343 @@
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2022 by Frank Brehm, Berlin
+@summary: A base module for application classes with mail sending support
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import logging
+import copy
+import pipes
+import os
+
+from email.mime.text import MIMEText
+from email import charset
+
+from subprocess import Popen, PIPE
+
+import smtplib
+
+# Third party modules
+from fb_tools.common import pp
+
+from fb_tools.cfg_app import FbConfigApplication
+
+from fb_tools.errors import FbAppError
+
+from fb_tools.xlate import format_list
+
+from fb_tools import MailAddress
+
+# Own modules
+from .. import __version__ as GLOBAL_VERSION
+from .. import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
+
+from ..xlate import XLATOR
+
+from ..argparse_actions import PortOptionAction
+
+from ..config.mail import MailConfiguration
+from ..config.mail import VALID_MAIL_METHODS
+
+__version__ = '0.2.8'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+ngettext = XLATOR.ngettext
+
+
+# =============================================================================
+class MailAppError(FbAppError):
+    """ Base exception class for all exceptions in all mail sending application classes."""
+    pass
+
+
+# =============================================================================
+class BaseMailApplication(FbConfigApplication):
+    """
+    Base class for all mail sending application classes.
+    """
+
+    charset.add_charset('utf-8', charset.SHORTEST, charset.QP)
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=GLOBAL_VERSION, base_dir=None,
+            cfg_class=MailConfiguration, initialized=False, usage=None, description=None,
+            argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None,
+            config_dir=DEFAULT_CONFIG_DIR):
+
+        super(BaseMailApplication, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            description=description, cfg_class=cfg_class, initialized=False,
+            argparse_epilog=argparse_epilog, argparse_prefix_chars=argparse_prefix_chars,
+            env_prefix=env_prefix, config_dir=config_dir
+        )
+
+    # -------------------------------------------------------------------------
+    def post_init(self):
+        """
+        Method to execute before calling run(). Here could be done some
+        finishing actions after reading in commandline parameters,
+        configuration a.s.o.
+
+        This method could be overwritten by descendant classes, these
+        methhods should allways include a call to post_init() of the
+        parent class.
+
+        """
+
+        self.initialized = False
+
+        super(BaseMailApplication, self).post_init()
+
+        v = getattr(self.args, 'mail_method', None)
+        if v:
+            self.cfg.mail_method = v
+
+        v = getattr(self.args, 'mail_server', None)
+        if v:
+            self.cfg.mail_server = v
+
+        v = getattr(self.args, 'smtp_port', None)
+        if v is not None:
+            if v <= 0 or v > MAX_PORT_NUMBER:
+                msg = _("Got invalid SMTP port number {!r}.").format(v)
+                LOG.error(msg)
+            else:
+                self.cfg.smtp_port = v
+
+        self._perform_cmdline_mail_from()
+        self._perform_cmdline_mail_rcpt()
+        self._perform_cmdline_mail_cc()
+        self._perform_cmdline_reply_to()
+
+    # -------------------------------------------------------------------------
+    def _perform_cmdline_mail_from(self):
+
+        v = getattr(self.args, 'mail_from', None)
+        if not v:
+            return
+
+        if not MailAddress.valid_address(v):
+            msg = _("Got invalid mail from address {!r}.").format(v)
+            LOG.error(msg)
+            self.exit(1)
+
+        self.cfg.mail_from = v
+
+    # -------------------------------------------------------------------------
+    def _perform_cmdline_mail_rcpt(self):
+
+        v = getattr(self.args, 'mail_recipients', None)
+        if v is None:
+            return
+
+        recipients = []
+        bad_rcpts = []
+
+        for addr in v:
+            if MailAddress.valid_address(addr):
+                recipients.append(addr)
+            else:
+                bad_rcpts.append(addr)
+
+        if bad_rcpts:
+            msg = _("Got invalid recipient mail addresses:")
+            msg += " " + format_list(bad_rcpts, do_repr=True)
+            LOG.error(msg)
+            self.exit(1)
+
+        self.cfg.mail_recipients = copy.copy(recipients)
+
+        if not self.cfg.mail_recipients:
+            msg = ("Did not found any valid recipient mail addresses.")
+            LOG.error(msg)
+
+    # -------------------------------------------------------------------------
+    def _perform_cmdline_mail_cc(self):
+
+        v = getattr(self.args, 'mail_cc', None)
+        if v is None:
+            return
+
+        cc = []
+        bad_cc = []
+
+        for addr in v:
+            if MailAddress.valid_address(addr):
+                cc.append(addr)
+            else:
+                bad_cc.append(addr)
+
+        if bad_cc:
+            msg = _("Got invalid cc mail addresses:")
+            msg += " " + format_list(bad_cc, do_repr=True)
+            LOG.error(msg)
+            self.exit(1)
+
+        self.cfg.mail_cc = copy.copy(cc)
+
+    # -------------------------------------------------------------------------
+    def _perform_cmdline_reply_to(self):
+
+        v = getattr(self.args, 'mail_reply_to', None)
+        if not v:
+            return
+
+        if not MailAddress.valid_address(v):
+            msg = _("Got invalid reply mail address {!r}.").format(v)
+            LOG.error(msg)
+            self.exit(1)
+
+        self.cfg.reply_to = v
+
+    # -------------------------------------------------------------------------
+    def init_arg_parser(self):
+        """
+        Public available method to initiate the argument parser.
+        """
+
+        super(BaseMailApplication, self).init_arg_parser()
+
+        mail_group = self.arg_parser.add_argument_group(_('Mailing options'))
+
+        mail_from = MailConfiguration.default_mail_from_complete
+        mail_method = MailConfiguration.default_mail_method
+        mail_server = MailConfiguration.default_mail_server
+        smtp_port = MailConfiguration.default_smtp_port
+
+        if self.cfg:
+            mail_from = self.cfg.mail_from
+            mail_method = self.cfg.mail_method
+            mail_server = self.cfg.mail_server
+            smtp_port = self.cfg.smtp_port
+
+        mail_group.add_argument(
+            '--from', '--mail-from',
+            metavar=_("ADDRESS"), dest="mail_from",
+            help=_(
+                "Sender mail address for mails generated by this script. "
+                "Default: {!r}").format(mail_from),
+        )
+
+        mail_group.add_argument(
+            '--recipients', '--mail-recipients',
+            metavar=_("ADDRESS"), nargs='+', dest="mail_recipients",
+            help=_("Mail addresses of all recipients for mails generated by this script.")
+        )
+
+        mail_group.add_argument(
+            '--cc', '--mail-cc',
+            metavar=_("ADDRESS"), nargs='*', dest="mail_cc",
+            help=_("Mail addresses of all CC recipients for mails generated by this script.")
+        )
+
+        mail_group.add_argument(
+            '--reply-to', '--mail-reply-to',
+            metavar=_("ADDRESS"), dest="mail_reply_to",
+            help=_("Reply mail address for mails generated by this script.")
+        )
+
+        method_list = format_list(VALID_MAIL_METHODS, do_repr=True)
+        mail_group.add_argument(
+            '--mail-method',
+            metavar=_("METHOD"), choices=VALID_MAIL_METHODS, dest="mail_method",
+            help=_(
+                "Method for sending the mails generated by this script. "
+                "Valid values: {v}, default: {d!r}.").format(
+                    v=method_list, d=mail_method)
+        )
+
+        mail_group.add_argument(
+            '--mail-server',
+            metavar=_("SERVER"), dest="mail_server",
+            help=_(
+                "Mail server for submitting generated by this script if "
+                "the mail method of this script is 'smtp'. Default: {!r}.").format(mail_server)
+        )
+
+        mail_group.add_argument(
+            '--smtp-port',
+            metavar=_("PORT"), type=int, dest='smtp_port', what="SMTP",
+            action=PortOptionAction,
+            help=_(
+                "The port to use for submitting generated by this script if "
+                "the mail method of this script is 'smtp'. Default: {}.").format(smtp_port)
+        )
+
+    # -------------------------------------------------------------------------
+    def perform_arg_parser(self):
+
+        if self.verbose > 2:
+            LOG.debug(_("Got command line arguments:") + '\n' + pp(self.args))
+
+    # -------------------------------------------------------------------------
+    def send_mail(self, subject, body):
+
+        mail = MIMEText(body, 'plain', 'utf-8')
+        mail['Subject'] = subject
+        mail['From'] = self.cfg.mail_from
+        mail['To'] = ', '.join(self.cfg.mail_recipients)
+        mail['Reply-To'] = self.cfg.reply_to
+        mail['X-Mailer'] = self.cfg.xmailer
+        if self.mail_cc:
+            mail['Cc'] = ', '.join(self.mail_cc)
+
+        if self.verbose > 1:
+            LOG.debug(_("Mail to send:") + '\n' + mail.as_string(unixfrom=True))
+
+        if self.mail_method == 'smtp':
+            self._send_mail_smtp(mail)
+        else:
+            self._send_mail_sendmail(mail)
+
+    # -------------------------------------------------------------------------
+    def _send_mail_smtp(self, mail):
+
+        with smtplib.SMTP(self.cfg.mail_server, self.cfg.smtp_port) as smtp:
+            if self.verbose > 2:
+                smtp.set_debuglevel(2)
+            elif self.verbose > 1:
+                smtp.set_debuglevel(1)
+
+            smtp.send_message(mail)
+
+    # -------------------------------------------------------------------------
+    def _send_mail_sendmail(self, mail):
+
+        # Searching for the location of sendmail ...
+        paths = (
+            '/usr/sbin/sendmail',
+            '/usr/lib/sendmail',
+        )
+        sendmail = None
+        for path in paths:
+            if os.path.isfile(path) and os.access(path, os.X_OK):
+                sendmail = path
+                break
+
+        if not sendmail:
+            msg = _("Did not found sendmail executable.")
+            LOG.error(msg)
+            return
+
+        cmd = [sendmail, "-t", "-oi"]
+        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
+        LOG.debug(_("Executing: {}").format(cmd_str))
+
+        p = Popen(cmd, stdin=PIPE, universal_newlines=True)
+        p.communicate(mail.as_string())
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/app/pdns.py b/lib/pp_admintools/app/pdns.py
new file mode 100644 (file)
index 0000000..76118dc
--- /dev/null
@@ -0,0 +1,602 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2022 by Frank Brehm, Berlin
+@summary: The module for a application object related to PowerDNS.
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import logging
+import logging.config
+import re
+# import copy
+import os
+import ipaddress
+import socket
+
+# Third party modules
+import psutil
+
+# Own modules
+from fb_tools.common import pp
+
+from fb_pdnstools.zone import PowerDNSZone
+from fb_pdnstools.server import PowerDNSServer
+from fb_pdnstools.errors import PDNSApiNotFoundError
+from fb_pdnstools.errors import PDNSApiValidationError
+from fb_tools.xlate import format_list
+
+from .. import __version__ as GLOBAL_VERSION
+
+from ..argparse_actions import PortOptionAction, TimeoutOptionAction
+
+from ..app.mail import MailAppError, BaseMailApplication
+
+from ..config.pdns import PdnsConfiguration
+# from ..config.pdns import PdnsConfigError, PdnsConfiguration
+
+from ..xlate import XLATOR
+
+__version__ = '0.9.2'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+
+
+# =============================================================================
+class PpPDNSAppError(MailAppError):
+    """Base error class for all exceptions happened during
+    execution this configured application"""
+    pass
+
+
+# =============================================================================
+class PpPDNSApplication(BaseMailApplication):
+    """
+    Class for configured application objects related to PowerDNS.
+    """
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=GLOBAL_VERSION, base_dir=None,
+            cfg_class=PdnsConfiguration, initialized=False, usage=None, description=None,
+            argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None,
+            instance=None):
+
+        if instance:
+            self._instance = instance
+        else:
+            self._instance = PdnsConfiguration.default_pdns_instance
+
+        self._api_key = None
+        self._api_host = None
+        self._api_port = None
+        self._api_servername = None
+        self._api_server_version = 'unknown'
+
+        self.local_addresses = []
+
+        self.pdns = None
+
+        super(PpPDNSApplication, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            description=description, cfg_class=cfg_class, initialized=False,
+            argparse_epilog=argparse_epilog, argparse_prefix_chars=argparse_prefix_chars,
+            env_prefix=env_prefix,
+        )
+
+        for interface, snics in psutil.net_if_addrs().items():
+            for snic in snics:
+                if snic.family == socket.AF_INET or snic.family == socket.AF_INET6:
+                    addr = str(ipaddress.ip_address(re.sub(r'%.*', '', snic.address)))
+                    if addr not in self.local_addresses:
+                        self.local_addresses.append(addr)
+
+        if not self.cfg:
+            msg = _("Configuration not available.")
+            raise PpPDNSAppError(msg)
+
+        self.eval_instance(instance)
+
+    # -----------------------------------------------------------
+    @property
+    def api_key(self):
+        "The API key to use the PowerDNS API"
+        return self._api_key
+
+    @api_key.setter
+    def api_key(self, value):
+        if value is None or str(value).strip() == '':
+            raise PpPDNSAppError(_("Invalid API key {!r} given.").format(value))
+        self._api_key = str(value).strip()
+
+    # -----------------------------------------------------------
+    @property
+    def api_host(self):
+        "The host name or address providing the PowerDNS API."
+        return self._api_host
+
+    @api_host.setter
+    def api_host(self, value):
+        if value is None or str(value).strip() == '':
+            raise PpPDNSAppError(_("Invalid API host {!r} given.").format(value))
+        self._api_host = str(value).strip().lower()
+
+    # -----------------------------------------------------------
+    @property
+    def api_port(self):
+        "The TCP port number of the PowerDNS API."
+        return self._api_port
+
+    @api_port.setter
+    def api_port(self, value):
+        v = int(value)
+        if v < 1:
+            raise PpPDNSAppError(_("Invalid API port {!r} given.").format(value))
+        self._api_port = v
+
+    # -----------------------------------------------------------
+    @property
+    def api_servername(self):
+        "The (virtual) name of the PowerDNS server used in API calls."
+        return self._api_servername
+
+    @api_servername.setter
+    def api_servername(self, value):
+        if value is None or str(value).strip() == '':
+            raise PpPDNSAppError(_("Invalid API server name {!r} given.").format(value))
+        self._api_servername = str(value).strip()
+
+    # -----------------------------------------------------------
+    @property
+    def api_server_version(self):
+        "The version of the PowerDNS server, how provided by API."
+        return self._api_server_version
+
+    # -----------------------------------------------------------
+    @property
+    def instance(self):
+        "The name of the PowerDNS instance."
+        return self._instance
+
+    @instance.setter
+    def instance(self, value):
+        if value is None:
+            raise PpPDNSAppError(_("Invalid instance {!r} given.").format(None))
+        v = str(value).strip().lower()
+        if v not in self.api_keys.keys():
+            raise PpPDNSAppError(_("Invalid instance {!r} given.").format(value))
+
+        self.eval_instance(v)
+
+    # -------------------------------------------------------------------------
+    def eval_instance(self, inst_name):
+
+        if self.verbose > 2:
+            msg = _("Evaluating instance {!r} ...").format(inst_name)
+            LOG.debug(msg)
+
+        if not self.cfg:
+            msg = _("Configuration not available.")
+            raise PpPDNSAppError(msg)
+
+        if inst_name not in self.cfg.pdns_api_instances:
+            msg = _("PDNS instance {!r} is not configured.").format(inst_name)
+            raise PpPDNSAppError(msg)
+
+        self._instance = inst_name
+        if self.cfg.pdns_host:
+            self.api_host = self.cfg.pdns_host
+        if self.cfg.pdns_key:
+            self.api_key = self.cfg.pdns_key
+        if self.cfg.pdns_port:
+            self.api_port = self.cfg.pdns_port
+        if self.cfg.pdns_servername:
+            self.api_servername = self.cfg.pdns_servername
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(PpPDNSApplication, self).as_dict(short=short)
+        res['api_host'] = self.api_host
+        res['api_port'] = self.api_port
+        res['api_servername'] = self.api_servername
+        res['instance'] = self.instance
+        res['api_server_version'] = self.api_server_version
+
+        if self.api_key:
+            if self.verbose > 4:
+                res['api_key'] = self.api_key
+            else:
+                res['api_key'] = '******'
+        else:
+            res['api_key'] = None
+
+        return res
+
+    # -------------------------------------------------------------------------
+    def init_arg_parser(self):
+        """
+        Method to initiate the argument parser.
+
+        This method should be explicitely called by all init_arg_parser()
+        methods in descendant classes.
+        """
+
+        super(PpPDNSApplication, self).init_arg_parser()
+
+        pdns_group = self.arg_parser.add_argument_group(_('PowerDNS API options'))
+        inst_group = pdns_group.add_mutually_exclusive_group()
+
+        insts = PdnsConfiguration.valid_pdns_api_instances
+        inst_list = format_list(insts, do_repr=True)
+        default_timeout = PdnsConfiguration.default_pdns_timeout
+
+        inst_group.add_argument(
+            '-I', '--inst', '--instance',
+            metavar=_("INSTANCE"), choices=insts, dest="inst",
+            help=_(
+                "Select, which PowerDNS instance to use. Valid values: {v}, "
+                "default: {d!r}.").format(v=inst_list, d=self.instance),
+        )
+
+        inst_group.add_argument(
+            '-G', '--global',
+            action='store_true', dest="inst_global",
+            help=_("Using the {!r} PowerDNS instance.").format('global'),
+        )
+
+        inst_group.add_argument(
+            '-L', '--local',
+            action='store_true', dest="inst_local",
+            help=_("Using the {!r} PowerDNS instance.").format('local'),
+        )
+
+        inst_group.add_argument(
+            '-P', '--public',
+            action='store_true', dest="inst_public",
+            help=_("Using the {!r} PowerDNS instance.").format('public'),
+        )
+
+        pdns_group.add_argument(
+            '-p', '--port',
+            metavar=_("PORT"), type=int, dest='api_port',
+            default=PdnsConfiguration.default_pdns_api_port,
+            what="PowerDNS API", action=PortOptionAction,
+            help=_("Which port to connect to PowerDNS API, default: {}.").format(
+                PdnsConfiguration.default_pdns_api_port),
+        )
+
+        pdns_group.add_argument(
+            '-t', '--timeout',
+            metavar=_("SECS"), type=int, dest='timeout', default=default_timeout,
+            what=_("PowerDNS API access"), action=TimeoutOptionAction,
+            help=_("The timeout in seconds to request the PowerDNS API, default: {}.").format(
+                default_timeout),
+        )
+
+    # -------------------------------------------------------------------------
+    def perform_arg_parser(self):
+        """
+        Public available method to execute some actions after parsing
+        the command line parameters.
+        """
+
+    # -------------------------------------------------------------------------
+    def _check_path_config(self, section, section_name, key, class_prop, absolute=True, desc=None):
+
+        if key not in section:
+            return
+
+        d = ''
+        if desc:
+            d = ' ' + str(desc).strip()
+
+        path = section[key].strip()
+        if not path:
+            msg = _("No path given for{d} [{s}]/{k} in configuration.").format(
+                d=d, s=section_name, k=key)
+            LOG.error(msg)
+            self.config_has_errors = True
+            return
+
+        if absolute and not os.path.isabs(path):
+            msg = _(
+                "Path {p!r} for{d} [{s}]/{k} in configuration must be an absolute "
+                "path.").format(p=path, d=d, s=section_name, k=key)
+            LOG.error(msg)
+            self.config_has_errors = True
+            return
+
+        setattr(self, class_prop, path)
+
+    # -------------------------------------------------------------------------
+    def post_init(self):
+        """
+        Method to execute before calling run(). Here could be done some
+        finishing actions after reading in commandline parameters,
+        configuration a.s.o.
+
+        This method could be overwritten by descendant classes, these
+        methods should allways include a call to post_init() of the
+        parent class.
+
+        """
+
+        if self.verbose > 1:
+            LOG.debug(_("Executing {} ...").format('post_init()'))
+
+        super(PpPDNSApplication, self).post_init()
+
+        if self.args.inst:
+            self.instance = self.args.inst
+        elif self.args.inst_global:
+            self.instance = 'global'
+        elif self.args.inst_local:
+            self.instance = 'local'
+        elif self.args.inst_public:
+            self.instance = 'public'
+
+        if self.args.api_port:
+            self.api_port = self.args.api_port
+
+        if self.args.timeout:
+            self.cfg.pdns_timeout = self.args.timeout
+
+        self.pdns = PowerDNSServer(
+            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
+            master_server=self.cfg.pdns_host, port=self.cfg.pdns_port,
+            key=self.cfg.pdns_key, use_https=False,
+            simulate=self.simulate, force=self.force, initialized=False,
+        )
+        self.pdns.initialized = True
+
+    # -------------------------------------------------------------------------
+    def pre_run(self):
+        """
+        Dummy function to run before the main routine.
+        Could be overwritten by descendant classes.
+
+        """
+
+        if self.verbose > 1:
+            LOG.debug(_("Executing {} ...").format('pre_run()'))
+
+        LOG.debug(_("Setting Loglevel of the requests module to {}.").format('WARNING'))
+        logging.getLogger("requests").setLevel(logging.WARNING)
+
+        super(PpPDNSApplication, self).pre_run()
+        self.get_api_server_version()
+
+    # -------------------------------------------------------------------------
+    def _run(self):
+        """
+        Dummy function as main routine.
+
+        MUST be overwritten by descendant classes.
+
+        """
+        LOG.debug(_("Executing nothing ..."))
+
+    # -------------------------------------------------------------------------
+    def post_run(self):
+        """
+        Dummy function to run after the main routine.
+        Could be overwritten by descendant classes.
+
+        """
+
+        if self.verbose > 1:
+            LOG.debug(_("Executing {} ...").format('post_run()'))
+
+        if self.pdns:
+            self.pdns = None
+
+    # -------------------------------------------------------------------------
+    def get_api_server_version(self):
+
+        if not self.pdns:
+            raise PpPDNSAppError(_("The PDNS server object does not exists."))
+        if not self.pdns.initialized:
+            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
+
+        return self.pdns.get_api_server_version()
+
+    # -------------------------------------------------------------------------
+    def _build_url(self, path):
+
+        url = 'http://{}'.format(self.api_host)
+        if self.api_port != 80:
+            url += ':{}'.format(self.api_port)
+
+        url += '/api/v1' + path
+        LOG.debug("Used URL: {!r}".format(url))
+        return url
+
+    # -------------------------------------------------------------------------
+    def perform_request(self, path, method='GET', data=None, headers=None, may_simulate=False):
+        """Performing the underlying API request."""
+
+        if not self.pdns:
+            raise PpPDNSAppError(_("The PDNS server object does not exists."))
+        if not self.pdns.initialized:
+            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
+
+        return self.pdns.perform_request(
+            path, method=method, data=data, headers=headers, may_simulate=may_simulate)
+
+    # -------------------------------------------------------------------------
+    def get_api_zones(self):
+
+        if not self.pdns:
+            raise PpPDNSAppError(_("The PDNS server object does not exists."))
+        if not self.pdns.initialized:
+            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
+
+        return self.pdns.get_api_zones()
+
+    # -------------------------------------------------------------------------
+    def get_api_zone(self, zone_name):
+
+        if not self.pdns:
+            raise PpPDNSAppError(_("The PDNS server object does not exists."))
+        if not self.pdns.initialized:
+            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
+
+        zone_unicode = zone_name
+        json_response = None
+        zout = "{!r}".format(zone_name)
+        if 'xn--' in zone_name:
+            zone_unicode = zone_name.encode('idna').decode('idna')
+            zout = "{!r} ({})".format(zone_name, zone_unicode)
+        LOG.debug(_("Trying to get complete information about zone {!r} ...").format(zone_name))
+
+        path = "/servers/{}/zones/{}".format(self.pdns.api_servername, zone_name)
+        try:
+            json_response = self.perform_request(path)
+        except (PDNSApiNotFoundError, PDNSApiValidationError):
+            LOG.error(_("The given zone {} was not found.").format(zout))
+            return None
+        if self.verbose > 2:
+            LOG.debug(_("Got a response:") + '\n' + pp(json_response))
+
+        zone = PowerDNSZone.init_from_dict(
+            json_response, appname=self.appname, verbose=self.verbose, base_dir=self.base_dir)
+        if self.verbose > 2:
+            LOG.debug(_("Zone object:") + '\n' + pp(zone.as_dict()))
+
+        return zone
+
+#    # -------------------------------------------------------------------------
+#    def patch_zone(self, zone, payload):
+#
+#        return zone.patch(payload)
+#
+#    # -------------------------------------------------------------------------
+#    def update_soa(self, zone, new_soa, comment=None, ttl=None):
+#
+#        return zone.update_soa(new_soa=new_soa, comment=comment, ttl=ttl)
+#
+#     # -------------------------------------------------------------------------
+#     def set_nameservers(
+#         self, zone, new_nameservers, for_zone=None, comment=None, new_ttl=None,
+#             do_serial=True, do_notify=True):
+#
+#         current_nameservers = zone.get_zone_nameservers(for_zone=for_zone)
+#         if for_zone:
+#             LOG.debug("Current nameservers of {f!r} in zone {z!r}:\n{ns}".format(
+#                 f=for_zone, z=zone.name, ns=pp(current_nameservers)))
+#         else:
+#             LOG.debug("Current nameservers of zone {z!r}:\n{ns}".format(
+#                 z=zone.name, ns=pp(current_nameservers)))
+#
+#         ns2remove = []
+#         ns2add = []
+#
+#         for ns in current_nameservers:
+#             if ns not in new_nameservers:
+#                 ns2remove.append(ns)
+#         for ns in new_nameservers:
+#             if ns not in current_nameservers:
+#                 ns2add.append(ns)
+#
+#         if not ns2remove and not ns2add:
+#             if for_zone:
+#                 msg = "Subzone {f!r} has already the expected nameservers in zone {z!r}."
+#             else:
+#                 msg = "Zone {z!r} has already the expected nameservers."
+#             LOG.info(msg.format(f=for_zone, z=zone.name))
+#             return False
+#
+#         LOG.debug("Nameservers to remove from zone {z!r}:\n{ns}".format(
+#             z=zone.name, ns=pp(ns2remove)))
+#         LOG.debug("Nameservers to add to zone {z!r}:\n{ns}".format(
+#             z=zone.name, ns=pp(ns2add)))
+#
+#         ns_ttl = None
+#         if not new_ttl:
+#             cur_rrset = zone.get_ns_rrset(for_zone=for_zone)
+#             if cur_rrset:
+#                 ns_ttl = cur_rrset.ttl
+#             else:
+#                 soa = zone.get_soa()
+#                 ns_ttl = soa.ttl
+#                 del soa
+#         else:
+#             ns_ttl = int(new_ttl)
+#         if ns_ttl <= 0:
+#             ns_ttl = 3600
+#         LOG.debug("TTL for NS records: {}.".format(ns_ttl))
+#
+#         rrset_name = zone.name.lower()
+#         if for_zone:
+#             rrset_name = for_zone.lower()
+#
+#         records = []
+#         for ns in new_nameservers:
+#             record = {
+#                 "name": rrset_name,
+#                 "type": "NS",
+#                 "content": ns,
+#                 "disabled": False,
+#                 "set-ptr": False,
+#             }
+#             records.append(record)
+#         rrset = {
+#             "name": rrset_name,
+#             "type": "NS",
+#             "ttl": ns_ttl,
+#             "changetype": "REPLACE",
+#             "records": records,
+#         }
+#
+#         if comment:
+#             comment_rec = {
+#                 'content': comment,
+#                 'account': getpass.getuser(),
+#                 'modified_at': int(time.time() + 0.5),
+#             }
+#             rrset['comments'] = [comment_rec]
+#
+#         payload = {"rrsets": [rrset]}
+#
+#         self.patch_zone(zone, payload)
+#
+#         if do_serial:
+#             zone.increase_serial()
+#
+#         if do_notify:
+#             self.notify_zone(zone)
+#
+#         return True
+#
+#    # -------------------------------------------------------------------------
+#    def notify_zone(self, zone):
+#
+#        LOG.info("Notifying slaves of zone {!r} ...".format(zone.name))
+#
+#        path = "/servers/{}/zones/{}/notify".format(self.api_servername, zone.name)
+#        return self.perform_request(path, 'PUT', '', may_simulate=True)
+
+# =============================================================================
+
+
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/app/remove_ldap_user.py b/lib/pp_admintools/app/remove_ldap_user.py
new file mode 100644 (file)
index 0000000..29670cc
--- /dev/null
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2022 by Frank Brehm, Berlin
+@summary: An application module for disabling or removing a user from LDAP
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import logging
+
+# Third party modules
+
+# Own modules
+from ..xlate import XLATOR
+
+from ..app.ldap import LdapAppError
+from ..app.ldap import BaseLdapApplication
+
+__version__ = '0.1.1'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+ngettext = XLATOR.ngettext
+
+
+# =============================================================================
+class RemoveLdapUserError(LdapAppError):
+    """Special exception class for exceptions inside this module."""
+
+    pass
+
+
+# =============================================================================
+class RemoveLdapUserApplication(BaseLdapApplication):
+    """Application class for disabling or removing a user from LDAP."""
+
+    pass
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/apps/__init__.py b/lib/pp_admintools/apps/__init__.py
deleted file mode 100644 (file)
index 41823bf..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/env python3
-# -*- coding: utf-8 -*-
-
-__version__ = '0.1.0'
-
-# vim: ts=4 et list
-
diff --git a/lib/pp_admintools/apps/remove_ldap_user.py b/lib/pp_admintools/apps/remove_ldap_user.py
deleted file mode 100644 (file)
index 7e22745..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-@author: Frank Brehm
-@contact: frank.brehm@pixelpark.com
-@copyright: © 2022 by Frank Brehm, Berlin
-@summary: An application module for disabling or removing a user from LDAP
-"""
-from __future__ import absolute_import
-
-# Standard modules
-import logging
-
-# Third party modules
-
-# Own modules
-from ..xlate import XLATOR
-
-from ..ldap_app import LdapAppError
-from ..ldap_app import BaseLdapApplication
-
-__version__ = '0.1.0'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-ngettext = XLATOR.ngettext
-
-
-# =============================================================================
-class RemoveLdapUserError(LdapAppError):
-    """Special exception class for exceptions inside this module."""
-
-    pass
-
-
-# =============================================================================
-class RemoveLdapUserApplication(BaseLdapApplication):
-    """Application class for disabling or removing a user from LDAP."""
-
-    pass
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/config.py b/lib/pp_admintools/config.py
deleted file mode 100644 (file)
index df09b38..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/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 providing a configuration for different things in this module.
-"""
-from __future__ import absolute_import
-
-# Standard module
-import logging
-import pwd
-import socket
-import os
-import copy
-
-# Third party modules
-
-from fb_tools.multi_config import MultiConfigError, BaseMultiConfig
-from fb_tools.multi_config import DEFAULT_ENCODING
-
-from fb_tools import MailAddress
-
-# Own modules
-
-from .errors import PpError
-
-from .xlate import XLATOR
-
-CONFIG_DIR = 'pixelpark'
-__version__ = '0.1.2'
-LOG = logging.getLogger(__name__)
-VALID_MAIL_METHODS = ('smtp', 'sendmail')
-
-_ = XLATOR.gettext
-
-
-# =============================================================================
-class PpConfigurationError(PpError, MultiConfigError):
-    """Base error class for all exceptions happened during
-    evaluation of configuration."""
-
-    pass
-
-
-# =============================================================================
-class PpBaseConfiguration(BaseMultiConfig):
-    """Base class for reading and providing configuration."""
-
-    default_mail_recipients = [
-        'frank.brehm@pixelpark.com'
-    ]
-    default_mail_cc = [
-        'thomas.dalichow@pixelpark.com',
-        'reinhard.schmitz@pixelpark.com',
-    ]
-
-    default_reply_to = 'solution@pixelpark.com'
-
-    default_mail_server = 'mx.pixelpark.com'
-
-    current_user_name = pwd.getpwuid(os.getuid()).pw_name
-    current_user_gecos = pwd.getpwuid(os.getuid()).pw_gecos
-    default_mail_from = MailAddress(user=current_user_name, domain=socket.getfqdn())
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=__version__, base_dir=None,
-            append_appname_to_stems=True, config_dir=CONFIG_DIR, additional_stems=None,
-            additional_cfgdirs=None, encoding=DEFAULT_ENCODING, additional_config_file=None,
-            use_chardet=True, raise_on_error=True, initialized=False):
-
-        self.mail_recipients = copy.copy(self.default_mail_recipients)
-        self.mail_from = '{n} <{m}>'.format(
-            n=self.current_user_gecos, m=self.default_mail_from)
-        self.mail_cc = copy.copy(self.default_mail_cc)
-        self.reply_to = self.default_reply_to
-        self.mail_method = 'smtp'
-        self.mail_server = self.default_mail_server
-        self.smtp_port = 25
-        self._config_has_errors = None
-
-        super(PpBaseConfiguration, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
-            additional_stems=additional_stems, additional_cfgdirs=additional_cfgdirs,
-            encoding=encoding, additional_config_file=additional_config_file,
-            use_chardet=use_chardet, raise_on_error=raise_on_error, initialized=False)
-
-        if initialized:
-            self.initialized = True
-
-
-# =============================================================================
-
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/config/__init__.py b/lib/pp_admintools/config/__init__.py
new file mode 100644 (file)
index 0000000..b8556b6
--- /dev/null
@@ -0,0 +1,103 @@
+#!/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 providing a configuration for different things in this module.
+"""
+from __future__ import absolute_import
+
+# Standard module
+import logging
+import pwd
+import socket
+import os
+import copy
+
+# Third party modules
+
+from fb_tools.multi_config import MultiConfigError, BaseMultiConfig
+from fb_tools.multi_config import DEFAULT_ENCODING
+
+from fb_tools import MailAddress
+
+# Own modules
+
+from ..errors import PpError
+
+from ..xlate import XLATOR
+
+CONFIG_DIR = 'pixelpark'
+__version__ = '0.1.3'
+LOG = logging.getLogger(__name__)
+VALID_MAIL_METHODS = ('smtp', 'sendmail')
+
+_ = XLATOR.gettext
+
+
+# =============================================================================
+class PpConfigurationError(PpError, MultiConfigError):
+    """Base error class for all exceptions happened during
+    evaluation of configuration."""
+
+    pass
+
+
+# =============================================================================
+class PpBaseConfiguration(BaseMultiConfig):
+    """Base class for reading and providing configuration."""
+
+    default_mail_recipients = [
+        'frank.brehm@pixelpark.com'
+    ]
+    default_mail_cc = [
+        'thomas.dalichow@pixelpark.com',
+        'reinhard.schmitz@pixelpark.com',
+    ]
+
+    default_reply_to = 'solution@pixelpark.com'
+
+    default_mail_server = 'mx.pixelpark.com'
+
+    current_user_name = pwd.getpwuid(os.getuid()).pw_name
+    current_user_gecos = pwd.getpwuid(os.getuid()).pw_gecos
+    default_mail_from = MailAddress(user=current_user_name, domain=socket.getfqdn())
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=__version__, base_dir=None,
+            append_appname_to_stems=True, config_dir=CONFIG_DIR, additional_stems=None,
+            additional_cfgdirs=None, encoding=DEFAULT_ENCODING, additional_config_file=None,
+            use_chardet=True, raise_on_error=True, initialized=False):
+
+        self.mail_recipients = copy.copy(self.default_mail_recipients)
+        self.mail_from = '{n} <{m}>'.format(
+            n=self.current_user_gecos, m=self.default_mail_from)
+        self.mail_cc = copy.copy(self.default_mail_cc)
+        self.reply_to = self.default_reply_to
+        self.mail_method = 'smtp'
+        self.mail_server = self.default_mail_server
+        self.smtp_port = 25
+        self._config_has_errors = None
+
+        super(PpBaseConfiguration, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
+            additional_stems=additional_stems, additional_cfgdirs=additional_cfgdirs,
+            encoding=encoding, additional_config_file=additional_config_file,
+            use_chardet=use_chardet, raise_on_error=raise_on_error, initialized=False)
+
+        if initialized:
+            self.initialized = True
+
+
+# =============================================================================
+
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/config/dns_deploy_zones.py b/lib/pp_admintools/config/dns_deploy_zones.py
new file mode 100644 (file)
index 0000000..d90c819
--- /dev/null
@@ -0,0 +1,606 @@
+#!/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 providing a configuration the dns-deploy-zones applications.
+          It's based on class PdnsConfiguration.
+"""
+from __future__ import absolute_import
+
+# Standard module
+import logging
+import re
+import copy
+import socket
+
+from pathlib import Path
+
+# Third party modules
+
+# Own modules
+
+from fb_tools.common import is_sequence, pp, to_bool
+
+# from .config import ConfigError, BaseConfiguration
+from fb_tools.multi_config import DEFAULT_ENCODING
+
+from .pdns import PdnsConfigError, PdnsConfiguration
+from .mail import DEFAULT_CONFIG_DIR
+
+from ..xlate import XLATOR
+
+__version__ = '0.2.2'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+
+
+# =============================================================================
+class DnsDeployZonesConfigError(PdnsConfigError):
+    """Base error class for all exceptions happened during
+    execution this configured application"""
+
+    pass
+
+
+# =============================================================================
+class DnsDeployZonesConfig(PdnsConfiguration):
+    """
+    A class for providing a configuration for an arbitrary PowerDNS Application
+    and methods to read it from configuration files.
+    """
+
+    default_pidfile = Path('/run/dns-deploy-zones.pid')
+    default_keep_backup = False
+
+    default_named_conf_dir = Path('/etc')
+    default_named_zones_cfg_file = Path('named.zones.conf')
+    default_named_basedir = Path('/var/named')
+    default_named_slavedir = Path('slaves')
+
+    default_zone_masters_local = ['master-local.pp-dns.com']
+    default_zone_masters_public = ['master-public.pp-dns.com']
+
+    default_rndc = Path('/usr/sbin/rndc')
+    default_systemctl = Path('/usr/bin/systemctl')
+    default_named_checkconf = Path('/usr/sbin/named-checkconf')
+
+    default_named_listen_on_v6 = False
+    default_named_internal = False
+
+    re_split_addresses = re.compile(r'[,;\s]+')
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=__version__, base_dir=None,
+            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
+            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
+            ensure_privacy=True, use_chardet=True, initialized=False):
+
+        self.pidfile = self.default_pidfile
+        self.keep_backup = self.default_keep_backup
+
+        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_local = []
+        for master in self.default_zone_masters_local:
+            self.zone_masters_local.append(master)
+
+        self.zone_masters_public = []
+        for master in self.default_zone_masters_public:
+            self.zone_masters_public.append(master)
+
+        self.rndc = self.default_rndc
+        self.systemctl = self.default_systemctl
+        self.named_checkconf = self.default_named_checkconf
+
+        self._named_listen_on_v6 = self.default_named_listen_on_v6
+        self._named_internal = self.default_named_internal
+
+        self.masters = set()
+
+        add_stems = []
+        if additional_stems:
+            if is_sequence(additional_stems):
+                for stem in additional_stems:
+                    add_stems.append(stem)
+            else:
+                add_stems.append(additional_stems)
+
+        if 'named' not in add_stems:
+            add_stems.append('named')
+
+        super(DnsDeployZonesConfig, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
+            additional_stems=add_stems, additional_config_file=additional_config_file,
+            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
+            ensure_privacy=ensure_privacy, initialized=False,
+        )
+
+        if initialized:
+            self.initialized = True
+
+    # -------------------------------------------------------------------------
+    @property
+    def named_internal(self):
+        """Is the BIND nameserver on the current host a local resolver (True)
+        or an authoritative nameserver for outside."""
+        return self._named_internal
+
+    @named_internal.setter
+    def named_internal(self, value):
+        self._named_internal = to_bool(value)
+
+    # -------------------------------------------------------------------------
+    @property
+    def named_listen_on_v6(self):
+        """Is the BIND nameserver on the current listening on some IPv6 addresses?"""
+        return self._named_listen_on_v6
+
+    @named_listen_on_v6.setter
+    def named_listen_on_v6(self, value):
+        self._named_listen_on_v6 = to_bool(value)
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(DnsDeployZonesConfig, self).as_dict(short=short)
+
+        res['default_pidfile'] = self.default_pidfile
+        res['default_keep_backup'] = self.default_keep_backup
+        res['default_named_conf_dir'] = self.default_named_conf_dir
+        res['default_named_zones_cfg_file'] = self.default_named_zones_cfg_file
+        res['default_named_basedir'] = self.default_named_basedir
+        res['default_named_slavedir'] = self.default_named_slavedir
+        res['default_zone_masters_local'] = copy.copy(self.default_zone_masters_local)
+        res['default_zone_masters_public'] = copy.copy(self.default_zone_masters_public)
+        res['default_rndc'] = self.default_rndc
+        res['default_systemctl'] = self.default_systemctl
+        res['default_named_checkconf'] = self.default_named_checkconf
+        res['default_named_listen_on_v6'] = self.default_named_listen_on_v6
+        res['default_named_internal'] = self.default_named_internal
+        res['named_listen_on_v6'] = self.named_listen_on_v6
+        res['named_internal'] = self.named_internal
+
+        res['masters'] = copy.copy(self.masters)
+
+        return res
+
+    # -------------------------------------------------------------------------
+    def eval_section(self, section_name):
+
+        super(DnsDeployZonesConfig, self).eval_section(section_name)
+        sn = section_name.lower()
+
+        if sn == 'named':
+            section = self.cfg[section_name]
+            return self._eval_named(section_name, section)
+
+        if sn == self.appname.lower() or sn == 'app':
+            section = self.cfg[section_name]
+            return self._eval_app(section_name, section)
+
+    # -------------------------------------------------------------------------
+    def _eval_named(self, section_name, section):
+
+        if self.verbose > 2:
+            msg = _("Evaluating config section {!r}:").format(section_name)
+            LOG.debug(msg + '\n' + pp(section))
+
+        re_config_dir = re.compile(r'^\s*(?:named[_-]?)?conf(?:ig)?[_-]?dir\s*$', re.IGNORECASE)
+        re_config_file = re.compile(
+            r'^\s*(?:named[_-]?)?zones[_-]?(?:conf(?:ig)?|cfg)[_-]*file\s*$', re.IGNORECASE)
+        re_base_dir = re.compile(r'^\s*(?:named[_-]?)?base[_-]?dir\s*$', re.IGNORECASE)
+        re_slave_dir = re.compile(r'^\s*(?:named[_-]?)?slave[_-]?dir\s*$', re.IGNORECASE)
+        re_named_checkconf = re.compile(r'^named[_-]?checkconf$', re.IGNORECASE)
+        re_internal = re.compile(
+            r'^\s*(?:named[_-]?)?(?:is[_-]?)?intern(?:al)?\s*$', re.IGNORECASE)
+        re_listen_v6 = re.compile(r'^\s*listen[_-](?:on[_-])?(?:ip)v6\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+
+            if key.lower() == 'masters':
+                self._eval_named_masters(section_name, key, section)
+                continue
+
+            if key.lower() == 'rndc':
+                self._eval_named_rndc(section_name, key, section)
+                continue
+
+            if key.lower() == 'systemctl':
+                self._eval_named_systemctl(section_name, key, section)
+                continue
+
+            if re_config_dir.search(key):
+                self._eval_named_configdir(section_name, key, section)
+                continue
+
+            if re_config_file.search(key):
+                self._eval_named_configfile(section_name, key, section)
+                continue
+
+            if re_base_dir.search(key):
+                self._eval_named_basedir(section_name, key, section)
+                continue
+
+            if re_slave_dir.search(key):
+                self._eval_named_slavedir(section_name, key, section)
+                continue
+
+            if re_named_checkconf.search(key):
+                self._eval_named_checkconf(section_name, key, section)
+                continue
+
+            if re_internal.search(key):
+                self._eval_named_internal(section_name, key, section)
+                continue
+
+            if re_listen_v6.search(key):
+                self._eval_named_listen_v6(section_name, key, section)
+                continue
+
+    # -------------------------------------------------------------------------
+    def _eval_named_masters(self, section_name, key, section):
+
+        val = section[key]
+
+        if not val:
+            return
+
+        master_list = set()
+
+        if is_sequence(val):
+            for value in val:
+                masters = self._eval_named_master_list(value)
+                if masters:
+                    master_list |= masters
+        else:
+            masters = self._eval_named_master_list(val)
+            if masters:
+                master_list |= masters
+
+        self.masters = master_list
+
+    # -------------------------------------------------------------------------
+    def _eval_named_master_list(self, value):
+
+        masters = set()
+
+        for m in self.re_split_addresses.split(value):
+            if not m:
+                continue
+
+            m = m.strip().lower()
+            if self.verbose > 1:
+                LOG.debug(_("Checking given master address {!r} ...").format(m))
+            addr_list = self.get_addresses(m)
+            masters |= addr_list
+
+        return masters
+
+    # -------------------------------------------------------------------------
+    def get_addresses(self, host):
+
+        addr_list = set()
+
+        if self.verbose > 3:
+            msg = _("Trying to evaluate address of host {!r} ...").format(host)
+            LOG.debug(msg)
+
+        try:
+            addr_infos = socket.getaddrinfo(host, 53, proto=socket.IPPROTO_TCP)
+            for addr_info in addr_infos:
+                addr = addr_info[4][0]
+                addr_list.add(addr)
+        except socket.gaierror as e:
+            msg = _("Invalid hostname or address {a!r} found in masters: {e}")
+            msg = msg.format(a=host, e=e)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return set()
+        if self.verbose > 3:
+            msg = _("Got addresses {a!r} for host {h!r}.")
+            LOG.debug(msg.format(a=addr_list, h=host))
+
+        return addr_list
+
+    # -------------------------------------------------------------------------
+    def _eval_named_rndc(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        path = Path(val)
+        if not path.is_absolute():
+            msg = _("The path to {what} must be an absolute path, found {path!r}.")
+            msg = msg.format(what='rndc', path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what='rndc', path=val)
+            LOG.debug(msg)
+
+        self.rndc = path
+
+    # -------------------------------------------------------------------------
+    def _eval_named_systemctl(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        path = Path(val)
+        if not path.is_absolute():
+            msg = _("The path to {what} must be an absolute path, found {path!r}.")
+            msg = msg.format(what='systemctl', path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what='systemctl', path=val)
+            LOG.debug(msg)
+
+        self.systemctl = path
+
+    # -------------------------------------------------------------------------
+    def _eval_named_configdir(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        what = _("the named config directory")
+        path = Path(val)
+
+        if not path.is_absolute():
+            msg = _("The path to {what} must be an absolute path, found {path!r}.")
+            msg = msg.format(what=what, path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
+            LOG.debug(msg)
+
+        self.named_conf_dir = path
+
+    # -------------------------------------------------------------------------
+    def _eval_named_configfile(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        what = _("the named config file for zones")
+        path = Path(val)
+
+        if path.is_absolute():
+            msg = _("The path to {what} must not be an absolute path, found {path!r}.")
+            msg = msg.format(what=what, path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
+            LOG.debug(msg)
+
+        self.named_zones_cfg_file = path
+
+    # -------------------------------------------------------------------------
+    def _eval_named_basedir(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        what = _("the named base directory")
+        path = Path(val)
+        if not path.is_absolute():
+            msg = _("The path to {what} must be an absolute path, found {path!r}.")
+            msg = msg.format(what=what, path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
+            LOG.debug(msg)
+
+        self.named_basedir = path
+
+    # -------------------------------------------------------------------------
+    def _eval_named_slavedir(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        what = _("the directory for slave zones of named")
+        path = Path(val)
+
+        if path.is_absolute():
+            msg = _("The path to {what} must not be an absolute path, found {path!r}.")
+            msg = msg.format(what=what, path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
+            LOG.debug(msg)
+
+        self.named_slavedir = path
+
+    # -------------------------------------------------------------------------
+    def _eval_named_checkconf(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        what = "named-checkconf"
+        path = Path(val)
+        if not path.is_absolute():
+            msg = _("The path to {what} must be an absolute path, found {path!r}.")
+            msg = msg.format(what=what, path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
+            LOG.debug(msg)
+
+        self.named_checkconf = path
+
+    # -------------------------------------------------------------------------
+    def _eval_named_internal(self, iname, key, section):
+
+        val = section[key]
+        if val is None:
+            return
+
+        self.named_internal = to_bool(val)
+
+    # -------------------------------------------------------------------------
+    def _eval_named_listen_v6(self, iname, key, section):
+
+        val = section[key]
+        if val is None:
+            return
+
+        self.named_listen_on_v6 = to_bool(val)
+
+    # -------------------------------------------------------------------------
+    def _eval_app(self, section_name, section):
+
+        if self.verbose > 2:
+            msg = _("Evaluating config section {!r}:").format(section_name)
+            LOG.debug(msg + '\n' + pp(section))
+
+        re_pidfile = re.compile(r'^\s*pid[_-]?file$', re.IGNORECASE)
+        re_keep_backup = re.compile(r'^\s*keep[_-]?backup$', re.IGNORECASE)
+
+        for key in section.keys():
+
+            if re_pidfile.search(key):
+                self._eval_pidfile(section_name, key, section)
+                continue
+
+            if re_keep_backup.search(key):
+                self._eval_keep_backup(section_name, key, section)
+                continue
+
+    # -------------------------------------------------------------------------
+    def _eval_pidfile(self, iname, key, section):
+
+        val = section[key].strip()
+        if not val:
+            return
+
+        what = _("the PID file")
+        path = Path(val)
+        if not path.is_absolute():
+            msg = _("The path to {what} must be an absolute path, found {path!r}.")
+            msg = msg.format(what=what, path=val)
+            if self.raise_on_error:
+                raise DnsDeployZonesConfigError(msg)
+            else:
+                LOG.error(msg)
+                return
+
+        if self.verbose > 2:
+            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
+            LOG.debug(msg)
+
+        self.pidfile = path
+
+    # -------------------------------------------------------------------------
+    def _eval_keep_backup(self, iname, key, section):
+
+        val = section[key]
+        if val is None:
+            return
+
+        self.keep_backup = to_bool(val)
+
+    # -------------------------------------------------------------------------
+    def eval(self):
+        """Evaluating read configuration and storing them in object properties."""
+
+        super(DnsDeployZonesConfig, self).eval()
+
+        addr_list = set()
+        if self.named_internal:
+            for host in self.default_zone_masters_local:
+                addr_list |= self.get_addresses(host)
+        else:
+            for host in self.default_zone_masters_public:
+                addr_list |= self.get_addresses(host)
+
+        self.masters |= addr_list
+
+        if not self.named_listen_on_v6:
+
+            addresses = set()
+            for addr in self.masters:
+                if ':' not in addr:
+                    addresses.add(addr)
+            self.masters = addresses
+
+        if self.masters:
+            if self.verbose > 2:
+                LOG.debug(_("Using configured masters:") + '\n' + pp(self.masters))
+        else:
+            LOG.warn(_("No valid masters found in configuration."))
+
+        if self.verbose > 2:
+            msg = _("Evaluated configuration:")
+            msg += " " + pp(self.as_dict())
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/config/ldap.py b/lib/pp_admintools/config/ldap.py
new file mode 100644 (file)
index 0000000..377d892
--- /dev/null
@@ -0,0 +1,465 @@
+#!/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 providing a configuration for applications,
+          which are performing LDAP actions, like search a.s.o.
+"""
+from __future__ import absolute_import
+
+# Standard module
+import logging
+import copy
+import re
+
+# Third party modules
+
+# Own modules
+# from fb_tools.common import pp
+from fb_tools.common import is_sequence, to_bool
+
+# from .config import ConfigError, BaseConfiguration
+from fb_tools.multi_config import MultiConfigError, BaseMultiConfig
+from fb_tools.multi_config import DEFAULT_ENCODING
+
+from fb_tools.obj import FbGenericBaseObject, FbBaseObject
+
+from .. import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
+
+from ..xlate import XLATOR
+
+__version__ = '0.2.6'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+
+DEFAULT_PORT_LDAP = 389
+DEFAULT_PORT_LDAPS = 636
+DEFAULT_TIMEOUT = 20
+MAX_TIMEOUT = 3600
+
+# =============================================================================
+class LdapConfigError(MultiConfigError):
+    """Base error class for all exceptions happened during
+    execution this configured application"""
+
+    pass
+
+
+# =============================================================================
+class LdapConnectionInfo(FbBaseObject):
+    """Encapsulating all necessary data to connect to a LDAP server."""
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=__version__, base_dir=None,
+            host=None, use_ldaps=False, port=DEFAULT_PORT_LDAP, base_dn=None,
+            bind_dn=None, bind_pw=None, initialized=False):
+
+        self._host = None
+        self._use_ldaps = False
+        self._port = DEFAULT_PORT_LDAP
+        self._base_dn = None
+        self._bind_dn = None
+        self._bind_pw = None
+
+        super(LdapConnectionInfo, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            initialized=False)
+
+        if host is not None:
+            self.host = host
+        self.use_ldaps = use_ldaps
+        self.port = port
+        if base_dn is not None:
+            self.base_dn = base_dn
+        if bind_dn is not None:
+            self.bind_dn = bind_dn
+        if bind_pw is not None:
+            self.bind_pw = bind_pw
+
+        if initialized:
+            self.initialized = True
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(LdapConnectionInfo, self).as_dict(short=short)
+
+        res['host'] = self.host
+        res['use_ldaps'] = self.use_ldaps
+        res['port'] = self.port
+        res['base_dn'] = self.base_dn
+        res['bind_dn'] = self.bind_dn
+        res['bind_pw'] = None
+        res['schema'] = self.schema
+        res['url'] = self.url
+
+        if self.bind_pw:
+            if self.verbose > 4:
+                res['bind_pw'] = self.bind_pw
+            else:
+                res['bind_pw'] = '******'
+
+        return res
+
+    # -----------------------------------------------------------
+    @property
+    def host(self):
+        """The host name (or IP address) of the LDAP server."""
+        return self._host
+
+    @host.setter
+    def host(self, value):
+        if value is None or str(value).strip() == '':
+            self._host = None
+            return
+        self._host = str(value).strip().lower()
+
+    # -----------------------------------------------------------
+    @property
+    def use_ldaps(self):
+        """Should there be used LDAPS for communicating with the LDAP server?"""
+        return self._use_ldaps
+
+    @use_ldaps.setter
+    def use_ldaps(self, value):
+        self._use_ldaps = to_bool(value)
+
+    # -----------------------------------------------------------
+    @property
+    def port(self):
+        "The TCP port number of the LDAP server."
+        return self._port
+
+    @port.setter
+    def port(self, value):
+        v = int(value)
+        if v < 1 or v > MAX_PORT_NUMBER:
+            raise LdapConfigError(_("Invalid port {!r} for LDAP server given.").format(value))
+        self._port = v
+
+    # -----------------------------------------------------------
+    @property
+    def base_dn(self):
+        """The DN used to connect to the LDAP server, anonymous bind is used, if
+            this DN is empty or None."""
+        return self._base_dn
+
+    @base_dn.setter
+    def base_dn(self, value):
+        if value is None or str(value).strip() == '':
+            msg = _("An empty Base DN for LDAP searches is not allowed.")
+            raise LdapConfigError(msg)
+        self._base_dn = str(value).strip()
+
+    # -----------------------------------------------------------
+    @property
+    def bind_dn(self):
+        """The DN used to connect to the LDAP server, anonymous bind is used, if
+            this DN is empty or None."""
+        return self._bind_dn
+
+    @bind_dn.setter
+    def bind_dn(self, value):
+        if value is None or str(value).strip() == '':
+            self._bind_dn = None
+            return
+        self._bind_dn = str(value).strip()
+
+    # -----------------------------------------------------------
+    @property
+    def bind_pw(self):
+        """The password of the DN used to connect to the LDAP server."""
+        return self._bind_pw
+
+    @bind_pw.setter
+    def bind_pw(self, value):
+        if value is None or str(value).strip() == '':
+            self._bind_pw = None
+            return
+        self._bind_pw = str(value).strip()
+
+    # -----------------------------------------------------------
+    @property
+    def schema(self):
+        """The schema as part of the URL to connect to the LDAP server."""
+        if self.use_ldaps:
+            return 'ldaps'
+        return 'ldap'
+
+    # -----------------------------------------------------------
+    @property
+    def url(self):
+        """The URL, which ca be used to connect to the LDAP server."""
+        if not self.host:
+            return None
+
+        port = ''
+        if self.use_ldaps:
+            if self.port != DEFAULT_PORT_LDAPS:
+                port = ':{}'.format(self.port)
+        else:
+            if self.port != DEFAULT_PORT_LDAP:
+                port = ':{}'.format(self.port)
+
+        return '{s}://{h}{p}'.format(s=self.schema, h=self.host, p=port)
+
+    # -------------------------------------------------------------------------
+    def __repr__(self):
+        """Typecasting into a string for reproduction."""
+
+        out = "<%s(" % (self.__class__.__name__)
+
+        fields = []
+        fields.append("appname={!r}".format(self.appname))
+        fields.append("host={!r}".format(self.host))
+        fields.append("use_ldaps={!r}".format(self.use_ldaps))
+        fields.append("port={!r}".format(self.port))
+        fields.append("base_dn={!r}".format(self.base_dn))
+        fields.append("bind_dn={!r}".format(self.bind_dn))
+        fields.append("bind_pw={!r}".format(self.bind_pw))
+        fields.append("initialized={!r}".format(self.initialized))
+
+        out += ", ".join(fields) + ")>"
+        return out
+
+    # -------------------------------------------------------------------------
+    def __copy__(self):
+
+        new = self.__class__(
+            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, host=self.host,
+            use_ldaps=self.use_ldaps, port=self.port, base_dn=self.base_dn, bind_dn=self.bind_dn,
+            bind_pw=self.bind_pw, initialized=self.initialized)
+
+        return new
+
+
+# =============================================================================
+class LdapConnectionDict(dict, FbGenericBaseObject):
+    """A dictionary containing LdapConnectionInfo as values and their names as keys."""
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(LdapConnectionDict, self).as_dict(short=short)
+
+        for key in self.keys():
+            res[key] = self[key].as_dict(short=short)
+
+        return res
+
+    # -------------------------------------------------------------------------
+    def __copy__(self):
+
+        new = self.__class__()
+
+        for key in self.keys():
+            new[key] = copy.copy(self[key])
+
+        return new
+
+
+# =============================================================================
+class LdapConfiguration(BaseMultiConfig):
+    """
+    A class for providing a configuration for an arbitrary Application working
+    with one or more LDAP connections, and methods to read it from configuration files.
+    """
+
+    default_ldap_server = 'prd-ds.pixelpark.com'
+    use_ssl_on_default = True
+    default_ldap_port = DEFAULT_PORT_LDAPS
+    default_base_dn = 'o=isp'
+    default_bind_dn = 'uid=readonly,ou=People,o=isp'
+
+    re_ldap_section_w_name = re.compile(r'^\s*ldap\s*:\s*(\S+)')
+
+    re_ldap_host_key = re.compile(r'^\s*(?:host|server)\s*$', re.IGNORECASE)
+    re_ldap_ldaps_key = re.compile(r'^\s*(?:use[_-]?)?(?:ldaps|ssl)\s*$', re.IGNORECASE)
+    re_ldap_port_key = re.compile(r'^\s*port\s*$', re.IGNORECASE)
+    re_ldap_base_dn_key = re.compile(r'^\s*base[_-]*dn\s*$', re.IGNORECASE)
+    re_ldap_bind_dn_key = re.compile(r'^\s*bind[_-]*dn\s*$', re.IGNORECASE)
+    re_ldap_bind_pw_key = re.compile(r'^\s*bind[_-]*pw\s*$', re.IGNORECASE)
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=__version__, base_dir=None,
+            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
+            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
+            ensure_privacy=False, use_chardet=True, initialized=False):
+
+        add_stems = []
+        if additional_stems:
+            if is_sequence(additional_stems):
+                for stem in additional_stems:
+                    add_stems.append(stem)
+            else:
+                add_stems.append(additional_stems)
+
+        if 'ldap' not in add_stems:
+            add_stems.append('ldap')
+
+        self.ldap_timeout = DEFAULT_TIMEOUT
+
+        super(LdapConfiguration, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
+            additional_stems=add_stems, additional_config_file=additional_config_file,
+            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
+            ensure_privacy=ensure_privacy, initialized=False,
+        )
+
+        self.ldap_connection = LdapConnectionDict()
+
+        default_connection = LdapConnectionInfo(
+            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
+            host=self.default_ldap_server, use_ldaps=self.use_ssl_on_default,
+            port=self.default_ldap_port, base_dn=self.default_base_dn,
+            bind_dn=self.default_bind_dn, initialized=False)
+
+        self.ldap_connection['default'] = default_connection
+
+    # -------------------------------------------------------------------------
+    def eval_section(self, section_name):
+
+        super(LdapConfiguration, self).eval_section(section_name)
+
+        sn = section_name.lower()
+        section = self.cfg[section_name]
+
+        if sn == 'ldap':
+            LOG.debug(_("Evaluating LDAP config ..."))
+
+            for key in section.keys():
+                if self.verbose > 1:
+                    LOG.debug(_("Evaluating LDAP section {!r} ...").format(key))
+                sub = section[key]
+                if key.lower().strip() == 'timeout':
+                    self._eval_ldap_timeout(sub)
+                    continue
+                self._eval_ldap_connection(key, sub)
+            return
+
+        match = self.re_ldap_section_w_name.match(sn)
+        if match:
+            connection_name = match.group(1)
+            self._eval_ldap_connection(connection_name, section)
+
+    # -------------------------------------------------------------------------
+    def _eval_ldap_timeout(self, value):
+
+        timeout = DEFAULT_TIMEOUT
+        msg_invalid = _("Value {!r} for a timeout is invalid.")
+
+        try:
+            timeout = int(value)
+        except (ValueError, TypeError) as e:
+            msg = msg_invalid.format(value)
+            msg += ': ' + str(e)
+            LOG.error(msg)
+            return
+        if timeout <= 0 or timeout > MAX_TIMEOUT:
+            msg = msg_invalid.format(value)
+            LOG.error(msg)
+            return
+
+        self.ldap_timeout = timeout
+
+    # -------------------------------------------------------------------------
+    def _eval_ldap_connection(self, connection_name, section):
+
+        if self.verbose > 2:
+            msg = _("Reading configuration of LDAP instance {!r} ...").format(connection_name)
+            LOG.debug(msg)
+
+        connection = LdapConnectionInfo(
+            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
+            initialized=False)
+
+        section_name = "ldap:" + connection_name
+        msg_invalid = _("Invalid value {val!r} in section {sec!r} for a LDAP {what}.")
+
+        for key in section.keys():
+
+            value = section[key]
+
+            if self.re_ldap_host_key.match(key):
+                if value.strip():
+                    connection.host = value
+                else:
+                    msg = msg_invalid.format(val=value, sec=section_name, what='host')
+                    LOG.error(msg)
+                continue
+
+            if self.re_ldap_ldaps_key.match(key):
+                connection.use_ldaps = value
+                continue
+
+            if self.re_ldap_port_key.match(key):
+                port = DEFAULT_PORT_LDAP
+                try:
+                    port = int(value)
+                except (ValueError, TypeError) as e:
+                    msg = msg_invalid.format(val=value, sec=section_name, what='port')
+                    msg += ' ' + str(e)
+                    LOG.error(msg)
+                    continue
+                if port <= 0 or port > MAX_PORT_NUMBER:
+                    msg = msg_invalid.format(val=value, sec=section_name, what='port')
+                    LOG.error(msg)
+                    continue
+                connection.port = port
+                continue
+
+            if self.re_ldap_base_dn_key.match(key):
+                if value.strip():
+                    connection.base_dn = value
+                else:
+                    msg = msg_invalid.format(val=value, sec=section_name, what='base_dn')
+                    LOG.error(msg)
+                continue
+
+            if self.re_ldap_bind_dn_key.match(key):
+                connection.bind_dn = value
+                continue
+
+            if self.re_ldap_bind_pw_key.match(key):
+                connection.bind_pw = value
+                continue
+
+            msg = _("Unknown LDAP configuration key {key} found in section {sec!r}.").format(
+                key=key, sec=section_name)
+            LOG.error(msg)
+
+        self.ldap_connection[connection_name] = connection
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/config/mail.py b/lib/pp_admintools/config/mail.py
new file mode 100644 (file)
index 0000000..5f8527c
--- /dev/null
@@ -0,0 +1,370 @@
+#!/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 providing a configuration for applications,
+          which are sending mails
+"""
+from __future__ import absolute_import
+
+# Standard module
+import logging
+import pwd
+import re
+import copy
+import os
+import socket
+
+# Third party modules
+
+# Own modules
+
+from fb_tools.common import is_sequence, pp
+
+# from .config import ConfigError, BaseConfiguration
+from fb_tools.multi_config import MultiConfigError, BaseMultiConfig
+from fb_tools.multi_config import DEFAULT_ENCODING
+
+from fb_tools import MailAddress
+
+from .. import __version__ as GLOBAL_VERSION
+from .. import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
+
+from ..xlate import XLATOR
+
+__version__ = '0.1.11'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+
+VALID_MAIL_METHODS = ('smtp', 'sendmail')
+DEFAULT_DOMAIN = 'pixelpark.com'
+
+
+# =============================================================================
+class MailConfigError(MultiConfigError):
+    """Base error class for all exceptions happened during
+    execution this configured application"""
+
+    pass
+
+
+# =============================================================================
+class MailConfiguration(BaseMultiConfig):
+    """
+    A class for providing a configuration for an arbitrary PowerDNS Application
+    and methods to read it from configuration files.
+    """
+
+    default_mail_recipients = [
+        'frank.brehm@pixelpark.com'
+    ]
+    default_mail_cc = [
+        'thomas.dalichow@pixelpark.com',
+    ]
+
+    default_reply_to = 'solution@pixelpark.com'
+
+    default_mail_server = 'localhost'
+    default_smtp_port = 25
+
+    default_domain = socket.getfqdn()
+    if default_domain is None:
+        default_domain = DEFAULT_DOMAIN
+    else:
+        default_domain = default_domain.strip()
+        if not MailAddress.re_valid_domain.match(default_domain):
+            default_domain = DEFAULT_DOMAIN
+
+    current_user_name = pwd.getpwuid(os.getuid()).pw_name
+    current_user_gecos = pwd.getpwuid(os.getuid()).pw_gecos
+    default_mail_from = MailAddress(user=current_user_name, domain=default_domain)
+    default_mail_from_complete = '{n} <{m}>'.format(n=current_user_gecos, m=default_mail_from)
+
+    valid_mail_methods = VALID_MAIL_METHODS
+    default_mail_method = 'smtp'
+
+    whitespace_re = re.compile(r'(?:[,;]+|\s*[,;]*\s+)+')
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=__version__, base_dir=None,
+            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
+            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
+            ensure_privacy=False, use_chardet=True, initialized=False):
+
+        add_stems = []
+        if additional_stems:
+            if is_sequence(additional_stems):
+                for stem in additional_stems:
+                    add_stems.append(stem)
+            else:
+                add_stems.append(additional_stems)
+
+        if 'mail' not in add_stems:
+            add_stems.append('mail')
+
+        self.mail_recipients = copy.copy(self.default_mail_recipients)
+        self.mail_from = self.default_mail_from_complete
+        self.mail_cc = copy.copy(self.default_mail_cc)
+        self.reply_to = self.default_reply_to
+        self.mail_method = self.default_mail_method
+        self.mail_server = self.default_mail_server
+        self.smtp_port = self.default_smtp_port
+        self._mail_cc_configured = False
+
+        super(MailConfiguration, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
+            additional_stems=add_stems, additional_config_file=additional_config_file,
+            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
+            ensure_privacy=ensure_privacy, initialized=False,
+        )
+
+        self.xmailer = "{a} (Admin Tools version {v})".format(
+            a=self.appname, v=GLOBAL_VERSION)
+
+        if initialized:
+            self.initialized = True
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(MailConfiguration, self).as_dict(short=short)
+
+        res['default_mail_recipients'] = self.default_mail_recipients
+        res['default_mail_cc'] = self.default_mail_cc
+        res['default_reply_to'] = self.default_reply_to
+        res['default_mail_server'] = self.default_mail_server
+        res['default_smtp_port'] = self.default_smtp_port
+        res['current_user_name'] = self.current_user_name
+        res['current_user_gecos'] = self.current_user_gecos
+        res['default_mail_from'] = self.default_mail_from
+        res['default_mail_from_complete'] = self.default_mail_from_complete
+        res['default_mail_method'] = self.default_mail_method
+
+        return res
+
+    # -------------------------------------------------------------------------
+    def eval(self):
+
+        self.mail_recipients = []
+        self.mail_cc = []
+
+        super(MailConfiguration, self).eval()
+
+        if not self.mail_recipients:
+            self.mail_recipients = copy.copy(self.default_mail_recipients)
+
+        if not self.mail_cc and not self._mail_cc_configured:
+            self.mail_cc = copy.copy(self.default_mail_cc)
+
+    # -------------------------------------------------------------------------
+    def eval_section(self, section_name):
+
+        super(MailConfiguration, self).eval_section(section_name)
+        sn = section_name.lower()
+
+        if sn == 'mail':
+            section = self.cfg[section_name]
+            return self._eval_mail(section_name, section)
+
+    # -------------------------------------------------------------------------
+    def _eval_mail(self, section_name, section):
+
+        if self.verbose > 2:
+            msg = _("Evaluating config section {!r}:").format(section_name)
+            LOG.debug(msg + '\n' + pp(section))
+
+        self._eval_mail_from(section_name, section)
+        self._eval_mail_rcpt(section_name, section)
+        self._eval_mail_cc(section_name, section)
+        self._eval_mail_reply_to(section_name, section)
+        self._eval_mail_method(section_name, section)
+        self._eval_mail_server(section_name, section)
+        self._eval_smtp_port(section_name, section)
+
+    # -------------------------------------------------------------------------
+    def _split_mailaddress_tokens(self, value, what=None):
+
+        result = []
+
+        tokens = self.whitespace_re.split(value)
+        for token in tokens:
+            if MailAddress.valid_address(token):
+                result.append(token)
+            else:
+                msg = _("Found invalid {what} {addr!r} in configuration.")
+                LOG.error(msg.format(what=what, addr=token))
+
+        return result
+
+    # -------------------------------------------------------------------------
+    def _eval_mail_from(self, section_name, section):
+
+        re_from = re.compile(r'^\s*(mail[_-]?)?from\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if not re_from.search(key):
+                continue
+
+            val = section[key]
+
+            if is_sequence(val):
+                if not len(val):
+                    continue
+                val = val[0]
+
+            if MailAddress.valid_address(val):
+                self.mail_from = val
+            else:
+                msg = _("Found invalid {what} {addr!r} in configuration.")
+                LOG.error(msg.format(what=_("from address"), addr=val))
+
+    # -------------------------------------------------------------------------
+    def _eval_mail_rcpt(self, section_name, section):
+
+        re_rcpt = re.compile(r'^\s*(mail[_-]?)?(recipients?|rcpt)\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if not re_rcpt.search(key):
+                continue
+
+            val = section[key]
+            if not val:
+                continue
+            if is_sequence(val):
+                for v in val:
+                    result = self._split_mailaddress_tokens(v, _("recipient mail address"))
+                    if result:
+                        self.mail_recipients.expand(result)
+            else:
+                result = self._split_mailaddress_tokens(val, _("recipient mail address"))
+                if result:
+                    self.mail_recipients.expand(result)
+
+    # -------------------------------------------------------------------------
+    def _eval_mail_cc(self, section_name, section):
+
+        re_cc = re.compile(r'^\s*(mail[_-]?)?cc\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+
+            self._mail_cc_configured = True
+            if not re_cc.search(key):
+                continue
+
+            val = section[key]
+            if not val:
+                continue
+            if is_sequence(val):
+                for v in val:
+                    result = self._split_mailaddress_tokens(v, _("cc mail address"))
+                    if result:
+                        self.mail_cc.expand(result)
+            else:
+                result = self._split_mailaddress_tokens(val, _("cc mail address"))
+                if result:
+                    self.mail_cc.expand(result)
+
+    # -------------------------------------------------------------------------
+    def _eval_mail_reply_to(self, section_name, section):
+
+        re_reply = re.compile(r'^\s*(mail[_-]?)?reply([-_]?to)?\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if not re_reply.search(key):
+                continue
+
+            val = section[key]
+
+            if is_sequence(val):
+                if not len(val):
+                    continue
+                val = val[0]
+
+            if MailAddress.valid_address(val):
+                self.reply_to = val
+            else:
+                msg = _("Found invalid {what} {addr!r} in configuration.")
+                LOG.error(msg.format(what=_("reply to address"), addr=val))
+
+    # -------------------------------------------------------------------------
+    def _eval_mail_method(self, section_name, section):
+
+        re_method = re.compile(r'^\s*(mail[_-]?)?method\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if not re_method.search(key):
+                continue
+
+            val = section[key].strip().lower()
+            if not val:
+                continue
+
+            if val not in self.valid_mail_methods:
+                msg = _("Found invalid mail method {!r} in configuration.")
+                LOG.error(msg.format(section[key]))
+                continue
+
+            self.mail_method = val
+
+    # -------------------------------------------------------------------------
+    def _eval_mail_server(self, section_name, section):
+
+        re_server = re.compile(r'^\s*(mail[_-]?)?server\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if not re_server.search(key):
+                continue
+
+            val = section[key].strip().lower()
+            if not val:
+                continue
+
+            self.mail_server = val
+
+    # -------------------------------------------------------------------------
+    def _eval_smtp_port(self, section_name, section):
+
+        re_server = re.compile(r'^\s*(smtp[_-]?)?port\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if not re_server.search(key):
+                continue
+
+            val = section[key]
+            try:
+                port = int(val)
+            except (ValueError, TypeError) as e:
+                msg = _("Value {!r} for SMTP port is invalid:").format(val)
+                msg += ' ' + str(e)
+                LOG.error(msg)
+                continue
+            if port <= 0 or port > MAX_PORT_NUMBER:
+                msg = _("Found invalid SMTP port number {} in configuration.").format(port)
+                LOG.error(msg)
+                continue
+
+            self.smtp_port = port
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/config/pdns.py b/lib/pp_admintools/config/pdns.py
new file mode 100644 (file)
index 0000000..31e97c7
--- /dev/null
@@ -0,0 +1,474 @@
+#!/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 providing a configuration for applications,
+          which are Working with PowerDNS.
+          It's based on class MailConfigError.
+"""
+from __future__ import absolute_import
+
+# Standard module
+import logging
+import re
+import copy
+
+# Third party modules
+
+# Own modules
+
+from fb_tools.common import is_sequence, pp
+
+# from .config import ConfigError, BaseConfiguration
+from fb_tools.multi_config import DEFAULT_ENCODING
+
+from .. import __version__ as GLOBAL_VERSION
+from .. import MAX_TIMEOUT, MAX_PORT_NUMBER
+
+from .mail import MailConfigError, MailConfiguration
+from .mail import DEFAULT_CONFIG_DIR
+
+from ..xlate import XLATOR
+
+LIBRARY_NAME = "pp-pdns-api-client"
+
+__version__ = '0.2.3'
+LOG = logging.getLogger(__name__)
+
+_ = XLATOR.gettext
+
+
+# =============================================================================
+class PdnsConfigError(MailConfigError):
+    """Base error class for all exceptions happened during
+    execution this configured application"""
+
+    pass
+
+
+# =============================================================================
+class PdnsConfiguration(MailConfiguration):
+    """
+    A class for providing a configuration for an arbitrary PowerDNS Application
+    and methods to read it from configuration files.
+    """
+
+    valid_pdns_api_instances = ('global', 'public', 'local')
+
+    default_pdns_api_instances = {
+        'global': {
+            'host': "dnsmaster.pp-dns.com",
+        },
+        'public': {
+            'host': "dnsmaster-public.pixelpark.com",
+        },
+        'local': {
+            'host': "dnsmaster-local.pixelpark.com",
+        },
+    }
+
+    default_pdns_api_port = 8081
+    default_pdns_api_servername = "localhost"
+    default_pdns_timeout = 20
+
+    default_pdns_instance = 'global'
+
+    # -------------------------------------------------------------------------
+    def __init__(
+        self, appname=None, verbose=0, version=__version__, base_dir=None,
+            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
+            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
+            ensure_privacy=True, use_chardet=True, initialized=False):
+
+        self.api_user_agent = '{}/{}'.format(LIBRARY_NAME, GLOBAL_VERSION)
+
+        self.pdns_api_instances = {}
+        for inst_name in self.default_pdns_api_instances.keys():
+
+            def_inst = self.default_pdns_api_instances[inst_name]
+
+            inst = {}
+            inst['host'] = def_inst['host']
+            inst['port'] = self.default_pdns_api_port
+            inst['key'] = None
+            inst['servername'] = self.default_pdns_api_servername
+
+            self.pdns_api_instances[inst_name] = inst
+
+        self.pdns_timeout = self.default_pdns_timeout
+
+        self.pdns_instance = self.default_pdns_instance
+        self.pdns_host = None
+        self.pdns_port = None
+        self.pdns_key = None
+        self.pdns_servername = None
+
+        add_stems = []
+        if additional_stems:
+            if is_sequence(additional_stems):
+                for stem in additional_stems:
+                    add_stems.append(stem)
+            else:
+                add_stems.append(additional_stems)
+
+        if 'pdns' not in add_stems:
+            add_stems.append('pdns')
+
+        if 'powerdns' not in add_stems:
+            add_stems.append('powerdns')
+
+        super(PdnsConfiguration, self).__init__(
+            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
+            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
+            additional_stems=add_stems, additional_config_file=additional_config_file,
+            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
+            ensure_privacy=ensure_privacy, initialized=False,
+        )
+
+        if initialized:
+            self.initialized = True
+
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(PdnsConfiguration, self).as_dict(short=short)
+
+        res['default_pdns_api_instances'] = self.default_pdns_api_instances
+        res['default_pdns_api_port'] = self.default_pdns_api_port
+        res['default_pdns_api_servername'] = self.default_pdns_api_servername
+        res['default_pdns_timeout'] = self.default_pdns_timeout
+        res['default_pdns_instance'] = self.default_pdns_instance
+        res['valid_pdns_api_instances'] = self.valid_pdns_api_instances
+
+        res['pdns_key'] = None
+        if self.pdns_key:
+            if self.verbose <= 4:
+                res['pdns_key'] = '******'
+            else:
+                res['pdns_key'] = self.pdns_key
+
+        res['pdns_api_instances'] = {}
+        for iname in self.pdns_api_instances.keys():
+            inst = self.pdns_api_instances[iname]
+            res['pdns_api_instances'][iname] = copy.copy(inst)
+            if 'key' in inst:
+                if self.verbose <= 4:
+                    res['pdns_api_instances'][iname]['key'] = '******'
+
+        return res
+
+    # -------------------------------------------------------------------------
+    def eval_section(self, section_name):
+
+        super(PdnsConfiguration, self).eval_section(section_name)
+        sn = section_name.lower()
+
+        re_pdns = re.compile(r'^(?:pdns|powerdns)(?:[_-]?api)?$')
+
+        if re_pdns.search(sn):
+            section = self.cfg[section_name]
+            return self._eval_pdns(section_name, section)
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns(self, section_name, section):
+
+        if self.verbose > 2:
+            msg = _("Evaluating config section {!r}:").format(section_name)
+            LOG.debug(msg + '\n' + pp(section))
+
+        re_agent = re.compile(r'^\s*(?:api[_-]?)?user[_-]?agent\s*$', re.IGNORECASE)
+        re_timeout = re.compile(r'^\s*timeout\s*$', re.IGNORECASE)
+        re_inst = re.compile(r'^\s*instances\s*$', re.IGNORECASE)
+        re_env = re.compile(r'^\s*(?:env(?:ironment)?|inst(?:ance)?)\s*$', re.IGNORECASE)
+        re_host = re.compile(r'^\s*(?:api[_-]?)?host\s*$', re.IGNORECASE)
+        re_port = re.compile(r'^\s*(?:api[_-]?)?port\s*$', re.IGNORECASE)
+        re_key = re.compile(r'^\s*(?:api[_-]?)?key\s*$', re.IGNORECASE)
+        re_servername = re.compile(r'^\s*(?:api[_-]?)?servername\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+
+            if re_agent.search(key):
+                self._eval_api_user_agent(section_name, key, section)
+                continue
+
+            if re_timeout.search(key):
+                self._eval_pdns_timeout(section_name, key, section)
+                continue
+
+            if re_env.search(key):
+                self._eval_pdns_environment(section_name, key, section)
+                continue
+
+            if re_host.search(key):
+                self._eval_pdns_host(section_name, key, section)
+                continue
+
+            if re_port.search(key):
+                self._eval_pdns_port(section_name, key, section)
+                continue
+
+            if re_key.search(key):
+                self._eval_pdns_key(section_name, key, section)
+                continue
+
+            if re_servername.search(key):
+                self._eval_pdns_re_servername(section_name, key, section)
+                continue
+
+            if re_inst.search(key):
+                self._eval_pdns_instances(section_name, key, section)
+                continue
+
+    # -------------------------------------------------------------------------
+    def _eval_api_user_agent(self, section_name, key, section):
+
+        val = section[key].strip()
+        if val:
+            self.api_user_agent = val
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_timeout(self, section_name, key, section):
+
+        val = section[key]
+        try:
+            timeout = int(val)
+            if timeout <= 0 or timeout > MAX_TIMEOUT:
+                msg = _("A timeout has to be between 1 and {} seconds.")
+                msg = msg.format(MAX_TIMEOUT)
+                raise ValueError(msg)
+        except (ValueError, TypeError) as e:
+            msg = _("Value {!r} for PowerDNS API timeout is invalid:").format(val)
+            msg += " " + str(e)
+            if self.raise_on_error:
+                raise PdnsConfigError(msg)
+            LOG.error(msg)
+            return
+
+        self.pdns_timeout = timeout
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_environment(self, section_name, key, section):
+
+        env = section[key].strip().lower()
+
+        if not env:
+            return
+
+        if env not in self.pdns_api_instances:
+            msg = _("Found invalid PDNS environment/instance {!r} in configuration.")
+            msg = msg.format(section[key])
+            if self.raise_on_error:
+                raise PdnsConfigError(msg)
+            LOG.error(msg)
+            return
+
+        self.pdns_instance = env
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_host(self, section_name, key, section):
+
+        val = section[key].strip().lower()
+        if val:
+            if self.verbose > 2:
+                msg = _("Found PDNS host: {!r}.").format(val)
+                LOG.debug(msg)
+
+            self.pdns_host = val
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_port(self, section_name, key, section):
+
+        val = section[key]
+        if not val:
+            return
+
+        port = None
+        try:
+            port = int(val)
+            if port <= 0 or port > MAX_PORT_NUMBER:
+                msg = _("A port must be greater than 0 and less than {}.")
+                raise ValueError(msg.format(MAX_PORT_NUMBER))
+        except (TypeError, ValueError) as e:
+            msg = _("Wrong PDNS port number {p!r} found: {e}").format(p=val, e=e)
+            if self.raise_on_error:
+                raise PdnsConfigError(msg)
+            else:
+                LOG.error(msg)
+                port = None
+
+        if port:
+            if self.verbose > 2:
+                msg = _("Found port number for PDNS: {}.").format(port)
+                LOG.debug(msg)
+
+            self.pdns_port = port
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_key(self, section_name, key, section):
+
+        val = section[key].strip()
+        if val:
+            if self.verbose > 2:
+                key_show = '******'
+                if self.verbose > 4:
+                    key_show = val
+                msg = _("Found API key for PDNS: {!r}.").format(key_show)
+                LOG.debug(msg)
+
+            self.pdns_key = val
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_servername(self, section_name, key, section):
+
+        val = section[key].strip()
+        if val:
+            if self.verbose > 2:
+                msg = _("Found PDNS API servername: {!r}.").format(val)
+                LOG.debug(msg)
+
+            self.pdns_servername = val
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_instances(self, section_name, key, section):
+
+        for instance_name in section[key].keys():
+            self._eval_pdns_instance(self, instance_name, section[key][instance_name])
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_instance(self, instance_name, section):
+
+        iname = instance_name.lower()
+
+        if self.verbose > 2:
+            msg = _("Evaluating PowerDNS instance {!r}:").format(iname)
+            LOG.debug(msg + '\n' + pp(section))
+
+        self._eval_pdns_inst_host(iname, section)
+        self._eval_pdns_inst_port(iname, section)
+        self._eval_pdns_inst_servername(iname, section)
+        self._eval_pdns_inst_key(iname, section)
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_inst_host(self, iname, section):
+
+        if self.verbose > 2:
+            msg = _("Searching for host for PDNS instance {!r} ..")
+            LOG.debug(msg.format(iname))
+
+        for key in section.keys():
+            if key.lower() == 'host':
+                host = section[key].lower().strip()
+                if host:
+                    if self.verbose > 2:
+                        msg = _("Found host for PDNS instance {inst!r}: {host!r}.")
+                        LOG.debug(msg.format(inst=iname, host=host))
+                    self.pdns_api_instances[iname]['host'] = host
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_inst_port(self, iname, section):
+
+        if self.verbose > 2:
+            msg = _("Searching for post number for PDNS instance {!r} ..")
+            LOG.debug(msg.format(iname))
+
+        for key in section.keys():
+            if key.lower() == 'port':
+                port = None
+                val = section[key]
+                try:
+                    port = int(val)
+                    if port <= 0 or port > MAX_PORT_NUMBER:
+                        msg = _("A port must be greater than 0 and less than {}.")
+                        raise ValueError(msg.format(MAX_PORT_NUMBER))
+                except (TypeError, ValueError) as e:
+                    msg = _("Wrong port number {p!r} for PDNS instance {inst!r} found: {e}")
+                    msg = msg.format(p=val, inst=iname, e=e)
+                    if self.raise_on_error:
+                        raise PdnsConfigError(msg)
+                    else:
+                        LOG.error(msg)
+                        port = None
+                if port:
+                    if self.verbose > 2:
+                        msg = _("Found port number for PDNS instance {inst!r}: {p}.")
+                        LOG.debug(msg.format(inst=iname, p=port))
+                    self.pdns_api_instances[iname]['port'] = port
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_inst_servername(self, iname, section):
+
+        if self.verbose > 2:
+            msg = _("Searching for internal server name of PDNS instance {!r} ..")
+            LOG.debug(msg.format(iname))
+
+        re_servername = re.compile(r'^\s*server[_-]?(name|id)\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if re_servername.search(key):
+                servername = section[key].lower().strip()
+                if servername:
+                    if self.verbose > 2:
+                        msg = _("Found internal server name PDNS instance {inst!r}: {sn!r}.")
+                        LOG.debug(msg.format(inst=iname, sn=servername))
+                    self.pdns_api_instances[iname]['servername'] = servername
+
+    # -------------------------------------------------------------------------
+    def _eval_pdns_inst_key(self, iname, section):
+
+        if self.verbose > 2:
+            msg = _("Searching for API key of PDNS instance {!r} ..")
+            LOG.debug(msg.format(iname))
+
+        re_key = re.compile(r'^\s*(api[_-]?)?key\s*$', re.IGNORECASE)
+
+        for key in section.keys():
+            if re_key.search(key):
+                api_key = section[key].lower().strip()
+                if api_key:
+                    if self.verbose > 2:
+                        key_show = '******'
+                        if self.verbose > 4:
+                            key_show = api_key
+                        msg = _("Found API key of PDNS instance {inst!r}: {key!r}.")
+                        LOG.debug(msg.format(inst=iname, key=key_show))
+                    self.pdns_api_instances[iname]['key'] = api_key
+
+    # -------------------------------------------------------------------------
+    def eval(self):
+
+        super(PdnsConfiguration, self).eval()
+
+        inst = self.pdns_instance
+
+        if not self.pdns_host:
+            self.pdns_host = self.pdns_api_instances[inst]['host']
+
+        if not self.pdns_port:
+            self.pdns_port = self.pdns_api_instances[inst]['port']
+
+        if not self.pdns_key:
+            self.pdns_key = self.pdns_api_instances[inst]['key']
+
+        if not self.pdns_servername:
+            self.pdns_servername = self.pdns_api_instances[inst]['servername']
+
+
+# =============================================================================
+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
deleted file mode 100644 (file)
index 0b693f8..0000000
+++ /dev/null
@@ -1,970 +0,0 @@
-#!/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 datetime
-import tempfile
-import time
-import shutil
-import pipes
-import ipaddress
-
-from subprocess import Popen, TimeoutExpired, PIPE
-
-from pathlib import Path
-
-# Third party modules
-import six
-from pytz import timezone, UnknownTimeZoneError
-
-# Own modules
-from fb_tools.common import pp, to_str
-
-from fb_tools.app import BaseApplication
-
-from fb_tools.pidfile import PidFileError, PidFile
-
-from . import __version__ as GLOBAL_VERSION
-
-from .pdns_app import PpPDNSAppError, PpPDNSApplication
-
-from .dns_deploy_zones_config import DnsDeployZonesConfig
-
-from .xlate import XLATOR
-
-__version__ = '0.8.2'
-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.
-    """
-
-    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'\.+$')
-
-    default_local_tz_name = 'Europe/Berlin'
-
-    open_args = {}
-    if six.PY3:
-        open_args = {
-            'encoding': 'utf-8',
-            'errors': 'surrogateescape',
-        }
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, base_dir=None, version=GLOBAL_VERSION,
-            cfg_class=DnsDeployZonesConfig):
-
-        self.zones = {}
-        self.pidfile = None
-
-        self._show_simulate_opt = True
-        self.cfg = None
-
-        # Configuration files and directories
-
-        self.tempdir = None
-        self.temp_zones_cfg_file = None
-        self.keep_tempdir = False
-        self.keep_backup = False
-
-        self.local_tz = None
-        self.local_tz_name = self.default_local_tz_name
-
-        self.backup_suffix = (
-            '.' + datetime.datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S') + '.bak')
-
-        self.reload_necessary = False
-        self.restart_necessary = False
-
-        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_class=cfg_class, initialized=False, instance="public",
-        )
-
-        masters = []
-        for addr in sorted(self.cfg.masters, key=ipaddress.ip_address):
-            if addr not in self.local_addresses:
-                masters.append(addr)
-
-        self.cfg.masters = masters
-
-        self.initialized = True
-
-    # -------------------------------------------
-    @property
-    def cmd_named_checkconf(self):
-        """The OS command for named-checkconf."""
-
-        checkconf = DnsDeployZonesConfig.default_named_checkconf
-        if self.cfg:
-            checkconf = self.cfg.named_checkconf
-        return str(checkconf)
-
-    # -------------------------------------------
-    @property
-    def cmd_named_reload(self):
-        """The OS command to reload the BIND nameserver."""
-
-        rndc = DnsDeployZonesConfig.default_rndc
-        if self.cfg:
-            rndc = self.cfg.rndc
-
-        return "{} reload".format(rndc)
-
-    # -------------------------------------------
-    @property
-    def cmd_named_status(self):
-        """The OS command to show the status of the BIND nameserver service."""
-
-        systemctl = DnsDeployZonesConfig.default_systemctl
-        if self.cfg:
-            systemctl = self.cfg.systemctl
-
-        return "{} status named.service".format(systemctl)
-
-    # -------------------------------------------
-    @property
-    def cmd_named_start(self):
-        """The OS command to start the BIND nameserver service."""
-
-        systemctl = DnsDeployZonesConfig.default_systemctl
-        if self.cfg:
-            systemctl = self.cfg.systemctl
-
-        return "{} start named.service".format(systemctl)
-
-    # -------------------------------------------
-    @property
-    def cmd_named_restart(self):
-        """The OS command to restart the BIND nameserver service."""
-
-        systemctl = DnsDeployZonesConfig.default_systemctl
-        if self.cfg:
-            systemctl = self.cfg.systemctl
-
-        return "{} restart named.service".format(systemctl)
-
-    # -------------------------------------------
-    @property
-    def named_zones_cfg_file(self):
-        """The file for configuration of all own zones."""
-
-        conf_dir = DnsDeployZonesConfig.default_named_conf_dir
-        zones_cfg_file = DnsDeployZonesConfig.default_named_zones_cfg_file
-        if self.cfg:
-            conf_dir = self.cfg.named_conf_dir
-            zones_cfg_file = self.cfg.named_zones_cfg_file
-
-        return (conf_dir / zones_cfg_file).resolve()
-
-    # -------------------------------------------
-    @property
-    def named_slavedir_rel(self):
-        """The directory for zone files of slave zones."""
-
-        if self.cfg:
-            return self.cfg.named_slavedir
-        return DnsDeployZonesConfig.default_named_slavedir
-
-    # -------------------------------------------
-    @property
-    def named_basedir(self):
-        """The base directory of named, where all volatile data are stored."""
-
-        if self.cfg:
-            return self.cfg.named_basedir
-        return DnsDeployZonesConfig.default_named_basedir
-
-    # -------------------------------------------
-    @property
-    def named_slavedir_abs(self):
-        """The directory for zone files of slave zones."""
-
-        return (self.named_basedir / self.named_slavedir_rel).resolve()
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(PpDeployZonesApp, self).as_dict(short=short)
-
-        res['named_slavedir_abs'] = self.named_slavedir_abs
-        res['cmd_named_checkconf'] = self.cmd_named_checkconf
-        res['cmd_named_reload'] = self.cmd_named_reload
-        res['cmd_named_status'] = self.cmd_named_status
-        res['cmd_named_start'] = self.cmd_named_start
-        res['cmd_named_restart'] = self.cmd_named_restart
-        res['named_zones_cfg_file'] = self.named_zones_cfg_file
-        res['named_basedir'] = self.named_basedir
-        res['named_slavedir_rel'] = self.named_slavedir_rel
-        res['named_slavedir_abs'] = self.named_slavedir_abs
-
-        return res
-
-    # -------------------------------------------------------------------------
-    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 post_init(self):
-
-        if not self.quiet:
-            print('')
-
-        LOG.debug(_("Post init phase."))
-
-        super(PpDeployZonesApp, self).post_init()
-
-        LOG.debug(_("My own post init phase."))
-
-        cmd_namedcheckconf = self.get_command('named-checkconf', resolve=True)
-        if not cmd_namedcheckconf:
-            self.exit(1)
-        self.cfg.named_checkconf = cmd_namedcheckconf
-
-        self.pidfile = PidFile(
-            filename=self.cfg.pidfile, appname=self.appname, verbose=self.verbose,
-            base_dir=self.base_dir, simulate=self.simulate)
-
-        if 'TZ' in os.environ and os.environ['TZ']:
-            self.local_tz_name = os.environ['TZ']
-        try:
-            self.local_tz = timezone(self.local_tz_name)
-        except UnknownTimeZoneError:
-            LOG.error(_("Unknown time zone: {!r}.").format(self.local_tz_name))
-            self.exit(6)
-
-    # -------------------------------------------------------------------------
-    def current_timestamp(self):
-
-        if self.local_tz:
-            return datetime.datetime.now(self.local_tz).strftime('%Y-%m-%d %H:%M:%S %Z')
-        return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
-    # -------------------------------------------------------------------------
-    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:
-                msg += ' ' + _("But in simulation mode we are continuing nevertheless.")
-                LOG.warn(msg)
-                time.sleep(1)
-            else:
-                LOG.error(msg)
-                self.exit(1)
-
-        super(PpDeployZonesApp, self).pre_run()
-
-        if self.cfg.pdns_instance == 'global':
-            LOG.error(_(
-                "Using the global DNS master is not supported, "
-                "please use 'local' or 'public'"))
-            self.exit(1)
-
-    # -------------------------------------------------------------------------
-    def _run(self):
-
-        LOG.info(_("Starting: {}").format(self.current_timestamp()))
-
-        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(self.current_timestamp()))
-
-    # -------------------------------------------------------------------------
-    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 backup_file.exists():
-                if self.keep_backup:
-                    LOG.info(_("Keep existing backup file {!r}.").format(str(backup_file)))
-                else:
-                    LOG.info(_("Removing {!r} ...").format(str(backup_file)))
-                    if not self.simulate:
-                        backup_file.unlink()
-
-        # -----------------------
-        def emit_rm_err(function, path, excinfo):
-            LOG.error(_("Error removing {p!r} - {c}: {e}").format(
-                p=str(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(str(self.tempdir))
-                LOG.warn(msg)
-            else:
-                LOG.debug(_("Destroying temporary directory {!r} ...").format(str(self.tempdir)))
-                shutil.rmtree(str(self.tempdir), False, emit_rm_err)
-                self.tempdir = None
-
-    # -------------------------------------------------------------------------
-    def init_temp_objects(self):
-        """Init temporary objects and properties."""
-
-        self.tempdir = Path(tempfile.mkdtemp(prefix=(self.appname + '.'), suffix='.tmp.d'))
-        LOG.debug(_("Temporary directory: {!r}.").format(str(self.tempdir)))
-
-        self.temp_zones_cfg_file = self.tempdir / self.cfg.named_zones_cfg_file
-
-        if self.verbose > 1:
-            LOG.debug(_("Temporary zones conf: {!r}").format(str(self.temp_zones_cfg_file)))
-
-    # -------------------------------------------------------------------------
-    def get_named_keys(self):
-
-        LOG.info(_("Trying to get all keys from named.conf ..."))
-
-        cmd = shlex.split(self.cmd_named_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.cfg.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(str(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 self.temp_zones_cfg_file.open('w', **self.open_args) as fh:
-            fh.write(content)
-
-        if self.verbose > 2:
-            LOG.debug(
-                _("Generated file {!r}:").format(
-                    str(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('// {}'.format(show_name))
-        lines.append('zone "{}" in {{'.format(zname))
-        lines.append('\tmasters {')
-        for master in self.cfg.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.cfg.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=str(file_src), two=str(file_tgt)))
-
-        if not file_src.exists():
-            msg = _("{what} {f!r} does not exists.").format(
-                what=_("Source file"), f=str(file_src))
-            raise PpDeployZonesError(msg)
-        if not file_src.is_file():
-            msg = _("{what} {f!r} is not a regular file.").format(
-                what=_("Source file"), f=str(file_src))
-            raise PpDeployZonesError(msg)
-
-        if not file_tgt.exists():
-            msg = _("{what} {f!r} does not exists.").format(
-                what=_("Target file"), f=str(file_tgt))
-            LOG.debug(msg)
-            return False
-        if not file_tgt.is_file():
-            msg = _("{what} {f!r} is not a regular file.").format(
-                what=_("Target file"), f=str(file_tgt))
-            raise PpDeployZonesError(msg)
-
-        # Reading source file
-        content_src = ''
-        if self.verbose > 2:
-            LOG.debug(_("Reading {!r} ...").format(str(file_src)))
-        content_src = file_src.read_text(**self.open_args)
-        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:
-            msg = _("Cleaned version of {!r}:").format(str(file_src))
-            msg += '\n' + '\n'.join(lines_src)
-            LOG.debug(msg)
-
-        # Reading target file
-        content_tgt = ''
-        if self.verbose > 2:
-            LOG.debug(_("Reading {!r} ...").format(str(file_tgt)))
-        content_tgt = file_tgt.read_text(**self.open_args)
-        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:
-            msg = _("Cleaned version of {!r}:").format(str(file_tgt))
-            msg += '\n' + '\n'.join(lines_tgt)
-            LOG.debug(msg)
-
-        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=str(file_src), sl=len(lines_src), tf=str(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=str(file_src), tf=str(file_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 = Path(str(tgt_file) + self.backup_suffix)
-
-            if tgt_file.exists():
-                self.moved_files[tgt_file] = backup_file
-                LOG.info(_("Copying {frm!r} => {to!r} ...").format(
-                    frm=str(tgt_file), to=str(backup_file)))
-                if not self.simulate:
-                    shutil.copy2(str(tgt_file), str(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=str(src_file), to=str(tgt_file)))
-            if not self.simulate:
-                shutil.copy2(str(src_file), str(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=str(backup_file), to=str(tgt_file)))
-            if not self.simulate:
-                if backup_file.exists():
-                    backup_file.rename(tgt_file)
-                else:
-                    LOG.error(_("Could not find backup file {!r}.").format(str(backup_file)))
-
-    # -------------------------------------------------------------------------
-    def check_namedconf(self):
-
-        LOG.info(_("Checking syntax correctness of named.conf ..."))
-        cmd = shlex.split(self.cmd_named_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_named_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_named_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_named_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_named_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_config.py b/lib/pp_admintools/dns_deploy_zones_config.py
deleted file mode 100644 (file)
index 76de842..0000000
+++ /dev/null
@@ -1,606 +0,0 @@
-#!/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 providing a configuration the dns-deploy-zones applications.
-          It's based on class PdnsConfiguration.
-"""
-from __future__ import absolute_import
-
-# Standard module
-import logging
-import re
-import copy
-import socket
-
-from pathlib import Path
-
-# Third party modules
-
-# Own modules
-
-from fb_tools.common import is_sequence, pp, to_bool
-
-# from .config import ConfigError, BaseConfiguration
-from fb_tools.multi_config import DEFAULT_ENCODING
-
-from .pdns_config import PdnsConfigError, PdnsConfiguration
-from .mail_config import DEFAULT_CONFIG_DIR
-
-from .xlate import XLATOR
-
-__version__ = '0.2.1'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-
-
-# =============================================================================
-class DnsDeployZonesConfigError(PdnsConfigError):
-    """Base error class for all exceptions happened during
-    execution this configured application"""
-
-    pass
-
-
-# =============================================================================
-class DnsDeployZonesConfig(PdnsConfiguration):
-    """
-    A class for providing a configuration for an arbitrary PowerDNS Application
-    and methods to read it from configuration files.
-    """
-
-    default_pidfile = Path('/run/dns-deploy-zones.pid')
-    default_keep_backup = False
-
-    default_named_conf_dir = Path('/etc')
-    default_named_zones_cfg_file = Path('named.zones.conf')
-    default_named_basedir = Path('/var/named')
-    default_named_slavedir = Path('slaves')
-
-    default_zone_masters_local = ['master-local.pp-dns.com']
-    default_zone_masters_public = ['master-public.pp-dns.com']
-
-    default_rndc = Path('/usr/sbin/rndc')
-    default_systemctl = Path('/usr/bin/systemctl')
-    default_named_checkconf = Path('/usr/sbin/named-checkconf')
-
-    default_named_listen_on_v6 = False
-    default_named_internal = False
-
-    re_split_addresses = re.compile(r'[,;\s]+')
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=__version__, base_dir=None,
-            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
-            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
-            ensure_privacy=True, use_chardet=True, initialized=False):
-
-        self.pidfile = self.default_pidfile
-        self.keep_backup = self.default_keep_backup
-
-        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_local = []
-        for master in self.default_zone_masters_local:
-            self.zone_masters_local.append(master)
-
-        self.zone_masters_public = []
-        for master in self.default_zone_masters_public:
-            self.zone_masters_public.append(master)
-
-        self.rndc = self.default_rndc
-        self.systemctl = self.default_systemctl
-        self.named_checkconf = self.default_named_checkconf
-
-        self._named_listen_on_v6 = self.default_named_listen_on_v6
-        self._named_internal = self.default_named_internal
-
-        self.masters = set()
-
-        add_stems = []
-        if additional_stems:
-            if is_sequence(additional_stems):
-                for stem in additional_stems:
-                    add_stems.append(stem)
-            else:
-                add_stems.append(additional_stems)
-
-        if 'named' not in add_stems:
-            add_stems.append('named')
-
-        super(DnsDeployZonesConfig, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
-            additional_stems=add_stems, additional_config_file=additional_config_file,
-            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
-            ensure_privacy=ensure_privacy, initialized=False,
-        )
-
-        if initialized:
-            self.initialized = True
-
-    # -------------------------------------------------------------------------
-    @property
-    def named_internal(self):
-        """Is the BIND nameserver on the current host a local resolver (True)
-        or an authoritative nameserver for outside."""
-        return self._named_internal
-
-    @named_internal.setter
-    def named_internal(self, value):
-        self._named_internal = to_bool(value)
-
-    # -------------------------------------------------------------------------
-    @property
-    def named_listen_on_v6(self):
-        """Is the BIND nameserver on the current listening on some IPv6 addresses?"""
-        return self._named_listen_on_v6
-
-    @named_listen_on_v6.setter
-    def named_listen_on_v6(self, value):
-        self._named_listen_on_v6 = to_bool(value)
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(DnsDeployZonesConfig, self).as_dict(short=short)
-
-        res['default_pidfile'] = self.default_pidfile
-        res['default_keep_backup'] = self.default_keep_backup
-        res['default_named_conf_dir'] = self.default_named_conf_dir
-        res['default_named_zones_cfg_file'] = self.default_named_zones_cfg_file
-        res['default_named_basedir'] = self.default_named_basedir
-        res['default_named_slavedir'] = self.default_named_slavedir
-        res['default_zone_masters_local'] = copy.copy(self.default_zone_masters_local)
-        res['default_zone_masters_public'] = copy.copy(self.default_zone_masters_public)
-        res['default_rndc'] = self.default_rndc
-        res['default_systemctl'] = self.default_systemctl
-        res['default_named_checkconf'] = self.default_named_checkconf
-        res['default_named_listen_on_v6'] = self.default_named_listen_on_v6
-        res['default_named_internal'] = self.default_named_internal
-        res['named_listen_on_v6'] = self.named_listen_on_v6
-        res['named_internal'] = self.named_internal
-
-        res['masters'] = copy.copy(self.masters)
-
-        return res
-
-    # -------------------------------------------------------------------------
-    def eval_section(self, section_name):
-
-        super(DnsDeployZonesConfig, self).eval_section(section_name)
-        sn = section_name.lower()
-
-        if sn == 'named':
-            section = self.cfg[section_name]
-            return self._eval_named(section_name, section)
-
-        if sn == self.appname.lower() or sn == 'app':
-            section = self.cfg[section_name]
-            return self._eval_app(section_name, section)
-
-    # -------------------------------------------------------------------------
-    def _eval_named(self, section_name, section):
-
-        if self.verbose > 2:
-            msg = _("Evaluating config section {!r}:").format(section_name)
-            LOG.debug(msg + '\n' + pp(section))
-
-        re_config_dir = re.compile(r'^\s*(?:named[_-]?)?conf(?:ig)?[_-]?dir\s*$', re.IGNORECASE)
-        re_config_file = re.compile(
-            r'^\s*(?:named[_-]?)?zones[_-]?(?:conf(?:ig)?|cfg)[_-]*file\s*$', re.IGNORECASE)
-        re_base_dir = re.compile(r'^\s*(?:named[_-]?)?base[_-]?dir\s*$', re.IGNORECASE)
-        re_slave_dir = re.compile(r'^\s*(?:named[_-]?)?slave[_-]?dir\s*$', re.IGNORECASE)
-        re_named_checkconf = re.compile(r'^named[_-]?checkconf$', re.IGNORECASE)
-        re_internal = re.compile(
-            r'^\s*(?:named[_-]?)?(?:is[_-]?)?intern(?:al)?\s*$', re.IGNORECASE)
-        re_listen_v6 = re.compile(r'^\s*listen[_-](?:on[_-])?(?:ip)v6\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-
-            if key.lower() == 'masters':
-                self._eval_named_masters(section_name, key, section)
-                continue
-
-            if key.lower() == 'rndc':
-                self._eval_named_rndc(section_name, key, section)
-                continue
-
-            if key.lower() == 'systemctl':
-                self._eval_named_systemctl(section_name, key, section)
-                continue
-
-            if re_config_dir.search(key):
-                self._eval_named_configdir(section_name, key, section)
-                continue
-
-            if re_config_file.search(key):
-                self._eval_named_configfile(section_name, key, section)
-                continue
-
-            if re_base_dir.search(key):
-                self._eval_named_basedir(section_name, key, section)
-                continue
-
-            if re_slave_dir.search(key):
-                self._eval_named_slavedir(section_name, key, section)
-                continue
-
-            if re_named_checkconf.search(key):
-                self._eval_named_checkconf(section_name, key, section)
-                continue
-
-            if re_internal.search(key):
-                self._eval_named_internal(section_name, key, section)
-                continue
-
-            if re_listen_v6.search(key):
-                self._eval_named_listen_v6(section_name, key, section)
-                continue
-
-    # -------------------------------------------------------------------------
-    def _eval_named_masters(self, section_name, key, section):
-
-        val = section[key]
-
-        if not val:
-            return
-
-        master_list = set()
-
-        if is_sequence(val):
-            for value in val:
-                masters = self._eval_named_master_list(value)
-                if masters:
-                    master_list |= masters
-        else:
-            masters = self._eval_named_master_list(val)
-            if masters:
-                master_list |= masters
-
-        self.masters = master_list
-
-    # -------------------------------------------------------------------------
-    def _eval_named_master_list(self, value):
-
-        masters = set()
-
-        for m in self.re_split_addresses.split(value):
-            if not m:
-                continue
-
-            m = m.strip().lower()
-            if self.verbose > 1:
-                LOG.debug(_("Checking given master address {!r} ...").format(m))
-            addr_list = self.get_addresses(m)
-            masters |= addr_list
-
-        return masters
-
-    # -------------------------------------------------------------------------
-    def get_addresses(self, host):
-
-        addr_list = set()
-
-        if self.verbose > 3:
-            msg = _("Trying to evaluate address of host {!r} ...").format(host)
-            LOG.debug(msg)
-
-        try:
-            addr_infos = socket.getaddrinfo(host, 53, proto=socket.IPPROTO_TCP)
-            for addr_info in addr_infos:
-                addr = addr_info[4][0]
-                addr_list.add(addr)
-        except socket.gaierror as e:
-            msg = _("Invalid hostname or address {a!r} found in masters: {e}")
-            msg = msg.format(a=host, e=e)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return set()
-        if self.verbose > 3:
-            msg = _("Got addresses {a!r} for host {h!r}.")
-            LOG.debug(msg.format(a=addr_list, h=host))
-
-        return addr_list
-
-    # -------------------------------------------------------------------------
-    def _eval_named_rndc(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        path = Path(val)
-        if not path.is_absolute():
-            msg = _("The path to {what} must be an absolute path, found {path!r}.")
-            msg = msg.format(what='rndc', path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what='rndc', path=val)
-            LOG.debug(msg)
-
-        self.rndc = path
-
-    # -------------------------------------------------------------------------
-    def _eval_named_systemctl(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        path = Path(val)
-        if not path.is_absolute():
-            msg = _("The path to {what} must be an absolute path, found {path!r}.")
-            msg = msg.format(what='systemctl', path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what='systemctl', path=val)
-            LOG.debug(msg)
-
-        self.systemctl = path
-
-    # -------------------------------------------------------------------------
-    def _eval_named_configdir(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        what = _("the named config directory")
-        path = Path(val)
-
-        if not path.is_absolute():
-            msg = _("The path to {what} must be an absolute path, found {path!r}.")
-            msg = msg.format(what=what, path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
-            LOG.debug(msg)
-
-        self.named_conf_dir = path
-
-    # -------------------------------------------------------------------------
-    def _eval_named_configfile(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        what = _("the named config file for zones")
-        path = Path(val)
-
-        if path.is_absolute():
-            msg = _("The path to {what} must not be an absolute path, found {path!r}.")
-            msg = msg.format(what=what, path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
-            LOG.debug(msg)
-
-        self.named_zones_cfg_file = path
-
-    # -------------------------------------------------------------------------
-    def _eval_named_basedir(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        what = _("the named base directory")
-        path = Path(val)
-        if not path.is_absolute():
-            msg = _("The path to {what} must be an absolute path, found {path!r}.")
-            msg = msg.format(what=what, path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
-            LOG.debug(msg)
-
-        self.named_basedir = path
-
-    # -------------------------------------------------------------------------
-    def _eval_named_slavedir(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        what = _("the directory for slave zones of named")
-        path = Path(val)
-
-        if path.is_absolute():
-            msg = _("The path to {what} must not be an absolute path, found {path!r}.")
-            msg = msg.format(what=what, path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
-            LOG.debug(msg)
-
-        self.named_slavedir = path
-
-    # -------------------------------------------------------------------------
-    def _eval_named_checkconf(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        what = "named-checkconf"
-        path = Path(val)
-        if not path.is_absolute():
-            msg = _("The path to {what} must be an absolute path, found {path!r}.")
-            msg = msg.format(what=what, path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
-            LOG.debug(msg)
-
-        self.named_checkconf = path
-
-    # -------------------------------------------------------------------------
-    def _eval_named_internal(self, iname, key, section):
-
-        val = section[key]
-        if val is None:
-            return
-
-        self.named_internal = to_bool(val)
-
-    # -------------------------------------------------------------------------
-    def _eval_named_listen_v6(self, iname, key, section):
-
-        val = section[key]
-        if val is None:
-            return
-
-        self.named_listen_on_v6 = to_bool(val)
-
-    # -------------------------------------------------------------------------
-    def _eval_app(self, section_name, section):
-
-        if self.verbose > 2:
-            msg = _("Evaluating config section {!r}:").format(section_name)
-            LOG.debug(msg + '\n' + pp(section))
-
-        re_pidfile = re.compile(r'^\s*pid[_-]?file$', re.IGNORECASE)
-        re_keep_backup = re.compile(r'^\s*keep[_-]?backup$', re.IGNORECASE)
-
-        for key in section.keys():
-
-            if re_pidfile.search(key):
-                self._eval_pidfile(section_name, key, section)
-                continue
-
-            if re_keep_backup.search(key):
-                self._eval_keep_backup(section_name, key, section)
-                continue
-
-    # -------------------------------------------------------------------------
-    def _eval_pidfile(self, iname, key, section):
-
-        val = section[key].strip()
-        if not val:
-            return
-
-        what = _("the PID file")
-        path = Path(val)
-        if not path.is_absolute():
-            msg = _("The path to {what} must be an absolute path, found {path!r}.")
-            msg = msg.format(what=what, path=val)
-            if self.raise_on_error:
-                raise DnsDeployZonesConfigError(msg)
-            else:
-                LOG.error(msg)
-                return
-
-        if self.verbose > 2:
-            msg = _("Found path to {what}: {path!r}.").format(what=what, path=val)
-            LOG.debug(msg)
-
-        self.pidfile = path
-
-    # -------------------------------------------------------------------------
-    def _eval_keep_backup(self, iname, key, section):
-
-        val = section[key]
-        if val is None:
-            return
-
-        self.keep_backup = to_bool(val)
-
-    # -------------------------------------------------------------------------
-    def eval(self):
-        """Evaluating read configuration and storing them in object properties."""
-
-        super(DnsDeployZonesConfig, self).eval()
-
-        addr_list = set()
-        if self.named_internal:
-            for host in self.default_zone_masters_local:
-                addr_list |= self.get_addresses(host)
-        else:
-            for host in self.default_zone_masters_public:
-                addr_list |= self.get_addresses(host)
-
-        self.masters |= addr_list
-
-        if not self.named_listen_on_v6:
-
-            addresses = set()
-            for addr in self.masters:
-                if ':' not in addr:
-                    addresses.add(addr)
-            self.masters = addresses
-
-        if self.masters:
-            if self.verbose > 2:
-                LOG.debug(_("Using configured masters:") + '\n' + pp(self.masters))
-        else:
-            LOG.warn(_("No valid masters found in configuration."))
-
-        if self.verbose > 2:
-            msg = _("Evaluated configuration:")
-            msg += " " + pp(self.as_dict())
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/ldap_app.py b/lib/pp_admintools/ldap_app.py
deleted file mode 100644 (file)
index d88de65..0000000
+++ /dev/null
@@ -1,368 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-@author: Frank Brehm
-@contact: frank.brehm@pixelpark.com
-@copyright: © 2022 by Frank Brehm, Berlin
-@summary: A base module for application classes with LDAP support
-"""
-from __future__ import absolute_import
-
-# Standard modules
-import logging
-import os
-import argparse
-
-try:
-    from pathlib import Path
-except ImportError:
-    from pathlib2 import Path
-
-# Third party modules
-from fb_tools.cfg_app import FbConfigApplication
-
-from fb_tools.errors import FbAppError
-
-# Own modules
-from . import __version__ as GLOBAL_VERSION
-
-from .xlate import XLATOR
-
-from . import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
-
-# from .argparse_actions import PortOptionAction
-
-# from .ldap_config import LdapConfigError
-from .ldap_config import LdapConnectionInfo, LdapConfiguration
-# rom .ldap_config import DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS
-from .ldap_config import DEFAULT_TIMEOUT, MAX_TIMEOUT
-
-__version__ = '0.1.3'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-ngettext = XLATOR.ngettext
-
-
-# =============================================================================
-class LdapAppError(FbAppError):
-    """ Base exception class for all exceptions in all LDAP using application classes."""
-    pass
-
-
-# =============================================================================
-class PasswordFileOptionAction(argparse.Action):
-
-    # -------------------------------------------------------------------------
-    def __init__(self, option_strings, must_exists=True, *args, **kwargs):
-
-        self.must_exists = bool(must_exists)
-
-        super(PasswordFileOptionAction, self).__init__(
-            option_strings=option_strings, *args, **kwargs)
-
-    # -------------------------------------------------------------------------
-    def __call__(self, parser, namespace, given_path, option_string=None):
-
-        path = Path(given_path)
-        if not path.is_absolute():
-            msg = _("The path {!r} must be an absolute path.").format(given_path)
-            raise argparse.ArgumentError(self, msg)
-
-        if self.must_exists:
-
-            if not path.exists():
-                msg = _("The file {!r} does not exists.").format(str(path))
-                raise argparse.ArgumentError(self, msg)
-
-            if not path.is_file():
-                msg = _("The given path {!r} exists, but is not a regular file.").format(str(path))
-                raise argparse.ArgumentError(self, msg)
-
-            if not os.access(str(path), os.R_OK):
-                msg = _("The given file {!r} is not readable.").format(str(path))
-                raise argparse.ArgumentError(self, msg)
-
-        setattr(namespace, self.dest, path)
-
-
-# =============================================================================
-class LdapPortOptionAction(argparse.Action):
-
-    # -------------------------------------------------------------------------
-    def __init__(self, option_strings, *args, **kwargs):
-
-        super(LdapPortOptionAction, self).__init__(
-            option_strings=option_strings, *args, **kwargs)
-
-    # -------------------------------------------------------------------------
-    def __call__(self, parser, namespace, given_port, option_string=None):
-
-        try:
-            port = int(given_port)
-            if port <= 0 or port > MAX_PORT_NUMBER:
-                msg = _(
-                    "a port number must be greater than zero and less "
-                    "or equal to {}.").format(MAX_PORT_NUMBER)
-                raise ValueError(msg)
-        except (ValueError, TypeError) as e:
-            msg = _("Wrong port number {!r}:").format(given_port)
-            msg += ' ' + str(e)
-            raise argparse.ArgumentError(self, msg)
-
-        setattr(namespace, self.dest, port)
-
-
-# =============================================================================
-class TimeoutOptionAction(argparse.Action):
-
-    # -------------------------------------------------------------------------
-    def __init__(self, option_strings, *args, **kwargs):
-
-        super(TimeoutOptionAction, self).__init__(
-            option_strings=option_strings, *args, **kwargs)
-
-    # -------------------------------------------------------------------------
-    def __call__(self, parser, namespace, given_timeout, option_string=None):
-
-        try:
-            timeout = int(given_timeout)
-            if timeout <= 0 or timeout > MAX_TIMEOUT:
-                msg = _(
-                    "a timeout must be greater than zero and less "
-                    "or equal to {}.").format(MAX_TIMEOUT)
-                raise ValueError(msg)
-        except (ValueError, TypeError) as e:
-            msg = _("Wrong timeout {!r}:").format(given_timeout)
-            msg += ' ' + str(e)
-            raise argparse.ArgumentError(self, msg)
-
-        setattr(namespace, self.dest, timeout)
-
-
-# =============================================================================
-class BaseLdapApplication(FbConfigApplication):
-    """
-    Base class for all application classes using LDAP.
-    """
-
-    use_default_ldap_connection = True
-    show_cmdline_ldap_timeout = True
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=GLOBAL_VERSION, base_dir=None,
-            cfg_class=LdapConfiguration, initialized=False, usage=None, description=None,
-            argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None,
-            config_dir=DEFAULT_CONFIG_DIR):
-
-        self._password_file = None
-
-        super(BaseLdapApplication, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            description=description, cfg_class=cfg_class, initialized=False,
-            argparse_epilog=argparse_epilog, argparse_prefix_chars=argparse_prefix_chars,
-            env_prefix=env_prefix, config_dir=config_dir
-        )
-
-    # -----------------------------------------------------------
-    @property
-    def password_file(self):
-        """The file containing the password of the Bind DN of the default LDAP connection."""
-        return self._password_file
-
-    @password_file.setter
-    def password_file(self, value):
-
-        path = Path(value)
-        if not path.is_absolute():
-            msg = _("The path {!r} must be an absolute path.").format(value)
-            raise LdapAppError(msg)
-
-        if not path.exists():
-            msg = _("The file {!r} does not exists.").format(str(path))
-            raise LdapAppError(msg)
-
-        if not path.is_file():
-            msg = _("The given path {!r} exists, but is not a regular file.").format(str(path))
-            raise LdapAppError(msg)
-
-        if not os.access(str(path), os.R_OK):
-            msg = _("The given file {!r} is not readable.").format(str(path))
-            raise LdapAppError(msg)
-
-        self._password_file = path
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(BaseLdapApplication, self).as_dict(short=short)
-
-        res['password_file'] = self.password_file
-        res['show_cmdline_ldap_timeout'] = self.show_cmdline_ldap_timeout
-        res['use_default_ldap_connection'] = self.use_default_ldap_connection
-
-        return res
-
-    # -------------------------------------------------------------------------
-    def init_arg_parser(self):
-        """
-        Public available method to initiate the argument parser.
-        """
-
-        super(BaseLdapApplication, self).init_arg_parser()
-
-        ldap_group = self.arg_parser.add_argument_group(_(
-            'Options for the default LDAP connection'))
-
-        if self.use_default_ldap_connection:
-
-            ldap_host = LdapConfiguration.default_ldap_server
-            ldap_ssl = LdapConfiguration.use_ssl_on_default
-            ldap_ssl_str = _('No')
-            if ldap_ssl:
-                ldap_ssl_str = _('Yes')
-            ldap_port = LdapConfiguration.default_ldap_port
-            ldap_base_dn = LdapConfiguration.default_base_dn
-            ldap_bind_dn = LdapConfiguration.default_bind_dn
-
-            ldap_group.add_argument(
-                '-H', '--ldap-host', metavar=_("HOST"), dest="ldap_host",
-                help=_(
-                    "Hostname or address of the LDAP server to use. Default: {!r}").format(
-                    ldap_host),
-            )
-
-            ldap_group.add_argument(
-                '--ssl', '--ldaps', '--ldap-ssl', dest="ldap_ssl", action="store_true",
-                help=_("Use ldaps to connect to the LDAP server. Default: {}").format(
-                    ldap_ssl_str),
-            )
-
-            ldap_group.add_argument(
-                '-p', '--ldap-port', metavar=_("PORT"), type=int, dest="ldap_port",
-                action=LdapPortOptionAction,
-                help=_("The port number to connect to the LDAP server. Default: {}").format(
-                    ldap_port),
-            )
-
-            ldap_group.add_argument(
-                '-b', '--base-dn', metavar="DN", dest="ldap_base_dn",
-                help=_(
-                    "The base DN used as the root for the LDAP searches. "
-                    "Default: {!r}").format(ldap_base_dn),
-            )
-
-            ldap_group.add_argument(
-                '-D', '--bind-dn', metavar="DN", dest="ldap_bind_dn",
-                help=_(
-                    "The Bind DN to use to connect to the LDAP server. Default: {!r}").format(
-                    ldap_bind_dn),
-            )
-
-            pw_group = ldap_group.add_mutually_exclusive_group()
-
-            pw_group.add_argument(
-                '-w', '--bind-pw', '--password', metavar=_("PASSWORD"), dest="ldap_bind_pw",
-                help=_("Use PASSWORD as the password for simple LDAP authentication."),
-            )
-
-            pw_group.add_argument(
-                '-W', '--password-prompt', action="store_true", dest="ldap_pw_prompt",
-                help=_(
-                    "Prompt for simple LDAP authentication. This is used instead of "
-                    "specifying the password on the command line."),
-            )
-
-            pw_group.add_argument(
-                '-y', '--password-file', metavar=_('PASSWORD_FILE'), dest="ldap_pw_file",
-                action=PasswordFileOptionAction,
-                help=_("Use contents of PASSWORD_FILE as the password for simple authentication."),
-            )
-
-        if self.show_cmdline_ldap_timeout:
-            self.arg_parser.add_argument(
-                '-T', '--timeout', metavar=_('SECONDS'), dest="ldap_timeout",
-                action=TimeoutOptionAction,
-                help=_(
-                    "Using the given timeout in seconds for all LDAP operations. "
-                    "Default: {}").format(DEFAULT_TIMEOUT),
-            )
-
-    # -------------------------------------------------------------------------
-    def post_init(self):
-        """
-        Method to execute before calling run(). Here could be done some
-        finishing actions after reading in commandline parameters,
-        configuration a.s.o.
-
-        This method could be overwritten by descendant classes, these
-        methhods should allways include a call to post_init() of the
-        parent class.
-
-        """
-
-        self.initialized = False
-
-        super(BaseLdapApplication, self).post_init()
-
-        if not self.use_default_ldap_connection:
-            return
-
-        if 'default' in self.cfg.ldap_connection:
-            default_connection = self.cfg.ldap_connection['default']
-        else:
-            default_connection = LdapConnectionInfo(
-                appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
-                host=LdapConfiguration.default_ldap_server,
-                use_ldaps=LdapConfiguration.use_ssl_on_default,
-                port=LdapConfiguration.default_ldap_port,
-                base_dn=LdapConfiguration.default_base_dn,
-                bind_dn=LdapConfiguration.default_bind_dn,
-                initialized=False)
-            self.cfg.ldap_connection['default'] = default_connection
-
-        v = getattr(self.args, 'ldap_host', None)
-        if v:
-            default_connection.host = v
-
-        if getattr(self.args, 'ldap_ssl', False):
-            default_connection.use_ldaps = True
-
-        v = getattr(self.args, 'ldap_port', None)
-        if v is not None:
-            default_connection.port = v
-
-        v = getattr(self.args, 'ldap_base_dn', None)
-        if v:
-            default_connection.base_dn = v
-
-        v = getattr(self.args, 'ldap_bind_dn', None)
-        if v:
-            default_connection.bind_dn = v
-
-        v = getattr(self.args, 'ldap_bind_pw', None)
-        if v:
-            default_connection.bind_pw = v
-
-        v = getattr(self.args, 'ldap_timeout', None)
-        if v:
-            self.cfg.ldap_timeout = v
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/ldap_config.py b/lib/pp_admintools/ldap_config.py
deleted file mode 100644 (file)
index 42bd061..0000000
+++ /dev/null
@@ -1,465 +0,0 @@
-#!/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 providing a configuration for applications,
-          which are performing LDAP actions, like search a.s.o.
-"""
-from __future__ import absolute_import
-
-# Standard module
-import logging
-import copy
-import re
-
-# Third party modules
-
-# Own modules
-# from fb_tools.common import pp
-from fb_tools.common import is_sequence, to_bool
-
-# from .config import ConfigError, BaseConfiguration
-from fb_tools.multi_config import MultiConfigError, BaseMultiConfig
-from fb_tools.multi_config import DEFAULT_ENCODING
-
-from fb_tools.obj import FbGenericBaseObject, FbBaseObject
-
-from . import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
-
-from .xlate import XLATOR
-
-__version__ = '0.2.5'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-
-DEFAULT_PORT_LDAP = 389
-DEFAULT_PORT_LDAPS = 636
-DEFAULT_TIMEOUT = 20
-MAX_TIMEOUT = 3600
-
-# =============================================================================
-class LdapConfigError(MultiConfigError):
-    """Base error class for all exceptions happened during
-    execution this configured application"""
-
-    pass
-
-
-# =============================================================================
-class LdapConnectionInfo(FbBaseObject):
-    """Encapsulating all necessary data to connect to a LDAP server."""
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=__version__, base_dir=None,
-            host=None, use_ldaps=False, port=DEFAULT_PORT_LDAP, base_dn=None,
-            bind_dn=None, bind_pw=None, initialized=False):
-
-        self._host = None
-        self._use_ldaps = False
-        self._port = DEFAULT_PORT_LDAP
-        self._base_dn = None
-        self._bind_dn = None
-        self._bind_pw = None
-
-        super(LdapConnectionInfo, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            initialized=False)
-
-        if host is not None:
-            self.host = host
-        self.use_ldaps = use_ldaps
-        self.port = port
-        if base_dn is not None:
-            self.base_dn = base_dn
-        if bind_dn is not None:
-            self.bind_dn = bind_dn
-        if bind_pw is not None:
-            self.bind_pw = bind_pw
-
-        if initialized:
-            self.initialized = True
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(LdapConnectionInfo, self).as_dict(short=short)
-
-        res['host'] = self.host
-        res['use_ldaps'] = self.use_ldaps
-        res['port'] = self.port
-        res['base_dn'] = self.base_dn
-        res['bind_dn'] = self.bind_dn
-        res['bind_pw'] = None
-        res['schema'] = self.schema
-        res['url'] = self.url
-
-        if self.bind_pw:
-            if self.verbose > 4:
-                res['bind_pw'] = self.bind_pw
-            else:
-                res['bind_pw'] = '******'
-
-        return res
-
-    # -----------------------------------------------------------
-    @property
-    def host(self):
-        """The host name (or IP address) of the LDAP server."""
-        return self._host
-
-    @host.setter
-    def host(self, value):
-        if value is None or str(value).strip() == '':
-            self._host = None
-            return
-        self._host = str(value).strip().lower()
-
-    # -----------------------------------------------------------
-    @property
-    def use_ldaps(self):
-        """Should there be used LDAPS for communicating with the LDAP server?"""
-        return self._use_ldaps
-
-    @use_ldaps.setter
-    def use_ldaps(self, value):
-        self._use_ldaps = to_bool(value)
-
-    # -----------------------------------------------------------
-    @property
-    def port(self):
-        "The TCP port number of the LDAP server."
-        return self._port
-
-    @port.setter
-    def port(self, value):
-        v = int(value)
-        if v < 1 or v > MAX_PORT_NUMBER:
-            raise LdapConfigError(_("Invalid port {!r} for LDAP server given.").format(value))
-        self._port = v
-
-    # -----------------------------------------------------------
-    @property
-    def base_dn(self):
-        """The DN used to connect to the LDAP server, anonymous bind is used, if
-            this DN is empty or None."""
-        return self._base_dn
-
-    @base_dn.setter
-    def base_dn(self, value):
-        if value is None or str(value).strip() == '':
-            msg = _("An empty Base DN for LDAP searches is not allowed.")
-            raise LdapConfigError(msg)
-        self._base_dn = str(value).strip()
-
-    # -----------------------------------------------------------
-    @property
-    def bind_dn(self):
-        """The DN used to connect to the LDAP server, anonymous bind is used, if
-            this DN is empty or None."""
-        return self._bind_dn
-
-    @bind_dn.setter
-    def bind_dn(self, value):
-        if value is None or str(value).strip() == '':
-            self._bind_dn = None
-            return
-        self._bind_dn = str(value).strip()
-
-    # -----------------------------------------------------------
-    @property
-    def bind_pw(self):
-        """The password of the DN used to connect to the LDAP server."""
-        return self._bind_pw
-
-    @bind_pw.setter
-    def bind_pw(self, value):
-        if value is None or str(value).strip() == '':
-            self._bind_pw = None
-            return
-        self._bind_pw = str(value).strip()
-
-    # -----------------------------------------------------------
-    @property
-    def schema(self):
-        """The schema as part of the URL to connect to the LDAP server."""
-        if self.use_ldaps:
-            return 'ldaps'
-        return 'ldap'
-
-    # -----------------------------------------------------------
-    @property
-    def url(self):
-        """The URL, which ca be used to connect to the LDAP server."""
-        if not self.host:
-            return None
-
-        port = ''
-        if self.use_ldaps:
-            if self.port != DEFAULT_PORT_LDAPS:
-                port = ':{}'.format(self.port)
-        else:
-            if self.port != DEFAULT_PORT_LDAP:
-                port = ':{}'.format(self.port)
-
-        return '{s}://{h}{p}'.format(s=self.schema, h=self.host, p=port)
-
-    # -------------------------------------------------------------------------
-    def __repr__(self):
-        """Typecasting into a string for reproduction."""
-
-        out = "<%s(" % (self.__class__.__name__)
-
-        fields = []
-        fields.append("appname={!r}".format(self.appname))
-        fields.append("host={!r}".format(self.host))
-        fields.append("use_ldaps={!r}".format(self.use_ldaps))
-        fields.append("port={!r}".format(self.port))
-        fields.append("base_dn={!r}".format(self.base_dn))
-        fields.append("bind_dn={!r}".format(self.bind_dn))
-        fields.append("bind_pw={!r}".format(self.bind_pw))
-        fields.append("initialized={!r}".format(self.initialized))
-
-        out += ", ".join(fields) + ")>"
-        return out
-
-    # -------------------------------------------------------------------------
-    def __copy__(self):
-
-        new = self.__class__(
-            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, host=self.host,
-            use_ldaps=self.use_ldaps, port=self.port, base_dn=self.base_dn, bind_dn=self.bind_dn,
-            bind_pw=self.bind_pw, initialized=self.initialized)
-
-        return new
-
-
-# =============================================================================
-class LdapConnectionDict(dict, FbGenericBaseObject):
-    """A dictionary containing LdapConnectionInfo as values and their names as keys."""
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(LdapConnectionDict, self).as_dict(short=short)
-
-        for key in self.keys():
-            res[key] = self[key].as_dict(short=short)
-
-        return res
-
-    # -------------------------------------------------------------------------
-    def __copy__(self):
-
-        new = self.__class__()
-
-        for key in self.keys():
-            new[key] = copy.copy(self[key])
-
-        return new
-
-
-# =============================================================================
-class LdapConfiguration(BaseMultiConfig):
-    """
-    A class for providing a configuration for an arbitrary Application working
-    with one or more LDAP connections, and methods to read it from configuration files.
-    """
-
-    default_ldap_server = 'prd-ds.pixelpark.com'
-    use_ssl_on_default = True
-    default_ldap_port = DEFAULT_PORT_LDAPS
-    default_base_dn = 'o=isp'
-    default_bind_dn = 'uid=readonly,ou=People,o=isp'
-
-    re_ldap_section_w_name = re.compile(r'^\s*ldap\s*:\s*(\S+)')
-
-    re_ldap_host_key = re.compile(r'^\s*(?:host|server)\s*$', re.IGNORECASE)
-    re_ldap_ldaps_key = re.compile(r'^\s*(?:use[_-]?)?(?:ldaps|ssl)\s*$', re.IGNORECASE)
-    re_ldap_port_key = re.compile(r'^\s*port\s*$', re.IGNORECASE)
-    re_ldap_base_dn_key = re.compile(r'^\s*base[_-]*dn\s*$', re.IGNORECASE)
-    re_ldap_bind_dn_key = re.compile(r'^\s*bind[_-]*dn\s*$', re.IGNORECASE)
-    re_ldap_bind_pw_key = re.compile(r'^\s*bind[_-]*pw\s*$', re.IGNORECASE)
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=__version__, base_dir=None,
-            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
-            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
-            ensure_privacy=False, use_chardet=True, initialized=False):
-
-        add_stems = []
-        if additional_stems:
-            if is_sequence(additional_stems):
-                for stem in additional_stems:
-                    add_stems.append(stem)
-            else:
-                add_stems.append(additional_stems)
-
-        if 'ldap' not in add_stems:
-            add_stems.append('ldap')
-
-        self.ldap_timeout = DEFAULT_TIMEOUT
-
-        super(LdapConfiguration, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
-            additional_stems=add_stems, additional_config_file=additional_config_file,
-            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
-            ensure_privacy=ensure_privacy, initialized=False,
-        )
-
-        self.ldap_connection = LdapConnectionDict()
-
-        default_connection = LdapConnectionInfo(
-            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
-            host=self.default_ldap_server, use_ldaps=self.use_ssl_on_default,
-            port=self.default_ldap_port, base_dn=self.default_base_dn,
-            bind_dn=self.default_bind_dn, initialized=False)
-
-        self.ldap_connection['default'] = default_connection
-
-    # -------------------------------------------------------------------------
-    def eval_section(self, section_name):
-
-        super(LdapConfiguration, self).eval_section(section_name)
-
-        sn = section_name.lower()
-        section = self.cfg[section_name]
-
-        if sn == 'ldap':
-            LOG.debug(_("Evaluating LDAP config ..."))
-
-            for key in section.keys():
-                if self.verbose > 1:
-                    LOG.debug(_("Evaluating LDAP section {!r} ...").format(key))
-                sub = section[key]
-                if key.lower().strip() == 'timeout':
-                    self._eval_ldap_timeout(sub)
-                    continue
-                self._eval_ldap_connection(key, sub)
-            return
-
-        match = self.re_ldap_section_w_name.match(sn)
-        if match:
-            connection_name = match.group(1)
-            self._eval_ldap_connection(connection_name, section)
-
-    # -------------------------------------------------------------------------
-    def _eval_ldap_timeout(self, value):
-
-        timeout = DEFAULT_TIMEOUT
-        msg_invalid = _("Value {!r} for a timeout is invalid.")
-
-        try:
-            timeout = int(value)
-        except (ValueError, TypeError) as e:
-            msg = msg_invalid.format(value)
-            msg += ': ' + str(e)
-            LOG.error(msg)
-            return
-        if timeout <= 0 or timeout > MAX_TIMEOUT:
-            msg = msg_invalid.format(value)
-            LOG.error(msg)
-            return
-
-        self.ldap_timeout = timeout
-
-    # -------------------------------------------------------------------------
-    def _eval_ldap_connection(self, connection_name, section):
-
-        if self.verbose > 2:
-            msg = _("Reading configuration of LDAP instance {!r} ...").format(connection_name)
-            LOG.debug(msg)
-
-        connection = LdapConnectionInfo(
-            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
-            initialized=False)
-
-        section_name = "ldap:" + connection_name
-        msg_invalid = _("Invalid value {val!r} in section {sec!r} for a LDAP {what}.")
-
-        for key in section.keys():
-
-            value = section[key]
-
-            if self.re_ldap_host_key.match(key):
-                if value.strip():
-                    connection.host = value
-                else:
-                    msg = msg_invalid.format(val=value, sec=section_name, what='host')
-                    LOG.error(msg)
-                continue
-
-            if self.re_ldap_ldaps_key.match(key):
-                connection.use_ldaps = value
-                continue
-
-            if self.re_ldap_port_key.match(key):
-                port = DEFAULT_PORT_LDAP
-                try:
-                    port = int(value)
-                except (ValueError, TypeError) as e:
-                    msg = msg_invalid.format(val=value, sec=section_name, what='port')
-                    msg += ' ' + str(e)
-                    LOG.error(msg)
-                    continue
-                if port <= 0 or port > MAX_PORT_NUMBER:
-                    msg = msg_invalid.format(val=value, sec=section_name, what='port')
-                    LOG.error(msg)
-                    continue
-                connection.port = port
-                continue
-
-            if self.re_ldap_base_dn_key.match(key):
-                if value.strip():
-                    connection.base_dn = value
-                else:
-                    msg = msg_invalid.format(val=value, sec=section_name, what='base_dn')
-                    LOG.error(msg)
-                continue
-
-            if self.re_ldap_bind_dn_key.match(key):
-                connection.bind_dn = value
-                continue
-
-            if self.re_ldap_bind_pw_key.match(key):
-                connection.bind_pw = value
-                continue
-
-            msg = _("Unknown LDAP configuration key {key} found in section {sec!r}.").format(
-                key=key, sec=section_name)
-            LOG.error(msg)
-
-        self.ldap_connection[connection_name] = connection
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/mail_app.py b/lib/pp_admintools/mail_app.py
deleted file mode 100644 (file)
index 0cee324..0000000
+++ /dev/null
@@ -1,343 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-@author: Frank Brehm
-@contact: frank.brehm@pixelpark.com
-@copyright: © 2022 by Frank Brehm, Berlin
-@summary: A base module for application classes with mail sending support
-"""
-from __future__ import absolute_import
-
-# Standard modules
-import logging
-import copy
-import pipes
-import os
-
-from email.mime.text import MIMEText
-from email import charset
-
-from subprocess import Popen, PIPE
-
-import smtplib
-
-# Third party modules
-from fb_tools.common import pp
-
-from fb_tools.cfg_app import FbConfigApplication
-
-from fb_tools.errors import FbAppError
-
-from fb_tools.xlate import format_list
-
-from fb_tools import MailAddress
-
-# Own modules
-from . import __version__ as GLOBAL_VERSION
-from . import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
-
-from .xlate import XLATOR
-
-from .argparse_actions import PortOptionAction
-
-from .mail_config import MailConfiguration
-from .mail_config import VALID_MAIL_METHODS
-
-__version__ = '0.2.7'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-ngettext = XLATOR.ngettext
-
-
-# =============================================================================
-class MailAppError(FbAppError):
-    """ Base exception class for all exceptions in all mail sending application classes."""
-    pass
-
-
-# =============================================================================
-class BaseMailApplication(FbConfigApplication):
-    """
-    Base class for all mail sending application classes.
-    """
-
-    charset.add_charset('utf-8', charset.SHORTEST, charset.QP)
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=GLOBAL_VERSION, base_dir=None,
-            cfg_class=MailConfiguration, initialized=False, usage=None, description=None,
-            argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None,
-            config_dir=DEFAULT_CONFIG_DIR):
-
-        super(BaseMailApplication, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            description=description, cfg_class=cfg_class, initialized=False,
-            argparse_epilog=argparse_epilog, argparse_prefix_chars=argparse_prefix_chars,
-            env_prefix=env_prefix, config_dir=config_dir
-        )
-
-    # -------------------------------------------------------------------------
-    def post_init(self):
-        """
-        Method to execute before calling run(). Here could be done some
-        finishing actions after reading in commandline parameters,
-        configuration a.s.o.
-
-        This method could be overwritten by descendant classes, these
-        methhods should allways include a call to post_init() of the
-        parent class.
-
-        """
-
-        self.initialized = False
-
-        super(BaseMailApplication, self).post_init()
-
-        v = getattr(self.args, 'mail_method', None)
-        if v:
-            self.cfg.mail_method = v
-
-        v = getattr(self.args, 'mail_server', None)
-        if v:
-            self.cfg.mail_server = v
-
-        v = getattr(self.args, 'smtp_port', None)
-        if v is not None:
-            if v <= 0 or v > MAX_PORT_NUMBER:
-                msg = _("Got invalid SMTP port number {!r}.").format(v)
-                LOG.error(msg)
-            else:
-                self.cfg.smtp_port = v
-
-        self._perform_cmdline_mail_from()
-        self._perform_cmdline_mail_rcpt()
-        self._perform_cmdline_mail_cc()
-        self._perform_cmdline_reply_to()
-
-    # -------------------------------------------------------------------------
-    def _perform_cmdline_mail_from(self):
-
-        v = getattr(self.args, 'mail_from', None)
-        if not v:
-            return
-
-        if not MailAddress.valid_address(v):
-            msg = _("Got invalid mail from address {!r}.").format(v)
-            LOG.error(msg)
-            self.exit(1)
-
-        self.cfg.mail_from = v
-
-    # -------------------------------------------------------------------------
-    def _perform_cmdline_mail_rcpt(self):
-
-        v = getattr(self.args, 'mail_recipients', None)
-        if v is None:
-            return
-
-        recipients = []
-        bad_rcpts = []
-
-        for addr in v:
-            if MailAddress.valid_address(addr):
-                recipients.append(addr)
-            else:
-                bad_rcpts.append(addr)
-
-        if bad_rcpts:
-            msg = _("Got invalid recipient mail addresses:")
-            msg += " " + format_list(bad_rcpts, do_repr=True)
-            LOG.error(msg)
-            self.exit(1)
-
-        self.cfg.mail_recipients = copy.copy(recipients)
-
-        if not self.cfg.mail_recipients:
-            msg = ("Did not found any valid recipient mail addresses.")
-            LOG.error(msg)
-
-    # -------------------------------------------------------------------------
-    def _perform_cmdline_mail_cc(self):
-
-        v = getattr(self.args, 'mail_cc', None)
-        if v is None:
-            return
-
-        cc = []
-        bad_cc = []
-
-        for addr in v:
-            if MailAddress.valid_address(addr):
-                cc.append(addr)
-            else:
-                bad_cc.append(addr)
-
-        if bad_cc:
-            msg = _("Got invalid cc mail addresses:")
-            msg += " " + format_list(bad_cc, do_repr=True)
-            LOG.error(msg)
-            self.exit(1)
-
-        self.cfg.mail_cc = copy.copy(cc)
-
-    # -------------------------------------------------------------------------
-    def _perform_cmdline_reply_to(self):
-
-        v = getattr(self.args, 'mail_reply_to', None)
-        if not v:
-            return
-
-        if not MailAddress.valid_address(v):
-            msg = _("Got invalid reply mail address {!r}.").format(v)
-            LOG.error(msg)
-            self.exit(1)
-
-        self.cfg.reply_to = v
-
-    # -------------------------------------------------------------------------
-    def init_arg_parser(self):
-        """
-        Public available method to initiate the argument parser.
-        """
-
-        super(BaseMailApplication, self).init_arg_parser()
-
-        mail_group = self.arg_parser.add_argument_group(_('Mailing options'))
-
-        mail_from = MailConfiguration.default_mail_from_complete
-        mail_method = MailConfiguration.default_mail_method
-        mail_server = MailConfiguration.default_mail_server
-        smtp_port = MailConfiguration.default_smtp_port
-
-        if self.cfg:
-            mail_from = self.cfg.mail_from
-            mail_method = self.cfg.mail_method
-            mail_server = self.cfg.mail_server
-            smtp_port = self.cfg.smtp_port
-
-        mail_group.add_argument(
-            '--from', '--mail-from',
-            metavar=_("ADDRESS"), dest="mail_from",
-            help=_(
-                "Sender mail address for mails generated by this script. "
-                "Default: {!r}").format(mail_from),
-        )
-
-        mail_group.add_argument(
-            '--recipients', '--mail-recipients',
-            metavar=_("ADDRESS"), nargs='+', dest="mail_recipients",
-            help=_("Mail addresses of all recipients for mails generated by this script.")
-        )
-
-        mail_group.add_argument(
-            '--cc', '--mail-cc',
-            metavar=_("ADDRESS"), nargs='*', dest="mail_cc",
-            help=_("Mail addresses of all CC recipients for mails generated by this script.")
-        )
-
-        mail_group.add_argument(
-            '--reply-to', '--mail-reply-to',
-            metavar=_("ADDRESS"), dest="mail_reply_to",
-            help=_("Reply mail address for mails generated by this script.")
-        )
-
-        method_list = format_list(VALID_MAIL_METHODS, do_repr=True)
-        mail_group.add_argument(
-            '--mail-method',
-            metavar=_("METHOD"), choices=VALID_MAIL_METHODS, dest="mail_method",
-            help=_(
-                "Method for sending the mails generated by this script. "
-                "Valid values: {v}, default: {d!r}.").format(
-                    v=method_list, d=mail_method)
-        )
-
-        mail_group.add_argument(
-            '--mail-server',
-            metavar=_("SERVER"), dest="mail_server",
-            help=_(
-                "Mail server for submitting generated by this script if "
-                "the mail method of this script is 'smtp'. Default: {!r}.").format(mail_server)
-        )
-
-        mail_group.add_argument(
-            '--smtp-port',
-            metavar=_("PORT"), type=int, dest='smtp_port', what="SMTP",
-            action=PortOptionAction,
-            help=_(
-                "The port to use for submitting generated by this script if "
-                "the mail method of this script is 'smtp'. Default: {}.").format(smtp_port)
-        )
-
-    # -------------------------------------------------------------------------
-    def perform_arg_parser(self):
-
-        if self.verbose > 2:
-            LOG.debug(_("Got command line arguments:") + '\n' + pp(self.args))
-
-    # -------------------------------------------------------------------------
-    def send_mail(self, subject, body):
-
-        mail = MIMEText(body, 'plain', 'utf-8')
-        mail['Subject'] = subject
-        mail['From'] = self.cfg.mail_from
-        mail['To'] = ', '.join(self.cfg.mail_recipients)
-        mail['Reply-To'] = self.cfg.reply_to
-        mail['X-Mailer'] = self.cfg.xmailer
-        if self.mail_cc:
-            mail['Cc'] = ', '.join(self.mail_cc)
-
-        if self.verbose > 1:
-            LOG.debug(_("Mail to send:") + '\n' + mail.as_string(unixfrom=True))
-
-        if self.mail_method == 'smtp':
-            self._send_mail_smtp(mail)
-        else:
-            self._send_mail_sendmail(mail)
-
-    # -------------------------------------------------------------------------
-    def _send_mail_smtp(self, mail):
-
-        with smtplib.SMTP(self.cfg.mail_server, self.cfg.smtp_port) as smtp:
-            if self.verbose > 2:
-                smtp.set_debuglevel(2)
-            elif self.verbose > 1:
-                smtp.set_debuglevel(1)
-
-            smtp.send_message(mail)
-
-    # -------------------------------------------------------------------------
-    def _send_mail_sendmail(self, mail):
-
-        # Searching for the location of sendmail ...
-        paths = (
-            '/usr/sbin/sendmail',
-            '/usr/lib/sendmail',
-        )
-        sendmail = None
-        for path in paths:
-            if os.path.isfile(path) and os.access(path, os.X_OK):
-                sendmail = path
-                break
-
-        if not sendmail:
-            msg = _("Did not found sendmail executable.")
-            LOG.error(msg)
-            return
-
-        cmd = [sendmail, "-t", "-oi"]
-        cmd_str = ' '.join(map(lambda x: pipes.quote(x), cmd))
-        LOG.debug(_("Executing: {}").format(cmd_str))
-
-        p = Popen(cmd, stdin=PIPE, universal_newlines=True)
-        p.communicate(mail.as_string())
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/mail_config.py b/lib/pp_admintools/mail_config.py
deleted file mode 100644 (file)
index f55d17b..0000000
+++ /dev/null
@@ -1,370 +0,0 @@
-#!/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 providing a configuration for applications,
-          which are sending mails
-"""
-from __future__ import absolute_import
-
-# Standard module
-import logging
-import pwd
-import re
-import copy
-import os
-import socket
-
-# Third party modules
-
-# Own modules
-
-from fb_tools.common import is_sequence, pp
-
-# from .config import ConfigError, BaseConfiguration
-from fb_tools.multi_config import MultiConfigError, BaseMultiConfig
-from fb_tools.multi_config import DEFAULT_ENCODING
-
-from fb_tools import MailAddress
-
-from . import __version__ as GLOBAL_VERSION
-from . import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR
-
-from .xlate import XLATOR
-
-__version__ = '0.1.10'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-
-VALID_MAIL_METHODS = ('smtp', 'sendmail')
-DEFAULT_DOMAIN = 'pixelpark.com'
-
-
-# =============================================================================
-class MailConfigError(MultiConfigError):
-    """Base error class for all exceptions happened during
-    execution this configured application"""
-
-    pass
-
-
-# =============================================================================
-class MailConfiguration(BaseMultiConfig):
-    """
-    A class for providing a configuration for an arbitrary PowerDNS Application
-    and methods to read it from configuration files.
-    """
-
-    default_mail_recipients = [
-        'frank.brehm@pixelpark.com'
-    ]
-    default_mail_cc = [
-        'thomas.dalichow@pixelpark.com',
-    ]
-
-    default_reply_to = 'solution@pixelpark.com'
-
-    default_mail_server = 'localhost'
-    default_smtp_port = 25
-
-    default_domain = socket.getfqdn()
-    if default_domain is None:
-        default_domain = DEFAULT_DOMAIN
-    else:
-        default_domain = default_domain.strip()
-        if not MailAddress.re_valid_domain.match(default_domain):
-            default_domain = DEFAULT_DOMAIN
-
-    current_user_name = pwd.getpwuid(os.getuid()).pw_name
-    current_user_gecos = pwd.getpwuid(os.getuid()).pw_gecos
-    default_mail_from = MailAddress(user=current_user_name, domain=default_domain)
-    default_mail_from_complete = '{n} <{m}>'.format(n=current_user_gecos, m=default_mail_from)
-
-    valid_mail_methods = VALID_MAIL_METHODS
-    default_mail_method = 'smtp'
-
-    whitespace_re = re.compile(r'(?:[,;]+|\s*[,;]*\s+)+')
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=__version__, base_dir=None,
-            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
-            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
-            ensure_privacy=False, use_chardet=True, initialized=False):
-
-        add_stems = []
-        if additional_stems:
-            if is_sequence(additional_stems):
-                for stem in additional_stems:
-                    add_stems.append(stem)
-            else:
-                add_stems.append(additional_stems)
-
-        if 'mail' not in add_stems:
-            add_stems.append('mail')
-
-        self.mail_recipients = copy.copy(self.default_mail_recipients)
-        self.mail_from = self.default_mail_from_complete
-        self.mail_cc = copy.copy(self.default_mail_cc)
-        self.reply_to = self.default_reply_to
-        self.mail_method = self.default_mail_method
-        self.mail_server = self.default_mail_server
-        self.smtp_port = self.default_smtp_port
-        self._mail_cc_configured = False
-
-        super(MailConfiguration, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
-            additional_stems=add_stems, additional_config_file=additional_config_file,
-            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
-            ensure_privacy=ensure_privacy, initialized=False,
-        )
-
-        self.xmailer = "{a} (Admin Tools version {v})".format(
-            a=self.appname, v=GLOBAL_VERSION)
-
-        if initialized:
-            self.initialized = True
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(MailConfiguration, self).as_dict(short=short)
-
-        res['default_mail_recipients'] = self.default_mail_recipients
-        res['default_mail_cc'] = self.default_mail_cc
-        res['default_reply_to'] = self.default_reply_to
-        res['default_mail_server'] = self.default_mail_server
-        res['default_smtp_port'] = self.default_smtp_port
-        res['current_user_name'] = self.current_user_name
-        res['current_user_gecos'] = self.current_user_gecos
-        res['default_mail_from'] = self.default_mail_from
-        res['default_mail_from_complete'] = self.default_mail_from_complete
-        res['default_mail_method'] = self.default_mail_method
-
-        return res
-
-    # -------------------------------------------------------------------------
-    def eval(self):
-
-        self.mail_recipients = []
-        self.mail_cc = []
-
-        super(MailConfiguration, self).eval()
-
-        if not self.mail_recipients:
-            self.mail_recipients = copy.copy(self.default_mail_recipients)
-
-        if not self.mail_cc and not self._mail_cc_configured:
-            self.mail_cc = copy.copy(self.default_mail_cc)
-
-    # -------------------------------------------------------------------------
-    def eval_section(self, section_name):
-
-        super(MailConfiguration, self).eval_section(section_name)
-        sn = section_name.lower()
-
-        if sn == 'mail':
-            section = self.cfg[section_name]
-            return self._eval_mail(section_name, section)
-
-    # -------------------------------------------------------------------------
-    def _eval_mail(self, section_name, section):
-
-        if self.verbose > 2:
-            msg = _("Evaluating config section {!r}:").format(section_name)
-            LOG.debug(msg + '\n' + pp(section))
-
-        self._eval_mail_from(section_name, section)
-        self._eval_mail_rcpt(section_name, section)
-        self._eval_mail_cc(section_name, section)
-        self._eval_mail_reply_to(section_name, section)
-        self._eval_mail_method(section_name, section)
-        self._eval_mail_server(section_name, section)
-        self._eval_smtp_port(section_name, section)
-
-    # -------------------------------------------------------------------------
-    def _split_mailaddress_tokens(self, value, what=None):
-
-        result = []
-
-        tokens = self.whitespace_re.split(value)
-        for token in tokens:
-            if MailAddress.valid_address(token):
-                result.append(token)
-            else:
-                msg = _("Found invalid {what} {addr!r} in configuration.")
-                LOG.error(msg.format(what=what, addr=token))
-
-        return result
-
-    # -------------------------------------------------------------------------
-    def _eval_mail_from(self, section_name, section):
-
-        re_from = re.compile(r'^\s*(mail[_-]?)?from\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if not re_from.search(key):
-                continue
-
-            val = section[key]
-
-            if is_sequence(val):
-                if not len(val):
-                    continue
-                val = val[0]
-
-            if MailAddress.valid_address(val):
-                self.mail_from = val
-            else:
-                msg = _("Found invalid {what} {addr!r} in configuration.")
-                LOG.error(msg.format(what=_("from address"), addr=val))
-
-    # -------------------------------------------------------------------------
-    def _eval_mail_rcpt(self, section_name, section):
-
-        re_rcpt = re.compile(r'^\s*(mail[_-]?)?(recipients?|rcpt)\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if not re_rcpt.search(key):
-                continue
-
-            val = section[key]
-            if not val:
-                continue
-            if is_sequence(val):
-                for v in val:
-                    result = self._split_mailaddress_tokens(v, _("recipient mail address"))
-                    if result:
-                        self.mail_recipients.expand(result)
-            else:
-                result = self._split_mailaddress_tokens(val, _("recipient mail address"))
-                if result:
-                    self.mail_recipients.expand(result)
-
-    # -------------------------------------------------------------------------
-    def _eval_mail_cc(self, section_name, section):
-
-        re_cc = re.compile(r'^\s*(mail[_-]?)?cc\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-
-            self._mail_cc_configured = True
-            if not re_cc.search(key):
-                continue
-
-            val = section[key]
-            if not val:
-                continue
-            if is_sequence(val):
-                for v in val:
-                    result = self._split_mailaddress_tokens(v, _("cc mail address"))
-                    if result:
-                        self.mail_cc.expand(result)
-            else:
-                result = self._split_mailaddress_tokens(val, _("cc mail address"))
-                if result:
-                    self.mail_cc.expand(result)
-
-    # -------------------------------------------------------------------------
-    def _eval_mail_reply_to(self, section_name, section):
-
-        re_reply = re.compile(r'^\s*(mail[_-]?)?reply([-_]?to)?\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if not re_reply.search(key):
-                continue
-
-            val = section[key]
-
-            if is_sequence(val):
-                if not len(val):
-                    continue
-                val = val[0]
-
-            if MailAddress.valid_address(val):
-                self.reply_to = val
-            else:
-                msg = _("Found invalid {what} {addr!r} in configuration.")
-                LOG.error(msg.format(what=_("reply to address"), addr=val))
-
-    # -------------------------------------------------------------------------
-    def _eval_mail_method(self, section_name, section):
-
-        re_method = re.compile(r'^\s*(mail[_-]?)?method\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if not re_method.search(key):
-                continue
-
-            val = section[key].strip().lower()
-            if not val:
-                continue
-
-            if val not in self.valid_mail_methods:
-                msg = _("Found invalid mail method {!r} in configuration.")
-                LOG.error(msg.format(section[key]))
-                continue
-
-            self.mail_method = val
-
-    # -------------------------------------------------------------------------
-    def _eval_mail_server(self, section_name, section):
-
-        re_server = re.compile(r'^\s*(mail[_-]?)?server\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if not re_server.search(key):
-                continue
-
-            val = section[key].strip().lower()
-            if not val:
-                continue
-
-            self.mail_server = val
-
-    # -------------------------------------------------------------------------
-    def _eval_smtp_port(self, section_name, section):
-
-        re_server = re.compile(r'^\s*(smtp[_-]?)?port\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if not re_server.search(key):
-                continue
-
-            val = section[key]
-            try:
-                port = int(val)
-            except (ValueError, TypeError) as e:
-                msg = _("Value {!r} for SMTP port is invalid:").format(val)
-                msg += ' ' + str(e)
-                LOG.error(msg)
-                continue
-            if port <= 0 or port > MAX_PORT_NUMBER:
-                msg = _("Found invalid SMTP port number {} in configuration.").format(port)
-                LOG.error(msg)
-                continue
-
-            self.smtp_port = port
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/pdns_app.py b/lib/pp_admintools/pdns_app.py
deleted file mode 100644 (file)
index 7fc33ca..0000000
+++ /dev/null
@@ -1,602 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-@author: Frank Brehm
-@contact: frank.brehm@pixelpark.com
-@copyright: © 2022 by Frank Brehm, Berlin
-@summary: The module for a application object related to PowerDNS.
-"""
-from __future__ import absolute_import
-
-# Standard modules
-import logging
-import logging.config
-import re
-# import copy
-import os
-import ipaddress
-import socket
-
-# Third party modules
-import psutil
-
-# Own modules
-from fb_tools.common import pp
-
-from fb_pdnstools.zone import PowerDNSZone
-from fb_pdnstools.server import PowerDNSServer
-from fb_pdnstools.errors import PDNSApiNotFoundError
-from fb_pdnstools.errors import PDNSApiValidationError
-from fb_tools.xlate import format_list
-
-from . import __version__ as GLOBAL_VERSION
-
-from .argparse_actions import PortOptionAction, TimeoutOptionAction
-
-from .mail_app import MailAppError, BaseMailApplication
-
-from .pdns_config import PdnsConfiguration
-# from .pdns_config import PdnsConfigError, PdnsConfiguration
-
-from .xlate import XLATOR
-
-__version__ = '0.9.1'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-
-
-# =============================================================================
-class PpPDNSAppError(MailAppError):
-    """Base error class for all exceptions happened during
-    execution this configured application"""
-    pass
-
-
-# =============================================================================
-class PpPDNSApplication(BaseMailApplication):
-    """
-    Class for configured application objects related to PowerDNS.
-    """
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=GLOBAL_VERSION, base_dir=None,
-            cfg_class=PdnsConfiguration, initialized=False, usage=None, description=None,
-            argparse_epilog=None, argparse_prefix_chars='-', env_prefix=None,
-            instance=None):
-
-        if instance:
-            self._instance = instance
-        else:
-            self._instance = PdnsConfiguration.default_pdns_instance
-
-        self._api_key = None
-        self._api_host = None
-        self._api_port = None
-        self._api_servername = None
-        self._api_server_version = 'unknown'
-
-        self.local_addresses = []
-
-        self.pdns = None
-
-        super(PpPDNSApplication, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            description=description, cfg_class=cfg_class, initialized=False,
-            argparse_epilog=argparse_epilog, argparse_prefix_chars=argparse_prefix_chars,
-            env_prefix=env_prefix,
-        )
-
-        for interface, snics in psutil.net_if_addrs().items():
-            for snic in snics:
-                if snic.family == socket.AF_INET or snic.family == socket.AF_INET6:
-                    addr = str(ipaddress.ip_address(re.sub(r'%.*', '', snic.address)))
-                    if addr not in self.local_addresses:
-                        self.local_addresses.append(addr)
-
-        if not self.cfg:
-            msg = _("Configuration not available.")
-            raise PpPDNSAppError(msg)
-
-        self.eval_instance(instance)
-
-    # -----------------------------------------------------------
-    @property
-    def api_key(self):
-        "The API key to use the PowerDNS API"
-        return self._api_key
-
-    @api_key.setter
-    def api_key(self, value):
-        if value is None or str(value).strip() == '':
-            raise PpPDNSAppError(_("Invalid API key {!r} given.").format(value))
-        self._api_key = str(value).strip()
-
-    # -----------------------------------------------------------
-    @property
-    def api_host(self):
-        "The host name or address providing the PowerDNS API."
-        return self._api_host
-
-    @api_host.setter
-    def api_host(self, value):
-        if value is None or str(value).strip() == '':
-            raise PpPDNSAppError(_("Invalid API host {!r} given.").format(value))
-        self._api_host = str(value).strip().lower()
-
-    # -----------------------------------------------------------
-    @property
-    def api_port(self):
-        "The TCP port number of the PowerDNS API."
-        return self._api_port
-
-    @api_port.setter
-    def api_port(self, value):
-        v = int(value)
-        if v < 1:
-            raise PpPDNSAppError(_("Invalid API port {!r} given.").format(value))
-        self._api_port = v
-
-    # -----------------------------------------------------------
-    @property
-    def api_servername(self):
-        "The (virtual) name of the PowerDNS server used in API calls."
-        return self._api_servername
-
-    @api_servername.setter
-    def api_servername(self, value):
-        if value is None or str(value).strip() == '':
-            raise PpPDNSAppError(_("Invalid API server name {!r} given.").format(value))
-        self._api_servername = str(value).strip()
-
-    # -----------------------------------------------------------
-    @property
-    def api_server_version(self):
-        "The version of the PowerDNS server, how provided by API."
-        return self._api_server_version
-
-    # -----------------------------------------------------------
-    @property
-    def instance(self):
-        "The name of the PowerDNS instance."
-        return self._instance
-
-    @instance.setter
-    def instance(self, value):
-        if value is None:
-            raise PpPDNSAppError(_("Invalid instance {!r} given.").format(None))
-        v = str(value).strip().lower()
-        if v not in self.api_keys.keys():
-            raise PpPDNSAppError(_("Invalid instance {!r} given.").format(value))
-
-        self.eval_instance(v)
-
-    # -------------------------------------------------------------------------
-    def eval_instance(self, inst_name):
-
-        if self.verbose > 2:
-            msg = _("Evaluating instance {!r} ...").format(inst_name)
-            LOG.debug(msg)
-
-        if not self.cfg:
-            msg = _("Configuration not available.")
-            raise PpPDNSAppError(msg)
-
-        if inst_name not in self.cfg.pdns_api_instances:
-            msg = _("PDNS instance {!r} is not configured.").format(inst_name)
-            raise PpPDNSAppError(msg)
-
-        self._instance = inst_name
-        if self.cfg.pdns_host:
-            self.api_host = self.cfg.pdns_host
-        if self.cfg.pdns_key:
-            self.api_key = self.cfg.pdns_key
-        if self.cfg.pdns_port:
-            self.api_port = self.cfg.pdns_port
-        if self.cfg.pdns_servername:
-            self.api_servername = self.cfg.pdns_servername
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(PpPDNSApplication, self).as_dict(short=short)
-        res['api_host'] = self.api_host
-        res['api_port'] = self.api_port
-        res['api_servername'] = self.api_servername
-        res['instance'] = self.instance
-        res['api_server_version'] = self.api_server_version
-
-        if self.api_key:
-            if self.verbose > 4:
-                res['api_key'] = self.api_key
-            else:
-                res['api_key'] = '******'
-        else:
-            res['api_key'] = None
-
-        return res
-
-    # -------------------------------------------------------------------------
-    def init_arg_parser(self):
-        """
-        Method to initiate the argument parser.
-
-        This method should be explicitely called by all init_arg_parser()
-        methods in descendant classes.
-        """
-
-        super(PpPDNSApplication, self).init_arg_parser()
-
-        pdns_group = self.arg_parser.add_argument_group(_('PowerDNS API options'))
-        inst_group = pdns_group.add_mutually_exclusive_group()
-
-        insts = PdnsConfiguration.valid_pdns_api_instances
-        inst_list = format_list(insts, do_repr=True)
-        default_timeout = PdnsConfiguration.default_pdns_timeout
-
-        inst_group.add_argument(
-            '-I', '--inst', '--instance',
-            metavar=_("INSTANCE"), choices=insts, dest="inst",
-            help=_(
-                "Select, which PowerDNS instance to use. Valid values: {v}, "
-                "default: {d!r}.").format(v=inst_list, d=self.instance),
-        )
-
-        inst_group.add_argument(
-            '-G', '--global',
-            action='store_true', dest="inst_global",
-            help=_("Using the {!r} PowerDNS instance.").format('global'),
-        )
-
-        inst_group.add_argument(
-            '-L', '--local',
-            action='store_true', dest="inst_local",
-            help=_("Using the {!r} PowerDNS instance.").format('local'),
-        )
-
-        inst_group.add_argument(
-            '-P', '--public',
-            action='store_true', dest="inst_public",
-            help=_("Using the {!r} PowerDNS instance.").format('public'),
-        )
-
-        pdns_group.add_argument(
-            '-p', '--port',
-            metavar=_("PORT"), type=int, dest='api_port',
-            default=PdnsConfiguration.default_pdns_api_port,
-            what="PowerDNS API", action=PortOptionAction,
-            help=_("Which port to connect to PowerDNS API, default: {}.").format(
-                PdnsConfiguration.default_pdns_api_port),
-        )
-
-        pdns_group.add_argument(
-            '-t', '--timeout',
-            metavar=_("SECS"), type=int, dest='timeout', default=default_timeout,
-            what=_("PowerDNS API access"), action=TimeoutOptionAction,
-            help=_("The timeout in seconds to request the PowerDNS API, default: {}.").format(
-                default_timeout),
-        )
-
-    # -------------------------------------------------------------------------
-    def perform_arg_parser(self):
-        """
-        Public available method to execute some actions after parsing
-        the command line parameters.
-        """
-
-    # -------------------------------------------------------------------------
-    def _check_path_config(self, section, section_name, key, class_prop, absolute=True, desc=None):
-
-        if key not in section:
-            return
-
-        d = ''
-        if desc:
-            d = ' ' + str(desc).strip()
-
-        path = section[key].strip()
-        if not path:
-            msg = _("No path given for{d} [{s}]/{k} in configuration.").format(
-                d=d, s=section_name, k=key)
-            LOG.error(msg)
-            self.config_has_errors = True
-            return
-
-        if absolute and not os.path.isabs(path):
-            msg = _(
-                "Path {p!r} for{d} [{s}]/{k} in configuration must be an absolute "
-                "path.").format(p=path, d=d, s=section_name, k=key)
-            LOG.error(msg)
-            self.config_has_errors = True
-            return
-
-        setattr(self, class_prop, path)
-
-    # -------------------------------------------------------------------------
-    def post_init(self):
-        """
-        Method to execute before calling run(). Here could be done some
-        finishing actions after reading in commandline parameters,
-        configuration a.s.o.
-
-        This method could be overwritten by descendant classes, these
-        methods should allways include a call to post_init() of the
-        parent class.
-
-        """
-
-        if self.verbose > 1:
-            LOG.debug(_("Executing {} ...").format('post_init()'))
-
-        super(PpPDNSApplication, self).post_init()
-
-        if self.args.inst:
-            self.instance = self.args.inst
-        elif self.args.inst_global:
-            self.instance = 'global'
-        elif self.args.inst_local:
-            self.instance = 'local'
-        elif self.args.inst_public:
-            self.instance = 'public'
-
-        if self.args.api_port:
-            self.api_port = self.args.api_port
-
-        if self.args.timeout:
-            self.cfg.pdns_timeout = self.args.timeout
-
-        self.pdns = PowerDNSServer(
-            appname=self.appname, verbose=self.verbose, base_dir=self.base_dir,
-            master_server=self.cfg.pdns_host, port=self.cfg.pdns_port,
-            key=self.cfg.pdns_key, use_https=False,
-            simulate=self.simulate, force=self.force, initialized=False,
-        )
-        self.pdns.initialized = True
-
-    # -------------------------------------------------------------------------
-    def pre_run(self):
-        """
-        Dummy function to run before the main routine.
-        Could be overwritten by descendant classes.
-
-        """
-
-        if self.verbose > 1:
-            LOG.debug(_("Executing {} ...").format('pre_run()'))
-
-        LOG.debug(_("Setting Loglevel of the requests module to {}.").format('WARNING'))
-        logging.getLogger("requests").setLevel(logging.WARNING)
-
-        super(PpPDNSApplication, self).pre_run()
-        self.get_api_server_version()
-
-    # -------------------------------------------------------------------------
-    def _run(self):
-        """
-        Dummy function as main routine.
-
-        MUST be overwritten by descendant classes.
-
-        """
-        LOG.debug(_("Executing nothing ..."))
-
-    # -------------------------------------------------------------------------
-    def post_run(self):
-        """
-        Dummy function to run after the main routine.
-        Could be overwritten by descendant classes.
-
-        """
-
-        if self.verbose > 1:
-            LOG.debug(_("Executing {} ...").format('post_run()'))
-
-        if self.pdns:
-            self.pdns = None
-
-    # -------------------------------------------------------------------------
-    def get_api_server_version(self):
-
-        if not self.pdns:
-            raise PpPDNSAppError(_("The PDNS server object does not exists."))
-        if not self.pdns.initialized:
-            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
-
-        return self.pdns.get_api_server_version()
-
-    # -------------------------------------------------------------------------
-    def _build_url(self, path):
-
-        url = 'http://{}'.format(self.api_host)
-        if self.api_port != 80:
-            url += ':{}'.format(self.api_port)
-
-        url += '/api/v1' + path
-        LOG.debug("Used URL: {!r}".format(url))
-        return url
-
-    # -------------------------------------------------------------------------
-    def perform_request(self, path, method='GET', data=None, headers=None, may_simulate=False):
-        """Performing the underlying API request."""
-
-        if not self.pdns:
-            raise PpPDNSAppError(_("The PDNS server object does not exists."))
-        if not self.pdns.initialized:
-            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
-
-        return self.pdns.perform_request(
-            path, method=method, data=data, headers=headers, may_simulate=may_simulate)
-
-    # -------------------------------------------------------------------------
-    def get_api_zones(self):
-
-        if not self.pdns:
-            raise PpPDNSAppError(_("The PDNS server object does not exists."))
-        if not self.pdns.initialized:
-            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
-
-        return self.pdns.get_api_zones()
-
-    # -------------------------------------------------------------------------
-    def get_api_zone(self, zone_name):
-
-        if not self.pdns:
-            raise PpPDNSAppError(_("The PDNS server object does not exists."))
-        if not self.pdns.initialized:
-            raise PpPDNSAppError(_("The PDNS server object is not initialized."))
-
-        zone_unicode = zone_name
-        json_response = None
-        zout = "{!r}".format(zone_name)
-        if 'xn--' in zone_name:
-            zone_unicode = zone_name.encode('idna').decode('idna')
-            zout = "{!r} ({})".format(zone_name, zone_unicode)
-        LOG.debug(_("Trying to get complete information about zone {!r} ...").format(zone_name))
-
-        path = "/servers/{}/zones/{}".format(self.pdns.api_servername, zone_name)
-        try:
-            json_response = self.perform_request(path)
-        except (PDNSApiNotFoundError, PDNSApiValidationError):
-            LOG.error(_("The given zone {} was not found.").format(zout))
-            return None
-        if self.verbose > 2:
-            LOG.debug(_("Got a response:") + '\n' + pp(json_response))
-
-        zone = PowerDNSZone.init_from_dict(
-            json_response, appname=self.appname, verbose=self.verbose, base_dir=self.base_dir)
-        if self.verbose > 2:
-            LOG.debug(_("Zone object:") + '\n' + pp(zone.as_dict()))
-
-        return zone
-
-#    # -------------------------------------------------------------------------
-#    def patch_zone(self, zone, payload):
-#
-#        return zone.patch(payload)
-#
-#    # -------------------------------------------------------------------------
-#    def update_soa(self, zone, new_soa, comment=None, ttl=None):
-#
-#        return zone.update_soa(new_soa=new_soa, comment=comment, ttl=ttl)
-#
-#     # -------------------------------------------------------------------------
-#     def set_nameservers(
-#         self, zone, new_nameservers, for_zone=None, comment=None, new_ttl=None,
-#             do_serial=True, do_notify=True):
-#
-#         current_nameservers = zone.get_zone_nameservers(for_zone=for_zone)
-#         if for_zone:
-#             LOG.debug("Current nameservers of {f!r} in zone {z!r}:\n{ns}".format(
-#                 f=for_zone, z=zone.name, ns=pp(current_nameservers)))
-#         else:
-#             LOG.debug("Current nameservers of zone {z!r}:\n{ns}".format(
-#                 z=zone.name, ns=pp(current_nameservers)))
-#
-#         ns2remove = []
-#         ns2add = []
-#
-#         for ns in current_nameservers:
-#             if ns not in new_nameservers:
-#                 ns2remove.append(ns)
-#         for ns in new_nameservers:
-#             if ns not in current_nameservers:
-#                 ns2add.append(ns)
-#
-#         if not ns2remove and not ns2add:
-#             if for_zone:
-#                 msg = "Subzone {f!r} has already the expected nameservers in zone {z!r}."
-#             else:
-#                 msg = "Zone {z!r} has already the expected nameservers."
-#             LOG.info(msg.format(f=for_zone, z=zone.name))
-#             return False
-#
-#         LOG.debug("Nameservers to remove from zone {z!r}:\n{ns}".format(
-#             z=zone.name, ns=pp(ns2remove)))
-#         LOG.debug("Nameservers to add to zone {z!r}:\n{ns}".format(
-#             z=zone.name, ns=pp(ns2add)))
-#
-#         ns_ttl = None
-#         if not new_ttl:
-#             cur_rrset = zone.get_ns_rrset(for_zone=for_zone)
-#             if cur_rrset:
-#                 ns_ttl = cur_rrset.ttl
-#             else:
-#                 soa = zone.get_soa()
-#                 ns_ttl = soa.ttl
-#                 del soa
-#         else:
-#             ns_ttl = int(new_ttl)
-#         if ns_ttl <= 0:
-#             ns_ttl = 3600
-#         LOG.debug("TTL for NS records: {}.".format(ns_ttl))
-#
-#         rrset_name = zone.name.lower()
-#         if for_zone:
-#             rrset_name = for_zone.lower()
-#
-#         records = []
-#         for ns in new_nameservers:
-#             record = {
-#                 "name": rrset_name,
-#                 "type": "NS",
-#                 "content": ns,
-#                 "disabled": False,
-#                 "set-ptr": False,
-#             }
-#             records.append(record)
-#         rrset = {
-#             "name": rrset_name,
-#             "type": "NS",
-#             "ttl": ns_ttl,
-#             "changetype": "REPLACE",
-#             "records": records,
-#         }
-#
-#         if comment:
-#             comment_rec = {
-#                 'content': comment,
-#                 'account': getpass.getuser(),
-#                 'modified_at': int(time.time() + 0.5),
-#             }
-#             rrset['comments'] = [comment_rec]
-#
-#         payload = {"rrsets": [rrset]}
-#
-#         self.patch_zone(zone, payload)
-#
-#         if do_serial:
-#             zone.increase_serial()
-#
-#         if do_notify:
-#             self.notify_zone(zone)
-#
-#         return True
-#
-#    # -------------------------------------------------------------------------
-#    def notify_zone(self, zone):
-#
-#        LOG.info("Notifying slaves of zone {!r} ...".format(zone.name))
-#
-#        path = "/servers/{}/zones/{}/notify".format(self.api_servername, zone.name)
-#        return self.perform_request(path, 'PUT', '', may_simulate=True)
-
-# =============================================================================
-
-
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/lib/pp_admintools/pdns_config.py b/lib/pp_admintools/pdns_config.py
deleted file mode 100644 (file)
index d3e1a06..0000000
+++ /dev/null
@@ -1,474 +0,0 @@
-#!/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 providing a configuration for applications,
-          which are Working with PowerDNS.
-          It's based on class MailConfigError.
-"""
-from __future__ import absolute_import
-
-# Standard module
-import logging
-import re
-import copy
-
-# Third party modules
-
-# Own modules
-
-from fb_tools.common import is_sequence, pp
-
-# from .config import ConfigError, BaseConfiguration
-from fb_tools.multi_config import DEFAULT_ENCODING
-
-from . import __version__ as GLOBAL_VERSION
-from . import MAX_TIMEOUT, MAX_PORT_NUMBER
-
-from .mail_config import MailConfigError, MailConfiguration
-from .mail_config import DEFAULT_CONFIG_DIR
-
-from .xlate import XLATOR
-
-LIBRARY_NAME = "pp-pdns-api-client"
-
-__version__ = '0.2.2'
-LOG = logging.getLogger(__name__)
-
-_ = XLATOR.gettext
-
-
-# =============================================================================
-class PdnsConfigError(MailConfigError):
-    """Base error class for all exceptions happened during
-    execution this configured application"""
-
-    pass
-
-
-# =============================================================================
-class PdnsConfiguration(MailConfiguration):
-    """
-    A class for providing a configuration for an arbitrary PowerDNS Application
-    and methods to read it from configuration files.
-    """
-
-    valid_pdns_api_instances = ('global', 'public', 'local')
-
-    default_pdns_api_instances = {
-        'global': {
-            'host': "dnsmaster.pp-dns.com",
-        },
-        'public': {
-            'host': "dnsmaster-public.pixelpark.com",
-        },
-        'local': {
-            'host': "dnsmaster-local.pixelpark.com",
-        },
-    }
-
-    default_pdns_api_port = 8081
-    default_pdns_api_servername = "localhost"
-    default_pdns_timeout = 20
-
-    default_pdns_instance = 'global'
-
-    # -------------------------------------------------------------------------
-    def __init__(
-        self, appname=None, verbose=0, version=__version__, base_dir=None,
-            append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR,
-            additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING,
-            ensure_privacy=True, use_chardet=True, initialized=False):
-
-        self.api_user_agent = '{}/{}'.format(LIBRARY_NAME, GLOBAL_VERSION)
-
-        self.pdns_api_instances = {}
-        for inst_name in self.default_pdns_api_instances.keys():
-
-            def_inst = self.default_pdns_api_instances[inst_name]
-
-            inst = {}
-            inst['host'] = def_inst['host']
-            inst['port'] = self.default_pdns_api_port
-            inst['key'] = None
-            inst['servername'] = self.default_pdns_api_servername
-
-            self.pdns_api_instances[inst_name] = inst
-
-        self.pdns_timeout = self.default_pdns_timeout
-
-        self.pdns_instance = self.default_pdns_instance
-        self.pdns_host = None
-        self.pdns_port = None
-        self.pdns_key = None
-        self.pdns_servername = None
-
-        add_stems = []
-        if additional_stems:
-            if is_sequence(additional_stems):
-                for stem in additional_stems:
-                    add_stems.append(stem)
-            else:
-                add_stems.append(additional_stems)
-
-        if 'pdns' not in add_stems:
-            add_stems.append('pdns')
-
-        if 'powerdns' not in add_stems:
-            add_stems.append('powerdns')
-
-        super(PdnsConfiguration, self).__init__(
-            appname=appname, verbose=verbose, version=version, base_dir=base_dir,
-            append_appname_to_stems=append_appname_to_stems, config_dir=config_dir,
-            additional_stems=add_stems, additional_config_file=additional_config_file,
-            additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet,
-            ensure_privacy=ensure_privacy, initialized=False,
-        )
-
-        if initialized:
-            self.initialized = True
-
-    # -------------------------------------------------------------------------
-    def as_dict(self, short=True):
-        """
-        Transforms the elements of the object into a dict
-
-        @param short: don't include local properties in resulting dict.
-        @type short: bool
-
-        @return: structure as dict
-        @rtype:  dict
-        """
-
-        res = super(PdnsConfiguration, self).as_dict(short=short)
-
-        res['default_pdns_api_instances'] = self.default_pdns_api_instances
-        res['default_pdns_api_port'] = self.default_pdns_api_port
-        res['default_pdns_api_servername'] = self.default_pdns_api_servername
-        res['default_pdns_timeout'] = self.default_pdns_timeout
-        res['default_pdns_instance'] = self.default_pdns_instance
-        res['valid_pdns_api_instances'] = self.valid_pdns_api_instances
-
-        res['pdns_key'] = None
-        if self.pdns_key:
-            if self.verbose <= 4:
-                res['pdns_key'] = '******'
-            else:
-                res['pdns_key'] = self.pdns_key
-
-        res['pdns_api_instances'] = {}
-        for iname in self.pdns_api_instances.keys():
-            inst = self.pdns_api_instances[iname]
-            res['pdns_api_instances'][iname] = copy.copy(inst)
-            if 'key' in inst:
-                if self.verbose <= 4:
-                    res['pdns_api_instances'][iname]['key'] = '******'
-
-        return res
-
-    # -------------------------------------------------------------------------
-    def eval_section(self, section_name):
-
-        super(PdnsConfiguration, self).eval_section(section_name)
-        sn = section_name.lower()
-
-        re_pdns = re.compile(r'^(?:pdns|powerdns)(?:[_-]?api)?$')
-
-        if re_pdns.search(sn):
-            section = self.cfg[section_name]
-            return self._eval_pdns(section_name, section)
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns(self, section_name, section):
-
-        if self.verbose > 2:
-            msg = _("Evaluating config section {!r}:").format(section_name)
-            LOG.debug(msg + '\n' + pp(section))
-
-        re_agent = re.compile(r'^\s*(?:api[_-]?)?user[_-]?agent\s*$', re.IGNORECASE)
-        re_timeout = re.compile(r'^\s*timeout\s*$', re.IGNORECASE)
-        re_inst = re.compile(r'^\s*instances\s*$', re.IGNORECASE)
-        re_env = re.compile(r'^\s*(?:env(?:ironment)?|inst(?:ance)?)\s*$', re.IGNORECASE)
-        re_host = re.compile(r'^\s*(?:api[_-]?)?host\s*$', re.IGNORECASE)
-        re_port = re.compile(r'^\s*(?:api[_-]?)?port\s*$', re.IGNORECASE)
-        re_key = re.compile(r'^\s*(?:api[_-]?)?key\s*$', re.IGNORECASE)
-        re_servername = re.compile(r'^\s*(?:api[_-]?)?servername\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-
-            if re_agent.search(key):
-                self._eval_api_user_agent(section_name, key, section)
-                continue
-
-            if re_timeout.search(key):
-                self._eval_pdns_timeout(section_name, key, section)
-                continue
-
-            if re_env.search(key):
-                self._eval_pdns_environment(section_name, key, section)
-                continue
-
-            if re_host.search(key):
-                self._eval_pdns_host(section_name, key, section)
-                continue
-
-            if re_port.search(key):
-                self._eval_pdns_port(section_name, key, section)
-                continue
-
-            if re_key.search(key):
-                self._eval_pdns_key(section_name, key, section)
-                continue
-
-            if re_servername.search(key):
-                self._eval_pdns_re_servername(section_name, key, section)
-                continue
-
-            if re_inst.search(key):
-                self._eval_pdns_instances(section_name, key, section)
-                continue
-
-    # -------------------------------------------------------------------------
-    def _eval_api_user_agent(self, section_name, key, section):
-
-        val = section[key].strip()
-        if val:
-            self.api_user_agent = val
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_timeout(self, section_name, key, section):
-
-        val = section[key]
-        try:
-            timeout = int(val)
-            if timeout <= 0 or timeout > MAX_TIMEOUT:
-                msg = _("A timeout has to be between 1 and {} seconds.")
-                msg = msg.format(MAX_TIMEOUT)
-                raise ValueError(msg)
-        except (ValueError, TypeError) as e:
-            msg = _("Value {!r} for PowerDNS API timeout is invalid:").format(val)
-            msg += " " + str(e)
-            if self.raise_on_error:
-                raise PdnsConfigError(msg)
-            LOG.error(msg)
-            return
-
-        self.pdns_timeout = timeout
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_environment(self, section_name, key, section):
-
-        env = section[key].strip().lower()
-
-        if not env:
-            return
-
-        if env not in self.pdns_api_instances:
-            msg = _("Found invalid PDNS environment/instance {!r} in configuration.")
-            msg = msg.format(section[key])
-            if self.raise_on_error:
-                raise PdnsConfigError(msg)
-            LOG.error(msg)
-            return
-
-        self.pdns_instance = env
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_host(self, section_name, key, section):
-
-        val = section[key].strip().lower()
-        if val:
-            if self.verbose > 2:
-                msg = _("Found PDNS host: {!r}.").format(val)
-                LOG.debug(msg)
-
-            self.pdns_host = val
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_port(self, section_name, key, section):
-
-        val = section[key]
-        if not val:
-            return
-
-        port = None
-        try:
-            port = int(val)
-            if port <= 0 or port > MAX_PORT_NUMBER:
-                msg = _("A port must be greater than 0 and less than {}.")
-                raise ValueError(msg.format(MAX_PORT_NUMBER))
-        except (TypeError, ValueError) as e:
-            msg = _("Wrong PDNS port number {p!r} found: {e}").format(p=val, e=e)
-            if self.raise_on_error:
-                raise PdnsConfigError(msg)
-            else:
-                LOG.error(msg)
-                port = None
-
-        if port:
-            if self.verbose > 2:
-                msg = _("Found port number for PDNS: {}.").format(port)
-                LOG.debug(msg)
-
-            self.pdns_port = port
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_key(self, section_name, key, section):
-
-        val = section[key].strip()
-        if val:
-            if self.verbose > 2:
-                key_show = '******'
-                if self.verbose > 4:
-                    key_show = val
-                msg = _("Found API key for PDNS: {!r}.").format(key_show)
-                LOG.debug(msg)
-
-            self.pdns_key = val
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_servername(self, section_name, key, section):
-
-        val = section[key].strip()
-        if val:
-            if self.verbose > 2:
-                msg = _("Found PDNS API servername: {!r}.").format(val)
-                LOG.debug(msg)
-
-            self.pdns_servername = val
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_instances(self, section_name, key, section):
-
-        for instance_name in section[key].keys():
-            self._eval_pdns_instance(self, instance_name, section[key][instance_name])
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_instance(self, instance_name, section):
-
-        iname = instance_name.lower()
-
-        if self.verbose > 2:
-            msg = _("Evaluating PowerDNS instance {!r}:").format(iname)
-            LOG.debug(msg + '\n' + pp(section))
-
-        self._eval_pdns_inst_host(iname, section)
-        self._eval_pdns_inst_port(iname, section)
-        self._eval_pdns_inst_servername(iname, section)
-        self._eval_pdns_inst_key(iname, section)
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_inst_host(self, iname, section):
-
-        if self.verbose > 2:
-            msg = _("Searching for host for PDNS instance {!r} ..")
-            LOG.debug(msg.format(iname))
-
-        for key in section.keys():
-            if key.lower() == 'host':
-                host = section[key].lower().strip()
-                if host:
-                    if self.verbose > 2:
-                        msg = _("Found host for PDNS instance {inst!r}: {host!r}.")
-                        LOG.debug(msg.format(inst=iname, host=host))
-                    self.pdns_api_instances[iname]['host'] = host
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_inst_port(self, iname, section):
-
-        if self.verbose > 2:
-            msg = _("Searching for post number for PDNS instance {!r} ..")
-            LOG.debug(msg.format(iname))
-
-        for key in section.keys():
-            if key.lower() == 'port':
-                port = None
-                val = section[key]
-                try:
-                    port = int(val)
-                    if port <= 0 or port > MAX_PORT_NUMBER:
-                        msg = _("A port must be greater than 0 and less than {}.")
-                        raise ValueError(msg.format(MAX_PORT_NUMBER))
-                except (TypeError, ValueError) as e:
-                    msg = _("Wrong port number {p!r} for PDNS instance {inst!r} found: {e}")
-                    msg = msg.format(p=val, inst=iname, e=e)
-                    if self.raise_on_error:
-                        raise PdnsConfigError(msg)
-                    else:
-                        LOG.error(msg)
-                        port = None
-                if port:
-                    if self.verbose > 2:
-                        msg = _("Found port number for PDNS instance {inst!r}: {p}.")
-                        LOG.debug(msg.format(inst=iname, p=port))
-                    self.pdns_api_instances[iname]['port'] = port
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_inst_servername(self, iname, section):
-
-        if self.verbose > 2:
-            msg = _("Searching for internal server name of PDNS instance {!r} ..")
-            LOG.debug(msg.format(iname))
-
-        re_servername = re.compile(r'^\s*server[_-]?(name|id)\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if re_servername.search(key):
-                servername = section[key].lower().strip()
-                if servername:
-                    if self.verbose > 2:
-                        msg = _("Found internal server name PDNS instance {inst!r}: {sn!r}.")
-                        LOG.debug(msg.format(inst=iname, sn=servername))
-                    self.pdns_api_instances[iname]['servername'] = servername
-
-    # -------------------------------------------------------------------------
-    def _eval_pdns_inst_key(self, iname, section):
-
-        if self.verbose > 2:
-            msg = _("Searching for API key of PDNS instance {!r} ..")
-            LOG.debug(msg.format(iname))
-
-        re_key = re.compile(r'^\s*(api[_-]?)?key\s*$', re.IGNORECASE)
-
-        for key in section.keys():
-            if re_key.search(key):
-                api_key = section[key].lower().strip()
-                if api_key:
-                    if self.verbose > 2:
-                        key_show = '******'
-                        if self.verbose > 4:
-                            key_show = api_key
-                        msg = _("Found API key of PDNS instance {inst!r}: {key!r}.")
-                        LOG.debug(msg.format(inst=iname, key=key_show))
-                    self.pdns_api_instances[iname]['key'] = api_key
-
-    # -------------------------------------------------------------------------
-    def eval(self):
-
-        super(PdnsConfiguration, self).eval()
-
-        inst = self.pdns_instance
-
-        if not self.pdns_host:
-            self.pdns_host = self.pdns_api_instances[inst]['host']
-
-        if not self.pdns_port:
-            self.pdns_port = self.pdns_api_instances[inst]['port']
-
-        if not self.pdns_key:
-            self.pdns_key = self.pdns_api_instances[inst]['key']
-
-        if not self.pdns_servername:
-            self.pdns_servername = self.pdns_api_instances[inst]['servername']
-
-
-# =============================================================================
-if __name__ == "__main__":
-
-    pass
-
-# =============================================================================
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
index 530ab986cec3ba15439dbe3e1f9c0a01152677a4..47f6741c7f22d24b13e827442aba4fa622f22401 100755 (executable)
@@ -26,7 +26,7 @@ sys.path.insert(0, libdir)
 
 from general import PpAdminToolsTestcase, get_arg_verbose, init_root_logger
 
-from fb_tools.common import pp, to_str, is_sequence
+from fb_tools.common import pp, to_str, is_sequence
 
 LOG = logging.getLogger('test-mailcfg')
 
@@ -50,23 +50,23 @@ class TestMailConfig(PpAdminToolsTestcase):
     # -------------------------------------------------------------------------
     def test_import(self):
 
-        LOG.info("Testing import of pp_admintools.mail_config ...")
-        import pp_admintools.mail_config
+        LOG.info("Testing import of pp_admintools.config.mail ...")
+        import pp_admintools.config.mail
         LOG.debug(
-            "Version of pp_admintools.mail_config: " + pp_admintools.mail_config.__version__)
+            "Version of pp_admintools.config.mail: " + pp_admintools.config.mail.__version__)
 
-        LOG.info("Testing import of MailConfigError from pp_admintools.mail_config ...")
-        from pp_admintools.mail_config import MailConfigError               # noqa
+        LOG.info("Testing import of MailConfigError from pp_admintools.config.mail ...")
+        from pp_admintools.config.mail import MailConfigError               # noqa
 
-        LOG.info("Testing import of MailConfiguration from pp_admintools.mail_config ...")
-        from pp_admintools.mail_config import MailConfiguration                 # noqa
+        LOG.info("Testing import of MailConfiguration from pp_admintools.config.mail ...")
+        from pp_admintools.config.mail import MailConfiguration                 # noqa
 
     # -------------------------------------------------------------------------
     def test_object(self):
 
         LOG.info("Testing init of a MailConfiguration object.")
 
-        from pp_admintools.mail_config import MailConfiguration
+        from pp_admintools.config.mail import MailConfiguration
 
         cfg = MailConfiguration(
             appname=self.appname,
index 5704de84026d9379609040eb9652891a1c1ddd70..b8ae32afee16216a0b7b3e92ed66830eda9db4e2 100755 (executable)
@@ -26,7 +26,7 @@ sys.path.insert(0, libdir)
 
 from general import PpAdminToolsTestcase, get_arg_verbose, init_root_logger
 
-from fb_tools.common import pp, to_str, is_sequence
+from fb_tools.common import pp, to_str, is_sequence
 
 LOG = logging.getLogger('test-ldapcfg')
 
@@ -50,26 +50,26 @@ class TestLdapConfig(PpAdminToolsTestcase):
     # -------------------------------------------------------------------------
     def test_import(self):
 
-        LOG.info("Testing import of pp_admintools.ldap_config ...")
-        import pp_admintools.ldap_config
+        LOG.info("Testing import of pp_admintools.config.ldap ...")
+        import pp_admintools.config.ldap
         LOG.debug(
-            "Version of pp_admintools.ldap_config: " + pp_admintools.ldap_config.__version__)
+            "Version of pp_admintools.config.ldap: " + pp_admintools.config.ldap.__version__)
 
-        LOG.info("Testing import of LdapConfigError from pp_admintools.ldap_config ...")
-        from pp_admintools.ldap_config import LdapConfigError               # noqa
+        LOG.info("Testing import of LdapConfigError from pp_admintools.config.ldap ...")
+        from pp_admintools.config.ldap import LdapConfigError               # noqa
 
-        LOG.info("Testing import of LdapConnectionInfo from pp_admintools.ldap_config ...")
-        from pp_admintools.ldap_config import LdapConnectionInfo                # noqa
+        LOG.info("Testing import of LdapConnectionInfo from pp_admintools.config.ldap ...")
+        from pp_admintools.config.ldap import LdapConnectionInfo                # noqa
 
-        LOG.info("Testing import of LdapConfiguration from pp_admintools.ldap_config ...")
-        from pp_admintools.ldap_config import LdapConfiguration                 # noqa
+        LOG.info("Testing import of LdapConfiguration from pp_admintools.config.ldap ...")
+        from pp_admintools.config.ldap import LdapConfiguration                 # noqa
 
     # -------------------------------------------------------------------------
     def test_object(self):
 
         LOG.info("Testing init of a LdapConfiguration object.")
 
-        from pp_admintools.ldap_config import LdapConfiguration
+        from pp_admintools.config.ldap import LdapConfiguration
 
         cfg = LdapConfiguration(
             appname=self.appname,