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'
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])
--- /dev/null
+#!/bin/env python3
+# -*- coding: utf-8 -*-
+
+__version__ = '0.1.0'
+
+# vim: ts=4 et list
--- /dev/null
+#!/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
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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
--- /dev/null
+#!/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
--- /dev/null
+# -*- 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
+++ /dev/null
-#!/bin/env python3
-# -*- coding: utf-8 -*-
-
-__version__ = '0.1.0'
-
-# vim: ts=4 et list
-
+++ /dev/null
-# -*- 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
+++ /dev/null
-#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
+++ /dev/null
-# -*- 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
+++ /dev/null
-#!/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
+++ /dev/null
-# -*- 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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
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')
# -------------------------------------------------------------------------
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,
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')
# -------------------------------------------------------------------------
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,