Initial commit of source
This commit is contained in:
35
config.sh
Normal file
35
config.sh
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# IP address of the DNS server
|
||||||
|
#
|
||||||
|
#DNS_IP="127.0.0.1"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Associative array containing path to key files used for zone transfer and DDNS updates.
|
||||||
|
# The key has to be in on of the following forms:
|
||||||
|
# - VIEW
|
||||||
|
# - ZONE@VIEW
|
||||||
|
#
|
||||||
|
#DNS_KEYS=(
|
||||||
|
# [_default]="/etc/bind/rndc.key"
|
||||||
|
#)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Paths to external binaries
|
||||||
|
#
|
||||||
|
#DIG="/usr/bin/dig"
|
||||||
|
#IDN2="/usr/bin/idn2"
|
||||||
|
#NAMED_CHECKCONF="/usr/bin/named-checkconf"
|
||||||
|
#NSUPDATE="/usr/bin/nsupdate"
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
89
dns-list-zones
Executable file
89
dns-list-zones
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/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
|
||||||
|
-j, --json print json format
|
||||||
|
-J, --json-pretty print pretty json format (implies -j)
|
||||||
|
-r, --raw print raw format
|
||||||
|
-h, --help print this help message
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
config_file="$SCRIPT_DIR"/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
Executable file
85
dns-record
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/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="$SCRIPT_DIR"/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
|
||||||
152
dns-record-add
Executable file
152
dns-record-add
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
SCRIPT_PATH=$(realpath -s "$0")
|
||||||
|
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||||
|
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW] NAME TTL TYPE VALUE
|
||||||
|
|
||||||
|
Add a new record to a DNS zone.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c, --config path to config file
|
||||||
|
-h, --help print this help message
|
||||||
|
-f, --force add record without confirmation prompt
|
||||||
|
-i, --interactive interactively ask for missing arguments
|
||||||
|
EOF
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
config_file="$SCRIPT_DIR"/config.sh
|
||||||
|
force=false
|
||||||
|
interactive=false
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
source "$config_file" || exit 2
|
||||||
|
|
||||||
|
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||||
|
source "$LIB_DIR"/dns.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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 [ "${view}" == "*" ]; then
|
||||||
|
json_array_to_bash views < <(dns_zone_views "$zone")
|
||||||
|
else
|
||||||
|
views=("$view")
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
203
dns-record-delete
Executable file
203
dns-record-delete
Executable file
@@ -0,0 +1,203 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
SCRIPT_PATH=$(realpath -s "$0")
|
||||||
|
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||||
|
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW] NAME TYPE [VALUE]
|
||||||
|
|
||||||
|
Delete records from a DNS zone.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c, --config path to config file
|
||||||
|
-h, --help print this help message
|
||||||
|
-f, --force delete record(s) without confirmation prompt
|
||||||
|
-i, --interactive interactively ask for missing arguments
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
config_file="$SCRIPT_DIR"/config.sh
|
||||||
|
force=false
|
||||||
|
interactive=false
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
source "$config_file" || exit 2
|
||||||
|
|
||||||
|
LIB_DIR=${LIB_DIR:-$SCRIPT_DIR/lib}
|
||||||
|
source "$LIB_DIR"/dns.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 zone_data
|
||||||
|
|
||||||
|
if [ "${view}" == "*" ]; then
|
||||||
|
json_array_to_bash views < <(dns_zone_views "$zone")
|
||||||
|
else
|
||||||
|
views=("$view")
|
||||||
|
fi
|
||||||
|
|
||||||
|
records=$(
|
||||||
|
for view in "${views[@]}"; do
|
||||||
|
dns_zone "$zone" "$view" || exit 22
|
||||||
|
done | "$JQ" --compact-output --slurp 'add'
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
records=$("$JQ" --compact-output --arg name "$name" '[ .[] | select(.name == $name) ]' <<<"$records")
|
||||||
|
[ "$records" == "[]" ] && echo "ERROR: no such record -- '$name'" >&2 && exit 5
|
||||||
|
|
||||||
|
json_array_to_bash rtypes < <("$JQ" --compact-output '[ .[] | .type ] | sort | unique' <<<"$records")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
records=$("$JQ" --compact-output --arg rtype "$rtype" '[ .[] | select(.type == $rtype) ]' <<<"$records")
|
||||||
|
[ "$records" == "[]" ] && echo "ERROR: no ${rtype} record found" >&2 && exit 5
|
||||||
|
|
||||||
|
json_array_to_bash values < <("$JQ" --compact-output '[ .[] | .value ] | sort | unique' <<<"$records")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
json_array_to_bash views < <("$JQ" --compact-output '[ .[] | .view ] | sort | unique' <<<"$records")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
81
dns-zone
Executable file
81
dns-zone
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/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:
|
||||||
|
* 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="$SCRIPT_DIR"/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
|
||||||
|
list)
|
||||||
|
"$SCRIPT_DIR"/dns-zone-$cmd $params "$@"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$SCRIPT: invalid command -- '$cmd'" >&2
|
||||||
|
exit 5
|
||||||
|
;;
|
||||||
|
esac
|
||||||
156
dns-zone-list
Executable file
156
dns-zone-list
Executable file
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
SCRIPT_PATH=$(realpath -s "${0}")
|
||||||
|
SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
|
||||||
|
SCRIPT=$(basename "$SCRIPT_PATH")
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $SCRIPT [OPTIONS]... ZONE[@VIEW]
|
||||||
|
|
||||||
|
Show DNS zone content.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-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
|
||||||
|
-u, --unfiltered also print not supported record types
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
config_file="$SCRIPT_DIR"/config.sh
|
||||||
|
interactive=false
|
||||||
|
json=false
|
||||||
|
json_pretty=false
|
||||||
|
raw=false
|
||||||
|
unfiltered=false
|
||||||
|
zone=""
|
||||||
|
|
||||||
|
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
|
||||||
|
;;
|
||||||
|
-j|--json)
|
||||||
|
json=true
|
||||||
|
;;
|
||||||
|
-J|--json-pretty)
|
||||||
|
json=true
|
||||||
|
json_pretty=true
|
||||||
|
;;
|
||||||
|
-r|--raw)
|
||||||
|
raw=true
|
||||||
|
;;
|
||||||
|
-u|--unfiltered)
|
||||||
|
unfiltered=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
|
||||||
|
|
||||||
|
if $raw && $json; 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
|
||||||
|
|
||||||
|
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 "$zone")
|
||||||
|
else
|
||||||
|
views=("$view")
|
||||||
|
fi
|
||||||
|
|
||||||
|
for view in "${views[@]}"; do
|
||||||
|
output["$view"]=$(dns_zone "$zone" "$view" "$unfiltered") || exit 12
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
#
|
||||||
|
|
||||||
|
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 $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
|
||||||
592
lib/dns.sh
Normal file
592
lib/dns.sh
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
####################
|
||||||
|
# config variables #
|
||||||
|
####################
|
||||||
|
|
||||||
|
if [ -z "${!DNS_KEYS[*]}" ]; then
|
||||||
|
declare -A DNS_KEYS=()
|
||||||
|
fi
|
||||||
|
|
||||||
|
DNS_IP=${DNS_IP:-127.0.0.1}
|
||||||
|
DIG=${DIG:-/usr/bin/dig}
|
||||||
|
IDN2=${IDN2:-/usr/bin/idn2}
|
||||||
|
JQ=${JQ:-/usr/bin/jq}
|
||||||
|
NAMED_CHECKCONF=${NAMED_CHECKCONF:-/usr/bin/named-checkconf}
|
||||||
|
NSUPDATE=${NSUPDATE:-/usr/bin/nsupdate}
|
||||||
|
TERMINAL_WITH=${MAX_TERMINAL_WITH:-$(/usr/bin/tput cols)}
|
||||||
|
|
||||||
|
####################
|
||||||
|
# global variables #
|
||||||
|
####################
|
||||||
|
|
||||||
|
# List of managable DNS record types
|
||||||
|
declare -a DNS_RECORD_TYPES=("A" "AAAA" "CAA" "CDS" "CNAME" "DS" "MX" "NS" "PTR" "SRV" "TLSA" "TXT")
|
||||||
|
|
||||||
|
# Global variables
|
||||||
|
NEWLINE=$'\n'
|
||||||
|
TAB=$'\t'
|
||||||
|
|
||||||
|
NAMED_DEFAULT_VIEW="_default"
|
||||||
|
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# bash array and JSON helper functions #
|
||||||
|
########################################
|
||||||
|
|
||||||
|
in_array() {
|
||||||
|
local search=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
local value
|
||||||
|
for value in "$@"; do
|
||||||
|
[ "$search" == "$value" ] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
join_by() {
|
||||||
|
local d=${1-} f=${2-}
|
||||||
|
|
||||||
|
if shift 2; then
|
||||||
|
printf %s "$f" "${@/#/$d}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_to_json_obj() {
|
||||||
|
local delim=$1
|
||||||
|
shift || return 1
|
||||||
|
|
||||||
|
local fields=()
|
||||||
|
local name index
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
IFS=":" read -r name index <<<"$1"
|
||||||
|
shift
|
||||||
|
fields+=("$name:.[$index]")
|
||||||
|
done
|
||||||
|
|
||||||
|
local jq_cmd=". | split(\$delim) | {$(join_by "," "${fields[@]}")}"
|
||||||
|
|
||||||
|
"$JQ" --raw-input --compact-output --arg delim "$delim" "$jq_cmd" < <(cat -)
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
json_array_to_bash() {
|
||||||
|
local retvar=${1:-REPLY}
|
||||||
|
|
||||||
|
local __values=()
|
||||||
|
while IFS=$NEWLINE read -r line; do
|
||||||
|
__values+=("$("$JQ" --raw-output --compact-output '.[] | @sh' <<<"$line")")
|
||||||
|
done < <(cat -)
|
||||||
|
|
||||||
|
declare -g -a $retvar
|
||||||
|
eval "$retvar=(${__values[@]})"
|
||||||
|
}
|
||||||
|
|
||||||
|
bash_array_to_json() {
|
||||||
|
"$JQ" --raw-input --compact-output '.' < <(cat -) | "$JQ" --compact-output --slurp
|
||||||
|
}
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# internal helper functions #
|
||||||
|
#############################
|
||||||
|
|
||||||
|
_get_keyfile() {
|
||||||
|
local retvar=$1
|
||||||
|
local zone=$2
|
||||||
|
local view=$3
|
||||||
|
|
||||||
|
local key_id
|
||||||
|
for key_id in "$zone@$view" "$view"; do
|
||||||
|
keyfile=${DNS_KEYS["$key_id"]}
|
||||||
|
if [ -n "$keyfile" ]; then
|
||||||
|
if ! test -r "$keyfile" -a -f "$keyfile"; then
|
||||||
|
echo "ERROR: key file does not exist or is not accessible -- '$keyfile'" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
declare -g $retvar="$keyfile"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$view" == "$NAMED_DEFAULT_VIEW" ]; then
|
||||||
|
declare -g $retvar=""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ERROR: config: no key found for '$zone@$view' or '$view'" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_nsupdate() {
|
||||||
|
local zone=$1
|
||||||
|
shift || return 1
|
||||||
|
local view=$1
|
||||||
|
shift || return 1
|
||||||
|
local update_script=$1
|
||||||
|
shift || return 1
|
||||||
|
local pretend=${1:-true}
|
||||||
|
|
||||||
|
_get_keyfile keyfile "$zone" "$view" || return $?
|
||||||
|
|
||||||
|
local nsupdate_args=""
|
||||||
|
[ -n "$keyfile" ] && nsupdate_args="-k ${keyfile}"
|
||||||
|
|
||||||
|
"$NSUPDATE" -v $nsupdate_args < <(
|
||||||
|
echo "server $DNS_IP"
|
||||||
|
echo "$update_script"
|
||||||
|
if $pretend; then
|
||||||
|
echo "show"
|
||||||
|
else
|
||||||
|
echo "send"
|
||||||
|
echo "answer"
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# data generation functions #
|
||||||
|
#############################
|
||||||
|
|
||||||
|
_DNS_ZONES_DATA=""
|
||||||
|
|
||||||
|
dns_zones() {
|
||||||
|
local zone=$1
|
||||||
|
|
||||||
|
if [ -z "$_DNS_ZONES_DATA" ]; then
|
||||||
|
local raw_data=$("$NAMED_CHECKCONF" -l | grep -P -i '^[^.][^ ]* IN ' | sort) || return 1
|
||||||
|
local json_data=$(raw_to_json_obj " " "zone:0" "view:2" "status:3" <<<"$raw_data") || return 2
|
||||||
|
declare -g _DNS_ZONES_DATA=$("$JQ" --compact-output --slurp 'sort_by(.zone)' <<<"$json_data") || return 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$zone" ] && echo "$_DNS_ZONES_DATA" && return 0
|
||||||
|
|
||||||
|
"$JQ" --compact-output --arg zone "$zone" '[ .[] | select(.zone == $zone) ]' <<<"$_DNS_ZONES_DATA"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_zone_views() {
|
||||||
|
local zone=$1
|
||||||
|
|
||||||
|
local zones=$(dns_zones "$zone") || return $?
|
||||||
|
"$JQ" --compact-output '[ .[] | .view ] | sort | unique' <<<"$zones"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_zone() {
|
||||||
|
local zone=$1
|
||||||
|
local view=$2
|
||||||
|
local unfiltered=${3:-false}
|
||||||
|
|
||||||
|
local keyfile
|
||||||
|
_get_keyfile keyfile "$zone" "$view" || return $?
|
||||||
|
|
||||||
|
local dig_args
|
||||||
|
[ -n "$keyfile" ] && dig_args="-k ${keyfile}"
|
||||||
|
local raw_data=$("$DIG" @${DNS_IP} $dig_args +nocmd +onesoa +nostats AXFR "$zone" | \
|
||||||
|
sed -r 's#^([^ \t]+)[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+(.+)#\1\t\2\t\3\t\4\t\5#g' | \
|
||||||
|
grep -P -i '^[^\t]+\t[0-9]+\tIN\t')
|
||||||
|
|
||||||
|
raw_data=$(
|
||||||
|
local name ttl _type rtype value
|
||||||
|
while read -r name ttl _type rtype value; do
|
||||||
|
if [ "${rtype^^}" == "TXT" ]; then
|
||||||
|
decoded_value=$(dns_decode_txt_value "$value")
|
||||||
|
else
|
||||||
|
decoded_value=$value
|
||||||
|
fi
|
||||||
|
printf "%s\t" "$name" "$ttl" "$rtype" "$value" "$decoded_value"
|
||||||
|
echo
|
||||||
|
done <<<"$raw_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
local query='.'
|
||||||
|
local rtypes="[]"
|
||||||
|
|
||||||
|
if ! $unfiltered; then
|
||||||
|
query='[ .[] | select(.type | IN($rtypes[])) ]'
|
||||||
|
rtypes=$(printf "%s\n" "${DNS_RECORD_TYPES[@]}" | bash_array_to_json)
|
||||||
|
fi
|
||||||
|
|
||||||
|
query+='| [ .[] | .view += "\($view)" | if .name == "\($zone)." then .name = "@" else .name = "\(.name | capture("(?<name>.+)\\.\($zone)").name )" end ] | sort_by(.name)'
|
||||||
|
|
||||||
|
raw_to_json_obj "$TAB" "name:0" "ttl:1" "type:2" "value:3" "decoded_value:4" <<<"$raw_data" | \
|
||||||
|
jq --compact-output --slurp --arg view "$view" --argjson rtypes "$rtypes" --arg zone "$zone" "$query"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# input validation functions #
|
||||||
|
##############################
|
||||||
|
|
||||||
|
dns_check_zone() {
|
||||||
|
local zone=$1
|
||||||
|
shift || return 1
|
||||||
|
local retvar=${1:-REPLY}
|
||||||
|
|
||||||
|
zone=$("$IDN2" <<<"$zone")
|
||||||
|
|
||||||
|
local zones=$(dns_zones "$zone") || return 1
|
||||||
|
|
||||||
|
local query='if . | length > 0 then "true" else "false" end'
|
||||||
|
local found=$("$JQ" --raw-output --arg zone "$zone" "$query" <<<"$zones")
|
||||||
|
|
||||||
|
$found && declare -g $retvar="$zone" && return 0
|
||||||
|
|
||||||
|
declare -g $retvar=""
|
||||||
|
echo "ERROR: zone does not exist -- '$zone'" >&2
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_check_view() {
|
||||||
|
local view=$1
|
||||||
|
shift || return 1
|
||||||
|
|
||||||
|
local query='. as $list | $view | IN($list[])'
|
||||||
|
$("$JQ" --raw-output --arg view "$view" "$query" < <(dns_zone_views)) && return 0
|
||||||
|
|
||||||
|
echo "ERROR: view does not exist -- '$view'" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_check_zone_view() {
|
||||||
|
local zone_view=$1
|
||||||
|
local zone_retvar=$2
|
||||||
|
local view_retvar=$3
|
||||||
|
|
||||||
|
local zone view
|
||||||
|
IFS='@' read -r zone view <<<"$zone_view"
|
||||||
|
|
||||||
|
dns_check_zone "$zone" zone || return 1
|
||||||
|
|
||||||
|
local -a views
|
||||||
|
json_array_to_bash views < <(dns_zone_views "$zone")
|
||||||
|
|
||||||
|
if [ -z "$view" ]; then
|
||||||
|
if (( ${#views[@]} > 1 )); then
|
||||||
|
echo "ERROR: zone is member of multiple views -- '$zone'" >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
if [ "${views[0]}" != "$NAMED_DEFAULT_VIEW" ]; then
|
||||||
|
echo "ERROR: zone is not a member of the default view -- '$zone'" >&2
|
||||||
|
return 3
|
||||||
|
fi
|
||||||
|
view=$NAMED_DEFAULT_VIEW
|
||||||
|
elif [ "${view}" != "*" ]; then
|
||||||
|
dns_check_view "$view" || return 4
|
||||||
|
if ! in_array "$view" "${views[@]}"; then
|
||||||
|
echo "ERROR: zone '$zone' is not part of view '$view'" >&2
|
||||||
|
return 5
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$zone_retvar" ] && declare -g $zone_retvar="$zone"
|
||||||
|
[ -n "$view_retvar" ] && declare -g $view_retvar="$view"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_check_zone_name() {
|
||||||
|
local name=${1,,}
|
||||||
|
|
||||||
|
[[ "$name" =~ ^[a-z0-9_][a-z0-9_.-]*$ ]] && [[ "$name" != *"." ]] && return 0
|
||||||
|
|
||||||
|
echo "ERROR: invalid zone name -- '$name'" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_check_record_name() {
|
||||||
|
local name=${1,,}
|
||||||
|
local retvar=${2:-REPLY}
|
||||||
|
|
||||||
|
[ "$name" == "@" ] && return 0
|
||||||
|
|
||||||
|
name=$("$IDN2" <<<"$name")
|
||||||
|
|
||||||
|
local LC_ALL=C
|
||||||
|
if [[ "$name" =~ ^[a-z0-9_][a-z0-9_.-]*$ ]] && [[ "$name" != *"." ]]; then
|
||||||
|
declare -g $retvar="$name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ERROR: invalid record name -- '$name'" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_check_ttl() {
|
||||||
|
local ttl=$1
|
||||||
|
|
||||||
|
[[ "$ttl" =~ ^[1-9][0-9]*$ ]] && (( $ttl <= 2147483647 )) && return 0
|
||||||
|
|
||||||
|
echo "ERROR: invalid TTL -- '$ttl'" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_check_record_type() {
|
||||||
|
local rtype=$1
|
||||||
|
|
||||||
|
[ -z "$rtype" ] && return 1
|
||||||
|
|
||||||
|
if ! in_array "${rtype^^}" "${DNS_RECORD_TYPES[@]}"; then
|
||||||
|
echo "ERROR: invalid record type -- '$rtype'" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_check_record_value() {
|
||||||
|
local rtype=$1
|
||||||
|
local value=$2
|
||||||
|
local retvar=${3:-REPLY}
|
||||||
|
|
||||||
|
[ -z "$rtype" ] && return 1
|
||||||
|
[ -z "$value" ] && return 1
|
||||||
|
|
||||||
|
case "${rtype^^}" in
|
||||||
|
A)
|
||||||
|
local digit_re='(0|1[0-9]{0,2}|2([0-9]|[0-4][0-9]|5[0-5])?)'
|
||||||
|
local ipv4_re="^$digit_re\.$digit_re\.$digit_re\.$digit_re\$"
|
||||||
|
local LC_ALL=C
|
||||||
|
if ! [[ "$value" =~ $ipv4_re ]]; then
|
||||||
|
echo "ERROR: invalid IPv4 address -- '$value'" 2>&1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
AAAA)
|
||||||
|
local ipv6_re='^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'
|
||||||
|
local LC_ALL=C
|
||||||
|
if ! [[ "$value" =~ $ipv6_re ]]; then
|
||||||
|
echo "ERROR: invalid IPv6 address -- '$value'" 2>&1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
CNAME|PTR|NS)
|
||||||
|
dns_check_zone_name "$value" || return 1
|
||||||
|
;;
|
||||||
|
MX)
|
||||||
|
local prio mx
|
||||||
|
read -r prio mx <<<"$value"
|
||||||
|
[ -z "$rio" -a -z "$mx" ] && echo "ERROR: invalid MX record format -- '$value'" >&2 && return 1
|
||||||
|
local LC_ALL=C
|
||||||
|
! [[ "$prio" =~ ^[1-9][0-9]*$ ]] && echo "ERROR: invalid priority -- '$prio'" >&2 && return 1
|
||||||
|
dns_check_zone_name "$mx" || return 1
|
||||||
|
;;
|
||||||
|
TXT)
|
||||||
|
if [ "${value:0:1}" != '"' ]; then
|
||||||
|
local -a parts=()
|
||||||
|
local index=0
|
||||||
|
while (( ${#value} - $index > 0 )); do
|
||||||
|
parts+=("${value:$index:255}")
|
||||||
|
((index+=255))
|
||||||
|
done
|
||||||
|
value="\"$(join_by '" "' "${parts[@]}")\""
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
declare -g $retvar="$value"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
########################
|
||||||
|
# user input functions #
|
||||||
|
########################
|
||||||
|
|
||||||
|
yes_no() {
|
||||||
|
local yn=""
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
read -p "$* (yes/no): " yn
|
||||||
|
case "$yn" in
|
||||||
|
[Yy]*) return 0 ;;
|
||||||
|
[Nn]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_select_zone() {
|
||||||
|
local zone_retvar=$1
|
||||||
|
local view_retvar=$2
|
||||||
|
|
||||||
|
local -a zones=()
|
||||||
|
|
||||||
|
local zones=$(dns_zones) || return $?
|
||||||
|
|
||||||
|
json_array_to_bash zones < <("$JQ" --compact-output '[ .[] | .zone ] | unique' <<<"$zones")
|
||||||
|
(( ${#zones[@]} == 0 )) && echo "ERROR: no zones found" >&2 && return 1
|
||||||
|
|
||||||
|
local COLUMNS=30
|
||||||
|
|
||||||
|
echo -e "Select zone:\n"
|
||||||
|
select zone in "${zones[@]}"; do
|
||||||
|
[ -n "$zone" ] && break
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
[ -z "$zone" ] && echo "ERROR: zone selection failed" >&2 && return 1
|
||||||
|
|
||||||
|
local views=()
|
||||||
|
json_array_to_bash views < <(dns_zone_views "$zone")
|
||||||
|
|
||||||
|
local view=""
|
||||||
|
if (( ${#views[@]} == 1 )) && [ "${views[0]}" == "$NAMED_DEFAULT_VIEW" ]; then
|
||||||
|
view=$NAMED_DEFAULT_VIEW
|
||||||
|
else
|
||||||
|
echo -e "Select view:\n"
|
||||||
|
select view in "${views[@]}"; do
|
||||||
|
[ -n "$view" ] && break
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
[ -z "$view" ] && echo "ERROR: view selection failed" >&2 && return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -g $zone_retvar="$zone"
|
||||||
|
declare -g $view_retvar="$view"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_select_record_type() {
|
||||||
|
local retvar=${1:-REPLY}
|
||||||
|
|
||||||
|
echo "Select record type:"
|
||||||
|
local rtype
|
||||||
|
select rtype in "${DNS_RECORD_TYPES[@]}"; do
|
||||||
|
[ -n "$rtype" ] && break
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
[ -z "$rtype" ] && echo "ERROR: record type selection failed" >&2 && return 1
|
||||||
|
|
||||||
|
declare -g $retvar="$rtype"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_read_zone_name() {
|
||||||
|
# TODO
|
||||||
|
exit
|
||||||
|
|
||||||
|
#if [ -n "$zone" ]; then
|
||||||
|
# if ! in_array "$zone" "${zones[@]}"; then
|
||||||
|
# echo "ERROR: unknown zone '$zone'" >&2
|
||||||
|
# return 1
|
||||||
|
# fi
|
||||||
|
#fi
|
||||||
|
#if [ -n "$view" ]; then
|
||||||
|
# if ! in_array "$view" "${views[@]}" ]]; then
|
||||||
|
# echo "ERROR: zone '$zone' is not part of view '$view'" >&2
|
||||||
|
# return 2
|
||||||
|
# fi
|
||||||
|
#fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_read_record_name() {
|
||||||
|
local retvar=${1:-REPLY}
|
||||||
|
|
||||||
|
local name=""
|
||||||
|
while [ -z "$name" ]; do
|
||||||
|
read -e -p "Record name (e.g. www or mail): " name
|
||||||
|
[ -n "$name" ] && ! dns_check_record_name "$name" name && name=""
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
declare -g $retvar="$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_read_ttl() {
|
||||||
|
local retvar=${1:-REPLY}
|
||||||
|
|
||||||
|
local ttl
|
||||||
|
while [ -z "$ttl" ]; do
|
||||||
|
read -e -p "TTL (seconds): " -i 7200 ttl
|
||||||
|
[ -n "$ttl" ] && ! dns_check_ttl "$ttl" && ttl=""
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
declare -g $retvar="$ttl"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_read_record_value() {
|
||||||
|
local rtype=$1
|
||||||
|
local retvar=${2:-REPLY}
|
||||||
|
|
||||||
|
local value
|
||||||
|
while [ -z "$value" ]; do
|
||||||
|
read -e -p "Value: " value
|
||||||
|
[ -n "$value" ] && ! dns_check_record_value "$rtype" "$value" value && value=""
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
declare -g $retvar="$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_decode_txt_value() {
|
||||||
|
local value=$1
|
||||||
|
[ "${value:0:1}" != '"' ] && echo "$value" && return
|
||||||
|
|
||||||
|
value=${value#\"}
|
||||||
|
value=${value%\"}
|
||||||
|
sed -r 's#([^\])" "#\1#g;s#\\"#"#g' <<<"$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# action functions #
|
||||||
|
####################
|
||||||
|
|
||||||
|
dns_record_add() {
|
||||||
|
local pretend=$1
|
||||||
|
local zone=$2
|
||||||
|
local view=$3
|
||||||
|
local name=$4
|
||||||
|
local ttl=$5
|
||||||
|
local rtype=$6
|
||||||
|
local value=$7
|
||||||
|
|
||||||
|
[ -z "$pretend" ] && echo "ERROR: missing argument -- PRETEND" >&2 && return 1
|
||||||
|
! in_array "$pretend" "true" "false" && echo "ERROR: invalid argument PRETEND -- '$pretend'" >&2 && return 1
|
||||||
|
[ -z "$zone" ] && echo "ERROR: missing argument -- ZONE" >&2 && return 1
|
||||||
|
[ -z "$view" ] && echo "ERROR: missing argument -- VIEW" >&2 && return 1
|
||||||
|
[ -z "$name" ] && echo "ERROR: missing argument -- NAME" >&2 && return 1
|
||||||
|
[ -z "$ttl" ] && echo "ERROR: missing argument -- TTL" >&2 && return 1
|
||||||
|
[ -z "$rtype" ] && echo "ERROR: missing argument -- TYPE" >&2 && return 1
|
||||||
|
[ -z "$value" ] && echo "ERROR: missing argument -- VALUE" >&2 && return 1
|
||||||
|
|
||||||
|
local fqdn
|
||||||
|
[ "$name" == "@" ] && fqdn=$zone || fqdn="$name.$zone"
|
||||||
|
|
||||||
|
update_script=""
|
||||||
|
if [ "${rtype^^}" == "CNAME" ]; then
|
||||||
|
update_script="prereq nxdomain $fqdn$NEWLINE"
|
||||||
|
fi
|
||||||
|
update_script+="update add $fqdn $ttl $rtype $value"
|
||||||
|
|
||||||
|
_nsupdate "$zone" "$view" "$update_script" "$pretend"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_record_delete() {
|
||||||
|
local pretend=$1
|
||||||
|
local zone=$2
|
||||||
|
local view=$3
|
||||||
|
local name=$4
|
||||||
|
local rtype=$5
|
||||||
|
local value=$6
|
||||||
|
|
||||||
|
[ -z "$pretend" ] && echo "ERROR: missing argument -- PRETEND" >&2 && return 1
|
||||||
|
! in_array "$pretend" "true" "false" && echo "ERROR: invalid argument PRETEND -- '$pretend'" >&2 && return 1
|
||||||
|
[ -z "$zone" ] && echo "ERROR: missing argument -- ZONE" >&2 && return 1
|
||||||
|
[ -z "$view" ] && echo "ERROR: missing argument -- VIEW" >&2 && return 1
|
||||||
|
[ -z "$name" ] && echo "ERROR: missing argument -- NAME" >&2 && return 1
|
||||||
|
[ -z "$rtype" ] && echo "ERROR: missing argument -- TYPE" >&2 && return 1
|
||||||
|
[ -z "$value" ] && echo "ERROR: missing argument -- VALUE" >&2 && return 1
|
||||||
|
|
||||||
|
local fqdn
|
||||||
|
[ "$name" == "@" ] && fqdn=$zone || fqdn="$name.$zone"
|
||||||
|
[ -n "$value" ] && value=" $value"
|
||||||
|
|
||||||
|
update_script="update delete $fqdn $rtype$value"
|
||||||
|
|
||||||
|
_nsupdate "$zone" "$view" "$update_script" "$pretend"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
95
lib/output.sh
Normal file
95
lib/output.sh
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
_prettytable_char_top_left="┌"
|
||||||
|
_prettytable_char_horizontal="─"
|
||||||
|
_prettytable_char_vertical="│"
|
||||||
|
_prettytable_char_bottom_left="└"
|
||||||
|
_prettytable_char_bottom_right="┘"
|
||||||
|
_prettytable_char_top_right="┐"
|
||||||
|
_prettytable_char_vertical_horizontal_left="├"
|
||||||
|
_prettytable_char_vertical_horizontal_right="┤"
|
||||||
|
_prettytable_char_vertical_horizontal_top="┬"
|
||||||
|
_prettytable_char_vertical_horizontal_bottom="┴"
|
||||||
|
_prettytable_char_vertical_horizontal="┼"
|
||||||
|
|
||||||
|
|
||||||
|
# Escape codes
|
||||||
|
|
||||||
|
# Default colors
|
||||||
|
_prettytable_color_blue="0;34"
|
||||||
|
_prettytable_color_green="0;32"
|
||||||
|
_prettytable_color_cyan="0;36"
|
||||||
|
_prettytable_color_red="0;31"
|
||||||
|
_prettytable_color_purple="0;35"
|
||||||
|
_prettytable_color_yellow="0;33"
|
||||||
|
_prettytable_color_gray="1;30"
|
||||||
|
_prettytable_color_light_blue="1;34"
|
||||||
|
_prettytable_color_light_green="1;32"
|
||||||
|
_prettytable_color_light_cyan="1;36"
|
||||||
|
_prettytable_color_light_red="1;31"
|
||||||
|
_prettytable_color_light_purple="1;35"
|
||||||
|
_prettytable_color_light_yellow="1;33"
|
||||||
|
_prettytable_color_light_gray="0;37"
|
||||||
|
|
||||||
|
# Somewhat special colors
|
||||||
|
_prettytable_color_black="0;30"
|
||||||
|
_prettytable_color_white="1;37"
|
||||||
|
_prettytable_color_none="0"
|
||||||
|
|
||||||
|
function _prettytable_prettify_lines() {
|
||||||
|
cat - | sed -e "s@^@${_prettytable_char_vertical}@;s@\$@ @;s@ @ ${_prettytable_char_vertical}@g"
|
||||||
|
}
|
||||||
|
|
||||||
|
function _prettytable_fix_border_lines() {
|
||||||
|
cat - | sed -e "1s@ @${_prettytable_char_horizontal}@g;3s@ @${_prettytable_char_horizontal}@g;\$s@ @${_prettytable_char_horizontal}@g"
|
||||||
|
}
|
||||||
|
|
||||||
|
function _prettytable_colorize_lines() {
|
||||||
|
local color="$1"
|
||||||
|
local range="$2"
|
||||||
|
local ansicolor="$(eval "echo \${_prettytable_color_${color}}")"
|
||||||
|
|
||||||
|
cat - | sed -e "${range}s@\\([^${_prettytable_char_vertical}]\\{1,\\}\\)@"$'\E'"[${ansicolor}m\1"$'\E'"[${_prettytable_color_none}m@g"
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettytable() {
|
||||||
|
local cols="${1}"
|
||||||
|
local color="${2:-none}"
|
||||||
|
local input="$(cat -)"
|
||||||
|
local header="$(echo -e "${input}"|head -n1)"
|
||||||
|
local body="$(echo -e "${input}"|tail -n+2)"
|
||||||
|
{
|
||||||
|
# Top border
|
||||||
|
echo -n "${_prettytable_char_top_left}"
|
||||||
|
for i in $(seq 2 ${cols}); do
|
||||||
|
echo -ne "\t${_prettytable_char_vertical_horizontal_top}"
|
||||||
|
done
|
||||||
|
echo -e "\t${_prettytable_char_top_right}"
|
||||||
|
|
||||||
|
echo -e "${header}" | _prettytable_prettify_lines
|
||||||
|
|
||||||
|
# Header/Body delimiter
|
||||||
|
echo -n "${_prettytable_char_vertical_horizontal_left}"
|
||||||
|
for i in $(seq 2 ${cols}); do
|
||||||
|
echo -ne "\t${_prettytable_char_vertical_horizontal}"
|
||||||
|
done
|
||||||
|
echo -e "\t${_prettytable_char_vertical_horizontal_right}"
|
||||||
|
|
||||||
|
echo -e "${body}" | _prettytable_prettify_lines
|
||||||
|
|
||||||
|
# Bottom border
|
||||||
|
echo -n "${_prettytable_char_bottom_left}"
|
||||||
|
for i in $(seq 2 ${cols}); do
|
||||||
|
echo -ne "\t${_prettytable_char_vertical_horizontal_bottom}"
|
||||||
|
done
|
||||||
|
echo -e "\t${_prettytable_char_bottom_right}"
|
||||||
|
} | column -t -s $'\t' | _prettytable_fix_border_lines | _prettytable_colorize_lines "${color}" "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
table_output() {
|
||||||
|
local n_cols=$#
|
||||||
|
{
|
||||||
|
printf "$(join_by "\t" "$@")\n"
|
||||||
|
cat -
|
||||||
|
} | prettytable "$n_cols" "${TABLE_HEADER_COLOR:-gray}"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user