(none)
authorTero Marttila <tero.marttila@aalto.fi>
Thu, 26 Feb 2015 21:38:09 +0200
changeset 627 a81206440be2
parent 626 5cd99761fe4d
child 628 b10ad946d01d
(none)
bin/update
lib/pvl/apply.sh
lib/pvl/apply/cat.sh
lib/pvl/apply/cmd.sh
lib/pvl/apply/dir.sh
lib/pvl/apply/link.sh
lib/pvl/cmd.sh
lib/pvl/commit.sh
lib/pvl/commit/hg.sh
lib/pvl/file.sh
lib/pvl/hosts/update.sh
lib/pvl/log.sh
lib/pvl/main.sh
lib/pvl/test.sh
lib/pvl/util.sh
lib/update
lib/update.args
lib/update.config
lib/update.hg
lib/update.log
lib/update.operations
lib/update.updates
lib/update.utils
--- a/bin/update	Thu Feb 26 19:49:10 2015 +0200
+++ b/bin/update	Thu Feb 26 21:38:09 2015 +0200
@@ -1,135 +1,21 @@
 #!/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
+SRV=${SRV:-.}
+SRC=${OPT:-.}
+OPT=${OPT:-./opt}
+ETC=${ETC:-$SRV/etc}
+LIB=${LIB:-$SRC/lib}
+VAR=${VAR:-$SRV/var}
 
-    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
+. $LIB/pvl/main.sh
 
-    ## Go
-    commit
-    update
-    deploy
-}
+MODULES=(log commit apply update)
+MODULE=update
+
+. $LIB/pvl/commit.sh
+. $LIB/pvl/apply.sh
+
+. $LIB/pvl/hosts/update.sh
 
 main "$@"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/apply.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,78 @@
