import logging
import logging.config
import textwrap
+import re
import shlex, subprocess
+import copy
+import datetime
+import socket
from subprocess import Popen, TimeoutExpired, PIPE
from .pdns_zone import PdnsApiZone
from .pdns_record import compare_rrsets
-__version__ = '0.1.2'
+__version__ = '0.2.1'
LOG = logging.getLogger(__name__)
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'
+
+ 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*$')
+
+ 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._show_simulate_opt = True
+
+ self.is_internal = 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.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.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.files2replace = {}
+ self.moved_files = {}
+
+
description = textwrap.dedent('''\
Generation of the BIND9 configuration file for slave zones.
''')
self.initialized = True
+ # -------------------------------------------
+ @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)
+
+ # -------------------------------------------------------------------------
+ def init_arg_parser(self):
+
+ super(PpDeployZonesApp, self).init_arg_parser()
+
+ 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
+
+ # -------------------------------------------------------------------------
+ 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 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 {n!r}:\n{s}".format(
+ n=section_name, s=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)
+
+ 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'):
+ 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()
+ try:
+ addr_info = socket.getaddrinfo(
+ m, 53, proto=socket.IPPROTO_TCP, family=socket.AF_INET)
+ except socket.gaierror as e:
+ msg = (
+ "Invalid hostname or address {!r} found in "
+ "[{}]/masters: {}").format(m, section_name, e)
+ LOG.error(msg)
+ self.config_has_errors = True
+ m = None
+ if m:
+ masters.append(m)
+ 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 pre_run(self):
+ """
+ Dummy function to run before the main routine.
+ Could be overwritten by descendant classes.
+
+ """
+
+ 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)
+
+
# =============================================================================