merge in the pvl-dns repo, with all of its history
authorTero Marttila <tero.marttila@aalto.fi>
Thu, 26 Feb 2015 19:49:10 +0200
changeset 626 5cd99761fe4d
parent 522 6bc714379a13 (current diff)
parent 625 57e4f48a7140 (diff)
child 627 a81206440be2
merge in the pvl-dns repo, with all of its history
.hgignore
README
--- 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'
+}