--- a/.hgignore Thu Feb 26 19:30:27 2015 +0200
+++ b/.hgignore Thu Feb 26 19:49:10 2015 +0200
@@ -1,11 +1,14 @@
+syntax:glob
+
# snmp
usr/mibs/
# tempfiles
-.*.pyc
+*.pyc
.*.sw[op]
# generated files
+var/
dist/
MANIFEST
log/
--- a/README Thu Feb 26 19:30:27 2015 +0200
+++ b/README Thu Feb 26 19:49:10 2015 +0200
@@ -180,7 +180,10 @@
fixed-address 10.2.0.1;
}
-== Host structure ==
+= `update` =
+A script to drive the *pvl.hosts* tools for maintaing a set of zone/host files for a DNS/DHCP server.
+
+== Source host files ==
Creating a tree of symlinks for managing split zonefile domains can be useful:
@@ -211,6 +214,75 @@
2 PTR bar.test.
5 PTR quux.asdf.test.
+== Usage ==
+
+=== `bin/update` ===
+*update* reads host/zone file sources from `etc/`, and generates zonefiles/dhcp configs under `var/`.
+
+`update` will also shows and commits changes to `etc/` in any supported version-control system, and use commit timestamps for stable zone serials.
+
+ -d DIR
+ Do data operations under given dir-root, as opposed to CWD.
+
+ -q
+ Quiet. No log messages except errors.
+
+ -vDV
+ Increasing logging verbosity.
+
+ -p
+ Show diffs for changed output on stdout.
+
+ -F
+ Force-update output files, even if newer than input files.
+
+ -S
+ Do not generate new serials for zones.
+
+ -d DIR
+ Do data operations under given dir-root, as opposed to CWD.
+
+ -q
+ Quiet. No log messages except errors.
+
+ -vDV
+ Increasing logging verbosity.
+
+ -p
+ Show diffs for changed output on stdout.
+
+ -F
+ Force-update output files, even if newer than input files.
+
+ -S
+ Do not generate new serials for zones.
+
+ -s
+ Generate new serials for all zones.
+
+ -n
+ Fake-update; show changes, but don't actually commit/deploy them.
+
+ Useful for testing.
+
+ -C
+ Do not commit source changes.
+
+ -c
+ Force-commit source changes, even though -n
+
+ -m MSG
+ Commit message for source changes; optional
+
+== Output structure ==
+Generated file structure.
+
+=== `var/dhcp/` ===
+Generated dhcpd.conf fragments, loaded by dhcpd.
+
+=== `var/zones/` ===
+Generated zonefiles, loaded by bind.
+
= Experimental features =
Features that are still under development
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/update Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,135 @@
+#!/bin/bash
+# vim: set ft=sh :
+
+# Bootstrap
+if [ $0 == './update' ]; then
+ SRV=$(pwd)
+ OPT=/opt/pvl-verkko
+ LIB=/opt/pvl-dns/lib
+else
+ SRV=${SRV:-/srv/verkko}
+ OPT=${SRV:-/opt/pvl-verkko}
+ LIB=/opt/pvl-dns/lib
+ cd $SRV
+fi
+
+source $LIB/update
+
+function commit {
+ ## Commit
+ # pre-commit check
+ log "Testing hosts..."
+ for hosts in $(list_files etc/hosts); do
+ log_warn "TODO: check_hosts $hosts"
+ done
+
+ # commit, unless noop'd
+ log "Commit..."
+ update_commit etc
+}
+
+function update {
+ if hg_modified etc; then
+ serial=$(unix_time)
+ log_warn "Using local unix time for uncommited changes: $serial"
+ else
+ serial=$(hg_time etc)
+ log_update "Using HG commit timestamp: $serial"
+ fi
+
+ ## Hosts
+ log "Updating forward host zones..."
+ for zone in $(list_dirs etc/hosts/forward); do
+ update_hosts_forward "var/zones/hosts/forward/$zone" "$zone" \
+ etc/hosts/forward/$zone/*
+ done
+
+ log "Updating DHCP hosts..."
+ for hosts in $(list etc/hosts/dhcp); do
+ update_hosts_dhcp "var/dhcp/hosts/$hosts.conf" $hosts \
+ $(expand_files "etc/hosts/dhcp/$hosts")
+ done
+
+ log "Updating reverse host zones..."
+ for zone in $(list_dirs etc/hosts/reverse); do
+ update_hosts_reverse "var/zones/hosts/reverse/$zone" "$zone" \
+ etc/hosts/reverse/$zone/*
+ done
+
+ ## Zones
+ log "Copying zone includes..."
+ for zone in $(list_files etc/zones/includes); do
+ copy "var/zones/includes/$zone" "etc/zones/includes/$zone"
+ done
+
+ log "Updating zone serials..."
+ for zone in $(list_files etc/zones); do
+ update_serial "var/serials/$zone" $serial \
+ "etc/zones/$zone" $(zone_includes var/include-cache/$zone etc/zones/$zone var/zones/)
+ done
+
+ log "Updating zones..."
+ for zone in $(list_files etc/zones); do
+ update_zone "var/zones/$zone" "etc/zones/$zone" "var/serials/$zone" \
+ $(zone_includes var/include-cache/$zone etc/zones/$zone var/zones/)
+ done
+
+ log "Updating DHCP confs..."
+ for conf in $(list_files etc/dhcp); do
+ update_dhcp_conf "var/dhcp/$conf" "etc/dhcp/$conf"
+ done
+}
+
+function deploy {
+ ## Check
+ log "Testing zones..."
+ for zone in $(list_files etc/zones); do
+ check_zone "var/zones/$zone" $zone
+ done
+
+ log "Testing DHCP confs..."
+ for conf in var/dhcp/*.conf; do
+ check_dhcp $conf
+ done
+
+ log "Reload zones..."
+ reload_zones
+
+ log "Reload dhcp..."
+ reload_dhcp
+
+}
+
+## Main entry point
+function main {
+ log_init
+
+ parse_args "$@"
+
+ ## Input dirs
+ for dir in etc etc/zones etc/hosts; do
+ [ -d $dir ] || die "Missing directory: $dir"
+ done
+
+ ## Output dirs
+ ensure_dir var
+ for dir in var/dhcp var/zones var/include-cache var/serials; do
+ ensure_dir $dir
+ done
+ for dir in var/dhcp/hosts; do
+ ensure_dir $dir
+ done
+ for dir in var/zones/includes var/zones/hosts; do
+ ensure_dir $dir
+ done
+ for dir in var/zones/hosts/forward var/zones/hosts/reverse; do
+ ensure_dir $dir
+ done
+
+ ## Go
+ commit
+ update
+ deploy
+}
+
+main "$@"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,13 @@
+#!/bin/bash
+#
+
+## Strict errors
+set -ue
+
+shopt -s globstar nullglob
+
+## Library includes
+for lib in $LIB/update.*; do
+ source $lib
+done
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update.args Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,111 @@
+#!/bin/bash
+# vim: set ft=sh :
+#
+# Command-line options
+
+## Options
+
+SERIAL_NOOP=
+SERIAL_FORCE=
+
+COMMIT_SKIP=
+COMMIT_FORCE=
+COMMIT_MSG=' '
+
+RELOAD_NOOP=
+RELOAD_FORCE=
+
+## Output command-line argument help.
+function help_args {
+ local prog=$1
+
+ cat <<END
+Usage: $prog [options]
+
+General:
+ -h display this help text
+ -d DIR datadir
+
+Logging:
+ -q quiet
+ -v verbose
+ -D debug
+ -V debug commands
+
+Updates:
+ -p show changes
+ -F force-updates without checking src mtime
+ -S do not update serial
+ -s update serials
+ -n no-op/mock-update; don't actually change/deploy anything; implies -SpC
+
+Commit:
+ -C do not commit changes
+ -c commit changes
+ -m MSG commit message
+
+Deploy:
+ -R do not reload zones/dhcp
+ -r force reload zones/dhcp
+END
+}
+
+## Parse any command-line arguments, setting the global options vars.
+function parse_args {
+ OPTIND=1
+
+ while getopts 'hd:qvDVpFSsnCcm:Rr' opt "$@"; do
+ case $opt in
+ h)
+ help_args $0
+ exit 0
+ ;;
+
+ d) SRV="$OPTARG" ;;
+
+ q)
+ LOG=
+ LOG_WARN=
+ LOG_UPDATE=
+ LOG_FORCE=
+ LOG_NOOP=
+ LOG_DIFF=
+ ;;
+
+ v) LOG_SKIP=y ;;
+ D)
+ LOG_DEBUG=y
+ LOG_INFO=y
+ ;;
+ V) LOG_CMD=y ;;
+
+ p) UPDATE_DIFF=y ;;
+ F) UPDATE_FORCE=y ;;
+ S) SERIAL_NOOP=y ;;
+ s) SERIAL_FORCE=y ;;
+
+ n)
+ UPDATE_NOOP=y
+ # implies -Sp
+ UPDATE_DIFF=y
+ SERIAL_NOOP=y
+ COMMIT_SKIP=y
+ RELOAD_NOOP=y
+ ;;
+
+ C) COMMIT_SKIP=y ;;
+ c) COMMIT_FORCE=y ;;
+ m) COMMIT_MSG="$OPTARG" ;;
+
+ R) RELOAD_NOOP=y ;;
+ r) RELOAD_FORCE=y ;;
+
+ ?)
+ die
+ ;;
+ esac
+
+ done
+}
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update.config Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,16 @@
+# charset for files under etc/
+CHARSET='utf-8'
+
+# External bins
+NAMED_CHECKZONE=/usr/sbin/named-checkzone
+
+DHCPD=/usr/sbin/dhcpd
+DHCPD_CONF=/etc/dhcp/dhcpd.conf
+DHCPD_INIT=/etc/init.d/isc-dhcp-server
+
+HG=/usr/bin/hg
+HG_ARGS=(--config trusted.users=root)
+
+RNDC=/usr/sbin/rndc
+RNDC_KEY=/etc/bind/rndc.key
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update.hg Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# HG wrappers
+
+## Run `hg ...` within $REPO.
+function hg {
+ local repo=$1; shift
+ cmd $HG -R "$repo" "${HG_ARGS[@]}" "$@"
+}
+
+## Does the repo have local modifications?
+function hg_modified {
+ hg $1 id -i | grep -q '+'
+}
+
+## Get the date for the current commit as an unix timestamp
+function hg_time {
+ local repo=$1
+ local hg_unix=
+ local hg_tz=
+
+ local hg_date=$(hg $repo log -r . --template '{date|hgdate}')
+ local hg_unix=${hg_date% *}
+ local hg_tz=${hg_date#* }
+
+ [ -n "$hg_unix" ] || fail "failed to read hg time"
+
+ echo "$hg_unix"
+}
+
+## Show changes in repo
+# hg_diff [path ...]
+function hg_diff {
+ local repo=$1; shift
+ hg $repo diff "$@"
+}
+
+## Commit changes in repo, with given message:
+#
+# hg_commit .../etc $msg
+#
+# Automatically determines possible -u to use when running with sudo.
+function hg_commit {
+ local repo="$1"
+ local msg="$2"
+ local user_opt=
+ local msg_opt=
+
+ if [ ${SUDO_USER:-} ]; then
+ user_opt=('-u' "$SUDO_USER")
+
+ elif [ $HOME ] && [ -e $HOME/.hgrc ]; then
+ debug "using .hgrc user"
+ user_opt=( )
+
+ else
+ user_opt=('-u' "$USER")
+ fi
+
+ if [ "$msg" ]; then
+ msg_opt=('-m' "$msg")
+ fi
+
+ # XXX: there's something about bash arrays that I don't like... empty arrays behave badly
+ # mercurial does not like it if you pass it '' as an argument
+ if [ -n "${user_opt:-}" -a -n "${msg_opt:-}" ]; then
+ hg $repo commit "${user_opt[@]}" "${msg_opt[@]}"
+ elif [ -n "${user_opt:-}" ]; then
+ hg $repo commit "${user_opt[@]}"
+ elif [ -n "${msg_opt:-}" ]; then
+ hg $repo commit "${msg_opt[@]}"
+ else
+ hg $repo commit
+ fi
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update.log Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,125 @@
+#!/bin/bash
+# vim: set ft=sh :
+#
+# Logging output
+
+# use color output?
+IS_TTY=
+
+LOG_ERROR=y
+LOG_WARN=y
+LOG=y
+LOG_FORCE=y
+LOG_UPDATE=y
+LOG_NOOP=y
+LOG_SKIP=
+LOG_DEBUG=
+LOG_CMD=
+LOG_DIFF=y
+
+function log_init {
+ [ -t 1 ] && IS_TTY=y
+}
+
+# Output message to stderr.
+function log_msg {
+ echo "$*" >&2
+}
+
+# Output message to stderr, optionally with given color, if TTY.
+function log_color {
+ local code=$1; shift
+
+ if [ $IS_TTY ]; then
+ echo $'\e['${code}'m'"$*"$'\e[00m' >&2
+ else
+ echo "$*" >&2
+ fi
+}
+
+## Log at various log-levels
+# plain
+function log {
+ [ $LOG ] && log_msg "$*" || true
+}
+
+function log_error {
+ [ $LOG_ERROR ] && log_color '31' "$*" || true
+}
+
+function log_warn {
+ [ $LOG_WARN ] && log_color '33' "$*" || true
+}
+
+function log_force {
+ [ $LOG_FORCE ] && log_color '2;33' " $*" || true
+}
+
+function log_update {
+ [ $LOG_UPDATE ] && log_color '36' " $*" || true
+}
+
+function log_check {
+ [ $LOG_UPDATE ] && log_color '37' " $*" || true
+}
+
+function log_noop {
+ [ $LOG_NOOP ] && log_color '2;34' " $*" || true
+}
+
+function log_skip {
+ [ $LOG_SKIP ] && log_color '1;34' " $*" || true
+}
+
+function log_debug {
+ [ $LOG_DEBUG ] && log_color '32' " $*" || true
+}
+
+function log_cmd {
+ [ $LOG_CMD ] && log_color '35' " \$ $*" || true
+}
+
+# Output stacktrace, 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
+}
+
+# Output calling function's name.
+function func_caller {
+ caller 1 | cut -d ' ' -f 2
+}
+
+### High-level logging output
+# Log with func_caller at log_debug
+function debug {
+ printf -v prefix "%s" $(func_caller)
+
+ log_debug "$prefix: $*"
+}
+
+function warn {
+ log_warn "$(func_caller): $*"
+}
+
+# Log with func_caller at log_error and exit, intended for internal errors...
+function fail {
+ log_error "$(func_caller): $*"
+
+ exit 2
+}
+
+# Log at log_error and exit
+function die {
+ log_error "$*"
+ exit 1
+}
+
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update.operations Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,373 @@
+#!/bin/bash
+## vim: set ft=sh :
+#
+# Operations on zonefiles/hosts/whatever
+
+function link {
+ local out="$1"
+ local tgt="$2"
+
+ if check_link "$out" "$tgt"; then
+ log_skip "Linking $out -> $tgt: not changed"
+ else
+ log_update "Linking $out -> $tgt..."
+
+ do_link "$out" "$tgt"
+ fi
+}
+
+function copy {
+ local out="$1"
+ local src="$2"
+
+ if check_update "$out" "$src"; then
+ log_update "Copying $out <- $src..."
+
+ do_update "$out" \
+ cat "$src"
+ else
+ log_skip "Copying $out <- $src: not changed"
+ fi
+}
+
+## Run check-command on given file, outputting results:
+#
+# check $src $cmd $args...
+#
+function check {
+ local src="$1"; shift
+ local cmd="$1"; shift
+
+ if cmd_test "$cmd" -q "$@"; then
+ log_skip "Check $src: OK"
+
+ else
+ log_error " Check $src: Failed"
+
+ indent " " "$cmd" "$@"
+
+ exit 1
+ fi
+}
+
+## Generate forward zone from hosts hosts using pvl.hosts-dns:
+#
+# update_hosts_forward out/hosts/$hosts $hosts in/hosts/$hosts
+function update_hosts_forward {
+ local out="$1"; shift
+ local domain="$1"; shift
+
+ if check_update "$out" "$@"; then
+ log_update "Generating forward hosts zone $out @ $domain <- $@..."
+
+ do_update "$out" $OPT/bin/pvl.hosts-dns \
+ --hosts-charset=$CHARSET \
+ --hosts-include=etc/hosts \
+ --forward-zone="$domain" \
+ "$@"
+ else
+ log_skip "Generating forward hosts $out <- $@: not changed"
+ fi
+}
+
+function update_hosts_dhcp {
+ local out=$1; shift
+ local domain="$1"; shift
+
+ if check_update $out "$@"; then
+ log_update "Generating DHCP hosts $out @ $domain <- $@..."
+
+ do_update $out $OPT/bin/pvl.hosts-dhcp \
+ --hosts-charset=$CHARSET \
+ --hosts-include=etc/hosts \
+ "$@"
+ else
+ log_skip "Generating DHCP hosts $out <- $@: not changed"
+ fi
+}
+
+## Generate reverse zone from hosts hosts using pvl.hosts-dns:
+#
+# update_hosts_reverse out/hosts/$reverse $reverse in/hosts/$hosts
+function update_hosts_reverse {
+ local out="$1"; shift
+ local reverse="$1"; shift
+
+ if check_update "$out" "$@"; then
+ log_update "Generating reverse hosts zone $out <- $@..."
+
+ do_update "$out" $OPT/bin/pvl.hosts-dns \
+ --hosts-charset=$CHARSET \
+ --hosts-include=etc/hosts \
+ --reverse-zone="$reverse" \
+ "$@"
+ else
+ log_skip "Generating reverse hosts $out <- $@: not changed"
+ fi
+}
+
+## Update .serial number:
+#
+# do_update_serial .../serials/$zone $serial
+#
+function do_update_serial {
+ local dst="$1"
+ local serial="$2"
+
+ echo $serial > $dst
+}
+
+
+## Generate new serial for zone using pvl.dns-serial, if the zone data has changed:
+#
+# update_serial .../serials/$zone $serial $deps...
+#
+# Supports SERIAL_FORCE/NOOP.
+# Updates $SERIALS/$zone.serial.
+function update_serial {
+ local dst="$1"; shift
+ local serial="$1"; shift
+
+ local old=$(test -e "$dst" && cat "$dst" || echo '')
+
+ # test
+ if [ $SERIAL_FORCE ]; then
+ log_force "Updating $dst: $old <- $serial: forced"
+
+ do_update_serial "$dst" "$serial"
+
+ elif ! check_update "$dst" "$@"; then
+ log_skip "Updating $dst: $old <- $serial: not changed"
+
+ elif [ $SERIAL_NOOP ]; then
+ log_noop "Updating $dst: $old <- $serial: skipped"
+
+ else
+ log_update "Updating $dst: $old <- $serial"
+
+ do_update_serial "$dst" "$serial"
+ fi
+}
+
+## Generate zone file from source using pvl.dns-zone:
+#
+# update_zone out/zones/$zone in/zones/$zone var/serials/$zone
+function update_zone {
+ local out="$1"; shift
+ local src="$1"; shift
+ local serial="$1"; shift
+ local serial_opt=
+
+ if [ -n "$serial" -a -f "$serial" ]; then
+ serial_opt="--serial=$(cat "$serial")"
+ elif [ $SERIAL_NOOP ]; then
+ warn "$out: noop'd serial, omitting"
+ else
+ fail "$out: missing serial: $serial"
+ fi
+
+ if check_update "$out" "$src" "$serial" "$@"; then
+ log_update "Generating $out <- $src..."
+
+ do_update "$out" $OPT/bin/pvl.dns-zone "$src" \
+ --include-path=$SRV/var/zones \
+ $serial_opt
+ else
+ log_skip "Generating $out <- $src: not changed"
+ fi
+}
+
+## Generate dhcp confs from source using pvl.dhcp-conf:
+function update_dhcp_conf {
+ local out="$1"
+ local src="$2"
+
+ if check_update "$out" "$src"; then
+ log_update "Generating $out <- $src..."
+
+ do_update "$out" $OPT/bin/pvl.dhcp-conf "$src" \
+ --include-path=$SRV/var/dhcp
+ else
+ log_skip "Generating $out <- $src: not changed"
+ fi
+}
+
+## Test hosts zone for validity using pvl.hosts-check:
+#
+# check_hosts .../hosts
+function check_hosts {
+ local hosts=$1; shift 1
+
+ # TODO
+ check $hosts \
+ $OPT/bin/pvl.hosts-check $hosts
+}
+
+## Test zone file for validity using named-checkzone:
+#
+# check_zone ..../$zone $origin
+function check_zone {
+ local zone=$1
+ local origin=$2
+
+ log_check "Checking $zone @ $origin..."
+
+ # checkzone is very specific about the order of arguments, -q must be first
+ check $zone $NAMED_CHECKZONE $origin $zone
+}
+
+## Test DHCP configuration for validity using dhcpd -t:
+#
+# check_dhcp [$conf]
+#
+# Defaults to the global $DHCPD_CONF.
+# Fails if the check fails.
+function check_dhcp {
+ local conf=${1:-$DHCPD_CONF}
+
+ log_check "Checking DHCP $conf..."
+
+ if [ ! -e $DHCPD ]; then
+ log_warn "check_dhcp: dhcpd not installed, skipping: $conf"
+ return 0
+ fi
+
+ check $conf \
+ $DHCPD -cf $conf -t
+}
+
+## Test DHCP configuration of given settings/dhcp using check_dhcp $DHCP_DATA/$host.conf:
+#
+# check_dhcp_conf $conf
+#
+function check_dhcp_conf {
+ local conf=$1;
+
+ check_dhcp $DHCP_DATA/$conf.conf
+}
+
+### Deploy
+# set by do_reload_zone if zone data has actually been reloaded
+RELOAD_ZONES=
+
+## Run rndc reload
+function do_reload_zones {
+ # run
+ indent " rndc: " \
+ $RNDC reload
+
+ # set flag
+ RELOAD_ZONES=y
+}
+
+## Load update zonefiles into bind:
+#
+# reload_zones
+#
+# Invokes `rndc reload`, showing its output.
+function reload_zones {
+ local msg="Reload zones"
+
+ if [ $RELOAD_FORCE ]; then
+ log_force "$msg..."
+
+ do_reload_zones
+
+ elif [ $RELOAD_NOOP ]; then
+ log_noop "$msg: skipped"
+
+ elif [ ! -e $RNDC ]; then
+ log_warn "reload_zones: rndc not installed, skipping"
+
+ elif [ ! -e $RNDC_KEY ]; then
+ log_warn " $msg: rndc: key not found: $RNDC_KEY"
+
+ elif [ ! -r $RNDC_KEY ]; then
+ log_error " $msg: rndc: permission denied: $RNDC_KEY"
+
+ return 1
+
+ else
+ log_update "$msg..."
+
+ # run
+ do_reload_zones
+ fi
+}
+
+## Reload DHCP by restarting it, if running:
+#
+# do_reload_dhcp
+#
+# Does NOT restart dhcp if it is not running (status).
+function do_reload_dhcp {
+ if cmd_test $DHCPD_INIT status >/dev/null; then
+ cmd $DHCPD_INIT restart
+ else
+ log_warn "dhcpd not running; did not restart"
+ fi
+}
+
+## Reload dhcp hosts
+#
+# reload_dhcp
+#
+# noop's if we haven't reloaded zones
+function reload_dhcp {
+ local msg="Reload DHCP hosts"
+
+ if [ $RELOAD_FORCE ]; then
+ log_force "$msg..."
+
+ do_reload_dhcp
+
+ elif [ $RELOAD_NOOP ]; then
+ log_noop "$msg: skipped"
+
+ elif [ ! -e $DHCPD ]; then
+ log_warn "reload_dhcp: dhcpd not installed, skipping: $conf"
+
+ else
+ log_update "$msg..."
+
+ # run
+ do_reload_dhcp
+ fi
+}
+
+### Commit
+## Commit changes to version control:
+#
+# update_commit .../etc "commit message"
+#
+# Invokes `hg commit`, first showing the diff.
+function update_commit {
+ local repo="$1"
+ local commit_msg="$COMMIT_MSG"
+
+ local msg="Commit changes"
+
+ # operate?
+ if [ $COMMIT_FORCE ]; then
+ log_force "$msg: $commit_msg"
+
+ [ $LOG_DIFF ] && indent " " hg_diff $repo
+
+ hg_commit "$repo" "$commit_msg"
+
+ elif ! hg_modified "$repo"; then
+ log_warn "$msg: no changes"
+
+ elif [ $COMMIT_SKIP ]; then
+ log_noop "$msg: skipped"
+
+ # still show diff, though
+ [ $LOG_DIFF ] && indent " " hg_diff "$repo"
+ else
+ log_update "$msg: $commit_msg"
+
+ [ $LOG_DIFF ] && indent " " hg_diff $repo
+
+ hg_commit "$repo" "$commit_msg"
+ fi
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update.updates Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,158 @@
+#!/bin/bash
+# vim: set ft=sh :
+#
+# Dependency-based updates + utils
+
+UPDATE_FORCE=
+UPDATE_NOOP=
+UPDATE_DIFF=
+
+## Compare the given output file with all given source files:
+#
+# check_update $out ${deps[@]} && do_update $out ... || ...
+#
+# Returns true if the output file needs to be updated.
+function check_update {
+ # target
+ local out="$1"; shift
+
+ debug "$out"
+
+ # need update?
+ local update=
+
+ if [ ${#@} == 0 ]; then
+ debug " update: unknown deps"
+ update=y
+
+ elif [ ! -e "$out" ]; then
+ debug " update: dest missing"
+ update=y
+
+ elif [ $UPDATE_FORCE ]; then
+ debug " update: forced"
+ update=y
+ fi
+
+ # check deps
+ for dep in "$@"; do
+ # don't bother checking if already figured out
+ [ $update ] && continue
+
+ # check
+ if [ ! -e "$dep" ]; then
+ warn "$out: Missing source: $dep"
+
+ elif [ "$out" -ot "$dep" ]; then
+ debug " update: $dep"
+ update=y
+ else
+ debug " check: $dep"
+ fi
+ done
+
+ [ ! $update ] && debug " up-to-date"
+
+ # return
+ [ $update ]
+}
+
+## Generate updated output file from given command's stdout:
+#
+# do_update $out $BIN/cmd --args
+#
+# Writes output to a temporary .new file, optionally shows a diff of changes, and commits
+# the new version to $out (unless noop'd).
+function do_update {
+ local out="$1"; shift
+ local tmp="$out.new"
+
+ debug "$out"
+ cmd "$@" > "$tmp"
+
+ # compare
+ if [ -e "$out" ] && [ $UPDATE_DIFF ]; then
+ debug " changes:"
+
+ # terse
+ indent " " diff --unified=1 "$out" "$tmp" || true
+ fi
+
+ # deploy
+ if [ $UPDATE_NOOP ]; then
+ # cleanup
+ debug " no-op"
+
+ cmd rm "$tmp"
+ else
+ # commit
+ debug " deploy"
+
+ cmd mv "$tmp" "$out"
+ fi
+}
+
+## Compare symlink to target:
+#
+# check_link $lnk $tgt && do_link $lnk $tgt || ...
+#
+# Tests if the symlink exists, and the target matches.
+# Fails if the symlink needs updating.
+function check_link {
+ local out="$1"
+ local tgt="$2"
+
+ [ -e "$out" ] || return 1
+
+ [ -L "$out" ] || fail "$out: is not a link"
+
+ [ "$(readlink "$out")" == "$tgt" ] || return 1
+
+ return 0
+}
+
+## Update symlink to point to target:
+#
+# do_link $lnk $tgt
+#
+function do_link {
+ local out="$1"
+ local tgt="$2"
+
+ cmd ln -sf "$tgt" "$out"
+
+ [ -e "$out" ] || fail "$out: given target does not exist: $tgt"
+}
+
+## Read include paths from file
+function read_zone_includes {
+ cmd sed -n -E 's/^\$INCLUDE\s+"(.+)"/\1/p' "$@"
+}
+
+## (cached) include paths for zone file
+function zone_includes {
+ local cache="$1"
+ local src="$2"
+ local prefix="${3:-}"
+
+ if [ ! -e "$cache" -o "$cache" -ot "$src" ]; then
+ read_zone_includes "$src" > "$cache"
+ fi
+
+ while read include; do
+ echo -n "$prefix$include "
+ done < "$cache"
+}
+
+## Search for prefix-matching includes in zone file
+function zone_includes_grep {
+ local cache="$1"
+ local src="$2"
+ local prefix="$3"
+
+ for include in $(zone_includes $cache $src); do
+ if [ "${include#$prefix}" != "$include" ]; then
+ echo -n " ${include#$prefix}"
+ fi
+ done
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/update.utils Thu Feb 26 19:49:10 2015 +0200
@@ -0,0 +1,102 @@
+#!/bin/bash
+#
+# Utility functions
+
+### Command execution
+## Execute command, possibly logging its execution.
+#
+# cmd $cmd...
+#
+# Fails if the command returns an error exit code.
+function cmd {
+ log_cmd "$@"
+
+ "$@" || die "Failed: $@"
+}
+
+## Execute command as a test, logging its execution at log_cmd
+#
+# cmd_test $cmd... && ... || ...
+#
+# Fails if the command returns an error exit code.
+function cmd_test {
+ log_cmd "$@"
+
+ "$@"
+}
+## Execute command, prefixing its output on stdout with given indent prefix.
+#
+# indent " " $cmd...
+#
+# Output is kept on stdout, exit status is that of the given command.
+function indent () {
+ local indent="$1"; shift
+
+ "$@" | sed "s/^/$indent/"
+
+ return ${PIPESTATUS[0]}
+}
+
+
+### FS utils
+# Create dir if not exists.
+function ensure_dir {
+ local dir="$1"
+
+ if [ ! -d "$dir" ]; then
+ log_warn "Creating output dir: $dir"
+ cmd mkdir "$dir"
+ fi
+}
+
+## Output absolute path
+#
+# abspath $path
+#
+# XXX: improve...?
+function abspath () {
+ local path="$1"
+
+ echo "$SRV/$path"
+}
+
+function _list {
+ local glob="$1"
+ local test="$2"
+ local prefix="$3"
+
+ for file in $glob; do
+ [ $test "$file" ] || continue
+ [ -n "$prefix" ] && file="${file#$prefix}"
+
+ echo -n "$file "
+ done
+}
+
+## List names of all files in dir
+function list {
+ _list "$1/*" '-e' ${2:-$1/}
+}
+
+## List names of files in dir:
+#
+# list_files $dir
+#
+function list_files {
+ _list "$1/*" '-f' ${2:-$1/}
+}
+
+## List names of dirs in dir:
+function list_dirs {
+ _list "$1/*" '-d' ${2:-$1/}
+}
+
+## List names of any files underneath dir or file:
+function expand_files {
+ _list "$1 $1/**" '-f' ''
+}
+
+## Get current unix (utc) timestamp
+function unix_time {
+ date +'%s'
+}