forked from CCCHH/ansible-infra
Vendor Galaxy Roles and Collections
This commit is contained in:
parent
c1e1897cda
commit
2aed20393f
3553 changed files with 387444 additions and 2 deletions
19
ansible_collections/debops/debops/roles/bind/COPYRIGHT
Normal file
19
ansible_collections/debops/debops/roles/bind/COPYRIGHT
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
debops.bind - Manage the BIND DNS server using Ansible
|
||||
|
||||
Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
This Ansible role is part of DebOps.
|
||||
|
||||
DebOps is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 3, as
|
||||
published by the Free Software Foundation.
|
||||
|
||||
DebOps is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with DebOps. If not, see https://www.gnu.org/licenses/.
|
||||
1551
ansible_collections/debops/debops/roles/bind/defaults/main.yml
Normal file
1551
ansible_collections/debops/debops/roles/bind/defaults/main.yml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,721 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
"""BIND DNSSEC key rollover parent zone update/notification script
|
||||
|
||||
This script is meant to be called periodically from cron and will check
|
||||
for keys which need to be added to, or removed from, the parent zone.
|
||||
|
||||
When such keys are detected, an external utility will be executed or
|
||||
an email will be sent to the administrator, requesting manual action.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import socket
|
||||
import smtplib
|
||||
import logging
|
||||
import argparse
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
__license__ = 'GPL-3.0'
|
||||
__author__ = 'David Härdeman <david@hardeman.nu>'
|
||||
__version__ = '1.0.0'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def die(msg):
|
||||
if msg is not None:
|
||||
LOG.error(msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_process(config, args, change_locale=True):
|
||||
"""Runs some external program and returns its stdout"""
|
||||
|
||||
result = {}
|
||||
LOG.debug('Executing: "{}"'.format(' '.join(args)))
|
||||
tmp_env = os.environ.copy()
|
||||
if change_locale:
|
||||
tmp_env["LC_ALL"] = "C"
|
||||
|
||||
with subprocess.Popen(args, text=True, env=tmp_env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE) as process:
|
||||
stdout, stderr = process.communicate()
|
||||
result.stdout = stdout.strip()
|
||||
result.stderr = stderr.strip()
|
||||
result.rc = process.returncode
|
||||
|
||||
if process.returncode != 0:
|
||||
cmd = ' '.join(args)
|
||||
msg = 'Command "{}" failed ({})'.format(cmd, stderr.strip())
|
||||
LOG.debug('Command {} rc: {}'.format(cmd, process.returncode))
|
||||
LOG.debug('Command {} stdout: {}'.format(cmd, stdout.strip()))
|
||||
LOG.debug('Command {} stderr: {}'.format(cmd, stderr.strip()))
|
||||
write_log_msg(config, msg + "\n")
|
||||
result.error_msg = msg
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def run_process_get_stdout(config, args):
|
||||
result = do_run_process(config, args, change_locale=True)
|
||||
if result.rc != 0:
|
||||
die(result.error_msg)
|
||||
else:
|
||||
return result.stdout
|
||||
|
||||
|
||||
def run_process_ok(config, args):
|
||||
result = do_run_process(config, args, change_locale=False)
|
||||
if result.rc != 0:
|
||||
LOG.error(result.error_msg)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class Key:
|
||||
"""Keeps track of a zone key.
|
||||
|
||||
Attributes:
|
||||
alg_num: Mapping from algorithm names to IANA assigned numbers
|
||||
|
||||
See https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml.
|
||||
"""
|
||||
alg_num = {
|
||||
"DSA": "3",
|
||||
"RSASHA1": "5",
|
||||
"DSA-NSEC3-SHA1": "6",
|
||||
"RSASHA1-NSEC3-SHA1": "7",
|
||||
"RSASHA256": "8",
|
||||
"RSASHA512": "10",
|
||||
"ECC-GOST": "12",
|
||||
"ECDSAP256SHA256": "13",
|
||||
"ECDSAP384SHA384": "14",
|
||||
"ED25519": "15",
|
||||
"ED448": "16"
|
||||
}
|
||||
|
||||
def __init__(self, zone, line):
|
||||
self.valid = False
|
||||
self.zone = zone
|
||||
self.goal = None
|
||||
self.ds_state = None
|
||||
self.key_id = None
|
||||
self.key_alg = None
|
||||
self.key_alg_num = None
|
||||
self.key_type = None
|
||||
|
||||
line = re.sub('\\s+', ' ', line.strip())
|
||||
parts = line.split()
|
||||
|
||||
if len(parts) != 4:
|
||||
LOG.debug('Invalid key description ({})'.format(line))
|
||||
return
|
||||
|
||||
self.key_id = parts[1]
|
||||
|
||||
self.key_alg = parts[2].strip(' ,()')
|
||||
if self.key_alg.upper() in Key.alg_num:
|
||||
self.key_alg_num = Key.alg_num[self.key_alg.upper()]
|
||||
else:
|
||||
LOG.debug('Unknown key alg {} for key {}'.format(self.key_alg, self.key_id))
|
||||
return
|
||||
|
||||
self.key_type = parts[3].upper()
|
||||
if self.key_type not in ['KSK', 'CSK', 'ZSK']:
|
||||
LOG.debug('Unknown key type {}, key {}'.format(self.key_type, self.key_id))
|
||||
return
|
||||
|
||||
self.valid = True
|
||||
|
||||
def print(self):
|
||||
if not self.valid:
|
||||
LOG.debug('Invalid key')
|
||||
return
|
||||
|
||||
LOG.debug('ID : {}'.format(self.key_id))
|
||||
LOG.debug('Alg : {}'.format(self.key_alg))
|
||||
LOG.debug('AlgN: {}'.format(self.key_alg_num))
|
||||
LOG.debug('Type: {}'.format(self.key_type))
|
||||
LOG.debug('Goal: {}'.format(
|
||||
self.goal if self.goal is not None else "<unknown>"))
|
||||
LOG.debug('DS : {}'.format(
|
||||
self.ds_state if self.ds_state is not None else "<unknown>"))
|
||||
|
||||
def is_publishable(self):
|
||||
if not self.valid:
|
||||
return False
|
||||
elif self.key_type.upper() not in ['KSK', 'CSK']:
|
||||
return False
|
||||
elif self.goal is None or self.ds_state is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def needs_publication(self):
|
||||
if not self.is_publishable():
|
||||
return False
|
||||
elif self.goal.lower() != 'omnipresent':
|
||||
return False
|
||||
elif self.ds_state.lower() != 'rumoured':
|
||||
return False
|
||||
return True
|
||||
|
||||
def needs_withdrawal(self):
|
||||
if not self.is_publishable():
|
||||
return False
|
||||
elif self.goal.lower() != 'hidden':
|
||||
return False
|
||||
elif self.ds_state.lower() != 'unretentive':
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse_line(self, line):
|
||||
line = re.sub('\\s+', ' ', line.strip().lower())
|
||||
parts = line.split(':')
|
||||
|
||||
if len(parts) != 2:
|
||||
return
|
||||
|
||||
param = parts[0].strip()
|
||||
value = parts[1].strip()
|
||||
if value in ['hidden', 'rumoured', 'omnipresent', 'unretentive']:
|
||||
state = value
|
||||
else:
|
||||
state = None
|
||||
|
||||
if param == '- goal':
|
||||
if state is None:
|
||||
LOG.debug('Unknown key goal: {}'.format(value))
|
||||
else:
|
||||
self.goal = state
|
||||
|
||||
elif param == '- ds':
|
||||
if state is None:
|
||||
LOG.debug('Unknown DS state: {}'.format(value))
|
||||
else:
|
||||
self.ds_state = state
|
||||
|
||||
|
||||
class Zone:
|
||||
"""Keeps track of a zone"""
|
||||
|
||||
def __init__(self, name, rr_class, view):
|
||||
self.name = name
|
||||
self.rr_class = rr_class
|
||||
self.view = view
|
||||
self.keys = None
|
||||
|
||||
def get_keys(self, config, rndc_output=None):
|
||||
"""Get the currently known keys for a zone"""
|
||||
|
||||
if self.keys is not None:
|
||||
return self.keys
|
||||
|
||||
if rndc_output is None:
|
||||
args = ["rndc", "dnssec", "-status",
|
||||
self.name, self.rr_class, self.view]
|
||||
rndc_output = run_process(config, args)
|
||||
|
||||
"""
|
||||
Entries can have the following states:
|
||||
|
||||
* hidden (not published)
|
||||
* rumoured (partially published)
|
||||
* omnipresent (completely published)
|
||||
* unretentive (stale)
|
||||
|
||||
This is an example of the kind of output we need to parse:
|
||||
|
||||
root@foobar:~# rndc dnssec -status example.com
|
||||
dnssec-policy: kskzsk
|
||||
current time: Sat Jul 30 20:43:25 2022
|
||||
|
||||
key: 38979 (ECDSAP256SHA256), KSK
|
||||
published: yes - since Fri Jul 29 23:12:42 2022
|
||||
key signing: yes - since Fri Jul 29 23:12:42 2022
|
||||
|
||||
Next rollover scheduled on Sat Jul 29 23:12:42 2023
|
||||
- goal: omnipresent
|
||||
- dnskey: omnipresent
|
||||
- ds: rumoured
|
||||
- key rrsig: omnipresent
|
||||
|
||||
key: 3732 (ECDSAP256SHA256), ZSK
|
||||
published: yes - since Fri Jul 29 22:58:05 2022
|
||||
zone signing: yes - since Fri Jul 29 22:58:05 2022
|
||||
|
||||
Next rollover scheduled on Tue Sep 27 22:55:05 2022
|
||||
- goal: omnipresent
|
||||
- dnskey: omnipresent
|
||||
- zone rrsig: omnipresent
|
||||
|
||||
key: 52180 (ECDSAP256SHA256), KSK
|
||||
published: yes - since Fri Jul 29 22:58:05 2022
|
||||
key signing: yes - since Fri Jul 29 22:58:05 2022
|
||||
|
||||
Rollover is due since Fri Jul 29 23:18:42 2022
|
||||
- goal: hidden
|
||||
- dnskey: omnipresent
|
||||
- ds: unretentive
|
||||
- key rrsig: omnipresent
|
||||
|
||||
|
||||
Note that there is:
|
||||
|
||||
* one KSK (38979) which has a goal state of omnipresent but where the DS
|
||||
record is rumoured (i.e. not known to be published in the parent zone)
|
||||
|
||||
* one KSK (52180) which has a goal state of hidden but where the DS record
|
||||
is unretentive (i.e. not known to be removed from the parent zone)
|
||||
|
||||
"""
|
||||
|
||||
self.keys = []
|
||||
key = None
|
||||
for line in rndc_output.splitlines():
|
||||
if line.startswith('key:'):
|
||||
if key is not None:
|
||||
self.keys.append(key)
|
||||
key = Key(self, line)
|
||||
continue
|
||||
elif key is None:
|
||||
continue
|
||||
else:
|
||||
key.parse_line(line)
|
||||
|
||||
if key is not None:
|
||||
self.keys.append(key)
|
||||
|
||||
def get_keys_to_publish(self):
|
||||
if self.rr_class.upper() != 'IN':
|
||||
return []
|
||||
elif self.keys is None:
|
||||
return []
|
||||
|
||||
keys = []
|
||||
for key in self.keys:
|
||||
if key.needs_publication():
|
||||
keys.append(key)
|
||||
|
||||
return keys
|
||||
|
||||
def get_keys_to_withdraw(self):
|
||||
if self.rr_class.upper() != 'IN':
|
||||
return []
|
||||
elif self.keys is None:
|
||||
return []
|
||||
|
||||
keys = []
|
||||
for key in self.keys:
|
||||
if key.needs_withdrawal():
|
||||
keys.append(key)
|
||||
|
||||
return keys
|
||||
|
||||
def print_keys(self):
|
||||
if self.keys is None:
|
||||
return
|
||||
|
||||
for key in self.keys:
|
||||
key.print()
|
||||
LOG.debug('')
|
||||
|
||||
def print_zone(self):
|
||||
LOG.debug('View: {}, Zone: {}, Keys: {}'.format(
|
||||
self.name,
|
||||
self.view,
|
||||
"<undefined>" if self.keys is None else len(self.keys)))
|
||||
|
||||
@classmethod
|
||||
def get_zones(cls, config, path='/var/cache/bind/named_dump.db'):
|
||||
"""Creates a list of zones, possibly by parsing BINDs dumpfile"""
|
||||
zones = []
|
||||
|
||||
if "zones" in config and len(config["zones"]) > 0:
|
||||
for zone in config["zones"]:
|
||||
parts = zone.split("/")
|
||||
|
||||
if len(parts) > 2:
|
||||
die('Invalid zone: "{}"'.format(zone))
|
||||
elif len(parts) > 1:
|
||||
view = parts[1].strip()
|
||||
else:
|
||||
view = "_default"
|
||||
|
||||
name = parts[0].strip()
|
||||
rr_class = "IN"
|
||||
|
||||
if len(name) < 1 or len(view) < 1:
|
||||
die('Invalid zone: "{}"'.format(zone))
|
||||
|
||||
zone = Zone(name, rr_class, view)
|
||||
zones.append(zone)
|
||||
|
||||
return zones
|
||||
|
||||
# No zones defined, time to dig in rndc dump files to autodetect them...
|
||||
args = ["rndc", "dumpdb", "-zones"]
|
||||
|
||||
if not run_process_ok(config, args):
|
||||
die(None)
|
||||
|
||||
"""
|
||||
This is an example of the kind of output we need to parse:
|
||||
|
||||
Note that the '(signed)' suffix may or may not be present for signed
|
||||
zones depending on the version of BIND.
|
||||
|
||||
Without views:
|
||||
;
|
||||
; Start view _default
|
||||
;
|
||||
; Zone dump of 'example.com/IN'
|
||||
;
|
||||
...
|
||||
;
|
||||
; Zone dump of 'example.net/IN (signed)'
|
||||
;
|
||||
...
|
||||
|
||||
With views:
|
||||
;
|
||||
; Start view foo
|
||||
;
|
||||
; Zone dump of 'example.com/IN/foo'
|
||||
;
|
||||
...
|
||||
;
|
||||
; Start view bar
|
||||
;
|
||||
; Zone dump of 'example.net/IN/bar (signed)'
|
||||
;
|
||||
...
|
||||
"""
|
||||
|
||||
start_view = '_default'
|
||||
with open(path) as file:
|
||||
for line in file:
|
||||
if not line.startswith(';'):
|
||||
continue
|
||||
line = line[1:].strip()
|
||||
if line.startswith('Start view '):
|
||||
start_view = line[len('Start view '):]
|
||||
continue
|
||||
elif not line.startswith('Zone dump of '):
|
||||
continue
|
||||
|
||||
line = line[len('Zone dump of '):].strip().strip("'")
|
||||
|
||||
if line.endswith('(signed)'):
|
||||
line = line[:-len('(signed)')].strip()
|
||||
|
||||
parts = line.split('/')
|
||||
if len(parts) < 2 or len(parts) > 3:
|
||||
LOG.debug('Invalid zone line ({})'.format(line))
|
||||
continue
|
||||
|
||||
name = parts[0].strip()
|
||||
rr_class = parts[1].strip()
|
||||
view = start_view
|
||||
|
||||
if len(parts) == 3:
|
||||
view = parts[2].strip()
|
||||
if view != start_view:
|
||||
LOG.debug('Invalid zone view ({}, expected {})'.format(
|
||||
view, start_view))
|
||||
continue
|
||||
|
||||
if rr_class != 'IN':
|
||||
continue
|
||||
|
||||
if view == '_bind':
|
||||
continue
|
||||
|
||||
zone = Zone(name, rr_class, view)
|
||||
zones.append(zone)
|
||||
|
||||
return zones
|
||||
|
||||
|
||||
def get_args_parser():
|
||||
args_parser = argparse.ArgumentParser(
|
||||
prog='debops-bind-rollkey',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
description="Publish/unpublish DNSSEC keys.",
|
||||
)
|
||||
args_parser.add_argument(
|
||||
'-V', '--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
)
|
||||
args_parser.add_argument(
|
||||
'-d', '--debug',
|
||||
help="Enable debug output",
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.DEBUG,
|
||||
)
|
||||
args_parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
help="Enable verbose output",
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.INFO,
|
||||
)
|
||||
args_parser.add_argument(
|
||||
'-c', '--config',
|
||||
help="Configuration file path",
|
||||
default="/etc/bind/debops-bind-rollkey.json",
|
||||
dest="config_path",
|
||||
)
|
||||
args_parser.add_argument(
|
||||
'-C', '--print-config',
|
||||
help="Print a configuration file example",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="print_config",
|
||||
)
|
||||
|
||||
return args_parser
|
||||
|
||||
|
||||
def get_default_config():
|
||||
return {
|
||||
"method": "log/email/external",
|
||||
"log_file": "/var/log/debops-bind-rollkey.log",
|
||||
"email_to": "root@localhost",
|
||||
"email_from": "noreply@{}".format(socket.getfqdn()),
|
||||
"email_host": "localhost",
|
||||
"email_port": 25,
|
||||
"email_subject": "BIND DNSSEC key updates",
|
||||
"external_script": "/usr/local/sbin/debops-bind-rollkey-action",
|
||||
}
|
||||
|
||||
|
||||
def print_example_config():
|
||||
config = get_default_config()
|
||||
config["zones"] = ["zone1/view1", "zone2/view2", "(optional)"]
|
||||
print(json.dumps(config, sort_keys=False, indent=4))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def read_config_file(path):
|
||||
config = get_default_config()
|
||||
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
data = f.read()
|
||||
except Exception as err:
|
||||
LOG.info("Can't open cfg file {}: {}".format(path, str(err)))
|
||||
data = None
|
||||
|
||||
if data is not None:
|
||||
try:
|
||||
j = json.loads(data)
|
||||
config.update(j)
|
||||
except Exception as err:
|
||||
die("Can't parse cfg file {}: {}".format(path, str(err)))
|
||||
|
||||
if not config["method"]:
|
||||
die("Method not configured")
|
||||
|
||||
elif config["method"] == "log":
|
||||
if not config["log_file"]:
|
||||
die("Log file path not configured")
|
||||
|
||||
elif config["method"] == "email":
|
||||
if not config["email_to"]:
|
||||
die("Recipient email address not configured")
|
||||
|
||||
if not config["email_host"]:
|
||||
die("SMTP host not configured")
|
||||
|
||||
if not config["email_port"]:
|
||||
die("SMTP port not configured")
|
||||
|
||||
if not config["email_subject"]:
|
||||
die("Email subject not configured")
|
||||
|
||||
elif config["method"] == "external":
|
||||
if not config["external_script"]:
|
||||
die("External script not configured")
|
||||
|
||||
if not os.access(config["external_script"], os.F_OK):
|
||||
die("External script not found")
|
||||
|
||||
if not os.access(config["external_script"], os.X_OK):
|
||||
die("External script not executable")
|
||||
|
||||
else:
|
||||
die("Invalid method configured")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_actionable_keys(zones):
|
||||
keys = []
|
||||
|
||||
for zone in zones:
|
||||
pub_keys = zone.get_keys_to_publish()
|
||||
for key in pub_keys:
|
||||
key.action = "Publish"
|
||||
key.action_goal = "published"
|
||||
keys.append(key)
|
||||
|
||||
unpub_keys = zone.get_keys_to_withdraw()
|
||||
for key in unpub_keys:
|
||||
key.action = "Withdraw"
|
||||
key.action_goal = "withdrawn"
|
||||
keys.append(key)
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
def do_external(config, zones):
|
||||
keys = get_actionable_keys(zones)
|
||||
failures = False
|
||||
|
||||
for key in keys:
|
||||
args_ext = [config["external_script"],
|
||||
key.action.lower(),
|
||||
key.key_id,
|
||||
key.key_alg_num,
|
||||
key.zone.name,
|
||||
key.zone.rr_class,
|
||||
key.zone.view]
|
||||
args_rndc = ["rndc",
|
||||
"dnssec",
|
||||
"-checkds",
|
||||
"-key",
|
||||
key.key_id,
|
||||
"-alg",
|
||||
key.key_alg_num,
|
||||
key.action_goal,
|
||||
key.zone.name,
|
||||
key.zone.rr_class]
|
||||
if key.zone.view != '_default':
|
||||
args_rndc.append(key.zone.view)
|
||||
|
||||
if not run_process_ok(config, args_ext):
|
||||
failures = True
|
||||
elif not run_process_ok(config, args_rndc):
|
||||
failures = True
|
||||
|
||||
if failures:
|
||||
write_log_msg(config, "Some commands failed\n\n")
|
||||
else:
|
||||
write_log_msg(config, "Done\n\n")
|
||||
|
||||
|
||||
def create_message(config, keys, sep):
|
||||
msg = "{}{}Executed at {}{}".format(
|
||||
sys.argv[0],
|
||||
sep,
|
||||
datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
|
||||
sep)
|
||||
|
||||
msg += "The following DNSSEC key changes need to be made" + sep
|
||||
msg += "================================================" + sep
|
||||
|
||||
if len(keys) == 0:
|
||||
msg += "None" + sep
|
||||
else:
|
||||
for key in keys:
|
||||
msg += "{} {} {} {} IN {}{}".format(
|
||||
key.action,
|
||||
key.key_id,
|
||||
key.key_alg_num,
|
||||
key.zone.name,
|
||||
key.zone.view,
|
||||
sep)
|
||||
|
||||
msg += "================================================" + sep
|
||||
msg += sep
|
||||
return msg
|
||||
|
||||
|
||||
def do_email(config, zones):
|
||||
keys = get_actionable_keys(zones)
|
||||
if len(keys) == 0:
|
||||
return
|
||||
|
||||
msg = "From: {}\r\nTo: {}\r\nSubject: {}\r\n\r\n".format(
|
||||
config["email_from"],
|
||||
config["email_to"],
|
||||
config["email_subject"])
|
||||
|
||||
msg += create_message(config, keys, "\r\n")
|
||||
|
||||
with smtplib.SMTP(host=config["email_host"],
|
||||
port=config["email_port"]) as smtp:
|
||||
# smtp.set_debuglevel(1)
|
||||
smtp.sendmail(config["email_from"], config["email_to"], msg)
|
||||
|
||||
|
||||
def write_log_msg(config, msg):
|
||||
if not config["log_file"]:
|
||||
return
|
||||
|
||||
with open(config["log_file"], "a") as f:
|
||||
f.write(msg)
|
||||
|
||||
|
||||
def do_log(config, zones):
|
||||
keys = get_actionable_keys(zones)
|
||||
msg = create_message(config, keys, "\n")
|
||||
write_log_msg(config, msg)
|
||||
if len(keys) == 0:
|
||||
LOG.debug("Nothing to do")
|
||||
|
||||
|
||||
def main():
|
||||
args_parser = get_args_parser()
|
||||
args_parser.set_defaults(loglevel=logging.WARN)
|
||||
args = args_parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
format='%(levelname)s{}, %(asctime)s: %(message)s'.format(
|
||||
' (%(filename)s:%(lineno)s)'
|
||||
if args.loglevel <= logging.DEBUG else '',
|
||||
),
|
||||
level=args.loglevel,
|
||||
)
|
||||
|
||||
if args.print_config:
|
||||
print_example_config()
|
||||
|
||||
config = read_config_file(args.config_path)
|
||||
|
||||
if shutil.which("rndc") is None:
|
||||
die("rndc executable not found")
|
||||
|
||||
zones = Zone.get_zones(config)
|
||||
|
||||
for zone in zones:
|
||||
zone.get_keys(config)
|
||||
zone.print_zone()
|
||||
zone.print_keys()
|
||||
|
||||
do_log(config, zones)
|
||||
|
||||
if config["method"] == "email":
|
||||
do_email(config, zones)
|
||||
elif config["method"] == "external":
|
||||
do_external(config, zones)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# This file is managed remotely, all changes will be lost
|
||||
|
||||
# Copyright (C) 2015-2019 Maciej Delmanowski <drybjed@gmail.com>
|
||||
# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2015-2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# Create .tar.gz backup snapshots of the BIND config and master zones.
|
||||
# The script will automatically freeze/sync/thaw the zones as necessary,
|
||||
# while keeping track of whether they were already frozen (e.g. manually
|
||||
# by the administrator) and not thawing them again in that case.
|
||||
|
||||
|
||||
set -o nounset -o pipefail -o errexit
|
||||
|
||||
umask 027
|
||||
|
||||
SCRIPT="$(basename "${0}")"
|
||||
readonly SCRIPT
|
||||
readonly PID="$$"
|
||||
readonly PIDFILE="/run/${SCRIPT}.pid"
|
||||
readonly SUBCOMMAND="${1:-}"
|
||||
readonly RNDC="/usr/sbin/rndc"
|
||||
|
||||
BACKUP_USER="backup"
|
||||
BACKUP_GROUP="backup"
|
||||
BACKUP_ROOT="$(getent passwd "${BACKUP_USER}" | cut -f6 -d:)/bind"
|
||||
FROZEN_FLAG="/var/lib/bind/.debops.frozen"
|
||||
TAR="/bin/tar"
|
||||
|
||||
if [ -f "/etc/default/debops-bind-snapshot" ] ; then
|
||||
# shellcheck disable=SC1091
|
||||
. "/etc/default/debops-bind-snapshot"
|
||||
fi
|
||||
|
||||
print_usage () {
|
||||
cat <<EOF
|
||||
Usage: ${SCRIPT} <daily|weekly|monthly|latest|now>
|
||||
|
||||
Create periodic backup snapshots of the BIND configuration and zones
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
log_message () {
|
||||
# Display a message if script is used interactively, otherwise send it to syslog
|
||||
local msg="${1:-}"
|
||||
|
||||
if [ -n "${msg}" ] ; then
|
||||
if tty -s > /dev/null 2>&1 ; then
|
||||
echo "${SCRIPT}: ${msg}" 1>&2
|
||||
elif type logger > /dev/null 2>&1 ; then
|
||||
logger -t "${SCRIPT}[${PID}]" "${msg}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
clean_up () {
|
||||
# Clean up pidfile
|
||||
local pidfile="${1}"
|
||||
|
||||
if [ -n "${pidfile}" ] && [ -r "${pidfile}" ] ; then
|
||||
rm -f "${pidfile}"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
||||
wait_for_pid () {
|
||||
# Wait for the specified process to exit
|
||||
local pidfile="${1}"
|
||||
|
||||
if [ -n "${pidfile}" ] && [ -r "${pidfile}" ] ; then
|
||||
local wait_pid
|
||||
wait_pid="$(< "${pidfile}")"
|
||||
while kill -0 "${wait_pid}" > /dev/null 2>&1 ; do
|
||||
log_message "Waiting for PID ${wait_pid} to finish"
|
||||
sleep $(( ( RANDOM % 30 ) + 5 ))
|
||||
done
|
||||
if [ -f "${pidfile}" ] ; then
|
||||
rm -f "${pidfile}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
create_lock () {
|
||||
local pidfile="${1}"
|
||||
local pid="${PID}"
|
||||
|
||||
# Try and lock the script operation
|
||||
echo "${pid}" > "${pidfile}"
|
||||
sleep 0.5
|
||||
|
||||
# Exclusive lock failed
|
||||
if [ "$(cat "${pidfile}")" != "${pid}" ] ; then
|
||||
log_message "BIND snapshot procedure started by $(< "${pidfile}")"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Exclusive lock succeeded
|
||||
# shellcheck disable=SC2064
|
||||
trap "clean_up ${pidfile}" EXIT
|
||||
}
|
||||
|
||||
|
||||
enable_read_only_mode () {
|
||||
local output
|
||||
|
||||
|
||||
if ! pidof -q named > /dev/null 2>&1; then
|
||||
log_message "Not freezing zones, named isn't running"
|
||||
else
|
||||
if ! output="$(LC_ALL=C "${RNDC}" freeze 2>&1)"; then
|
||||
if [[ "${output}" == *"already frozen"* ]]; then
|
||||
log_message "Not freezing zones, already frozen"
|
||||
else
|
||||
log_message "Unable to freeze zones: ${output}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_message "Freezing zone updates"
|
||||
touch "${FROZEN_FLAG}"
|
||||
fi
|
||||
|
||||
log_message "Synchronizing all zones"
|
||||
"${RNDC}" sync -clean
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
disable_read_only_mode () {
|
||||
if ! pidof -q named > /dev/null 2>&1; then
|
||||
log_message "Not thawing zones, named isn't running"
|
||||
elif ! [ -e "${FROZEN_FLAG}" ]; then
|
||||
log_message "Not thawing zones, not frozen by this script"
|
||||
else
|
||||
log_message "Thawing zone updates"
|
||||
"${RNDC}" thaw
|
||||
rm -f "${FROZEN_FLAG}"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
create_snapshot () {
|
||||
local backup_user
|
||||
local backup_group
|
||||
local backup_root
|
||||
local backup_file
|
||||
local backup_dir
|
||||
|
||||
backup_user="${BACKUP_USER}"
|
||||
backup_group="${BACKUP_GROUP}"
|
||||
backup_root="${BACKUP_ROOT}"
|
||||
backup_file="${1}"
|
||||
backup_dir="$(dirname "${backup_file}")"
|
||||
|
||||
if [ ! -d "${backup_dir}" ] ; then
|
||||
mkdir -p "${backup_dir}"
|
||||
chown -R "${backup_user}:${backup_group}" "${backup_root}"
|
||||
fi
|
||||
|
||||
enable_read_only_mode
|
||||
log_message "Dumping the BIND configuration and zones"
|
||||
"${TAR}" -cf "${backup_file}" -C / etc/bind var/lib/bind
|
||||
disable_read_only_mode
|
||||
|
||||
if [ -f "${backup_file}.gz.gpg" ] ; then
|
||||
rm -f "${backup_file}.gz.gpg"
|
||||
fi
|
||||
|
||||
if [ -f "${backup_file}.gz.asc" ] ; then
|
||||
rm -f "${backup_file}.gz.asc"
|
||||
fi
|
||||
|
||||
nice gzip -f "${backup_file}"
|
||||
chown "${backup_user}:${backup_group}" "${backup_file}.gz"
|
||||
}
|
||||
|
||||
|
||||
snapshot_daily () {
|
||||
local backup_root
|
||||
local day
|
||||
local weekday
|
||||
local backup_file
|
||||
|
||||
backup_root="${BACKUP_ROOT}"
|
||||
day="$(date +%w)"
|
||||
weekday="$(date +%A)"
|
||||
backup_file="${backup_root}/daily/bind_day${day}_${weekday}.tar"
|
||||
|
||||
log_message "Creating daily snapshot of the BIND zones and config"
|
||||
create_snapshot "${backup_file}"
|
||||
log_message "Daily snapshot of the BIND zones and config created successfully"
|
||||
}
|
||||
|
||||
|
||||
snapshot_weekly () {
|
||||
local backup_root
|
||||
local week
|
||||
local backup_file
|
||||
|
||||
backup_root="${BACKUP_ROOT}"
|
||||
# Find out the week number in the current month
|
||||
week="$(( ( $(date +%_d) - 1 ) / 7 + 1 ))"
|
||||
backup_file="${backup_root}/weekly/bind_week${week}.tar"
|
||||
|
||||
log_message "Creating weekly snapshot of the BIND zones and config"
|
||||
create_snapshot "${backup_file}"
|
||||
log_message "Weekly snapshot of the BIND zones and config created successfully"
|
||||
}
|
||||
|
||||
|
||||
snapshot_monthly () {
|
||||
local backup_root
|
||||
local month
|
||||
local month_name
|
||||
local backup_file
|
||||
|
||||
backup_root="${BACKUP_ROOT}"
|
||||
month="$(date +%m)"
|
||||
month_name="$(date +%B)"
|
||||
backup_file="${backup_root}/monthly/bind_month${month}_${month_name}.tar"
|
||||
|
||||
log_message "Creating monthly snapshot of the BIND zones and config"
|
||||
create_snapshot "${backup_file}"
|
||||
log_message "Monthly snapshot of the BIND zones and config created successfully"
|
||||
}
|
||||
|
||||
|
||||
snapshot_latest () {
|
||||
local backup_root
|
||||
local current_date
|
||||
local backup_file
|
||||
|
||||
backup_root="${BACKUP_ROOT}"
|
||||
current_date="$(date +%F_%R)"
|
||||
backup_file="${backup_root}/latest/bind_${current_date}.tar"
|
||||
|
||||
if [ -d "${backup_root}/latest" ] ; then
|
||||
# Remove old set of database backups
|
||||
find "${backup_root}/latest" -type f -name "bind_*.*" -delete
|
||||
fi
|
||||
|
||||
log_message "Creating a snapshot of the BIND zones and config"
|
||||
create_snapshot "${backup_file}"
|
||||
log_message "Snapshot of the BIND zones and config created successfully"
|
||||
}
|
||||
|
||||
|
||||
main () {
|
||||
local pidfile="${PIDFILE}"
|
||||
local subcommand="${SUBCOMMAND}"
|
||||
|
||||
wait_for_pid "${pidfile}"
|
||||
create_lock "${pidfile}"
|
||||
|
||||
case "${subcommand}" in
|
||||
daily) snapshot_daily ;;
|
||||
weekly) snapshot_weekly ;;
|
||||
monthly) snapshot_monthly ;;
|
||||
now|latest) snapshot_latest ;;
|
||||
*) print_usage && exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main
|
||||
32
ansible_collections/debops/debops/roles/bind/meta/main.yml
Normal file
32
ansible_collections/debops/debops/roles/bind/meta/main.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# Ensure that custom Ansible plugins and modules included in the main DebOps
|
||||
# collection are available to roles in other collections.
|
||||
collections: [ 'debops.debops' ]
|
||||
|
||||
dependencies: []
|
||||
|
||||
galaxy_info:
|
||||
|
||||
company: 'DebOps'
|
||||
author: 'David Härdeman'
|
||||
description: 'Configure the BIND DNS server'
|
||||
license: 'GPL-3.0-only'
|
||||
min_ansible_version: '2.9.0'
|
||||
|
||||
platforms:
|
||||
|
||||
- name: 'Ubuntu'
|
||||
versions: [ 'all' ]
|
||||
|
||||
- name: 'Debian'
|
||||
versions: [ 'all' ]
|
||||
|
||||
galaxy_tags:
|
||||
- bind
|
||||
- bind9
|
||||
- named
|
||||
- dns
|
||||
349
ansible_collections/debops/debops/roles/bind/tasks/main.yml
Normal file
349
ansible_collections/debops/debops/roles/bind/tasks/main.yml
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
---
|
||||
# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
- name: Import custom Ansible plugins
|
||||
ansible.builtin.import_role:
|
||||
name: 'ansible_plugins'
|
||||
|
||||
- name: Import DebOps global handlers
|
||||
ansible.builtin.import_role:
|
||||
name: 'global_handlers'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Import DebOps secret role
|
||||
ansible.builtin.import_role:
|
||||
name: 'secret'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Install packages
|
||||
ansible.builtin.package:
|
||||
name: '{{ q("flattened", bind__base_packages + bind__packages) }}'
|
||||
state: 'present'
|
||||
register: bind__register_packages
|
||||
until: bind__register_packages is succeeded
|
||||
tags: [ 'role::bind:packages' ]
|
||||
|
||||
- name: Make sure that the Ansible local facts directory exists
|
||||
ansible.builtin.file:
|
||||
path: '/etc/ansible/facts.d'
|
||||
state: 'directory'
|
||||
mode: '0755'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Save local facts
|
||||
ansible.builtin.template:
|
||||
src: 'etc/ansible/facts.d/bind.fact.j2'
|
||||
dest: '/etc/ansible/facts.d/bind.fact'
|
||||
mode: '0755'
|
||||
notify: [ 'Refresh host facts' ]
|
||||
tags: [ 'meta::facts', 'role::bind:config' ]
|
||||
|
||||
- name: Update Ansible facts if they were modified
|
||||
ansible.builtin.meta: 'flush_handlers'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Enable/disable resolvconf integration
|
||||
ansible.builtin.service:
|
||||
name: 'named-resolvconf'
|
||||
enabled: '{{ ansible_local.resolvconf.installed | d(False) | bool }}'
|
||||
when: ansible_service_mgr == "systemd"
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Allow access to additional UNIX groups
|
||||
ansible.builtin.user:
|
||||
name: '{{ bind__user }}'
|
||||
groups: '{{ bind__additional_groups }}'
|
||||
append: True
|
||||
state: 'present'
|
||||
register: bind__register_unix_groups
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
|
||||
- name: Create base directories
|
||||
ansible.builtin.file:
|
||||
path: '{{ item.path }}'
|
||||
state: 'directory'
|
||||
owner: 'root'
|
||||
group: 'bind'
|
||||
mode: '{{ item.mode | d("0775") }}'
|
||||
loop:
|
||||
- { path: '/etc/bind', mode: '02755' }
|
||||
- { path: '/etc/bind/keys', mode: '02750' }
|
||||
- { path: '/var/cache/bind' }
|
||||
- { path: '/var/lib/bind' }
|
||||
- { path: '/var/lib/bind/views' }
|
||||
- { path: '/var/lib/bind/dnssec-keys', mode: '02770' }
|
||||
- { path: '/var/log/named' }
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Create keys
|
||||
ansible.builtin.include_tasks: 'main_keys.yml'
|
||||
tags: [ 'role::bind:config', 'role::bind:keys' ]
|
||||
|
||||
- name: Generate list of toplevel views
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_top_views: '{{ bind__combined_zones
|
||||
| debops.debops.parse_kv_items(name="view")
|
||||
| selectattr("state", "eq", "present") }}'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Generate list of toplevel zones
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_top_zones: '{{ bind__combined_zones
|
||||
| debops.debops.parse_kv_items(name="name")
|
||||
| selectattr("state", "eq", "present") }}'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Make sure either zones or views are defined
|
||||
ansible.builtin.assert:
|
||||
that: bind__tmp_top_views | length == 0 or bind__tmp_top_zones | length == 0
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Assign zones to the default view
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_top_views:
|
||||
- view: '_default'
|
||||
zones: '{{ bind__tmp_top_zones }}'
|
||||
when: bind__tmp_top_views | length == 0
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Create list of generic zones
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_generic_zones: '{{ bind__combined_generic_zones
|
||||
| debops.debops.parse_kv_items(name="name")
|
||||
| selectattr("state", "eq", "present") }}'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Add generic zones to each view
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_full_views: '{{ bind__tmp_full_views | d([])
|
||||
+ [item | combine({"zones": item["zones"]
|
||||
+ bind__tmp_generic_zones})] }}'
|
||||
loop: '{{ bind__tmp_top_views }}'
|
||||
loop_control:
|
||||
label: '{{ item.view }}'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Register the view in each zone
|
||||
ansible.builtin.set_fact:
|
||||
bind__views: '{{ bind__views | d([])
|
||||
+ [item | combine({"zones": (item["zones"]
|
||||
| map("combine", {"view": item["view"]}))})] }}'
|
||||
loop: '{{ bind__tmp_full_views }}'
|
||||
loop_control:
|
||||
label: '{{ item.view }}'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Create zone directories
|
||||
ansible.builtin.file:
|
||||
path: '{{ item.dir | d("/var/lib/bind/views/" + item.view + "/" + item.name) }}'
|
||||
state: 'directory'
|
||||
owner: '{{ item.owner | d("root") }}'
|
||||
group: '{{ item.group | d("bind") }}'
|
||||
mode: '{{ item.mode | d("0775") }}'
|
||||
loop: '{{ bind__views | map(attribute="zones") | flatten }}'
|
||||
when:
|
||||
- item.content is defined
|
||||
- item.state | d("present") == "present"
|
||||
loop_control:
|
||||
label: '{{ item.dir | d("/var/lib/bind/views/" + item.view + "/" + item.name) }}'
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
# Note: the changed_when status for the freeze/thaw tasks is kind of a lie...
|
||||
- name: Suspend dynamic updates
|
||||
ansible.builtin.shell: |
|
||||
if pidof -q named > /dev/null 2>&1; then
|
||||
rndc freeze && exit 222
|
||||
else true; fi
|
||||
environment:
|
||||
LC_ALL: 'C'
|
||||
register: bind__register_freeze
|
||||
changed_when: False
|
||||
failed_when: >
|
||||
(bind__register_freeze.rc not in [ 0, 222 ]) and
|
||||
("already frozen" not in bind__register_freeze.stderr | lower)
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Register the fact that this role suspended dynamic updates
|
||||
ansible.builtin.file:
|
||||
path: '/var/lib/bind/.debops.frozen'
|
||||
state: 'touch'
|
||||
mode: '0644'
|
||||
changed_when: False
|
||||
when: bind__register_freeze.rc == 222
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Generate zone files
|
||||
ansible.builtin.template:
|
||||
src: 'var/lib/bind/views/view/zone/db.zone.j2'
|
||||
dest: '{{ (item.dir | d("/var/lib/bind/views/" + item.view + "/" + item.name)) + "/db.zone" }}'
|
||||
owner: '{{ item.owner | d("root") }}'
|
||||
group: '{{ item.group | d("bind") }}'
|
||||
mode: '{{ item.mode | d("0644") }}'
|
||||
force: '{{ item.force | d(False) }}'
|
||||
loop: '{{ bind__views | map(attribute="zones") | flatten }}'
|
||||
when:
|
||||
- item.content is defined
|
||||
- item.state | d("present") == "present"
|
||||
loop_control:
|
||||
label: '{{ (item.dir | d("/var/lib/bind/views/" + item.view + "/" + item.name)) + "/db.zone" }}'
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Divert the BIND configuration
|
||||
debops.debops.dpkg_divert:
|
||||
path: '/etc/bind/named.conf'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Generate BIND configuration
|
||||
ansible.builtin.template:
|
||||
src: 'etc/bind/named.conf.j2'
|
||||
dest: '/etc/bind/named.conf'
|
||||
owner: 'root'
|
||||
group: 'bind'
|
||||
mode: '0644'
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Make sure named is restarted, if necessary
|
||||
ansible.builtin.meta: 'flush_handlers'
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Check if dynamic updates should be resumed
|
||||
ansible.builtin.stat:
|
||||
path: '/var/lib/bind/.debops.frozen'
|
||||
changed_when: False
|
||||
register: bind__register_should_thaw
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Resume dynamic updates
|
||||
ansible.builtin.command: rndc thaw
|
||||
when: bind__register_should_thaw.stat.exists | d(False)
|
||||
register: bind__register_thaw
|
||||
until: bind__register_thaw is succeeded
|
||||
changed_when: False
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Remove dynamic update flag file
|
||||
ansible.builtin.file:
|
||||
path: '/var/lib/bind/.debops.frozen'
|
||||
state: 'absent'
|
||||
changed_when: False
|
||||
tags: [ 'role::bind:config' ]
|
||||
|
||||
- name: Install backup snapshot script
|
||||
ansible.builtin.copy:
|
||||
src: 'usr/local/sbin/debops-bind-snapshot'
|
||||
dest: '/usr/local/sbin/debops-bind-snapshot'
|
||||
owner: 'root'
|
||||
group: 'root'
|
||||
mode: '0755'
|
||||
tags: [ 'role::bind:backup' ]
|
||||
|
||||
- name: Configure backup snapshots as cron jobs
|
||||
ansible.builtin.cron:
|
||||
name: 'Create {{ item }} backup snapshots of BIND configuration and zones'
|
||||
special_time: '{{ item }}'
|
||||
cron_file: 'debops-bind-snapshot'
|
||||
user: 'root'
|
||||
state: '{{ "present"
|
||||
if (bind__snapshot_enabled | d(False) | bool
|
||||
and item in bind__snapshot_cron_jobs)
|
||||
else "absent" }}'
|
||||
job: '/usr/local/sbin/debops-bind-snapshot {{ item }}'
|
||||
loop: [ 'daily', 'weekly', 'monthly' ]
|
||||
loop_control:
|
||||
label: '{{ {"state": ("present"
|
||||
if (bind__snapshot_enabled | d(False) | bool
|
||||
and item in bind__snapshot_cron_jobs)
|
||||
else "absent"),
|
||||
"cron_job": item} }}'
|
||||
tags: [ 'role::bind:backup' ]
|
||||
|
||||
- name: Make sure that the PKI hook directory exists
|
||||
ansible.builtin.file:
|
||||
path: '{{ bind__pki_hook_path }}'
|
||||
state: 'directory'
|
||||
owner: 'root'
|
||||
group: 'root'
|
||||
mode: '0755'
|
||||
when: '"dot" in bind__features or "doh_https" in bind__features'
|
||||
tags: [ 'role::bind:pki' ]
|
||||
|
||||
- name: Install the BIND PKI hook
|
||||
ansible.builtin.template:
|
||||
src: 'etc/pki/hooks/bind.j2'
|
||||
dest: '{{ bind__pki_hook_path + "/" + bind__pki_hook_name }}'
|
||||
owner: 'root'
|
||||
group: 'root'
|
||||
mode: '0755'
|
||||
when: '"dot" in bind__features or "doh_https" in bind__features'
|
||||
tags: [ 'role::bind:pki' ]
|
||||
|
||||
- name: Remove the BIND PKI hook
|
||||
ansible.builtin.file:
|
||||
dest: '{{ bind__pki_hook_path + "/" + bind__pki_hook_name }}'
|
||||
state: 'absent'
|
||||
when: '"dot" not in bind__features and "doh_https" not in bind__features'
|
||||
tags: [ 'role::bind:pki' ]
|
||||
|
||||
- name: Install DNSSEC rollover configuration
|
||||
ansible.builtin.template:
|
||||
src: 'etc/bind/debops-bind-rollkey.json.j2'
|
||||
dest: '/etc/bind/debops-bind-rollkey.json'
|
||||
owner: 'root'
|
||||
group: 'root'
|
||||
mode: '0644'
|
||||
when: '"dnssec" in bind__features and bind__dnssec_script_enabled | d(False)'
|
||||
tags: [ 'role::bind:dnssec' ]
|
||||
|
||||
- name: Install DNSSEC rollover script
|
||||
ansible.builtin.copy:
|
||||
src: 'usr/local/sbin/debops-bind-rollkey'
|
||||
dest: '/usr/local/sbin/debops-bind-rollkey'
|
||||
owner: 'root'
|
||||
group: 'root'
|
||||
mode: '0755'
|
||||
when: '"dnssec" in bind__features and bind__dnssec_script_enabled | d(False)'
|
||||
tags: [ 'role::bind:dnssec' ]
|
||||
|
||||
- name: Install DNSSEC rollover external script
|
||||
ansible.builtin.copy:
|
||||
src: '{{ lookup("debops.debops.file_src",
|
||||
"usr/local/sbin/debops-bind-rollkey-action") }}'
|
||||
dest: '/usr/local/sbin/debops-bind-rollkey-action'
|
||||
owner: 'root'
|
||||
group: 'root'
|
||||
mode: '0755'
|
||||
when:
|
||||
- '"dnssec" in bind__features'
|
||||
- bind__dnssec_script_enabled | d(False)
|
||||
- bind__dnssec_script_method | d("") == 'external'
|
||||
tags: [ 'role::bind:dnssec' ]
|
||||
|
||||
- name: Enable DNSSEC rollover cron job
|
||||
ansible.builtin.cron:
|
||||
name: 'Rollover DNSSEC keys for BIND'
|
||||
special_time: 'weekly'
|
||||
cron_file: 'debops-bind-rollkey'
|
||||
user: 'root'
|
||||
job: '/usr/local/sbin/debops-bind-rollkey'
|
||||
state: '{{ "present"
|
||||
if ("dnssec" in bind__features and
|
||||
bind__dnssec_script_enabled | d(False))
|
||||
else "absent" }}'
|
||||
tags: [ 'role::bind:dnssec' ]
|
||||
|
||||
- name: Remove DNSSEC rollover script
|
||||
ansible.builtin.file:
|
||||
path: '{{ item }}'
|
||||
state: 'absent'
|
||||
loop:
|
||||
- '/usr/local/sbin/debops-bind-rollkey'
|
||||
- '/usr/local/sbin/debops-bind-rollkey-action'
|
||||
- '/etc/bind/debops-bind-rollkey.json'
|
||||
when: '"dnssec" not in bind__features or not bind__dnssec_script_enabled | d(False)'
|
||||
tags: [ 'role::bind:dnssec' ]
|
||||
214
ansible_collections/debops/debops/roles/bind/tasks/main_keys.yml
Normal file
214
ansible_collections/debops/debops/roles/bind/tasks/main_keys.yml
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
---
|
||||
# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
- name: Create sanitised list of keys
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_keys: '{{ bind__tmp_keys | d([])
|
||||
+ [item | combine({"state": item.state | d("present") | lower,
|
||||
"type": item.type | mandatory | upper,
|
||||
"dir": item.dir | d("/etc/bind/keys/"),
|
||||
"owner": item.owner | d("root"),
|
||||
"group": item.group | d("bind"),
|
||||
"include": (item.include | d(True) | bool)
|
||||
if item.type | d("") | upper == "TSIG"
|
||||
else False,
|
||||
"download": item.download | d(True if item.source | d("host") == "host" else False) | bool,
|
||||
"source": item.source | d("host"),
|
||||
"source_path": item.source_path | d(""),
|
||||
"remove_private_key": item.remove_private_key | d(True) | bool,
|
||||
"algorithm": (item.algorithm | mandatory)
|
||||
if item.type | d("") | upper == "TSIG"
|
||||
else ("%03d" | format(item.algorithm | mandatory | int))
|
||||
if item.type | d("") | upper == "SIG(0)"
|
||||
else omit,
|
||||
"creates": (item.name + ".key")
|
||||
if item.type | d("") | upper == "TSIG"
|
||||
else ("K" + item.name + "+" + "%03d" | format(item.algorithm | int) + "+*.key")
|
||||
if item.type | d("") | upper == "SIG(0)"
|
||||
else omit,
|
||||
"removes": (item.name + ".key")
|
||||
if item.type | d("") | upper == "TSIG"
|
||||
else ("K" + item.name + "+" + "%03d" | format(item.algorithm | int) + "+*.*")
|
||||
if item.type | d("") | upper == "SIG(0)"
|
||||
else omit,
|
||||
"public_key": (item.name + ".key")
|
||||
if item.type | d("") | upper == "TSIG"
|
||||
else ("K" + item.name + "+" + "%03d" | format(item.algorithm | int) + "+*.key")
|
||||
if item.type | d("") | upper == "SIG(0)"
|
||||
else omit,
|
||||
"private_key": (item.name + ".key")
|
||||
if item.type | d("") | upper == "TSIG"
|
||||
else ("K" + item.name + "+" + "%03d" | format(item.algorithm | int) + "+*.private")
|
||||
if item.type | d("") | upper == "SIG(0)"
|
||||
else omit})] }}'
|
||||
when:
|
||||
- item.name | d("") | length > 0
|
||||
- item.state | d("present") | lower in [ 'present', 'absent' ]
|
||||
loop: '{{ bind__combined_keys | d([]) | debops.debops.parse_kv_items(name="name") }}'
|
||||
loop_control:
|
||||
label: '{{ item.name | d("unknown") }}'
|
||||
tags: [ 'role::bind:config', 'role::bind:keys' ]
|
||||
|
||||
# Note, this ensures both:
|
||||
# a) that keynames are valid in the bind config; and
|
||||
# b) that working with unquoted globs is safe
|
||||
- name: Verify key sanity
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.name | regex_replace('([^a-zA-Z0-9-.])', '') == item.name
|
||||
- item.type in [ 'TSIG', 'SIG(0)' ]
|
||||
- item.algorithm != "000"
|
||||
- item.source in [ "host", "controller" ]
|
||||
- not (item.download and item.source == "controller")
|
||||
quiet: true
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
tags: [ 'role::bind:config', 'role::bind:keys' ]
|
||||
|
||||
- name: Create TSIG keys
|
||||
ansible.builtin.shell:
|
||||
chdir: '{{ item.dir }}'
|
||||
cmd: umask 027;
|
||||
tsig-keygen -a {{ item.algorithm | quote }} {{ item.name | quote }}
|
||||
> {{ item.creates | quote }} &&
|
||||
chown {{ (item.owner + ":" + item.group) | quote }} {{ item.creates | quote }}
|
||||
creates: '{{ item.creates }}'
|
||||
when:
|
||||
- item.state == 'present'
|
||||
- item.type == 'TSIG'
|
||||
- item.source == 'host'
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Fetch TSIG keys
|
||||
ansible.builtin.fetch:
|
||||
src: '{{ item.dir + "/" + item.public_key }}'
|
||||
dest: '{{ secret + "/bind/" + inventory_hostname + "/" }}'
|
||||
flat: True
|
||||
when:
|
||||
- item.state == 'present'
|
||||
- item.type == 'TSIG'
|
||||
- item.download
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Create SIG(0) keys
|
||||
ansible.builtin.shell:
|
||||
chdir: '{{ item.dir }}'
|
||||
cmd: umask 027;
|
||||
dnssec-keygen -C -a {{ item.algorithm | quote }} -n HOST -T KEY {{ item.name | quote }} &&
|
||||
chown {{ (item.owner + ":" + item.group) | quote }} {{ item.creates }}
|
||||
creates: '{{ item.creates }}'
|
||||
when:
|
||||
- item.state == 'present'
|
||||
- item.type == 'SIG(0)'
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Find SIG(0) keys to fetch
|
||||
ansible.builtin.find:
|
||||
paths: '{{ item.dir }}'
|
||||
use_regex: False
|
||||
recurse: False
|
||||
patterns:
|
||||
- '{{ item.public_key }}'
|
||||
- '{{ item.private_key }}'
|
||||
when:
|
||||
- item.state == 'present'
|
||||
- item.type == 'SIG(0)'
|
||||
- item.download
|
||||
register: bind__tmp_find_sig0_keys
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Build combined list of SIG(0) keys to fetch
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_sig0_fetch: '{{ bind__tmp_sig0_fetch | d([]) + item.files | map(attribute="path") }}' # noqa jinja[invalid]
|
||||
loop: '{{ bind__tmp_find_sig0_keys.results | d([]) }}'
|
||||
when: item.files is defined
|
||||
loop_control:
|
||||
label: '{{ item.item.name }}'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Fetch SIG(0) keys
|
||||
ansible.builtin.fetch:
|
||||
src: '{{ item }}'
|
||||
dest: '{{ secret + "/bind/" + inventory_hostname + "/" }}'
|
||||
flat: True
|
||||
loop: '{{ bind__tmp_sig0_fetch | d([]) }}'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Find SIG(0) private keys to remove
|
||||
ansible.builtin.find:
|
||||
paths: '{{ item.dir }}'
|
||||
use_regex: False
|
||||
recurse: False
|
||||
patterns:
|
||||
- '{{ item.private_key }}'
|
||||
when:
|
||||
- item.state == 'present'
|
||||
- item.type == 'SIG(0)'
|
||||
- item.remove_private_key
|
||||
register: bind__tmp_find_sig0_keys
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Build combined list of SIG(0) private keys to remove
|
||||
ansible.builtin.set_fact:
|
||||
bind__tmp_sig0_remove: '{{ bind__tmp_sig0_remove | d([]) + item.files | d([]) | map(attribute="path") }}'
|
||||
loop: '{{ bind__tmp_find_sig0_keys.results | d([]) }}'
|
||||
when: item.files is defined
|
||||
loop_control:
|
||||
label: '{{ item.item.name }}'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Remove SIG(0) private keys
|
||||
ansible.builtin.file:
|
||||
path: '{{ item }}'
|
||||
state: absent
|
||||
loop: '{{ bind__tmp_sig0_remove | d([]) }}'
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Remove TSIG/SIG(0) keys configured as absent
|
||||
ansible.builtin.shell: # noqa command-instead-of-shell no-free-form
|
||||
chdir: '{{ item.dir }}'
|
||||
cmd: rm -f {{ item.removes }}
|
||||
removes: '{{ item.removes }}'
|
||||
when:
|
||||
- item.state == 'absent'
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
||||
- name: Upload TSIG/SIG(0) keys from the controller
|
||||
ansible.builtin.copy:
|
||||
src: '{{ item.source_path if item.source_path.startswith("/") else secret + "/" + item.source_path }}'
|
||||
dest: '{{ item.dir + "/" + item.source_path | basename }}'
|
||||
owner: '{{ item.owner }}'
|
||||
group: '{{ item.group }}'
|
||||
mode: '0640'
|
||||
when:
|
||||
- item.state == 'present'
|
||||
- item.source == 'controller'
|
||||
loop: '{{ bind__tmp_keys | d([]) }}'
|
||||
loop_control:
|
||||
label: '{{ item.name }}'
|
||||
notify: [ 'Test named configuration and restart' ]
|
||||
tags: [ 'role::bind:keys' ]
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#!{{ ansible_python['executable'] }}
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# {{ ansible_managed }}
|
||||
|
||||
from __future__ import print_function
|
||||
from json import dumps
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
|
||||
def cmd_exists(cmd):
|
||||
return any(
|
||||
os.access(os.path.join(path, cmd), os.X_OK)
|
||||
for path in os.environ["PATH"].split(os.pathsep)
|
||||
)
|
||||
|
||||
|
||||
output = {'installed': cmd_exists('named')}
|
||||
|
||||
try:
|
||||
output['version'] = subprocess.check_output(
|
||||
["named", "-v"]
|
||||
).decode('utf-8').strip().split()[1]
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(dumps(output, sort_keys=True, indent=4))
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#}
|
||||
{% set output = { "_comments": [ ansible_managed ] } %}
|
||||
{% for opt in bind__dnssec_script_combined_configuration | debops.debops.parse_kv_items %}
|
||||
{% set _ = output.update({ opt.name : opt.value }) %}
|
||||
{% endfor %}
|
||||
{{ output | to_nice_json }}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
{# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org/>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#}
|
||||
//
|
||||
// /etc/bind/named.conf - Main configuration file for bind.
|
||||
//
|
||||
// {{ ansible_managed }}
|
||||
//
|
||||
{% macro print_zone(zone, commented, loop_first, level) %}
|
||||
{% if zone.state | d('present') not in [ 'absent', 'ignore', 'init' ] %}
|
||||
{% set prefix = ("{:^" + (level * global.indent) | string + "}").format('') %}
|
||||
{% set comment_prefix = (prefix + "// ") %}
|
||||
{% if commented or zone.state | d('present') == 'comment' %}
|
||||
{% set commented = True %}
|
||||
{% set prefix = comment_prefix %}
|
||||
{% endif %}
|
||||
{% if not loop_first %}
|
||||
{{ '' }}
|
||||
{% endif %}
|
||||
{% if zone.comment | d() %}
|
||||
{{ zone.comment | regex_replace('\n$', '') | comment(prefix='', decoration=comment_prefix, postfix='') -}}
|
||||
{% endif %}
|
||||
{{ '{}zone "{}" {{'.format(prefix, zone.name) }}
|
||||
{% for option in zone.options | d([]) %}
|
||||
{{ print_option(option, commented, loop.first, level + 1) -}}
|
||||
{% endfor %}
|
||||
{{ '{}}};'.format(prefix) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{##}
|
||||
{% macro print_views(commented, level) %}
|
||||
{% for view in bind__views | d([]) %}
|
||||
{% if view.state | d('present') not in [ 'absent', 'init', 'ignore' ] %}
|
||||
{% set global.view = view.view | d("<unknown>") %}
|
||||
{% set prefix = ("{:^" + (level * global.indent) | string + "}").format('') %}
|
||||
{% set comment_prefix = (prefix + "// ") %}
|
||||
{% if commented or view.state | d('present') == 'comment' %}
|
||||
{% set commented = True %}
|
||||
{% set prefix = comment_prefix %}
|
||||
{% endif %}
|
||||
{% if not loop.first %}
|
||||
{{ '' }}
|
||||
{% endif %}
|
||||
{% if view.comment | d() %}
|
||||
{{ view.comment | regex_replace('\n$', '') | comment(prefix='', decoration=comment_prefix, postfix='') -}}
|
||||
{% endif %}
|
||||
{% if view.view | d('_default') != '_default' %}
|
||||
{{ '{}view "{}" {{'.format(prefix, view.view) }}
|
||||
{% set level = level + 1 %}
|
||||
{% endif %}
|
||||
{% for option in view.options | d([]) %}
|
||||
{{ print_option(option, commented, loop.first, level) -}}
|
||||
{% endfor %}
|
||||
{% for zone in view.zones | d([]) %}
|
||||
{% set global.zone = zone.name | d("<unknown>") %}
|
||||
{% set global.zone_file = '"{}/db.zone"'.format(zone.dir | d("/var/lib/bind/views/" + zone.view + "/" + zone.name)) %}
|
||||
{{ print_zone(zone, commented, loop.first, level) -}}
|
||||
{% endfor %}
|
||||
{% if view.view | d('_default') != '_default' %}
|
||||
{{ '{}}};'.format(prefix) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{##}
|
||||
{% macro print_keys(commented, level) %}
|
||||
{% for key in bind__tmp_keys | d([]) %}
|
||||
{% if key.state | d('present') not in [ 'absent', 'init', 'ignore' ] %}
|
||||
{% set prefix = ("{:^" + (level * global.indent) | string + "}").format('') %}
|
||||
{% set comment_prefix = (prefix + "// ") %}
|
||||
{% if commented or key.state | d('present') == 'comment' %}
|
||||
{% set commented = True %}
|
||||
{% set prefix = comment_prefix %}
|
||||
{% endif %}
|
||||
{% if key.include | d(True) | bool and key.dir | d() and key.public_key | d() %}
|
||||
{{ '{}include "{}/{}";'.format(prefix, key.dir | regex_replace('/*$', ''), key.public_key) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{##}
|
||||
{% macro print_autovalue(option, autovalue, commented, level) %}
|
||||
{% set prefix = ("{:^" + (level * global.indent) | string + "}").format('') %}
|
||||
{% set comment_prefix = (prefix + "// ") %}
|
||||
{% if commented %}
|
||||
{% set commented = True %}
|
||||
{% set prefix = comment_prefix %}
|
||||
{% endif %}
|
||||
{% if autovalue == 'keys' %}
|
||||
{{ print_keys(commented, level) -}}
|
||||
{% elif autovalue == 'zones' %}
|
||||
{{ print_views(commented, level) -}}
|
||||
{% elif autovalue == 'zone_file_path' %}
|
||||
{{ '{}{} {};'.format(prefix, option.option | d(option.name), global.zone_file) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{##}
|
||||
{% macro print_option(option, commented, loop_first, level) %}
|
||||
{% if option.state | d('present') not in [ 'absent', 'init', 'ignore' ] %}
|
||||
{% set prefix = ("{:^" + (level * global.indent) | string + "}").format('') %}
|
||||
{% set comment_prefix = (prefix + "// ") %}
|
||||
{% if commented or option.state | d('present') == 'comment' %}
|
||||
{% set commented = True %}
|
||||
{% set prefix = comment_prefix %}
|
||||
{% endif %}
|
||||
{% if option.separator | d(False) or level == 0 or (option.comment | d() and not loop_first) %}
|
||||
{{ '' }}
|
||||
{% endif %}
|
||||
{% if option.comment | d() %}
|
||||
{{ option.comment | regex_replace('\n$', '') | comment(prefix='', decoration=comment_prefix, postfix='') -}}
|
||||
{% endif %}
|
||||
{% if option.raw | d() and commented %}
|
||||
{{ '{}'.format(option.raw | regex_replace('\n$', '') | comment(prefix='', decoration=comment_prefix, postfix='')) -}}
|
||||
{% elif option.raw | d() and not commented %}
|
||||
{{ '{}{}'.format(prefix, option.raw | regex_replace('\n$', '')) }}
|
||||
{% elif option.options | d() %}
|
||||
{{ '{}{} {{'.format(prefix, option.option | d(option.name) | regex_replace('\n$', '')) }}
|
||||
{% for option in option.options %}
|
||||
{{ print_option(option, commented, loop.first, level + 1) -}}
|
||||
{% endfor %}
|
||||
{{ '{}}};'.format(prefix) }}
|
||||
{% elif option.autovalue | d() %}
|
||||
{{ print_autovalue(option, option.autovalue, commented, level) -}}
|
||||
{% else %}
|
||||
{{ '{}{} {};'.format(prefix, option.option | d(option.name), option.value | d("")) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{##}
|
||||
{% set global = namespace() %}
|
||||
{% set global.indent = 8 %}
|
||||
{% set global.view = "<undefined>" %}
|
||||
{% set global.zone = "<undefined>" %}
|
||||
{% set global.zone_file = "<undefined>" %}
|
||||
{% for option in bind__combined_configuration | debops.debops.parse_kv_config %}
|
||||
{{ print_option(option, False, loop.first, 0) -}}
|
||||
{% endfor %}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2018-2022 DebOps <https://debops.org/>
|
||||
# Based on the postfix hook which is:
|
||||
# Copyright (C) 2020 Rainer Schuth <devel@reixd.net>
|
||||
# Copyright (C) 2018 Norbert Summer <git@o-g.at>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# {{ ansible_managed }}
|
||||
|
||||
# Reload or restart named on a certificate state change
|
||||
|
||||
set -o nounset -o pipefail -o errexit
|
||||
|
||||
bind_config="/etc/bind/named.conf"
|
||||
bind_action="{{ bind__pki_hook_action }}"
|
||||
bind_service="named.service"
|
||||
|
||||
# Check if current PKI realm is used by the 'named' server
|
||||
certificate=$(grep -r "${PKI_SCRIPT_DEFAULT_CRT:-}" ${bind_config} || true)
|
||||
|
||||
# Get list of current realm states
|
||||
read -r -a states <<< "$(echo "${PKI_SCRIPT_STATE:-}" | tr "," " ")"
|
||||
|
||||
if [ -n "${certificate}" ] && [[ ${states[*]} ]] ; then
|
||||
|
||||
for state in "${states[@]}" ; do
|
||||
|
||||
if [ "${state}" = "changed-certificate" ] || [ "${state}" = "changed-dhparam" ] ; then
|
||||
|
||||
# Check if current init is systemd
|
||||
if pidof systemd > /dev/null 2>&1 ; then
|
||||
|
||||
bind_state="$(systemctl is-active ${bind_service})"
|
||||
if [ "${bind_state}" = "active" ] ; then
|
||||
systemctl "${bind_action}" "${bind_service}"
|
||||
fi
|
||||
fi
|
||||
|
||||
break
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
fi
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
{# Copyright (C) 2022 David Härdeman <david@hardeman.nu>
|
||||
# Copyright (C) 2022 DebOps <https://debops.org>
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#}
|
||||
;
|
||||
{% if item.view | d("_default") == "_default" %}
|
||||
; BIND db file for zone {{ item.name }}
|
||||
{% else %}
|
||||
; BIND db file for zone {{ item.name }} in view {{ item.view }}
|
||||
{% endif %}
|
||||
;
|
||||
{% if item.comment | d() %}
|
||||
{{ item.comment | regex_replace('\n$', '') | comment(prefix='', decoration='; ', postfix='') -}}
|
||||
;
|
||||
{% endif %}
|
||||
|
||||
$TTL {{ item.ttl | d(bind__default_zone_ttl) }}
|
||||
$ORIGIN {{ item.origin | d(item.name) | regex_replace("\.*$", ".") }}
|
||||
@ IN SOA {{ item.soa_primary | d(bind__default_zone_soa_primary) }} {{ item.soa_email | d(bind__default_zone_soa_email) }} (
|
||||
{{ '{:<7}'.format(item.soa_serial | d(bind__default_zone_soa_serial) | int) }} ; Serial
|
||||
{{ '{:<7}'.format(item.soa_refresh | d(bind__default_zone_soa_refresh)) }} ; Refresh
|
||||
{{ '{:<7}'.format(item.soa_retry | d(bind__default_zone_soa_retry)) }} ; Retry
|
||||
{{ '{:<7}'.format(item.soa_expire | d(bind__default_zone_soa_expire)) }} ; Expire
|
||||
{{ '{:<7}'.format(item.soa_neg_ttl | d(bind__default_zone_soa_neg_ttl)) }} ; Negative Cache TTL
|
||||
)
|
||||
|
||||
{% if item.content is string %}
|
||||
{{ item.content }}
|
||||
{% elif item.content is iterable and item.content is not mapping and item.content[0] is string %}
|
||||
{% for line in item.content %}
|
||||
{{ line }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% set rrs = item.content | d([]) | debops.debops.parse_kv_items(name='name') %}
|
||||
{% set owner_maxlen = { 'value': 21 } %}
|
||||
{% for rr in rrs %}
|
||||
{% if rr.state | d("present") in [ "present", "commented" ] and rr.raw is not defined %}
|
||||
{% if rr.owner | d(rr.name) | d() and rr.type | d() and rr.rdata | d(rr.value) | d() %}
|
||||
{% set owner_len = rr.owner | d(rr.name) | length %}
|
||||
{% if rr.state | d("present") == "commented" %}
|
||||
{% set owner_len = owner_len + 2 %}
|
||||
{% endif %}
|
||||
{% if owner_len > owner_maxlen.value %}
|
||||
{% if owner_maxlen.update({ 'value': owner_len }) %}
|
||||
{# do nothing #}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for rr in rrs %}
|
||||
{% if rr.state | d("present") in [ "present", "commented "] %}
|
||||
{% if rr.comment | d() %}
|
||||
{{ rr.comment | regex_replace('\n$', '') | comment(prefix='', decoration='; ', postfix='') -}}
|
||||
{% endif %}
|
||||
{% if rr.state | d("present") == "commented" %}
|
||||
{% set prefix = "; " %}
|
||||
{% else %}
|
||||
{% set prefix = "" %}
|
||||
{% endif %}
|
||||
{% if rr.raw | d() %}
|
||||
{{ rr.raw | regex_replace('\n$', '') | comment(prefix='', decoration=prefix, postfix='') -}}
|
||||
{% elif rr.owner | d(rr.name) | d() and rr.type | d() and rr.rdata | d(rr.value) | d() %}
|
||||
{% set owner = rr.owner | d(rr.name) %}
|
||||
{% set ttl = rr.ttl | d("") %}
|
||||
{% set class = rr.class | d("") %}
|
||||
{% set type = rr.type %}
|
||||
{% set rdata = rr.rdata | d(rr.value) %}
|
||||
{{ ('{}{:<' + owner_maxlen.value | string + '} {:<7} {:<7} {:<7} {}').format(prefix, owner, ttl, class, type, rdata) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue