if module_dir.exists():
sys.path.insert(0, str(lib_dir))
-from pp_admintools.deploy_zones_from_pdns import PpDeployZonesApp
+from pp_admintools.dns_deploy_zones_app import PpDeployZonesApp
__author__ = 'Frank Brehm <frank.brehm@pixelpark.com>'
-__copyright__ = '(C) 2021 by Frank Brehm, Pixelpark GmbH, Berlin'
+__copyright__ = '(C) 2022 by Frank Brehm, Pixelpark GmbH, Berlin'
appname = os.path.basename(sys.argv[0])
+++ /dev/null
-#!/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
--- /dev/null
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2022 by Frank Brehm, Berlin
+@summary: A module for the application class for configuring named
+"""
+from __future__ import absolute_import
+
+import os
+import logging
+import logging.config
+import textwrap
+import re
+import shlex
+import 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