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