#!/bin/bash
# vim: set ft=sh :
set -ue
ROOT=$(pwd)
BIN=bin
PROCESS_ZONE=$BIN/process-zone
EXPAND_ZONE=$BIN/expand-zone
UPDATE_SERIAL=$BIN/update-serial
SETTINGS=settings
ZONES=zones
SERIALS=serials
PROCESS_ARGS='--input-charset latin-1'
FORWARD_MX=mail
REVERSE_ZONE=194.197.235
REVERSE_DOMAIN=paivola.fi
NAMED_CHECKZONE=/usr/sbin/named-checkzone
## options
IS_TTY=
LOG=y
LOG_INFO=
LOG_DEBUG=
LOG_CMD=
UPDATE_FORCE=
UPDATE_NOOP=
UPDATE_DIFF=
SERIAL_NOUPDATE=
function help_args {
local prog=$1
cat <<END
Usage: $prog [options]
-h display this help text
-q quiet
-v verbose
-D debug
-C debug commands
-p show changes
-F force-updates without checking src mtime
-S do not update serial
-n no-op/mock-update; do not actually change anything; implies -Sp
END
}
function parse_args {
OPTIND=1
while getopts 'hqvDCpFSn' opt "$@"; do
case $opt in
h)
help_args $1
exit 0
;;
q) LOG= ;;
v) LOG_INFO=y ;;
D) LOG_DEBUG=y ;;
C) LOG_CMD=y ;;
p) UPDATE_DIFF=y ;;
F) UPDATE_FORCE=y ;;
S) SERIAL_NOUPDATE=y ;;
n)
UPDATE_NOOP=y
# implies -Sp
UPDATE_DIFF=y
SERIAL_NOUPDATE=y
;;
?)
die
;;
esac
done
}
## lib
function log_msg {
echo "$*" >&2
}
function log_color {
local code=$1; shift
if [ $IS_TTY ]; then
echo $'\e[0;'${code}'m'"$*"$'\e[00m' >&2
else
echo "$*" >&2
fi
}
function log_error {
log_color 31 "$*"
}
function log {
[ $LOG ] && log_msg "$*" || true
}
function log_info {
[ $LOG_INFO ] && log_color 36 " $*" || true
}
function log_debug {
[ $LOG_DEBUG ] && log_color 32 " $*" || true
}
function log_cmd {
[ $LOG_CMD ] && log_color 35 " \$ $*" || true
}
# XXX: broken
function log_stack {
local level=1
while info=$(caller $level); do
echo $info | read line sub file
log_msg "$file:$lineno $sub()"
level=$(($level + 1))
done
}
function fail {
func=$(caller 1 | cut -d ' ' -f 2)
log_error "$func: $*"
exit 2
}
function die {
log_error "$*"
exit 1
}
function cmd {
log_cmd "$@"
"$@" || die "Failed"
}
function run_cmd {
local msg=$1; shift
log_info "$msg... "
cmd "$@"
}
function indent () {
local indent=$1; shift
"$@" | (
while read line; do
echo "$indent$line"
done
) || exit $?
}
## test
[ -d $SETTINGS ] || die "Missing settings: $SETTINGS"
[ -d $SERIALS ] || die "Missing serials: $SERIALS"
[ -d $ZONES ] || die "Missing zones: $ZONES"
## functions
function check_update {
# target
local dst=$1; shift
log_debug "check_update: $dst"
# need update?
local update=
if [ ! -e $dst ] || [ $UPDATE_FORCE ]; then
log_debug " update forced"
update=y
fi
# check deps
for dep in "$@"; do
# don't bother checking if already figured out
[ $update ] && continue
# check
if [ $dst -ot $dep ]; then
log_debug " changed: $dep"
update=y
fi
done
[ ! $update ] && log_debug " up-to-date"
# return
[ $update ]
}
function do_update {
local dst=$1; shift
local tmp=$dst.new
log_debug "update: $dst"
cmd "$@" > $tmp
# compare
if [ -e $dst ] && [ $UPDATE_DIFF ]; then
log_debug " changes:"
# terse
indent " " diff --unified=1 $dst $tmp
fi
if [ $UPDATE_NOOP ]; then
# cleanup
log_debug " no-op"
cmd rm $tmp
else
# commit
log_debug " update"
cmd mv $tmp $dst
fi
}
function update {
local dst=$1; shift;
local sep=
local dep=()
local cmd=()
for arg in "$@"; do
if [ $arg == '--' ]; then
sep=y
fi
if [ $sep ]; then
cmd=("${cmd[@]:-}" "$arg")
else
dep=("${dep[@]:-}" "$arg")
fi
done
[ ! $sep ] && fail "Invalid args given: $@"
check_update $dst "${dep[@]}" && do_update $dst "${cmd[@]}" || true
}
## bin wrappers
function update_serial {
local serial=$1; shift
local old=$(cat $serial)
log_info "Updating serial: $serial"
cmd $UPDATE_SERIAL $* $serial
local new=$(cat $serial)
log_debug " $old -> $new"
}
function expand_zone {
local output=$1; shift
local src=$1; shift
}
function process_zone {
local output=$1; shift
local src=$1; shift
check_update $output $src && update $output $PROCESS_ZONE $PROCESS_ARGS "$@" $src
}
## actions
function copy_zone_part {
local zone=$1
local part=$2
local name=$zone.zone.$part
local src=$SETTINGS/$name
local dst=$ZONES/$name
if check_update $dst $src; then
log_info "Copying zone $zone.$part..."
do_update $dst cat $src
else
log_info "Copying zone $zone.$part: not changed"
fi
}
function update_zone {
local zone=$1
local name=$zone.zone
local out=$ZONES/$name
local in=$SETTINGS/$zone.zone
local serial=$SERIALS/$zone.serial
if check_update $out $in $serial; then
log_info "Generating $zone zone headers..."
do_update $out \
$EXPAND_ZONE $SETTINGS/$zone.zone \
--serial $SERIALS/$zone.serial \
--expand zones=$ROOT/$ZONES
else
log_info "Generating $zone zone headers: not changed"
fi
}
function update_zone_view {
local zone=$1
local view=$2
local name=$view/$zone.zone
local out=$ZONES/$name
local in=$SETTINGS/$zone.zone
local serial=$SERIALS/$zone.serial
if check_update $out $in $serial; then
log_info "Generating $zone:$view zone headers..."
do_update $out \
$EXPAND_ZONE $SETTINGS/$zone.zone \
--serial $SERIALS/$zone.serial \
--expand zones=$ROOT/$ZONES \
--expand view=$view
else
log_info "Generating $zone:$view zone headers: not changed"
fi
}
function update_hosts {
local dst=$1; shift
local src=$1; shift
if check_update $dst $src; then
log_info "Generating $dst..."
do_update $dst $PROCESS_ZONE $PROCESS_ARGS $src "$@"
else
log_info "Generating $dst: not changed"
fi
}
function check_hosts {
local hosts=$1; shift 1
local cmd=($PROCESS_ZONE $PROCESS_ARGS $hosts --check-hosts "$@")
if "${cmd[@]}" -q; then
log_info "Check $hosts: OK"
else
log_error " Check $hosts: Failed"
indent " " "${cmd[@]}"
exit 1
fi
}
function check_zone {
local name=$1
local file=$2
local cmd=($NAMED_CHECKZONE $name $file)
# test
# XXX: checkzone is very specific about the order of arguments, -q must be first
if $NAMED_CHECKZONE -q $name $file; then
log_info "Check $file($name): OK"
else
log_error " Check $file($name): Failed:"
indent " " "${cmd[@]}"
exit 1
fi
}
function main {
# test tty
[ -t 1 ] && IS_TTY=y
parse_args "$@"
log "Testing hosts..."
check_hosts $SETTINGS/paivola.txt --check-exempt ufc
log "Generating host zones..."
update_hosts $ZONES/external/paivola.zone.hosts $SETTINGS/paivola.txt --forward-zone
update_hosts $ZONES/internal/paivola.zone.hosts $SETTINGS/paivola.txt --forward-zone --forward-txt --forward-mx $FORWARD_MX
update_hosts $ZONES/paivola-reverse.zone.hosts $SETTINGS/paivola.txt --reverse-zone $REVERSE_ZONE --reverse-domain $REVERSE_DOMAIN
log "Copying zone parts..."
copy_zone_part paivola auto
copy_zone_part paivola services
copy_zone_part paivola internal
copy_zone_part paivola external
log "Updating serials..."
if [ $SERIAL_NOUPDATE ]; then
log_info "Skipped"
else
update_serial $SERIALS/paivola.serial
update_serial $SERIALS/paivola-reverse.serial
fi
log "Updating zones headers..."
update_zone paivola-reverse
update_zone_view paivola internal
update_zone_view paivola external
log "Testing zones..."
check_zone paivola.fi $ZONES/external/paivola.zone
check_zone paivola.fi $ZONES/external/paivola.zone
check_zone 235.197.194.in-addr.arpa $ZONES/paivola-reverse.zone
}
main "$@"