Switch everything to Python
This commit is contained in:
42
dns-confgen
Executable file
42
dns-confgen
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import dnsmgr
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate Bind config files.')
|
||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f'config: {e}')
|
||||
sys.exit(100)
|
||||
|
||||
try:
|
||||
views = sorted(set([zone.view for zone in manager.zones]))
|
||||
|
||||
for view in views:
|
||||
print(f'Generate config of view \'{view}\'... ', end='')
|
||||
manager.generate_config(view)
|
||||
print('OK')
|
||||
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(150)
|
||||
|
||||
print('Reloading named... ', end='')
|
||||
|
||||
try:
|
||||
manager.named_reload()
|
||||
print('OK')
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(170)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
71
dns-list
Executable file
71
dns-list
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import dnsmgr
|
||||
import sys
|
||||
|
||||
from json import dumps
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='List DNS zones.',
|
||||
formatter_class=lambda prog: argparse.HelpFormatter(
|
||||
prog, max_help_position=45, width=140))
|
||||
parser.add_argument('-a', '--all-zones', help='do not ignore zones that are not managed', action='store_true')
|
||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
||||
|
||||
output = parser.add_mutually_exclusive_group()
|
||||
output.add_argument('-j', '--json', help='print json format', action='store_true')
|
||||
output.add_argument('-J', '--json-pretty', help='print pretty json format', action='store_true')
|
||||
output.add_argument('-r', '--raw', help='print raw format', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f'config: {e}')
|
||||
sys.exit(100)
|
||||
|
||||
try:
|
||||
zones = manager.all_zones if args.all_zones else manager.zones
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(150)
|
||||
|
||||
zones.sort(key=lambda z: z.origin.to_text())
|
||||
|
||||
if args.raw:
|
||||
for zone in zones:
|
||||
name = zone.origin.to_text()
|
||||
managed = zone.cfgfile is not None
|
||||
print(f'{name}\t{zone.view}\t{zone.status}\t{managed}')
|
||||
elif args.json or args.json_pretty:
|
||||
json_output = [{
|
||||
'zone': zone.origin.to_text(),
|
||||
'view': zone.view,
|
||||
'status': zone.status,
|
||||
'managed': zone.cfgfile is not None} for zone in zones]
|
||||
if args.json_pretty:
|
||||
print(dumps(json_output, indent=2))
|
||||
else:
|
||||
print(dumps(json_output))
|
||||
|
||||
else:
|
||||
field_names = ['Zone', 'View', 'Status']
|
||||
if args.all_zones:
|
||||
field_names.append('Managed')
|
||||
|
||||
rows = []
|
||||
for zone in zones:
|
||||
name = zone.origin.to_unicode(omit_final_dot=True)
|
||||
row = [name, zone.view, zone.status]
|
||||
if args.all_zones:
|
||||
row.append(zone.cfgfile is not None)
|
||||
rows.append(row)
|
||||
print(dnsmgr.prettytable(field_names, rows))
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "${0}")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]...
|
||||
|
||||
List available DNS zones.
|
||||
|
||||
Options:
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
-j, --json print json format
|
||||
-J, --json-pretty print pretty json format (implies -j)
|
||||
-r, --raw print raw format
|
||||
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
json=false
|
||||
json_pretty=false
|
||||
raw=false
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-c|--config)
|
||||
if [ -z "$1" ]; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
config_file=$1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-j|--json)
|
||||
json=true
|
||||
;;
|
||||
-J|--json-pretty)
|
||||
json=true
|
||||
json_pretty=true
|
||||
;;
|
||||
-r|--raw)
|
||||
raw=true
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "$SCRIPT: invalid argument -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if $json && $raw; then
|
||||
echo "$SCRIPT: invalid options: --raw and --json are mutually exclusive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "$config_file" || exit 2
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 3
|
||||
source "$LIB_DIR"/output.sh || exit 3
|
||||
|
||||
zones_data=$(dns_zones) || exit 10
|
||||
|
||||
if $raw; then
|
||||
"$JQ" --raw-output '.[] | "\(.zone)\t\(.view)\t\(.status)"' <<<"$zones_data"
|
||||
elif $json; then
|
||||
jq_opts=""
|
||||
$json_pretty || jq_opts="--compact-output"
|
||||
"$JQ" $jq_opts <<<"$zones_data"
|
||||
else
|
||||
while read -r zone view status; do
|
||||
zone=$("$IDN2" --decode <<<"$zone")
|
||||
echo "$zone$TAB$view$TAB$status"
|
||||
done < <("$JQ" --raw-output '.[] | "\(.zone)\t\(.view)\t\(.status)"' <<<"$zones_data") | table_output "ZONE" "VIEW" "STATUS"
|
||||
fi
|
||||
85
dns-record
85
dns-record
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "$0")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]... COMMAND [COMMAND OPTIONS]
|
||||
|
||||
Manage DNS records.
|
||||
|
||||
Commands:
|
||||
* help [COMMAND] show help message of commands
|
||||
* add add record to zone
|
||||
* delete delete record from zone
|
||||
|
||||
Options:
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
cmd=""
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-c|--config)
|
||||
if [ -z "$1" ]; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
config_file=$1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
cmd=$opt
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cmd=${cmd,,:-help}
|
||||
|
||||
source "$config_file" || exit 1
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 1
|
||||
source "$LIB_DIR"/output.sh || exit 1
|
||||
|
||||
params="--interactive"
|
||||
|
||||
if [ "$cmd" == "help" ]; then
|
||||
params="--help"
|
||||
cmd=${1,,}
|
||||
shift
|
||||
fi
|
||||
|
||||
case "$cmd" in
|
||||
add)
|
||||
"$SCRIPT_DIR"/dns-record-add $params "$@"
|
||||
;;
|
||||
del|delete)
|
||||
"$SCRIPT_DIR"/dns-record-delete $params "$@"
|
||||
;;
|
||||
"")
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "$SCRIPT: invalid command -- '$cmd'" >&2
|
||||
exit 5
|
||||
;;
|
||||
esac
|
||||
214
dns-record-add
214
dns-record-add
@@ -1,152 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env python3
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "$0")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
import argparse
|
||||
import dns.rdataclass
|
||||
import dns.rdataset
|
||||
import dnsmgr
|
||||
import sys
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW] NAME TTL TYPE VALUE
|
||||
|
||||
Add new records to a DNS zone.
|
||||
def main():
|
||||
preparser = argparse.ArgumentParser(add_help=False)
|
||||
preparser.add_argument('-b', '--batch', action='store_true')
|
||||
preargs, args = preparser.parse_known_args()
|
||||
nargs = None if preargs.batch else '?'
|
||||
nvalueargs = '+' if preargs.batch else '*'
|
||||
|
||||
Options:
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
-f, --force add records without confirmation prompt
|
||||
-i, --interactive interactively ask for missing arguments
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
parser = argparse.ArgumentParser(description='Add DNS records.')
|
||||
parser.add_argument('-a', '--all-zones', help='allow zones that are not managed', action='store_true')
|
||||
parser.add_argument('-A', '--all-types', help='allow unsupported record types', action='store_true')
|
||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||
parser.add_argument('name', metavar='NAME', nargs=nargs, help='DNS record name', default=None)
|
||||
parser.add_argument('ttl', metavar='TTL', nargs=nargs, help='DNS record TTL in seconds', type=int, default=None)
|
||||
parser.add_argument('type', metavar='TYPE', nargs=nargs, help='DNS record type', default=None)
|
||||
parser.add_argument('value', metavar='VALUE', nargs=nvalueargs, help='DNS record value, multiple values are choined by a space character', default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
force=false
|
||||
interactive=false
|
||||
try:
|
||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f'config: {e}')
|
||||
sys.exit(100)
|
||||
|
||||
declare -a args=()
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-c|--config)
|
||||
if [ -z "$1" ]; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
config_file=$1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-f|--force)
|
||||
force=true
|
||||
;;
|
||||
-i|--interactive)
|
||||
interactive=true
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$opt")
|
||||
if (( ${#args[@]} > 6 )); then
|
||||
echo "$SCRIPT: invalid argument -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
try:
|
||||
if args.zone is None:
|
||||
zones = manager.select_zones(args.all_zones)
|
||||
else:
|
||||
zones = manager.get_zones(args.zone, args.all_zones)
|
||||
|
||||
source "$config_file" || exit 2
|
||||
origin = zones[0].origin
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 3
|
||||
if args.name is None:
|
||||
name = dnsmgr.input_name(origin, prompt='Record name')
|
||||
else:
|
||||
name = dnsmgr.name_from_text(args.name, origin)
|
||||
|
||||
set -- "${args[@]}"
|
||||
if args.ttl is None:
|
||||
ttl = dnsmgr.input_ttl()
|
||||
else:
|
||||
ttl = dnsmgr.ttl_from_text(args.ttl)
|
||||
|
||||
zone=$1
|
||||
if shift; then
|
||||
dns_check_zone_view "$zone" zone view || exit 10
|
||||
elif $interactive; then
|
||||
dns_select_zone zone view || exit 11
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- ZONE[@VIEW]" >&2
|
||||
exit 1
|
||||
fi
|
||||
if args.type is None:
|
||||
rdtype = dnsmgr.select_type(args.all_types)
|
||||
else:
|
||||
rdtype = dnsmgr.type_from_text(args.type, args.all_types)
|
||||
|
||||
name=$1
|
||||
if shift; then
|
||||
dns_check_record_name "$name" name || exit 21
|
||||
elif $interactive; then
|
||||
dns_read_record_name name || exit 10
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
if args.value is None:
|
||||
rdata = dnsmgr.input_rdata(rdtype, origin)
|
||||
else:
|
||||
rdata = dnsmgr.rdata_from_text(rdtype, ' '.join(args.value), origin)
|
||||
|
||||
ttl=$1
|
||||
if shift; then
|
||||
dns_check_ttl "$ttl" || exit 22
|
||||
elif $interactive; then
|
||||
dns_read_ttl ttl || exit 10
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- TTL" >&2
|
||||
exit 1
|
||||
fi
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(150)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
|
||||
rtype=$1
|
||||
if shift; then
|
||||
dns_check_record_type "$rtype" || exit 23
|
||||
elif $interactive; then
|
||||
dns_select_record_type rtype || exit 10
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- TYPE" >&2
|
||||
exit 1
|
||||
fi
|
||||
rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, rdtype, ttl=ttl)
|
||||
rdataset.add(rdata)
|
||||
|
||||
value=$1
|
||||
if shift; then
|
||||
dns_check_record_value "$rtype" "$value" value || exit 24
|
||||
elif $interactive; then
|
||||
dns_read_record_value "$rtype" value || exit 10
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- VALUE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if not args.batch:
|
||||
for zone in zones:
|
||||
text = rdataset.to_text(origin=zone.origin, relativize=False)
|
||||
print(f'View: {zone.view}')
|
||||
print(f'\033[32m+ {name} {text}\033[0m\n')
|
||||
|
||||
if [ "${view}" == "*" ]; then
|
||||
json_array_to_bash views < <(dns_zone_views "$zone")
|
||||
else
|
||||
views=("$view")
|
||||
fi
|
||||
if not dnsmgr.input_yes_no():
|
||||
sys.exit(0)
|
||||
|
||||
if ! $force; then
|
||||
for view in "${views[@]}"; do
|
||||
echo "View: $view"
|
||||
output=$(dns_record_add "true" "$zone" "$view" "$name" "$ttl" "$rtype" "$value" 2>&1)
|
||||
if (( $? == 0 )); then
|
||||
echo -n -e "\e[32m+ $TAB"
|
||||
echo -n -e "$output\e[0m" | grep --color=never -v -E '^(Outgoing update query:|;.*)?$'
|
||||
else
|
||||
echo -e "\e[31mERROR:\n" >&2
|
||||
echo -e "$output\e[0m" >&2
|
||||
exit 30
|
||||
fi
|
||||
done
|
||||
echo
|
||||
! yes_no "Proceed?" && echo -e "Aborted" && exit
|
||||
echo
|
||||
fi
|
||||
for zone in zones:
|
||||
origin = zone.origin.to_text(omit_final_dot=True)
|
||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
origin = f'{origin}@{zone.view}'
|
||||
print(f"Sending DDNS updates for '{origin}'... ", end='')
|
||||
|
||||
echo -n "Sending DDNS update(s)... "
|
||||
for view in "${views[@]}"; do
|
||||
output=$(dns_record_add "false" "$zone" "$view" "$name" "$ttl" "$rtype" "$value" 2>&1)
|
||||
if (( $? != 0 )); then
|
||||
echo -e "ERROR\n" >&2
|
||||
echo "$output" >&2
|
||||
exit 31
|
||||
fi
|
||||
done
|
||||
echo "OK"
|
||||
try:
|
||||
manager.add_zone_record(zone, name, rdataset)
|
||||
print('OK')
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(160)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,203 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env python3
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "$0")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
import argparse
|
||||
import dns.rdataclass
|
||||
import dnsmgr
|
||||
import sys
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW] NAME TYPE [VALUE]
|
||||
|
||||
Delete DNS records.
|
||||
def main():
|
||||
preparser = argparse.ArgumentParser(add_help=False)
|
||||
preparser.add_argument('-b', '--batch', action='store_true')
|
||||
preargs, args = preparser.parse_known_args()
|
||||
nargs = None if preargs.batch else '?'
|
||||
|
||||
Options:
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
-f, --force delete records without confirmation prompt
|
||||
-i, --interactive interactively ask for missing arguments
|
||||
parser = argparse.ArgumentParser(description='Delete DNS records.')
|
||||
parser.add_argument('-a', '--all-zones', help='allow zones that are not managed', action='store_true')
|
||||
parser.add_argument('-A', '--all-types', help='allow unsupported record types', action='store_true')
|
||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||
parser.add_argument('name', metavar='NAME', nargs=nargs, help='DNS record name', default=None)
|
||||
parser.add_argument('type', metavar='TYPE', nargs=nargs, help='DNS record type', default=None)
|
||||
parser.add_argument('value', metavar='VALUE', nargs='*', help='DNS record value, multiple values are choined by a space character', default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
try:
|
||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f'config: {e}')
|
||||
sys.exit(100)
|
||||
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
force=false
|
||||
interactive=false
|
||||
try:
|
||||
if args.zone is None:
|
||||
zones = manager.select_zones(args.all_zones)
|
||||
else:
|
||||
zones = manager.get_zones(args.zone, args.all_zones)
|
||||
|
||||
declare -a args=()
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-c|--config)
|
||||
if [ -z "$1" ]; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
config_file=$1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-i|--interactive)
|
||||
interactive=true
|
||||
;;
|
||||
-f|--force)
|
||||
force=true
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$opt")
|
||||
if (( ${#args[@]} > 5 )); then
|
||||
echo "$SCRIPT: invalid argument -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
for zone in zones:
|
||||
manager.get_zone_content(zone)
|
||||
|
||||
source "$config_file" || exit 2
|
||||
origin = zones[0].origin
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 3
|
||||
if args.name is None:
|
||||
names = sorted(set([name.to_unicode() for name in zone for zone in zones]))
|
||||
rows = [[name] for name in names]
|
||||
index = dnsmgr.prettyselect(['Record name'], rows, prompt='Select record name')
|
||||
args.name = names[index]
|
||||
|
||||
set -- "${args[@]}"
|
||||
name = dnsmgr.name_from_text(args.name, origin)
|
||||
|
||||
zone=$1
|
||||
if shift; then
|
||||
dns_check_zone_view "$zone" zone view || exit 10
|
||||
elif $interactive; then
|
||||
dns_select_zone zone view || exit 11
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- ZONE[@VIEW]" >&2
|
||||
exit 1
|
||||
fi
|
||||
for zone in zones:
|
||||
zone.filter_by_name(name, origin)
|
||||
|
||||
declare -A zone_data
|
||||
zones = list(filter(lambda zone: zone.nodes, zones))
|
||||
if not zones:
|
||||
raise RuntimeError(f"No such DNS record -- '{name.to_text(True)}'")
|
||||
|
||||
if [ "${view}" == "*" ]; then
|
||||
json_array_to_bash views < <(dns_zone_views "$zone")
|
||||
else
|
||||
views=("$view")
|
||||
fi
|
||||
if args.type is None:
|
||||
rdtypes = sorted(set([rdataset.rdtype for rdataset in zone.get_node(name) for zone in zones]))
|
||||
if not args.all_types:
|
||||
rdtypes = list(filter(lambda rdtype: rdtype in dnsmgr.RECORD_TYPES, rdtypes))
|
||||
rdtypes = [rdtype.to_text(rdtype) for rdtype in rdtypes]
|
||||
rows = [[rdtype] for rdtype in rdtypes]
|
||||
index = dnsmgr.prettyselect(['Record type'], rows, prompt='Select record type')
|
||||
args.type = rdtypes[index]
|
||||
|
||||
records=$(
|
||||
for view in "${views[@]}"; do
|
||||
dns_zone "$zone" "$view" || exit 22
|
||||
done | "$JQ" --compact-output --slurp 'add'
|
||||
)
|
||||
rdtype = dnsmgr.type_from_text(args.type, args.all_types)
|
||||
|
||||
name=$1
|
||||
if shift; then
|
||||
dns_check_record_name "$name" name || exit 21
|
||||
elif $interactive; then
|
||||
if (( ${#zone_data[@]} > 1 )); then
|
||||
dns_read_record_name name || exit 10
|
||||
else
|
||||
json_array_to_bash names < <("$JQ" --compact-output '[ .[] | .name ] | sort | unique' <<<"$records")
|
||||
COLUMNS=30
|
||||
echo -e "Select record name:\n"
|
||||
select name in "${names[@]}"; do
|
||||
[ -n "$name" ] && break
|
||||
done
|
||||
[ -z "$name" ] && echo "ERROR: record name selection failed" >&2 && exit 11
|
||||
echo
|
||||
fi
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
for zone in zones:
|
||||
zone.filter_by_rdtype(rdtype)
|
||||
|
||||
records=$("$JQ" --compact-output --arg name "$name" '[ .[] | select(.name == $name) ]' <<<"$records")
|
||||
[ "$records" == "[]" ] && echo "ERROR: no such record -- '$name'" >&2 && exit 5
|
||||
zones = list(filter(lambda zone: zone.nodes, zones))
|
||||
if not zones:
|
||||
raise RuntimeError(f"No such {rdtype.to_text(rdtype)} record -- '{name.to_text(True)}'")
|
||||
|
||||
json_array_to_bash rtypes < <("$JQ" --compact-output '[ .[] | .type ] | sort | unique' <<<"$records")
|
||||
rdata = None
|
||||
if not args.value and not args.batch and not dnsmgr.input_yes_no(f'Delete all {rdtype.to_text(rdtype)}-records?'):
|
||||
values = []
|
||||
for zone in zones:
|
||||
for rdataset in zone.get_node(name):
|
||||
for rdata in rdataset:
|
||||
values.append(rdata.to_text(origin=zone.origin, relativize=False))
|
||||
values = sorted(set(values))
|
||||
rows = [[value] for value in values]
|
||||
index = dnsmgr.prettyselect(['Record value'], rows, prompt='Select record value', truncate=True)
|
||||
args.value = [values[index]]
|
||||
|
||||
rtype=${1^^}
|
||||
if shift; then
|
||||
dns_check_record_type "$rtype" || exit 23
|
||||
elif $interactive; then
|
||||
if [ "$view" == "*" ]; then
|
||||
dns_select_record_type rtype || exit 10
|
||||
else
|
||||
echo -e "Select record type:\n"
|
||||
select rtype in "${rtypes[@]}"; do
|
||||
[ -n "$rtype" ] && break
|
||||
done
|
||||
[ -z "$name" ] && echo "ERROR: record type selection failed" >&2 && exit 11
|
||||
echo
|
||||
fi
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- TYPE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if args.value:
|
||||
value = ' '.join(args.value)
|
||||
rdata = dnsmgr.rdata_from_text(rdtype, value, origin)
|
||||
for zone in zones:
|
||||
zone.filter_by_rdata(rdata)
|
||||
|
||||
records=$("$JQ" --compact-output --arg rtype "$rtype" '[ .[] | select(.type == $rtype) ]' <<<"$records")
|
||||
[ "$records" == "[]" ] && echo "ERROR: no ${rtype} record found" >&2 && exit 5
|
||||
zones = list(filter(lambda zone: zone.nodes, zones))
|
||||
if not zones:
|
||||
raise RuntimeError(f"No such DNS record found -- {name.to_text(True)} IN {rdtype.to_text(rdtype)} {value}")
|
||||
|
||||
json_array_to_bash values < <("$JQ" --compact-output '[ .[] | .value ] | sort | unique' <<<"$records")
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(150)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
|
||||
value=$1
|
||||
if shift; then
|
||||
dns_check_record_value "$rtype" "$value" value || exit 24
|
||||
elif $interactive; then
|
||||
if [ "$view" == "*" ]; then
|
||||
dns_read_record_value "$rtype" value || exit 10
|
||||
else
|
||||
if ! yes_no "Delete all ${rtype} records?"; then
|
||||
echo -e "\nSelect value:\n"
|
||||
select value in "${values[@]}"; do
|
||||
[ -n "$value" ] && break
|
||||
done
|
||||
fi
|
||||
[ -z "$name" ] && echo "ERROR: invalid answer" >&2 && exit 11
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
zones.sort(key=lambda zone: zone.view)
|
||||
|
||||
if [ -n "$value" ]; then
|
||||
decoded_value=$(dns_decode_txt_value "$value")
|
||||
records=$("$JQ" --compact-output --arg value "$decoded_value" '[ .[] | select(.decoded_value == $value) ]' <<<"$records")
|
||||
[ "$records" == "[]" ] && echo "ERROR: no $rtype record matches value" >&2 && exit 6
|
||||
fi
|
||||
if not args.batch:
|
||||
for zone in zones:
|
||||
print(f'View: {zone.view}')
|
||||
node = zone.find_node(name)
|
||||
rdataset = node.find_rdataset(dns.rdataclass.IN, rdtype)
|
||||
rdclassstr = rdataset.rdclass.to_text(rdataset.rdclass)
|
||||
rdtypestr = rdataset.rdtype.to_text(rdataset.rdtype)
|
||||
for rdata in rdataset:
|
||||
text = rdata.to_text(origin=zone.origin, relativize=False)
|
||||
print(f'\033[31m- {name} {rdataset.ttl} {rdclassstr} {rdtypestr} {text}\033[0m\n')
|
||||
|
||||
json_array_to_bash views < <("$JQ" --compact-output '[ .[] | .view ] | sort | unique' <<<"$records")
|
||||
if not dnsmgr.input_yes_no():
|
||||
sys.exit(0)
|
||||
|
||||
if ! $force; then
|
||||
for view in "${views[@]}"; do
|
||||
echo "View: $view"
|
||||
json_array_to_bash values < <("$JQ" --compact-output '[ .[] | .value ] | sort | unique' <<<"$records")
|
||||
for value in "${values[@]}"; do
|
||||
output=$(dns_record_delete "true" "$zone" "$view" "$name" "$rtype" "$value" 2>&1)
|
||||
if (( $? == 0 )); then
|
||||
echo -n -e "\e[31m- $TAB"
|
||||
echo -n -e "$output\e[0m" | grep --color=never -v -E '^(Outgoing update query:|;.*)?$'
|
||||
else
|
||||
echo -e "\e[31mERROR:\n" >&2
|
||||
echo -e "$output\e[0m" >&2
|
||||
exit 30
|
||||
fi
|
||||
done
|
||||
done
|
||||
echo
|
||||
! yes_no "Proceed?" && echo -e "Aborted" && exit
|
||||
echo
|
||||
fi
|
||||
for zone in zones:
|
||||
origin = zone.origin.to_text(omit_final_dot=True)
|
||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
origin = f'{origin}@{zone.view}'
|
||||
print(f"Sending DDNS updates for '{origin}'... ", end='')
|
||||
|
||||
echo -n "Sending DDNS update(s)... "
|
||||
for view in "${views[@]}"; do
|
||||
json_array_to_bash values < <("$JQ" --compact-output '[ .[] | .value ] | sort | unique' <<<"$records")
|
||||
for value in "${values[@]}"; do
|
||||
output=$(dns_record_delete "false" "$zone" "$view" "$name" "$rtype" "$value" 2>&1)
|
||||
if (( $? != 0 )); then
|
||||
echo -e "ERROR updating view -- '$view'\n" >&2
|
||||
echo "$output" >&2
|
||||
exit 31
|
||||
fi
|
||||
done
|
||||
done
|
||||
echo "OK"
|
||||
node = zone.find_node(name)
|
||||
rdataset = node.find_rdataset(dns.rdataclass.IN, rdtype)
|
||||
|
||||
try:
|
||||
manager.delete_zone_record(zone, name, rdataset)
|
||||
print('OK')
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(160)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
83
dns-zone
83
dns-zone
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "${0}")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]... COMMAND [COMMAND OPTIONS]
|
||||
|
||||
Manage DNS zones.
|
||||
|
||||
Commands:
|
||||
* add add new zone
|
||||
* delete delete zone
|
||||
* help [COMMAND] show help message of commands
|
||||
* list show zone content
|
||||
|
||||
Options:
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
cmd=""
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-c|--config)
|
||||
if [ -z "$1" ]; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
config_file=$1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
cmd=$opt
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cmd=${cmd,,:-help}
|
||||
|
||||
source "$config_file" || exit 2
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 3
|
||||
source "$LIB_DIR"/output.sh || exit 3
|
||||
|
||||
params="--interactive"
|
||||
|
||||
if [ "$cmd" == "help" ]; then
|
||||
params="--help"
|
||||
cmd=${1,,}
|
||||
shift
|
||||
fi
|
||||
|
||||
case "$cmd" in
|
||||
add|delete|list)
|
||||
"$SCRIPT_DIR"/dns-zone-$cmd $params "$@"
|
||||
;;
|
||||
"")
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "$SCRIPT: invalid command -- '$cmd'" >&2
|
||||
exit 5
|
||||
;;
|
||||
esac
|
||||
264
dns-zone-add
264
dns-zone-add
@@ -1,175 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env python3
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "${0}")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
import argparse
|
||||
import dns.rdataclass
|
||||
import dns.rdataset
|
||||
import dns.rdatatype
|
||||
import dnsmgr
|
||||
import sys
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW]
|
||||
from time import sleep
|
||||
|
||||
Add new DNS zones.
|
||||
|
||||
Options:
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
-f, --force add zones without confirmation prompt
|
||||
-i, --interactive interactively ask for missing arguments
|
||||
-t, --config-template config file/template (overrides value set in ZONE_TEMPLATES config option)
|
||||
-z, --zone-template zone file/template (overrides value set in ZONE_TEMPLATES config option)
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
def main():
|
||||
preparser = argparse.ArgumentParser(add_help=False)
|
||||
preparser.add_argument('-b', '--batch', action='store_true')
|
||||
preargs, args = preparser.parse_known_args()
|
||||
nargs = None if preargs.batch else '?'
|
||||
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
config_template=""
|
||||
force=false
|
||||
interactive=false
|
||||
zone=""
|
||||
zone_template=""
|
||||
parser = argparse.ArgumentParser(description='Add DNS zones.')
|
||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
||||
parser.add_argument('-t', '--config-template', help='config file/template (overrides value set in ZONE_TEMPLATES config option)', default=None)
|
||||
parser.add_argument('-z', '--zone-template', help='zone file/template (overrides value set in ZONE_TEMPLATES config option)', default=None)
|
||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
declare -a args=()
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-c|--config)
|
||||
config_file=$1
|
||||
if ! shift; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-f|--force)
|
||||
force=true
|
||||
;;
|
||||
-i|--interactive)
|
||||
interactive=true
|
||||
;;
|
||||
-t|--config-template)
|
||||
config_template=$1
|
||||
if ! shift; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-z|--zone-template)
|
||||
zone_template=$1
|
||||
if ! shift; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$opt")
|
||||
if (( ${#args[@]} > 1 )); then
|
||||
echo "$SCRIPT: invalid argument -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
try:
|
||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f'config: {e}')
|
||||
sys.exit(100)
|
||||
|
||||
source "$config_file" || exit 2
|
||||
managed_views = sorted(manager.config.zones_config.keys())
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 3
|
||||
source "$LIB_DIR"/output.sh || exit 3
|
||||
try:
|
||||
if args.zone is None:
|
||||
name = dnsmgr.input_name()
|
||||
rows = [[view] for view in managed_views]
|
||||
index = dnsmgr.prettyselect(['View'], rows, prompt='Select view', also_valid=['*'])
|
||||
views = managed_views if index == '*' else [managed_views[index]]
|
||||
else:
|
||||
(name, views) = dnsmgr.name_views_from_text(args.zone)
|
||||
if views is None:
|
||||
if len(managed_views) > 1:
|
||||
raise RuntimeError('multiple managed views configured but none specified')
|
||||
elif managed_views[0] != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
raise RuntimeError('the default view is not managed')
|
||||
views = managed_views
|
||||
elif views == '*':
|
||||
views = managed_views
|
||||
else:
|
||||
for view in views:
|
||||
if view not in managed_views:
|
||||
raise RuntimeError(f'managed view does not exist -- \'{view}\'')
|
||||
|
||||
set -- "${args[@]}"
|
||||
existing_views = [zone.view for zone in filter(lambda zone: zone.origin == name and zone.view in views, manager.all_zones)]
|
||||
if existing_views:
|
||||
views = 'and '.join(existing_views)
|
||||
raise RuntimeError(f'zone already exists in view {views}')
|
||||
|
||||
zone=$1
|
||||
if shift; then
|
||||
dns_check_zone_name_view "$zone" zone view || exit 10
|
||||
elif $interactive; then
|
||||
dns_read_zone_view zone view || exit 11
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- ZONE[@VIEW]" >&2
|
||||
exit 1
|
||||
fi
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(150)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
|
||||
declare -A output
|
||||
if [ "${view}" == "*" ]; then
|
||||
json_array_to_bash views < <(dns_zone_views)
|
||||
else
|
||||
views=("$view")
|
||||
fi
|
||||
if not args.batch:
|
||||
for view in views:
|
||||
origin = name.to_text(omit_final_dot=True)
|
||||
print(f'View: {view}')
|
||||
print(f'\033[32m+ {origin}\033[0m\n')
|
||||
|
||||
for view in "${views[@]}"; do
|
||||
dns_get_base_config "$view" zone_dir conf_dir || exit 13
|
||||
if not dnsmgr.input_yes_no():
|
||||
sys.exit(0)
|
||||
|
||||
zone_conf_file="$conf_dir/$zone.conf"
|
||||
[ -f "$zone_conf_file" ] && echo "ERROR: config file already exists -- '$zone_conf_file'" >&2 && exit 14
|
||||
zones = []
|
||||
for view in views:
|
||||
origin = name.to_text(omit_final_dot=True)
|
||||
if len(views) > 1 or view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
origin = f'{origin}@{view}'
|
||||
print(f"Adding zone '{origin}'... ", end='')
|
||||
|
||||
zone_file="$zone_dir/$zone.zone"
|
||||
[ -f "$zone_file" ] && echo "ERROR: zone file already exists -- '$zone_file'" >&2 && exit 15
|
||||
try:
|
||||
zone = manager.add_zone(name, view, args.config_template, args.zone_template)
|
||||
manager.generate_config(view)
|
||||
print('OK')
|
||||
if manager.config.zones_config[view].catalog_zone:
|
||||
zones.append(zone)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(160)
|
||||
|
||||
IFS=":" read -r cfg_zone_template cfg_config_template <<<"${ZONE_TEMPLATES["$view"]}"
|
||||
try:
|
||||
print('Reloading named... ', end='')
|
||||
manager.named_reload()
|
||||
print('OK')
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(170)
|
||||
|
||||
conf_template=${config_template:-${cfg_config_template}}
|
||||
[ -z "$conf_template" ] && echo "ERROR: config template not configured nor specified by '-t' option" >&2 && exit 16
|
||||
! [ -f "$conf_template" ] && echo "ERROR: zone config template: no such file -- '$conf_template'" >&2 && exit 17
|
||||
if zones:
|
||||
sleep(2)
|
||||
|
||||
zone_template=${zone_template:-${cfg_zone_template}}
|
||||
[ -z "$zone_template" ] && echo "ERROR: zone template not configured nor specified by '-z' option" >&2 && exit 18
|
||||
! [ -f "$zone_template" ] && echo "ERROR: zone template: no such file -- '$zone_template'" >&2 && exit 19
|
||||
for zone in zones:
|
||||
catalog_zone_name = manager.config.zones_config[zone.view].catalog_zone
|
||||
try:
|
||||
catalog_zones = manager.get_zones(catalog_zone_name, all_zones=True)
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError(f'catalog zone of view \'{zone.view}\': {e}')
|
||||
|
||||
dns_check_zone_view "$zone@$view" &>/dev/null && echo "ERROR: non-managed zone already exists in DNS -- '$zone@$view'" >&2 && exit 16
|
||||
done
|
||||
origin = zone.origin.to_text(omit_final_dot=True)
|
||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
origin = f'{origin}@{zone.view}'
|
||||
|
||||
if ! $force; then
|
||||
for view in "${views[@]}"; do
|
||||
echo "View: $view"
|
||||
echo -e "\e[32m+ $TAB$zone\e[0m"
|
||||
done
|
||||
echo
|
||||
! yes_no "Proceed?" && echo -e "Aborted" && exit
|
||||
echo
|
||||
fi
|
||||
for catalog_zone in catalog_zones:
|
||||
rdata = dnsmgr.rdata_from_text(dns.rdatatype.PTR, zone.origin.to_text(), catalog_zone.origin)
|
||||
rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.PTR, ttl=3600)
|
||||
rdataset.add(rdata)
|
||||
rdname = dns.name.from_text(zone.nfz() + '.zones', catalog_zone.origin)
|
||||
catalog_zone_origin = catalog_zone.origin.to_text(omit_final_dot=True)
|
||||
if catalog_zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
catalog_zone_origin += f'@{catalog_zone.view}'
|
||||
|
||||
echo -n "Adding zone to config... "
|
||||
for view in "${views[@]}"; do
|
||||
dns_get_base_config "$view" zone_dir conf_dir conf_file || exit 13
|
||||
try:
|
||||
print(f'Adding zone \'{origin}\' to catalog zone \'{catalog_zone_origin}\'... ', end='')
|
||||
manager.add_zone_record(catalog_zone, rdname, rdataset)
|
||||
print('OK')
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
|
||||
zone_conf_file="$conf_dir/$zone.conf"
|
||||
zone_file="$zone_dir/$zone.zone"
|
||||
|
||||
IFS=":" read -r cfg_zone_template cfg_config_template <<<"${ZONE_TEMPLATES["$view"]}"
|
||||
conf_template=${config_template:-${cfg_config_template}}
|
||||
zone_template=${zone_template:-${cfg_zone_template}}
|
||||
|
||||
! sed "s#%ZONE%#$zone#g;s#%ZONE_FILE%#$zone_file#g" "$conf_template" >"$zone_conf_file" && echo "ERROR: unable to write to config file -- '$zone_conf_file'" >&2 && exit 20
|
||||
! sed "s#%ZONE%#$zone#g" "$zone_template" >"$zone_file" && echo "ERROR: unable to write to zone file -- '$zone_file'" >&2 && exit 21
|
||||
! chown named:named "$zone_file" && echo "ERROR: unable to set ownership of zone file to 'named:named' -- '$zone_file'" >&2 && exit 22
|
||||
|
||||
tmp=$(mktemp)
|
||||
cat >"$tmp" <<EOF
|
||||
/*
|
||||
* This file was generated by DNS-Manager.
|
||||
* DO NOT EDIT, YOUR CHANGES WILL BE OVERWRITTEN!
|
||||
*/
|
||||
EOF
|
||||
while IFS=$NEWLINE read -r file; do
|
||||
if ! cat "$file" >>"$tmp"; then
|
||||
echo "ERROR: unable to write to temp file -- '$tmp'" >&2
|
||||
rm "$tmp"
|
||||
exit 23
|
||||
fi
|
||||
done < <(find "$conf_dir" -maxdepth 1 -type f -name '*.conf')
|
||||
if ! cat "$tmp" > "$conf_file"; then
|
||||
echo "ERROR: unable to write config file -- '$conf_file'" >&2
|
||||
rm "$tmp"
|
||||
exit 24
|
||||
fi
|
||||
rm "$tmp"
|
||||
done
|
||||
echo "Ok"
|
||||
|
||||
dns_reload_config || exit 25
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
249
dns-zone-delete
249
dns-zone-delete
@@ -1,156 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env python3
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "${0}")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW]
|
||||
|
||||
Delete DNS zones.
|
||||
|
||||
Options:
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
-f, --force delete zones without confirmation prompt
|
||||
-i, --interactive interactively ask for missing arguments
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
force=false
|
||||
interactive=false
|
||||
zone=""
|
||||
|
||||
declare -a args=()
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-c|--config)
|
||||
config_file=$1
|
||||
if ! shift; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-f|--force)
|
||||
force=true
|
||||
;;
|
||||
-i|--interactive)
|
||||
interactive=true
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$opt")
|
||||
if (( ${#args[@]} > 1 )); then
|
||||
echo "$SCRIPT: invalid argument -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
source "$config_file" || exit 2
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 3
|
||||
source "$LIB_DIR"/output.sh || exit 3
|
||||
|
||||
set -- "${args[@]}"
|
||||
|
||||
zone=$1
|
||||
if shift; then
|
||||
dns_check_zone_view "$zone" zone view || exit 10
|
||||
elif $interactive; then
|
||||
dns_select_zone zone view || exit 11
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- ZONE[@VIEW]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -A output
|
||||
if [ "${view}" == "*" ]; then
|
||||
json_array_to_bash views < <(dns_zone_views)
|
||||
else
|
||||
views=("$view")
|
||||
fi
|
||||
|
||||
for view in "${views[@]}"; do
|
||||
dns_get_base_config "$view" zone_dir conf_dir conf_file || exit 10
|
||||
|
||||
zone_conf_file="$conf_dir/$zone.conf"
|
||||
! [ -f "$zone_conf_file" ] && echo "ERROR: zone exists in DNS but no config file found, zone not managed by DNS-Manager -- '$zone_conf_file'" >&2 && exit 14
|
||||
|
||||
zone_file="$zone_dir/$zone.zone"
|
||||
! [ -f "$zone_file" ] && echo "ERROR: zone exists in DNS but no zone file found, zone not managed by DNS-Manager -- '$zone_file'" >&2 && exit 15
|
||||
done
|
||||
import argparse
|
||||
import dns.rdataclass
|
||||
import dns.rdataset
|
||||
import dns.rdatatype
|
||||
import dnsmgr
|
||||
import sys
|
||||
|
||||
|
||||
if ! $force; then
|
||||
for view in "${views[@]}"; do
|
||||
echo "View: $view"
|
||||
echo -e "\e[31m- $TAB$zone\e[0m"
|
||||
done
|
||||
echo
|
||||
! yes_no "Proceed?" && echo -e "Aborted" && exit
|
||||
echo
|
||||
fi
|
||||
def main():
|
||||
preparser = argparse.ArgumentParser(add_help=False)
|
||||
preparser.add_argument('-b', '--batch', action='store_true')
|
||||
preargs, args = preparser.parse_known_args()
|
||||
nargs = None if preargs.batch else '?'
|
||||
|
||||
echo -n "Deleting zone from config... "
|
||||
for view in "${views[@]}"; do
|
||||
dns_get_base_config "$view" zone_dir conf_dir conf_file || exit 10
|
||||
parser = argparse.ArgumentParser(description='Delete DNS zones.')
|
||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
zone_conf_file="$conf_dir/$zone.conf"
|
||||
! rm "$zone_conf_file" && echo "ERROR: unable to delete config file -- '$zone_conf_file'" >&2 && exit 14
|
||||
|
||||
tmp=$(mktemp)
|
||||
cat >"$tmp" <<EOF
|
||||
/*
|
||||
* This file was generated by DNS-Manager.
|
||||
* DO NOT EDIT, YOUR CHANGES WILL BE OVERWRITTEN!
|
||||
*/
|
||||
EOF
|
||||
while IFS=$NEWLINE read -r file; do
|
||||
if ! cat "$file" >>"$tmp"; then
|
||||
echo "ERROR: unable to write to temp file -- '$tmp'" >&2
|
||||
rm "$tmp"
|
||||
exit 23
|
||||
fi
|
||||
done < <(find "$conf_dir" -maxdepth 1 -type f -name '*.conf')
|
||||
if ! cat "$tmp" > "$conf_file"; then
|
||||
echo "ERROR: unable to write config file -- '$conf_file'" >&2
|
||||
rm "$tmp"
|
||||
exit 24
|
||||
fi
|
||||
rm "$tmp"
|
||||
done
|
||||
echo "Ok"
|
||||
try:
|
||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f'config: {e}')
|
||||
sys.exit(100)
|
||||
|
||||
echo -n "Reload Bind config... "
|
||||
rndc_args=""
|
||||
[ -n "$CONTROL_KEY" ] && rndc_args="-k $CONTROL_KEY"
|
||||
! "$RNDC" $rndc_args reconfig && echo "ERROR: rndc reconfig failed" >&2 && exit 25
|
||||
echo "Ok"
|
||||
try:
|
||||
if args.zone is None:
|
||||
zones = manager.select_zones()
|
||||
else:
|
||||
zones = manager.get_zones(args.zone)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(150)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
|
||||
error=false
|
||||
echo -n "Deleting zone files... "
|
||||
for view in "${views[@]}"; do
|
||||
dns_get_base_config "$view" zone_dir || exit 10
|
||||
if not args.batch:
|
||||
for zone in zones:
|
||||
origin = zone.origin.to_text(omit_final_dot=True)
|
||||
print(f'View: {zone.view}')
|
||||
print(f'\033[31m- {origin}\033[0m\n')
|
||||
|
||||
zone_file="$zone_dir/$zone.zone"
|
||||
! rm "$zone_file" && echo "ERROR: unable to delete zone file -- '$zone_file'" >&2 && error=true
|
||||
if not dnsmgr.input_yes_no():
|
||||
sys.exit(0)
|
||||
|
||||
while IFS=$NEWLINE read -r file; do
|
||||
! rm "$file" && echo "ERROR: unable to delete zone related file -- '$zone_file'" >&2 && error=true
|
||||
done < <(find "$zone_dir" -maxdepth 1 -type f -name "$zone.zone.*")
|
||||
done
|
||||
! $error && echo "Ok"
|
||||
for zone in zones:
|
||||
origin = zone.origin.to_text(omit_final_dot=True)
|
||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
origin = f'{origin}@{zone.view}'
|
||||
|
||||
try:
|
||||
catalog_zone_name = manager.config.zones_config[zone.view].catalog_zone
|
||||
if catalog_zone_name:
|
||||
try:
|
||||
catalog_zones = manager.get_zones(catalog_zone_name, all_zones=True)
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError(f'catalog zone of view \'{zone.view}\': {e}')
|
||||
|
||||
for catalog_zone in catalog_zones:
|
||||
manager.get_zone_content(catalog_zone)
|
||||
|
||||
rdname = dns.name.from_text(zone.nfz() + '.zones', catalog_zone.origin)
|
||||
node = catalog_zone.get_node(rdname)
|
||||
if not node:
|
||||
continue
|
||||
|
||||
rdataset = node.get_rdataset(dns.rdataclass.IN, dns.rdatatype.PTR)
|
||||
if not rdataset:
|
||||
continue
|
||||
|
||||
catalog_zone_origin = catalog_zone.origin.to_text(omit_final_dot=True)
|
||||
if catalog_zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
catalog_zone_origin += f'@{catalog_zone.view}'
|
||||
print(f'Removing zone \'{origin}\' from catalog zone \'{catalog_zone_origin}\'... ', end='')
|
||||
manager.delete_zone_record(catalog_zone, rdname, rdataset)
|
||||
print('OK')
|
||||
|
||||
print(f"Deleting config of zone '{origin}'... ", end='')
|
||||
manager.delete_zone(zone)
|
||||
print('OK')
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(160)
|
||||
|
||||
try:
|
||||
print('Reloading named... ', end='')
|
||||
manager.named_reload()
|
||||
print('OK')
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(170)
|
||||
|
||||
for zone in zones:
|
||||
origin = zone.origin.to_text(omit_final_dot=True)
|
||||
if len(zones) > 1 or zone.view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
origin = f'{origin}@{zone.view}'
|
||||
print(f"Cleanup zone files of zone '{origin}'... ", end='')
|
||||
|
||||
try:
|
||||
manager.cleanup_zone(zone)
|
||||
print('OK')
|
||||
except Exception as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(180)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
228
dns-zone-list
228
dns-zone-list
@@ -1,156 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env python3
|
||||
|
||||
SCRIPT_PATH=$(realpath -s "${0}")
|
||||
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||
import argparse
|
||||
import dnsmgr
|
||||
import sys
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW]
|
||||
from json import dumps
|
||||
|
||||
Show DNS zone content.
|
||||
|
||||
Options:
|
||||
-a, --all do not ignore unsupported record types
|
||||
-c, --config path to config file
|
||||
-h, --help print this help message
|
||||
-i, --interactive interactively ask for missing arguments
|
||||
-j, --json print json format
|
||||
-J, --json-pretty print pretty json format (implies -j)
|
||||
-r, --raw print raw format
|
||||
def main():
|
||||
preparser = argparse.ArgumentParser(add_help=False)
|
||||
preparser.add_argument('-b', '--batch', action='store_true')
|
||||
preargs, args = preparser.parse_known_args()
|
||||
nargs = None if preargs.batch else '?'
|
||||
|
||||
EOF
|
||||
exit
|
||||
}
|
||||
parser = argparse.ArgumentParser(description='Show DNS zone records.')
|
||||
parser.add_argument('-a', '--all-zones', help='do not ignore zones that are not managed', action='store_true')
|
||||
parser.add_argument('-A', '--all-records', help='do not ignore unsupported record types', action='store_true')
|
||||
parser.add_argument('-b', '--batch', help='run in batch mode (no user input)', action='store_true')
|
||||
parser.add_argument('-c', '--config', help='path to config file', default=dnsmgr.DEFAULT_CFGFILE)
|
||||
parser.add_argument('-d', '--decode', help='decode internationalized domain names (IDN)', action='store_true')
|
||||
parser.add_argument('zone', metavar='ZONE[@VIEWS]', nargs=nargs, help='DNS zone name and optional list of views (comma separated or asterisk to select all views)', default=None)
|
||||
output = parser.add_mutually_exclusive_group()
|
||||
output.add_argument('-j', '--json', help='print json format', action='store_true')
|
||||
output.add_argument('-J', '--json-pretty', help='print pretty json format', action='store_true')
|
||||
output.add_argument('-r', '--raw', help='print raw format', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
all=false
|
||||
config_file="/etc/dns-manager/config.sh"
|
||||
interactive=false
|
||||
json=false
|
||||
json_pretty=false
|
||||
raw=false
|
||||
zone=""
|
||||
try:
|
||||
manager = dnsmgr.DNSManager(cfgfile=args.config)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f'config: {e}')
|
||||
sys.exit(100)
|
||||
|
||||
declare -a args=()
|
||||
while [ -n "$1" ]; do
|
||||
opt=$1
|
||||
shift
|
||||
case "$opt" in
|
||||
-a|--all)
|
||||
all=true
|
||||
;;
|
||||
-c|--config)
|
||||
if [ -z "$1" ]; then
|
||||
echo "$SCRIPT: missing argument to option -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
config_file=$1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
-i|--interactive)
|
||||
interactive=true
|
||||
;;
|
||||
-j|--json)
|
||||
json=true
|
||||
;;
|
||||
-J|--json-pretty)
|
||||
json=true
|
||||
json_pretty=true
|
||||
;;
|
||||
-r|--raw)
|
||||
raw=true
|
||||
;;
|
||||
-*)
|
||||
echo "$SCRIPT: invalid option -- '$opt'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$opt")
|
||||
if (( ${#args[@]} > 1 )); then
|
||||
echo "$SCRIPT: invalid argument -- '$opt'" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
try:
|
||||
if args.zone is None:
|
||||
zones = manager.select_zones(args.all_zones)
|
||||
else:
|
||||
zones = manager.get_zones(args.zone, args.all_zones)
|
||||
|
||||
if $raw && $json; then
|
||||
echo "$SCRIPT: invalid options: --raw and --json are mutually exclusive" >&2
|
||||
exit 1
|
||||
fi
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(e)
|
||||
sys.exit(150)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
|
||||
source "$config_file" || exit 2
|
||||
zones.sort(key=lambda zone: zone.view)
|
||||
|
||||
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||
source "$LIB_DIR"/dns.sh || exit 3
|
||||
source "$LIB_DIR"/output.sh || exit 3
|
||||
zone_records = {}
|
||||
for zone in zones:
|
||||
try:
|
||||
manager.get_zone_content(zone)
|
||||
except RuntimeError as e:
|
||||
dnsmgr.printe(f"zone transfer of '{zone.origin.to_text(True)}@{zone.view}': {e}")
|
||||
sys.exit(160)
|
||||
|
||||
set -- "${args[@]}"
|
||||
records = []
|
||||
for name, node in zone.items():
|
||||
for rdataset in node:
|
||||
if not args.all_records and rdataset.rdtype not in dnsmgr.RECORD_TYPES:
|
||||
continue
|
||||
for value in rdataset:
|
||||
records.append({
|
||||
'name': name.to_text(),
|
||||
'name_unicode': name.to_unicode(),
|
||||
'ttl': str(rdataset.ttl),
|
||||
'type': str(rdataset.rdtype.to_text(rdataset.rdtype)),
|
||||
'value': value.to_text(origin=zone.origin, relativize=False)})
|
||||
|
||||
zone=$1
|
||||
if shift; then
|
||||
dns_check_zone_view "$zone" zone view || exit 10
|
||||
elif $interactive; then
|
||||
dns_select_zone zone view || exit 11
|
||||
else
|
||||
echo "$SCRIPT: missing argument -- ZONE[@VIEW]" >&2
|
||||
exit 1
|
||||
fi
|
||||
records.sort(key=lambda r: f'{r["name_unicode"]}{r["type"]}')
|
||||
zone_records[zone.view] = records
|
||||
|
||||
declare -A output
|
||||
if [ "${view}" == "*" ]; then
|
||||
json_array_to_bash views < <(dns_zone_views "$zone")
|
||||
else
|
||||
views=("$view")
|
||||
fi
|
||||
views = sorted(zone_records.keys())
|
||||
|
||||
for view in "${views[@]}"; do
|
||||
output["$view"]=$(dns_zone "$zone" "$view" "$all") || exit 12
|
||||
done
|
||||
if args.raw:
|
||||
for view in views:
|
||||
print(f';\n; View: {view}\n;\n')
|
||||
for record in zone_records[view]:
|
||||
print('\t'.join([record['name'], record['ttl'], record['type'], record['value']]))
|
||||
print(f'\n;\n; End of view: {view}\n;\n')
|
||||
elif args.json or args.json_pretty:
|
||||
json_output = []
|
||||
for view in views:
|
||||
json_output.append({'view': view, 'records': zone_records[view]})
|
||||
if args.json_pretty:
|
||||
print(dumps(json_output, indent=2))
|
||||
else:
|
||||
print(dumps(json_output))
|
||||
|
||||
if $raw; then
|
||||
n=0
|
||||
for view in $(printf "%s\n" "${!output[@]}" | sort); do
|
||||
if (( ${#output[@]} > 1 )) || [ "$view" != "$NAMED_DEFAULT_VIEW" ]; then
|
||||
cat <<EOF
|
||||
#
|
||||
# VIEW: $view
|
||||
#
|
||||
else:
|
||||
field_names = ['Name', 'TTL', 'Type', 'Value']
|
||||
for view in views:
|
||||
rows = []
|
||||
for record in zone_records[view]:
|
||||
name = record['name_unicode'] if args.decode else record['name']
|
||||
row = [name, record['ttl'], record['type'], record['value']]
|
||||
rows.append(row)
|
||||
|
||||
EOF
|
||||
fi
|
||||
echo "${output["$view"]}"
|
||||
"$JQ" --raw-output '.[] | "\(.name)\t\(.ttl)\t\(.type)\t\(.value)"' <<<"${output["$view"]}"
|
||||
((n++))
|
||||
(( $n < ${#output[@]} )) && echo
|
||||
done
|
||||
exit
|
||||
fi
|
||||
if len(views) > 1 or view != dnsmgr.NAMED_DEFAULT_VIEW:
|
||||
print(f'View: {view}')
|
||||
print(dnsmgr.prettytable(field_names, rows, truncate=True))
|
||||
print()
|
||||
|
||||
if $json; then
|
||||
jq_opts=""
|
||||
$json_pretty || jq_opts="--compact-output"
|
||||
for view in $(printf "%s\n" "${!output[@]}" | sort); do
|
||||
jq --compact-output --slurp "{ view: \"$view\", records: .[] }" <<<"${output["$view"]}"
|
||||
done | jq --slurp $jq_opts
|
||||
exit
|
||||
fi
|
||||
|
||||
max_value_len=$(( ${TERMINAL_WITH} / 2 - 15))
|
||||
n=0
|
||||
for view in $(printf "%s\n" "${!output[@]}" | sort); do
|
||||
(( ${#output[@]} > 1 )) || [ "$view" != "$NAMED_DEFAULT_VIEW" ] && echo "View: $view"
|
||||
{
|
||||
(( $max_value_len <= 0 )) && max_value_len=1
|
||||
while read -r name ttl rtype value; do
|
||||
name=$("$IDN2" --decode <<<"$name")
|
||||
(( ${#value} > $max_value_len )) && value="${value:0:$max_value_len} ...[TRUNCATED]"
|
||||
echo "$name$TAB$ttl$TAB$rtype$TAB$value"
|
||||
done < <("$JQ" --raw-output '.[] | "\(.name)\t\(.ttl)\t\(.type)\t\(.value)"' <<<"${output["$view"]}")
|
||||
} | table_output "NAME" "TTL" "TYPE" "VALUE"
|
||||
((n++))
|
||||
(( $n < ${#output[@]} )) && echo
|
||||
done
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
771
dnsmgr/__init__.py
Normal file
771
dnsmgr/__init__.py
Normal file
@@ -0,0 +1,771 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import dns.name
|
||||
import dns.tsig
|
||||
import dns.rcode
|
||||
import dns.rdataclass
|
||||
import dns.rdataset
|
||||
import dns.rdatatype
|
||||
import dns.query
|
||||
import dns.update
|
||||
import dns.zone
|
||||
import dns.xfr
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
from hashlib import sha1
|
||||
from prettytable import PrettyTable
|
||||
from shutil import chown
|
||||
|
||||
|
||||
DEFAULT_CFGFILE = '/etc/dns-manager/config.yml'
|
||||
|
||||
NAMED_DEFAULT_VIEW = '_default'
|
||||
|
||||
RECORD_TYPES = (
|
||||
dns.rdatatype.A,
|
||||
dns.rdatatype.AAAA,
|
||||
dns.rdatatype.CAA,
|
||||
dns.rdatatype.CDS,
|
||||
dns.rdatatype.CNAME,
|
||||
dns.rdatatype.DNAME,
|
||||
dns.rdatatype.DS,
|
||||
dns.rdatatype.MX,
|
||||
dns.rdatatype.NS,
|
||||
dns.rdatatype.PTR,
|
||||
dns.rdatatype.SRV,
|
||||
dns.rdatatype.TLSA,
|
||||
dns.rdatatype.TXT)
|
||||
|
||||
ALL_RECORD_TYPES = (dns.rdatatype.from_text(rdtype.name) for rdtype in dns.rdatatype.RdataType)
|
||||
|
||||
|
||||
def printe(msg):
|
||||
print(f'ERROR: {msg}')
|
||||
|
||||
|
||||
def prettytable(field_names, rows, truncate=False):
|
||||
t = PrettyTable()
|
||||
|
||||
t.field_names = field_names
|
||||
|
||||
for field in t.field_names:
|
||||
t.align[field] = 'l'
|
||||
|
||||
if truncate:
|
||||
max_total_len = 0
|
||||
max_name_val_ttl_len = 0
|
||||
for row in rows:
|
||||
max_total_len = max(max_total_len, sum(len(col) for col in row))
|
||||
max_name_val_ttl_len = max(max_name_val_ttl_len, sum(len(s) for s in row[0:-1]))
|
||||
|
||||
overhead_len = len(row) * 4
|
||||
|
||||
terminal_width = os.get_terminal_size().columns
|
||||
if max_total_len + overhead_len >= terminal_width:
|
||||
max_value_len = terminal_width - max_name_val_ttl_len - overhead_len
|
||||
if max_value_len < 5:
|
||||
raise RuntimeError('terminal is too small')
|
||||
for i in range(len(rows)):
|
||||
value = rows[i][-1]
|
||||
if len(value) > max_value_len:
|
||||
rows[i][-1] = f'{value[:max_value_len - 3]}...'
|
||||
|
||||
for row in rows:
|
||||
t.add_row(row)
|
||||
|
||||
return t
|
||||
|
||||
|
||||
def prettyselect(field_names, rows, prompt='Select entry', also_valid=[], truncate=False):
|
||||
length = len(rows)
|
||||
if length < 1:
|
||||
raise RuntimeError('no entries to select from')
|
||||
|
||||
field_names.insert(0, '#')
|
||||
for index in range(length):
|
||||
rows[index].insert(0, str(index + 1))
|
||||
print(prettytable(field_names, rows, truncate))
|
||||
print()
|
||||
|
||||
valid_str = f'1 - {length}'
|
||||
if also_valid:
|
||||
also_valid_str = ', '.join(also_valid)
|
||||
valid_str = f'{valid_str}, {also_valid_str}'
|
||||
|
||||
index = None
|
||||
while index is None:
|
||||
try:
|
||||
index = input(f'{prompt} ({valid_str}): ')
|
||||
except KeyboardInterrupt:
|
||||
print('\nAborted ...')
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if index in also_valid:
|
||||
print()
|
||||
return index
|
||||
|
||||
try:
|
||||
index = int(index) - 1
|
||||
if index < 0 or index > length - 1:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
index = None
|
||||
|
||||
print()
|
||||
return index
|
||||
|
||||
|
||||
def name_from_text(txt, origin=None):
|
||||
if not txt:
|
||||
raise RuntimeError('empty value')
|
||||
if origin is None:
|
||||
txt = txt.lower()
|
||||
if not txt.endswith('.'):
|
||||
txt += '.'
|
||||
|
||||
elif txt.endswith('.'):
|
||||
raise RuntimeError('record name is absolute (ends with dot)')
|
||||
|
||||
try:
|
||||
name = dns.name.from_text(txt, origin)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'{e}')
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def name_views_from_text(txt):
|
||||
try:
|
||||
(name, view_txt) = txt.split('@', maxsplit=1)
|
||||
if not view_txt:
|
||||
view_txt = None
|
||||
except ValueError:
|
||||
name = txt
|
||||
view_txt = None
|
||||
|
||||
name = name_from_text(name)
|
||||
if view_txt is None:
|
||||
views = None
|
||||
else:
|
||||
views = list(set([view.strip() for view in view_txt.split(',')]))
|
||||
if '*' in views:
|
||||
views = '*'
|
||||
|
||||
return name, views
|
||||
|
||||
|
||||
def input_name(origin=None, prompt='Zone name'):
|
||||
name = None
|
||||
while name is None:
|
||||
try:
|
||||
value = input(f'{prompt}: ')
|
||||
if not value:
|
||||
continue
|
||||
name = name_from_text(value, origin)
|
||||
print()
|
||||
except KeyboardInterrupt:
|
||||
print('\nAborted ...')
|
||||
raise KeyboardInterrupt
|
||||
except RuntimeError as e:
|
||||
print(f'ERROR: {e}\n')
|
||||
name = None
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def ttl_from_text(txt):
|
||||
try:
|
||||
ttl = int(txt)
|
||||
if ttl < 5:
|
||||
raise RuntimeError('TTL is too low (<5 seconds)')
|
||||
if ttl > 604800:
|
||||
raise RuntimeError('TTL is too high (>604800 seconds)')
|
||||
except ValueError as e:
|
||||
raise RuntimeError(f'{e}')
|
||||
|
||||
return ttl
|
||||
|
||||
|
||||
def input_ttl():
|
||||
ttl = None
|
||||
while ttl is None:
|
||||
try:
|
||||
value = input('TTL (5 - 604800): ')
|
||||
if not value:
|
||||
continue
|
||||
ttl = ttl_from_text(value)
|
||||
print()
|
||||
except KeyboardInterrupt:
|
||||
print('\nAborted ...')
|
||||
raise KeyboardInterrupt
|
||||
except RuntimeError as e:
|
||||
print(f'ERROR: {e}\n')
|
||||
ttl = None
|
||||
|
||||
return ttl
|
||||
|
||||
|
||||
def type_from_text(txt, all_types=False):
|
||||
try:
|
||||
rdtype = dns.rdatatype.from_text(txt)
|
||||
if not all_types and rdtype not in RECORD_TYPES:
|
||||
raise RuntimeError('record type is not supported')
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'{e}')
|
||||
|
||||
return rdtype
|
||||
|
||||
|
||||
def select_type(all_types=False):
|
||||
rdtypes = ALL_RECORD_TYPES if all_types else RECORD_TYPES
|
||||
rows = sorted([[rdtype.to_text(rdtype)] for rdtype in rdtypes])
|
||||
index = prettyselect(['Record type'], rows, prompt='Select record type')
|
||||
return rdtypes[index]
|
||||
|
||||
|
||||
def encode_txt_value(txt):
|
||||
if '"' in txt:
|
||||
return txt
|
||||
|
||||
txt = '" "'.join([txt[0+i:255+i] for i in range(0, len(txt), 255)])
|
||||
return f'"{txt}"'
|
||||
|
||||
|
||||
def rdata_from_text(rdtype, txt, origin):
|
||||
if rdtype == dns.rdatatype.TXT:
|
||||
txt = encode_txt_value(txt)
|
||||
|
||||
try:
|
||||
rdata = dns.rdata.from_text(dns.rdataclass.IN, rdtype, txt, origin)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'{e}')
|
||||
|
||||
return rdata
|
||||
|
||||
|
||||
def input_rdata(rdtype, origin):
|
||||
rdata = None
|
||||
while rdata is None:
|
||||
try:
|
||||
value = input('Record value: ')
|
||||
if not value:
|
||||
continue
|
||||
rdata = rdata_from_text(rdtype, value, origin)
|
||||
print()
|
||||
except KeyboardInterrupt:
|
||||
print('\nAborted ...')
|
||||
raise KeyboardInterrupt
|
||||
except RuntimeError as e:
|
||||
print(f'ERROR: {e}\n')
|
||||
rdata = None
|
||||
|
||||
return rdata
|
||||
|
||||
|
||||
def input_yes_no(prompt='Confirm?'):
|
||||
confirm = None
|
||||
while confirm is None:
|
||||
try:
|
||||
value = input(f'{prompt} (yes/no): ').lower()
|
||||
if value == 'yes':
|
||||
confirm = True
|
||||
print()
|
||||
elif value == 'no':
|
||||
confirm = False
|
||||
else:
|
||||
confirm = None
|
||||
except KeyboardInterrupt:
|
||||
print('\nAborted ...')
|
||||
confirm = False
|
||||
|
||||
return confirm
|
||||
|
||||
|
||||
class DNSViewConfig:
|
||||
def __init__(self, name, config, config_dir):
|
||||
if not isinstance(config, dict):
|
||||
raise RuntimeError(f'views: {name}: value is not an associative array')
|
||||
|
||||
self.config_dir = config.get('config_dir', os.path.join(config_dir, f'{name}.zones'))
|
||||
if not isinstance(self.config_dir, str):
|
||||
raise RuntimeError(f'views: {name}: config_dir: value is not a string')
|
||||
|
||||
self.config_file = config.get('config_file')
|
||||
if self.config_file is None:
|
||||
raise RuntimeError(f'views: {name}: missing mandatory parameter: config_file')
|
||||
if not isinstance(self.config_file, str):
|
||||
raise RuntimeError(f'views: {name}: config_file: value is not a string')
|
||||
|
||||
self.zone_dir = config.get('zone_dir')
|
||||
if self.zone_dir is None:
|
||||
raise RuntimeError(f'views: {name}: missing mandatory parameter: zone_dir')
|
||||
if not isinstance(self.zone_dir, str):
|
||||
raise RuntimeError(f'views: {name}: zone_dir: value is not a string')
|
||||
|
||||
self.catalog_zone = config.get('catalog_zone')
|
||||
if not isinstance(self.catalog_zone, (str, type(None))):
|
||||
raise RuntimeError(f'views: {name}: catalog_zone: value is not a string')
|
||||
|
||||
self.config_template = None
|
||||
self.zone_template = None
|
||||
|
||||
templates = config.get('templates')
|
||||
if templates is not None:
|
||||
if not isinstance(templates, dict):
|
||||
raise RuntimeError(f'views: {name}: templates: value is not an associative array')
|
||||
|
||||
self.config_template = templates.get('config')
|
||||
if not isinstance(self.config_template, (str, type(None))):
|
||||
raise RuntimeError(f'views: {name}: templates: config: value is not a string')
|
||||
|
||||
self.zone_template = templates.get('zone')
|
||||
if not isinstance(self.zone_template, (str, type(None))):
|
||||
raise RuntimeError(f'views: {name}: templates: zone: value is not a string')
|
||||
|
||||
|
||||
class DNSManagerConfig:
|
||||
def __init__(self, cfgfile):
|
||||
with open(cfgfile, 'r') as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
if not isinstance(config, dict):
|
||||
raise RuntimeError('config is not an associative array')
|
||||
|
||||
self.etc_dir = config.get('etc_dir', '/etc/dns-manager')
|
||||
if not isinstance(self.etc_dir, str):
|
||||
raise RuntimeError('etc_dir: value is not a string')
|
||||
|
||||
self.control_key = config.get('control_key')
|
||||
if not isinstance(self.control_key, (str, type(None))):
|
||||
raise RuntimeError('control_key: value is not a string')
|
||||
|
||||
self.dns_ip = config.get('dns_ip', '127.0.0.1')
|
||||
if not isinstance(self.dns_ip, str):
|
||||
raise RuntimeError('dns_ip value: is not a string')
|
||||
|
||||
self.named_checkconf = config.get('named_checkconf', None)
|
||||
if self.named_checkconf is None:
|
||||
self.named_checkconf = shutil.which('named-checkconf')
|
||||
if self.named_checkconf is None:
|
||||
raise RuntimeError('named-checkconf: executable not found')
|
||||
elif not isinstance(self.named_checkconf, str):
|
||||
raise RuntimeError('named_checkconf: value is not a string')
|
||||
|
||||
self.named_conf = config.get('named_conf')
|
||||
if not isinstance(self.named_conf, (str, type(None))):
|
||||
raise RuntimeError('named_conf: value is not a string')
|
||||
|
||||
self.rndc = config.get('rndc')
|
||||
if not isinstance(self.rndc, (str, type(None))):
|
||||
raise RuntimeError('rndc: value is not a string')
|
||||
|
||||
self.dns_keyfiles = config.get('dns_keyfiles')
|
||||
if self.dns_keyfiles is None:
|
||||
self.dns_keyfiles = {}
|
||||
elif not isinstance(self.dns_keyfiles, (dict)):
|
||||
raise RuntimeError('dns_keyfiles: value is not an associative array')
|
||||
|
||||
for view in self.dns_keyfiles:
|
||||
if not isinstance(self.dns_keyfiles[view], str):
|
||||
raise RuntimeError(f'dns_keyfiles: {view}: value is not a string')
|
||||
|
||||
self.zones_config = {}
|
||||
zones_config = config.get('zones_config')
|
||||
if zones_config is not None:
|
||||
if not isinstance(zones_config, dict):
|
||||
raise RuntimeError('zones_config: value is not a dictionary')
|
||||
for name, view_config in zones_config.items():
|
||||
self.zones_config[name] = DNSViewConfig(name, view_config, self.etc_dir)
|
||||
|
||||
|
||||
class Zone(dns.zone.Zone):
|
||||
def __init__(self, view=NAMED_DEFAULT_VIEW, status='unknown', cfgfile=None, zonefile=None, **kwargs):
|
||||
self.view = view
|
||||
self.status = status
|
||||
self.cfgfile = cfgfile
|
||||
self.zonefile = zonefile
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def filter_by_name(self, name, origin):
|
||||
name_relative = name.relativize(origin)
|
||||
self.nodes = dict((rdname, node) for rdname, node in self.items() if rdname == name_relative)
|
||||
|
||||
def filter_by_rdtype(self, rdtype):
|
||||
for name, node in self.items():
|
||||
rdataset = node.get_rdataset(dns.rdataclass.IN, rdtype)
|
||||
node.rdatasets.clear()
|
||||
if rdataset is not None:
|
||||
node.rdatasets.append(rdataset)
|
||||
self.nodes = dict((rdname, node) for rdname, node in self.items() if node.rdatasets)
|
||||
|
||||
def filter_by_rdata(self, rdata):
|
||||
for name, node in self.items():
|
||||
for rdataset in node:
|
||||
rdataset.items = list(filter(lambda item: rdata == item, rdataset.items))
|
||||
node.rdatasets = list(filter(lambda rdataset: rdataset.items, node.rdatasets))
|
||||
self.nodes = dict((rdname, node) for rdname, node in self.items() if node.rdatasets)
|
||||
|
||||
def nfz(self):
|
||||
return sha1(self.origin.to_wire()).hexdigest()
|
||||
|
||||
|
||||
def named_zones(named_checkconf, named_conf=None):
|
||||
cmd = [named_checkconf, '-l']
|
||||
if named_conf is not None:
|
||||
cmd.append(named_conf)
|
||||
|
||||
output = subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode()
|
||||
|
||||
zones = []
|
||||
for line in output.splitlines():
|
||||
try:
|
||||
(name, zclass, view, status) = line.split()
|
||||
except ValueError:
|
||||
raise RuntimeError(f"named-checkconf returned invalid line: '{line}'")
|
||||
if zclass.upper() == 'IN' and status.lower() in ('master', 'slave'):
|
||||
zone = Zone(origin=name, view=view, status=status)
|
||||
zones.append(zone)
|
||||
|
||||
return zones
|
||||
|
||||
|
||||
def managed_zones(config, bind_zones=[]):
|
||||
config_dirs = []
|
||||
zone_dirs = []
|
||||
|
||||
zones = []
|
||||
for view, cfg in config.items():
|
||||
if cfg.config_dir in config_dirs:
|
||||
raise RuntimeError(f'config directory used in multiple views: {cfg.config_dir}')
|
||||
config_dirs.append(cfg.config_dir)
|
||||
|
||||
if cfg.zone_dir in zone_dirs:
|
||||
raise RuntimeError(f'zone directory used in multiple views: {cfg.zone_dir}')
|
||||
zone_dirs.append(cfg.zone_dir)
|
||||
|
||||
for file in os.listdir(cfg.config_dir):
|
||||
cfgfile = os.path.join(cfg.config_dir, file)
|
||||
if not file.endswith('.conf') or not os.path.isfile(cfgfile):
|
||||
continue
|
||||
|
||||
name = file.removesuffix('.conf')
|
||||
|
||||
status = None
|
||||
if bind_zones:
|
||||
dns_name = dns.name.from_text(name)
|
||||
try:
|
||||
status = next(z.status for z in bind_zones if z.origin == dns_name)
|
||||
except StopIteration:
|
||||
status = None
|
||||
|
||||
zonefile = os.path.join(cfg.zone_dir, f'{name}.zone')
|
||||
if not os.path.isfile(zonefile):
|
||||
raise RuntimeError(f'missing zone file: {zonefile}')
|
||||
|
||||
zone = Zone(origin=name, view=view, status=status, cfgfile=cfgfile, zonefile=zonefile)
|
||||
zones.append(zone)
|
||||
# self.zones.append(ManagedZone("test.ch", "testview", "master", "/tmp/test.conf", "/tmp/test.zone"))
|
||||
return zones
|
||||
|
||||
|
||||
def keys_from_file(path):
|
||||
with open(path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
key_block_re = re.compile(r'^\s*key\s+"?(?P<name>[^"]+?)"?\s*{(?P<config>(.|\n)*?)}\s*;', re.MULTILINE)
|
||||
secret_re = re.compile(r'(.|\n)*secret\s+"?(?P<secret>[^"]+?)"?\s*;', re.MULTILINE)
|
||||
algorithm_re = re.compile(r'(.|\n)*algorithm\s+"?(?P<algorithm>[^"]+?)"?\s*;', re.MULTILINE)
|
||||
|
||||
matches = key_block_re.finditer(content)
|
||||
if not matches:
|
||||
raise RuntimeError(f'no key section found in config file: {path}')
|
||||
|
||||
keys = []
|
||||
for match in matches:
|
||||
groupdict = match.groupdict()
|
||||
name = groupdict['name']
|
||||
|
||||
match = secret_re.match(groupdict['config'])
|
||||
if not match:
|
||||
raise RuntimeError(f"missing secret in config of key '{name}'")
|
||||
secret = match.groupdict()['secret']
|
||||
|
||||
match = algorithm_re.match(groupdict['config'])
|
||||
if match:
|
||||
algorithm = match.groupdict()['algorithm']
|
||||
else:
|
||||
algorithm = None
|
||||
|
||||
keys.append(dns.tsig.Key(name, secret, algorithm=algorithm))
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
class DNSManager:
|
||||
def __init__(self, cfgfile=DEFAULT_CFGFILE):
|
||||
self.config = DNSManagerConfig(cfgfile)
|
||||
self._bind_zones = None
|
||||
self._zones = None
|
||||
self._all_zones = None
|
||||
|
||||
@property
|
||||
def bind_zones(self):
|
||||
if self._bind_zones is None:
|
||||
self._bind_zones = named_zones(named_checkconf=self.config.named_checkconf, named_conf=self.config.named_conf)
|
||||
return self._bind_zones
|
||||
|
||||
@property
|
||||
def zones(self):
|
||||
if self._zones is None:
|
||||
self._zones = managed_zones(self.config.zones_config, self.bind_zones)
|
||||
return self._zones
|
||||
|
||||
@property
|
||||
def all_zones(self):
|
||||
if self._all_zones is None:
|
||||
self._all_zones = self.zones
|
||||
for zone in self.bind_zones:
|
||||
if next((z for z in self.zones if z.origin == zone.origin and z.view == zone.view), None) is None:
|
||||
self._all_zones.append(zone)
|
||||
return self._all_zones
|
||||
|
||||
def get_zones(self, name_view, all_zones=False):
|
||||
(dns_name, views) = name_views_from_text(name_view)
|
||||
|
||||
zone_base = self.all_zones if all_zones else self.zones
|
||||
|
||||
zones = list(filter(lambda z: z.origin == dns_name, zone_base))
|
||||
if not zones:
|
||||
raise RuntimeError('zone not found')
|
||||
|
||||
if views is None:
|
||||
if len(zones) > 1:
|
||||
raise RuntimeError('zone is part of multiple views')
|
||||
elif zones[0].view != NAMED_DEFAULT_VIEW:
|
||||
raise RuntimeError('zone is not part of the default view')
|
||||
|
||||
elif views != '*':
|
||||
all_views = set([zone.view for zone in zone_base])
|
||||
zone_views = [zone.view for zone in zones]
|
||||
|
||||
for view in views:
|
||||
if view not in all_views:
|
||||
raise RuntimeError(f"view does not exist -- '{view}'")
|
||||
if view not in zone_views:
|
||||
raise RuntimeError(f"zone is not part of view -- '{view}'")
|
||||
zones = list(filter(lambda z: z.view in views, zones))
|
||||
|
||||
return zones
|
||||
|
||||
def select_zones(self, all_zones=False):
|
||||
zones = self.all_zones if all_zones else self.zones
|
||||
names = sorted(set([zone.origin.to_unicode(omit_final_dot=True) for zone in zones]))
|
||||
rows = [[name] for name in names]
|
||||
index = prettyselect(['Zone'], rows, prompt='Select zone')
|
||||
name = names[index]
|
||||
|
||||
try:
|
||||
selected_zones = self.get_zones(name, all_zones)
|
||||
except ValueError:
|
||||
dns_name = dns.name.from_text(name)
|
||||
zones = list(filter(lambda z: z.origin == dns_name, zones))
|
||||
views = [view for view in sorted(set([zone.view for zone in zones]))]
|
||||
rows = [[view] for view in sorted(set([zone.view for zone in zones]))]
|
||||
index = prettyselect(['View'], rows, prompt='Select view', also_valid=['*'])
|
||||
if index == '*':
|
||||
selected_zones = zones
|
||||
else:
|
||||
view = views[index]
|
||||
selected_zones = list(filter(lambda z: z.view == view, zones))
|
||||
|
||||
return selected_zones
|
||||
|
||||
def generate_config(self, view):
|
||||
view_cfg = self.config.zones_config[view]
|
||||
try:
|
||||
with open(view_cfg.config_file, 'w') as cfh:
|
||||
for file in os.listdir(view_cfg.config_dir):
|
||||
cfgfile = os.path.join(view_cfg.config_dir, file)
|
||||
if not file.endswith('.conf') or not os.path.isfile(cfgfile):
|
||||
continue
|
||||
with open(cfgfile, 'r') as fh:
|
||||
cfh.write(fh.read())
|
||||
cfh.write('\n')
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'unable to generate view config: {e}')
|
||||
|
||||
def named_reload(self):
|
||||
rndc = self.config.rndc if self.config.rndc else shutil.which('rndc')
|
||||
if rndc is None:
|
||||
raise RuntimeError('rndc executable not found')
|
||||
|
||||
cmd = [rndc]
|
||||
if self.config.control_key:
|
||||
cmd.extend(['-k', self.config.control_key])
|
||||
cmd.append('reconfig')
|
||||
|
||||
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f'error reloading named config: {res.stderr.decode()}')
|
||||
|
||||
return res.stdout.decode()
|
||||
|
||||
def get_keyfile(self, zone):
|
||||
name = zone.origin.to_text(omit_final_dot=True)
|
||||
keyfile = None
|
||||
for key_id in (f'{name}@{zone.view}', f'{zone.view}'):
|
||||
try:
|
||||
keyfile = self.config.dns_keyfiles[key_id]
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if keyfile is None and zone.view != NAMED_DEFAULT_VIEW:
|
||||
raise RuntimeError(f"no key configured for zone '{name}' in view '{zone.view}'")
|
||||
|
||||
return keyfile
|
||||
|
||||
def get_zone_content(self, zone):
|
||||
keyfile = self.get_keyfile(zone)
|
||||
|
||||
keys = keys_from_file(keyfile)
|
||||
key = keys[0] if keys else None
|
||||
|
||||
query, _ = dns.xfr.make_query(zone, keyring=key)
|
||||
try:
|
||||
dns.query.inbound_xfr(where=self.config.dns_ip, txn_manager=zone, query=query)
|
||||
except Exception as e:
|
||||
raise RuntimeError(e)
|
||||
|
||||
def add_zone_record(self, zone, rdname, rdataset):
|
||||
keyfile = self.get_keyfile(zone)
|
||||
|
||||
keys = keys_from_file(keyfile)
|
||||
key = keys[0] if keys else None
|
||||
|
||||
update = dns.update.Update(zone.origin, keyring=key)
|
||||
update.add(rdname, rdataset)
|
||||
|
||||
try:
|
||||
response = dns.query.tcp(update, self.config.dns_ip, timeout=10)
|
||||
except Exception as e:
|
||||
raise RuntimeError(e)
|
||||
|
||||
if response.rcode() != dns.rcode.NOERROR:
|
||||
raise RuntimeError(response.to_text())
|
||||
|
||||
return response
|
||||
|
||||
def delete_zone_record(self, zone, rdname, rdataset):
|
||||
keyfile = self.get_keyfile(zone)
|
||||
|
||||
keys = keys_from_file(keyfile)
|
||||
key = keys[0] if keys else None
|
||||
|
||||
update = dns.update.Update(zone.origin, keyring=key)
|
||||
update.delete(rdname, rdataset)
|
||||
|
||||
try:
|
||||
response = dns.query.tcp(update, self.config.dns_ip, timeout=10)
|
||||
except Exception as e:
|
||||
raise RuntimeError(e)
|
||||
|
||||
if response.rcode() != dns.rcode.NOERROR:
|
||||
raise RuntimeError(response.to_text())
|
||||
|
||||
return response
|
||||
|
||||
def add_zone(self, name, view, config_template=None, zone_template=None):
|
||||
config = self.config.zones_config[view]
|
||||
|
||||
origin = name.to_text(omit_final_dot=True)
|
||||
cfgfile = os.path.join(config.config_dir, f'{origin}.conf')
|
||||
zonefile = os.path.join(config.zone_dir, f'{origin}.zone')
|
||||
|
||||
zone = Zone(origin=name, view=view, status=None, cfgfile=cfgfile, zonefile=zonefile)
|
||||
|
||||
if os.path.exists(cfgfile):
|
||||
raise RuntimeError(f'config file already exists: {cfgfile}')
|
||||
|
||||
if os.path.exists(zonefile):
|
||||
raise RuntimeError(f'zone file already exists: {zonefile}')
|
||||
|
||||
if config_template is None:
|
||||
config_template = config.config_template
|
||||
if config_template is None:
|
||||
raise RuntimeError('no config template file configured')
|
||||
|
||||
if zone_template is None:
|
||||
zone_template = config.zone_template
|
||||
if zone_template is None:
|
||||
raise RuntimeError('no zone template file configured')
|
||||
|
||||
try:
|
||||
with open(config_template, 'r') as f:
|
||||
zone_config = f.read()
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'unable to open/read config template: {e}')
|
||||
|
||||
zone_config = zone_config.replace('%ZONE%', origin) \
|
||||
.replace('%ZONE_FILE%', zonefile) \
|
||||
.replace('%ZONE_FILENAME%', f'{origin}.zone')
|
||||
|
||||
try:
|
||||
with open(cfgfile, 'w') as f:
|
||||
f.write(zone_config)
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'unable to open/write config file: {e}')
|
||||
|
||||
try:
|
||||
with open(zone_template, 'r') as f:
|
||||
zone_content = f.read()
|
||||
|
||||
except Exception as e:
|
||||
os.remove(cfgfile)
|
||||
raise RuntimeError(f'unable to open/read zone template: {e}')
|
||||
|
||||
zone_content = zone_content.replace('%ZONE%', origin)
|
||||
|
||||
try:
|
||||
with open(zonefile, 'w') as f:
|
||||
f.write(zone_content)
|
||||
|
||||
except Exception as e:
|
||||
os.remove(cfgfile)
|
||||
raise RuntimeError(f'unable to open/write zone file: {e}')
|
||||
|
||||
try:
|
||||
chown(zonefile, 'named', 'named')
|
||||
except Exception as e:
|
||||
os.remove(cfgfile)
|
||||
os.remove(zonefile)
|
||||
raise RuntimeError(f'unable to change ownership of zone file: {e}')
|
||||
|
||||
return zone
|
||||
|
||||
def delete_zone(self, zone):
|
||||
try:
|
||||
os.remove(zone.cfgfile)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'unable to delete zone config file: {e}')
|
||||
|
||||
self.generate_config(zone.view)
|
||||
|
||||
def cleanup_zone(self, zone):
|
||||
try:
|
||||
os.remove(zone.zonefile)
|
||||
zone_dir = self.config.zones_config[zone.view].zone_dir
|
||||
for file in os.listdir(zone_dir):
|
||||
file = os.path.join(zone_dir, file)
|
||||
if not file.startswith(zone.zonefile + '.') or not os.path.isfile(file):
|
||||
continue
|
||||
os.remove(file)
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'unable to delete zone file: {e}')
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
####################
|
||||
## Global options ##
|
||||
####################
|
||||
|
||||
#
|
||||
# Paths to external binaries
|
||||
#
|
||||
#DIG="/usr/bin/dig"
|
||||
#IDN2="/usr/bin/idn2"
|
||||
#JQ="/usr/bin/jq"
|
||||
#NAMED_CHECKCONF="/usr/bin/named-checkconf"
|
||||
#NSUPDATE="/usr/bin/nsupdate"
|
||||
#RNDC="/usr/sbin/rndc"
|
||||
|
||||
#
|
||||
# Path to library directory
|
||||
#
|
||||
#LIB_DIR="/usr/local/dns-manager/lib"
|
||||
|
||||
#
|
||||
# Color of the header text when printing tables
|
||||
#
|
||||
#TABLE_HEADER_COLOR="red"
|
||||
#TERMINAL_WITH=$(/usr/bin/tput cols)
|
||||
|
||||
|
||||
##############################
|
||||
## Options for DDNS updates ##
|
||||
##############################
|
||||
|
||||
#
|
||||
# IP address of the DNS server (default is 127.0.0.1)
|
||||
#
|
||||
#DNS_IP="127.0.0.1"
|
||||
|
||||
#
|
||||
# Associative array of paths to key files (TSIG) per view used for zone transfers
|
||||
# and DDNS updates. This option mandatory when using views other than the default view.
|
||||
# The keys have to be in one of these forms:
|
||||
# - VIEW
|
||||
# - ZONE@VIEW
|
||||
#
|
||||
#declare -A DNS_KEYS=(
|
||||
# [_default]="/etc/bind/rndc.key"
|
||||
#)
|
||||
|
||||
|
||||
#######################################
|
||||
## Options for adding/removing zones ##
|
||||
#######################################
|
||||
|
||||
#
|
||||
# Optional control key used to run "rndc reconfig" after adding or deleting zones.
|
||||
# Otherwise the rndc default key is used.
|
||||
#
|
||||
#CONTROL_KEY="/etc/bind/rndc.key"
|
||||
|
||||
#
|
||||
# Associative array of directories and config files per view. This option is mandatory when adding
|
||||
# or removing zones. The syntax of the value is:
|
||||
# ZONEDIR:CONFDIR:CFGFILE
|
||||
#
|
||||
# Zone files are stored in ZONEDIR and config files are stored in CONFDIR.
|
||||
# The content of all config files in CONFDIR are concatenated and written to CFGFILE which
|
||||
# has to be included by "named.conf".
|
||||
# The detour via CONFDIR is necessary because Bind does not support wildcards when
|
||||
# including config files.
|
||||
#
|
||||
#declare -A BASE_CONFIG=(
|
||||
# [_default]="/etc/bind/dyn:/etc/dns-manager/default.zones:/etc/bind/default_zones.conf"
|
||||
#)
|
||||
|
||||
#
|
||||
# Optional associative array of templates per view used when adding new zones.
|
||||
# The syntax of the value is:
|
||||
# ZONE_TEMPLATE:CONF_TEMPLATE
|
||||
#
|
||||
#declare -A ZONE_TEMPLATES=(
|
||||
# [_default]="/etc/dns-manager/templates/zone.template:/etc/dns-manager/templates/zone.config.template"
|
||||
#)
|
||||
88
etc/config.yml
Normal file
88
etc/config.yml
Normal file
@@ -0,0 +1,88 @@
|
||||
#
|
||||
# Optional path to config directory (default: /etc/dns-manager).
|
||||
#
|
||||
|
||||
#etc_dir: /etc/dns-manager
|
||||
|
||||
#
|
||||
# Optional paths to named_checkconf and rndc binaries.
|
||||
#
|
||||
|
||||
#named_checkconf: /usr/bin/named-checkconf
|
||||
#rndc: /usr/bin/rndc
|
||||
|
||||
#
|
||||
# Optional path to named.conf.
|
||||
#
|
||||
|
||||
#named_conf: /etc/bind/named.conf
|
||||
|
||||
#
|
||||
# Optional IP address of the DNS server (default: 127.0.0.1).
|
||||
#
|
||||
|
||||
#dns_ip: 127.0.0.1
|
||||
|
||||
#
|
||||
# Dictionary of paths to key files (TSIG) per view used for zone transfers and DDNS updates.
|
||||
# This option is mandatory when using views other than the default (_default) or if the usage of a key
|
||||
# is mandatory to transfer and/or update zones. The keys of this dictionary are either the name of a view
|
||||
# or a specific zone in a view (zone@view).
|
||||
# Keep in mind that the first key found in the key files is used and all other keys are ignored.
|
||||
#
|
||||
|
||||
#dns_keyfiles:
|
||||
# _default: /etc/bind/rndc.key
|
||||
# example.tld@_default: /etc/bind/another.key
|
||||
|
||||
#
|
||||
# Optional path to key file passed to rndc (-k option), otherwise rndc uses its default key file.
|
||||
# This key is used to reconfigure Bind when adding or deleting zones.
|
||||
#
|
||||
|
||||
#control_key: /etc/bind/rndc.key
|
||||
|
||||
#
|
||||
# Dictionary of configuration options per view.
|
||||
#
|
||||
|
||||
zones_config:
|
||||
_default:
|
||||
#
|
||||
# Optional path to directory where config files per zone are stored.
|
||||
# Default: <etc_dir>/<view>.zones
|
||||
#
|
||||
|
||||
#config_dir: /etc/dns-manager/_default.zones
|
||||
|
||||
#
|
||||
# Path to config file that has to be included in named.conf.
|
||||
# All configs stored in <config_dir> will be concatenated and written to it. The detour via this file is necessary
|
||||
# because Bind does not support wildcards when including config files.
|
||||
#
|
||||
|
||||
config_file: /etc/bind/default_zones.conf
|
||||
|
||||
#
|
||||
# Path to the directory where the Bind zone files of this view are stored.
|
||||
# This is typically "/etc/bind/dyn" in an single view environment.
|
||||
#
|
||||
|
||||
zone_dir: /etc/bind/dyn
|
||||
|
||||
#
|
||||
# Optional Bind catalog zone that will be managed automatically when adding or deleting zones.
|
||||
# The view may also be specified if necessary (zone@view). It is
|
||||
#
|
||||
# IMPORTANT: It is recommended to manually add catalog zones to Bind to prevent accidental deletion.
|
||||
#
|
||||
|
||||
#catalog_zone: catalog.example.tld
|
||||
|
||||
#
|
||||
# Optional paths to config and zone default templates used when adding zones.
|
||||
#
|
||||
|
||||
#templates:
|
||||
# config: /etc/dns-manager/templates/zone.config.template
|
||||
# zone: /etc/dns-manager/templates/zone.template
|
||||
@@ -2,10 +2,10 @@ zone "%ZONE%" {
|
||||
type master;
|
||||
file "%ZONE_FILE%";
|
||||
update-policy {
|
||||
grant dns-manager-key zonesub any;
|
||||
grant rndc-key zonesub any;
|
||||
};
|
||||
allow-transfer {
|
||||
key dns-manager-key;
|
||||
key rndc-key;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
$TTL 86400 ; 1 day
|
||||
@ IN SOA dns1.%ZONE%. hostmaster.%ZONE%. (
|
||||
%ZONE%. IN SOA dns1.%ZONE%. hostmaster.%ZONE%. (
|
||||
1 ; serial
|
||||
10800 ; refresh (3 hours)
|
||||
3600 ; retry (1 hour)
|
||||
@@ -8,3 +8,4 @@ $TTL 86400 ; 1 day
|
||||
)
|
||||
NS dns1.%ZONE%.
|
||||
NS dns2.%ZONE%.
|
||||
$ORIGIN %ZONE%.
|
||||
|
||||
Reference in New Issue
Block a user