+# Idempotent refreshable operations
+
+apply_GETOPTS='pFn'
+
+APPLY=
+APPLY_DIFF=
+
+function apply_help {
+    cat <<END
+Apply:
+    -p      show changes
+    -F      force-updates without checking src mtime
+    -n      no-op/mock-update; don't actually change/deploy anything; implies -SpC
+END
+}
+
+function apply_opt {
+    local opt=$1
+    local optarg="$2"
+
+    case $opt in
+        p)  APPLY_DIFF=1   ;;
+        F)  APPLY=1 ;;
+        
+        n) 
+            APPLY=0
+            APPLY_DIFF=1
+        ;;
+        *)  return 1
+    esac
+}
+
+. $LIB/pvl/apply/dir.sh
+. $LIB/pvl/apply/link.sh
+. $LIB/pvl/apply/cmd.sh
+. $LIB/pvl/apply/cat.sh
+
+## 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 apply_check {
+    local update=
+    local out="$1"
+
+    debug "$out"
+
+    if [ ${#@} -eq 1 ]; then
+        debug "  update: unknown deps"
+        return 0
+
+    elif [ ! -e "$out" ]; then
+        debug "  update: dest missing"
+        return 0
+        
+    elif [ "$APPLY" = 1]; then
+        debug "  update: forced"
+        return 2
+    fi
+
+    # check deps
+    for dep in "${@:2}"; do
+        # check
+        if [ ! -e "$dep" ]; then
+            warn "$out: Missing source: $dep"
+
+        elif [ "$out" -ot "$dep" ]; then
+            debug "  update: $dep"
+            return 1
+        else
+            debug "  check: $dep"
+        fi
+    done
+
+    debug "  up-to-date"
+    return 0
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/apply/cat.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,15 @@
+function apply_cat {
+    local out="$1"
+    local src=("${@:2}")
+
+    if apply_check "$out" "${src[@]}"; then
+        log_skip "$out: skip cat: ${src[@]}"
+    elif [ "$APPLY" = 0 ]; then
+        log_noop "$out: noop cat: $tgt"
+    else
+        log_update "$out: cat ${src@}"
+
+        cmd_apply "$out" \
+            cat "${src[@]}"
+    fi
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/apply/cmd.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,35 @@
+## 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 cmd_apply {
+    local out="$1"
+    local tmp="$out.new"
+
+    debug "$out"
+    cmd "${@:1}" > "$tmp"
+
+    # compare
+    if [ -e "$out" -a -z "$APPLY_DIFF" ]; then
+        debug "  changes:"
+
+        # terse
+        indent "        " diff --unified=1 "$out" "$tmp" || true
+    fi
+    
+    # deploy
+    if [ "$APPLY" = 0 ]; then
+        # cleanup
+        debug "  no-op"
+
+        cmd rm "$tmp"
+    else
+        # commit
+        debug "  deploy"
+
+        cmd mv "$tmp" "$out"
+    fi
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/apply/dir.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,13 @@
+# Create dir if not exists.
+function apply_dir {
+    local dir="$1"
+
+    if [ -d "$dir" ]; then
+        log_skip "$dir: apply dir"
+    elif [ "$APPLY" = 0 ]; then
+        log_noop "$dir: apply dir"
+    else
+        log_apply "$dir: apply dir"
+        cmd mkdir "$dir"
+    fi
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/apply/link.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,47 @@
+## Compare symlink to target:
+#
+#   link_check $lnk $tgt && do_link $lnk $tgt || ...
+#
+# Tests if the symlink exists, and the target matches.
+# Fails if the symlink needs updating.
+function link_check {
+    local out="$1"
+    local lnk="$2"
+
+    [ -e "$out" ] || return 1
+
+    [ -L "$out" ] || fail "$out: is not a link"
+
+    [ "$(readlink "$out")" == "$lnk" ] || return 1
+
+    return 0
+}
+
+## Update symlink to point to target:
+#
+#   do_link $lnk $tgt
+#
+function link_apply {
+    local out="$1"
+    local lnk="$2"
+
+    cmd ln -srf "$lnk" "$out"
+
+    [ -e "$out" ] || fail "$out: given target does not exist: $lnk"
+}
+
+# Create symlink if not exists.
+function apply_link {
+    local out="$1"
+    local lnk="$2"
+
+    if link_check "$out" "$lnk"; then
+        log_skip "$out: not changed: $lnk"
+    elif [ "$APPLY" = 0 ]; then
+        log_noop "$out: skip link: $tgt"
+    else
+        log_apply "$out: $tgt..."
+
+        link_apply "$out" "$lnk"
+    fi
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/cmd.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,34 @@
+### 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 cmd_indent () {
+    local indent="$1"; shift
+
+    "$@" | sed "s/^/$indent/"
+
+    return ${PIPESTATUS[0]}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/commit.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,90 @@
+# VCS abstraction layer
+
+. $LIB/pvl/commit/git.sh
+. $LIB/pvl/commit/hg.sh
+
+commit_GETOPTS='cCm:'
+
+COMMIT=
+COMMIT_DIFF=
+COMMIT_MSG=' '
+
+function commit_help {
+    cat <<END
+Commit:
+    -C      do not commit changes
+    -c      commit changes
+    -m MSG  commit message
+END
+}
+
+function commit_opt {
+    local opt=$1
+    local optarg="$2"
+
+    case $opt in
+        c)  COMMIT=1 ;;
+        C)  COMMIT=0 ;;
+
+        m)  COMMIT_MSG="$optarg" ;;
+        
+        n)  COMMIT= ;;
+        p)  COMMIT_DIFF=1 ;;
+        *)  return 1
+    esac
+}
+
+function commit_probe {
+    local repo="$1"
+
+    for commit in git hg; do
+        if ${commit}_probe "$repo"; then
+            echo $commit
+            return 0
+        fi
+    done
+
+    return 1
+}
+
+## Commit changes to version control:
+#
+#   update_commit .../etc "commit message"
+#
+# Invokes `hg commit`, first showing the diff.
+function commit {
+    local repo="$1"
+    local commit_msg="$COMMIT_MSG"
+
+    # detect
+    local commit="$(commit_probe "$repo")"
+
+    if [ -z "$commit" ]; then
+        log_warn "$repo: Unable to detect VCS repo"
+        return 1
+    fi
+
+    # operate?
+    if [ "$COMMIT" = 1 ]; then
+        log_force   "$repo: force commit"
+
+        [ $COMMIT_DIFF ] && indent "    " ${commit}_diff "$repo"
+
+        ${commit}_commit "$repo" "$commit_msg"
+
+    elif ! ${commit}_modified "$repo"; then
+        log_warn    "$repo: no changes to commit"
+
+    elif [ "$COMMIT" = 0 ]; then
+        log_noop    "$repo: skip commit"
+        
+        # still show diff, though
+        [ $COMMIT_DIFF ] && indent "    " ${commit}_diff "$repo"
+    else
+        log_apply   "$repo: commit: $commit_msg"
+
+        [ $COMMIT_DIFF ] && indent "    " ${commit}_diff "$repo"
+
+        ${commit}_commit "$repo" "$commit_msg"
+    fi
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/commit/hg.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,70 @@
+# HG wrappers
+
+HG=/usr/bin/hg
+HG_ARGS=(--config trusted.users=root)
+
+function hg_probe {
+    local repo=$1
+
+    [ -d "$repo/.hg" ]
+}
+
+## Run `hg ...` within $REPO.
+function hg {
+    local repo=$1
+    cmd $HG -R "$repo" "${HG_ARGS[@]:-}" "${@:2}"
+}
+
+## 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
+    hg $repo diff "${@:2}"
+}
+
+## 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 opts=()
+
+    if [ ${SUDO_USER:-} ]; then
+        opts+=('-u' "$SUDO_USER")
+
+    elif [ $HOME ] && [ -e $HOME/.hgrc ]; then
+        debug "using .hgrc user"
+
+    else
+        opts+=('-u' "$USER")
+    fi
+    
+    if [ "$msg" ]; then
+        opts+=('-m' "$msg")
+    fi
+   
+    hg $repo commit "${opts[@]:-}"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/file.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,49 @@
+### FS utils
+
+## 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' ''
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/hosts/update.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,152 @@
+update_GETOPTS='sSrR'
+
+UPDATE_SERIAL=
+UPDATE_RELOAD=
+
+function update_help {
+    cat <<END
+Update:
+    -s      force update serials
+    -S      do not update serial
+
+    -r      force reload zones/dhcp
+    -R      do not reload zones/dhcp
+END
+}
+
+function update_opt {
+    local opt=$1
+    local optarg="$2"
+
+    case $opt in
+        s)  UPDATE_SERIAL=1 ;;
+        S)  UPDATE_SERIAL=0 ;;
+
+        r)  UPDATE_RELOAD=1 ;;
+        R)  UPDATE_RELOAD=0 ;;
+               
+        n)
+            UPDATE_SERIAL=0
+            UPDATE_RELOAD=0
+        ;;
+        *)  return 1
+    esac
+}
+
+function update_setup {
+    for dir in etc etc/zones etc/hosts; do
+        [ -d $dir ] || die "$dir: missing source directory"
+    done
+    
+    apply_dir       $VAR
+
+    for dir in $VAR/dhcp $VAR/zones $VAR/include-cache $VAR/serials; do
+        apply_dir   $dir
+    done
+    for dir in $VAR/dhcp/hosts; do
+        apply_dir   $dir
+    done
+    for dir in $VAR/zones/includes $VAR/zones/forward $VAR/zones/reverse; do
+        apply_dir   $dir
+    done
+}
+
+function update_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..."
+        commit  $SRV
+}
+
+function update_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 update_reload {
+    ## 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 update_main {
+    ## Setup source/output dirs
+    update_setup
+   
+    ## Commit source dirs
+    update_commit
+
+    ## Update output from sources
+    update_update
+
+    ## Reload output
+    update_reload
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/log.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,152 @@
+# Logging output
+
+log_GETOPTS="qvDV"
+
+LOG=y
+LOG_ERROR=y
+LOG_WARN=y
+LOG_FORCE=y
+LOG_APPLY=y
+LOG_NOOP=y
+LOG_SKIP=
+LOG_DEBUG=
+LOG_CMD=
+LOG_DIFF=y
+
+# use color output?
+IS_TTY=
+
+function log_help {
+    cat <<END
+Logging:    
+    -q      quiet
+    -v      verbose
+    -D      debug
+    -V      debug commands
+END
+}
+
+function log_opt {
+    local opt=$1
+    local optarg="$2"
+
+    case $opt in
+        q)  
+            LOG= 
+            LOG_WARN=
+            LOG_APPLY=
+            LOG_FORCE=
+            LOG_NOOP=
+            LOG_DIFF=
+            ;;
+
+        v)  LOG_SKIP=y ;;
+        D)  
+            LOG_DEBUG=y
+            LOG_INFO=y
+            ;;
+        V)  LOG_CMD=y ;;
+
+        *)  return 1
+    esac
+}
+
+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_apply {
+    [ $LOG_APPLY    ] && log_color '36'     "  $*"          || true
+}
+
+function log_check {
+    [ $LOG_APPLY    ] && 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
+}
+
+### 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/pvl/main.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,70 @@
+set -ue
+
+shopt -s globstar nullglob
+
+. $LIB/pvl/util.sh
+. $LIB/pvl/cmd.sh
+. $LIB/pvl/log.sh
+
+function main_help {
+    cat <<END
+Usage: $0 [options]
+
+General:
+    -h      display this help text
+END
+    
+    for module in ${MODULES[@]}; do
+        ${module}_help
+    done
+}
+
+function main_opts {
+    local module=
+
+    # build opts string
+    local opts=$(
+        echo -n 'h'
+
+        for module in ${MODULES[@]}; do
+            module_getopts=${module}_GETOPTS
+            echo -n ${!module_getopts}
+        done
+    )
+
+    local OPTIND
+    while getopts "$opts" opt; do
+        local opt_module=
+
+        if [ "$opt" = 'h' ]; then
+            main_help
+            exit 0;
+        fi
+
+        for module in ${MODULES[@]}; do
+            if ${module}_opt $opt "${OPTARG:-}"; then
+                opt_module=$module
+            fi
+        done
+
+        if [ "$opt_module" ]; then
+            continue
+        else
+            die "opt: $opt"
+        fi
+    done
+    shift $(($OPTIND - 1))
+}
+
+function main {
+    local module=
+
+    main_opts "$@"
+
+    for module in ${MODULES[@]}; do
+        func_test ${module}_init && ${module}_init
+    done
+
+    ${MODULE}_main     
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/test.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,22 @@
+
+## Run test command on given file, outputting results if it fails.
+#
+#   check    $src    $cmd $args...
+#
+function test_cmd {
+    local src="$1"
+    local cmd="$2"
+    local args="${@:3}"
+
+    if cmd_test "$cmd" -q "${args[@]}"; then
+        log_skip "$src: test: OK"
+
+    else
+        log_error "$src: test: Failed"
+
+        indent "    " "$cmd" "${args[@]}"
+
+        return 1
+    fi
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/pvl/util.sh	Thu Feb 26 21:38:09 2015 +0200
@@ -0,0 +1,15 @@
+## Output calling function's name.
+function func_caller {
+    caller 1 | cut -d ' ' -f 2
+}
+
+## Test if given symbol is a function
+# XXX: tests if it is anything atm?
+function func_test {
+    type -t "$1" > /dev/null
+}
+
+## Get current unix (utc) timestamp
+function unix_time {
+    date +'%s'
+}
--- a/lib/update	Thu Feb 26 19:49:10 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-#!/bin/bash
-#
-
-## Strict errors
-set -ue
-
-shopt -s globstar nullglob
-
-## Library includes
-for lib in $LIB/update.*; do
-    source $lib
-done
-
--- a/lib/update.args	Thu Feb 26 19:49:10 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-#!/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
-}
-
-
--- a/lib/update.config	Thu Feb 26 19:49:10 2015 +0200
+++ b/lib/update.config	Thu Feb 26 21:38:09 2015 +0200
@@ -8,9 +8,6 @@
 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
 
--- a/lib/update.hg	Thu Feb 26 19:49:10 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-#!/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
-}
--- a/lib/update.log	Thu Feb 26 19:49:10 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,125 +0,0 @@
-#!/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
-}
-
-
--- a/lib/update.operations	Thu Feb 26 19:49:10 2015 +0200
+++ b/lib/update.operations	Thu Feb 26 21:38:09 2015 +0200
@@ -3,51 +3,37 @@
 #
 # 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
+## Read include paths from file
+function read_zone_includes {
+    cmd sed -n -E 's/^\$INCLUDE\s+"(.+)"/\1/p' "$@"
 }
 
-function copy {
-    local out="$1"
+## (cached) include paths for zone file
+function zone_includes {
+    local cache="$1"
     local src="$2"
+    local prefix="${3:-}"
 
-    if check_update "$out" "$src"; then
-        log_update "Copying $out <- $src..."
+    if [ ! -e "$cache" -o "$cache" -ot "$src" ]; then
+        read_zone_includes "$src" > "$cache"
+    fi
 
-        do_update "$out" \
-            cat "$src"
-    else
-        log_skip "Copying $out <- $src: not changed"
-    fi
+    while read include; do
+        echo -n "$prefix$include "
+    done < "$cache"
 }
 
-## 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
+## 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
 }
 
 ## Generate forward zone from hosts hosts using pvl.hosts-dns:
@@ -335,39 +321,4 @@
     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
-}
--- a/lib/update.updates	Thu Feb 26 19:49:10 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,158 +0,0 @@
-#!/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
-}
--- a/lib/update.utils	Thu Feb 26 19:49:10 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-#!/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'
-}