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