bin/update
author Tero Marttila <terom@paivola.fi>
Tue, 20 Mar 2012 12:55:31 +0200
changeset 566 b2ac0fd85828
parent 565 ef5bcc4145de
child 567 8f49e2f51c0d
permissions -rwxr-xr-x
update: log_force/update/noop/skip, implemented for update_serial
#!/bin/bash
# vim: set ft=sh :

set -ue

ROOT=$(pwd)

# resolve $0
self=$0
while [ -L $self ]; do
    tgt=$(readlink $self)

    if [ "${tgt:0:1}" == "/" ]; then
        self=$tgt
    else
        self=$(dirname $self)/$tgt
    fi
done

# bin dir
BIN=$(dirname $self)

# data files
DATA=settings
ZONES=zones
SERIALS=$DATA
REPO=

# data args
PROCESS_ARGS='--input-charset latin-1'

FORWARD_MX=mail
REVERSE_ZONE=194.197.235
REVERSE_DOMAIN=paivola.fi

# external progs
NAMED_CHECKZONE=/usr/sbin/named-checkzone
HG=/usr/bin/hg
RNDC=/usr/sbin/rndc

## options
IS_TTY=

LOG_WARN=y
LOG=y
LOG_FORCE=y
LOG_UPDATE=y
LOG_NOOP=y
LOG_SKIP=
LOG_DEBUG=
LOG_CMD=

UPDATE_FORCE=
UPDATE_NOOP=
UPDATE_DIFF=

SERIAL_NOOP=
SERIAL_FORCE=

COMMIT_SKIP=
COMMIT_FORCE=
COMMIT_MSG=' '

DEPLOY_SKIP=

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 anything; implies -SpC

Commit    
    -C      do not commit changes
    -c      commit changes
    -m MSG  commit message
END
}

function parse_args {
    OPTIND=1

    while getopts 'hd:qvDVpFSsnCcm:' opt "$@"; do
        case $opt in
            h)  
                help_args $0
                exit 0
            ;;

            d)  ROOT="$OPTARG" ;;

            q)  
                LOG= 
                LOG_WARN=
                LOG_UPDATE=
                LOG_FORCE=
                LOG_NOOP=
                ;;

            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_NOUPDATE=y
                COMMIT_SKIP=y
                DEPLOY_SKIP=y
                ;;

            C)  COMMIT_SKIP=y ;;
            c)  COMMIT_FORCE=y ;;
            m)  COMMIT_MSG="$OPTARG" ;;
           
            ?)  
                die 
            ;;
        esac

    done
}

## lib
function log_msg {
    echo "$*" >&2
}

function log_color {
    local code=$1; shift

    if [ $IS_TTY ]; then
        echo $'\e['${code}'m'"$*"$'\e[00m' >&2
    else
        echo "$*" >&2
    fi
}

function log_error {
    log_color '31' "$*"
}

function log_warn {
    [ $LOG_WARN ] && log_color '33' "$*" || true
}

function log {
    [ $LOG ] && log_msg "$*" || true
}

function log_force {
    [ $LOG_FORCE ] && log_color '2;33' "  $*" || true
}

function log_update {
    [ $LOG_UPDATE ] && log_color '36' "  $*" || 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
}

# 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 func_caller {
    caller 1 | cut -d ' ' -f 2
}

function debug {
    printf -v prefix "%s" $(func_caller)

    log_debug "$prefix: $*"
}

function fail {
    log_error "$(func_caller): $*"

    exit 2
}

function die {
    log_error "$*"
    exit 1
}

function cmd {
    log_cmd "$@"

    "$@" || die "Failed"
}

function indent () {
    local indent=$1; shift

    log_cmd "$@"

    "$@" | sed "s/^/$indent/"

    return ${PIPESTATUS[0]}
}

function ensure_dir {
    local dir=$1

    if [ ! -d $ROOT/$dir ]; then
        log_warn "Creating output dir: $dir"
        cmd mkdir $ROOT/$dir
    fi
}

function abspath () {
    echo "$ROOT/$1"
}

## hg
function hg {
    local repo=$REPO; shift

    cmd $HG -R $ROOT/$repo "$@"
}

function hg_modified {
    hg id | grep -q '+'
}

function hg_user {
    if [ ${SUDO_USER:-} ]; then
        echo '-u' "$SUDO_USER"

    elif [ $HOME ] && [ -e $HOME/.hgrc ]; then
        debug "using .hgrc user"
        echo ''

    else
        echo '-u' "$USER"
    fi
}

function hg_diff {
    hg diff
}

function hg_commit {
    local msg=$2
    local user_opt=$(hg_user)
    
    debug "$user_opt: $msg"
    hg commit $user_opt -m "$msg"
}


## functions

## Dependency-based updates
# Compare the given output file with all given source files.
#
# Returns true if the output file needs to be updated.
function check_update {
    # target
    local dst=$1; shift

    debug "$dst"

    # need update?
    local update=

    if [ ${#@} == 0 ]; then
        debug "  update: unknown deps"
        update=y

    elif [ ! -e $dst ]; 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 $ROOT/$dep ]; then
            fail "$dst: Missing source: $dep"

        elif [ $ROOT/$dst -ot $ROOT/$dep ]; then
            debug "  update: $dep"
            update=y
        else
            debug "  check: $dep"
        fi
    done

    [ ! $update ] && debug "  up-to-date"

    # return
    [ $update ]
}

function do_update {
    local dst=$1; shift
    local tmp=$dst.new

    debug "$dst"
    cmd "$@" > $ROOT/$tmp

    # compare
    if [ -e $ROOT/$dst ] && [ $UPDATE_DIFF ]; then
        debug "  changes:"

        # terse
        indent "        " diff --unified=1 $ROOT/$dst $ROOT/$tmp || true
    fi
    
    if [ $UPDATE_NOOP ]; then
        # cleanup
        debug "  no-op"

        cmd rm $ROOT/$tmp
    else
        # commit
        debug "  commit"

        cmd mv $ROOT/$tmp $ROOT/$dst
    fi
}

# links
function check_link {
    local lnk=$1
    local tgt=$2
    
    [ ! -e $ROOT/$lnk ] || [ $(readlink $ROOT/$lnk) != $ROOT/$tgt ]
}

function do_link {
    local lnk=$1
    local tgt=$2

    cmd ln -sf $ROOT/$tgt $ROOT/$lnk
}

## hosts
# copy hosts input zone verbatim
function copy_hosts {
    local zone=$1
    local base=$2

    if check_update $zone $base; then
        log_update "Copying hosts $zone <- $base..."

        do_update $zone \
            cat $ROOT/$base
    else
        log_skip "Copying hosts $zone <- $base: not changed"
    fi
}

# generate hosts zone from input zone
function update_hosts {
    local zone=$1; shift
    local base=$1; shift

    if check_update $zone $base; then
        log_update "Generating hosts $zone <- $base..."

        do_update $zone \
            $BIN/process-zone $PROCESS_ARGS $ROOT/$base "$@"
    else
        log_skip "Generating hosts $zone <- $base: not changed"
    fi
}

## actions
# serial
function do_update_serial {
    local serial=$1

    # read
    local old=$(test -e $ROOT/$serial && cat $ROOT/$serial || echo '')


    cmd $BIN/update-serial $ROOT/$serial
    
    # read
    local new=$(cat $ROOT/$serial)
        
    debug "  $old -> $new"
}

function update_serial {
    local zone=$1; shift
    
    local serial=$SERIALS/$zone.serial

    # test
    if [ $SERIAL_FORCE ]; then
        log_force "Updating $serial: forced"

        do_update_serial $serial

    elif ! check_update $serial "$@"; then
        log_skip "Updating $serial: not changed"

    elif [ $SERIAL_NOOP ]; then
        log_noop "Updating $serial: skipped"

    else
        log_update "Updating $serial..."

        do_update_serial $serial
    fi
}

function link_serial {
    local zone=$1
    local base=$2

    local lnk=$SERIALS/$zone.serial
    local tgt=$SERIALS/$base.serial

    if check_link $lnk $tgt; then
        log_update "Linking $lnk -> $tgt..."

        do_link $lnk $tgt

    else
        log_skip "Linking $lnk -> $tgt: not changed"
    fi
}

# zone
function copy_zone {
    local view=$1
    local zone=$2
    local base=${3:-$zone}

    local out=$ZONES/$view/$zone
    local src=$DATA/$base

    if check_update $out $src; then
        log_update "Copying $out <- $src..."

        do_update $out \
            cat $ROOT/$src
    else
        log_skip "Copying $out <- $src: not changed"
    fi
}

function update_zone {
    local view=$1
    local zone=$2
    local base=${3:-$zone}

    local out=$ZONES/$view/$zone
    local src=$DATA/$base.zone
    local lnk=$ZONES/$base

    local serial=$SERIALS/$base.serial

    if check_update $out $src $serial; then
        log_update "Generating $out <- $src..." 

        do_update $out \
            $BIN/expand-zone $ROOT/$src \
                --serial $ROOT/$serial              \
                --expand zones=$(abspath $ZONES)    \
                --expand view=$view
    else
        log_skip "Generating $out <- $src: not changed" 
    fi
}

function link_zone {
    local view=$1
    local zone=$2
    local base=${3:-$zone}

    local out=$ZONES/$view/$zone
   
    # find tgt
    for tgt in $ZONES/$view/$base $ZONES/common/$base; do
        [ $tgt != $out ] && [ -e $tgt ] && break
    done

    if check_link $out $tgt; then
        log_update "Linking $out -> $tgt..."

        do_link $out $tgt
    else
        log_skip "Linking $out -> $tgt: not changed"
    fi
}

## Tests
function check_hosts {
    local hosts=$1; shift 1

    local cmd=($BIN/process-zone $PROCESS_ARGS $ROOT/$hosts --check-hosts "$@")

    if "${cmd[@]}" -q; then
        log_skip "Check $hosts: OK"
    else
        log_error "  Check $hosts: Failed"

        indent "    " "${cmd[@]}"

        exit 1
    fi
}

function check_zone {
    local view=$1
    local zone=$2
    local origin=$3

    local src=$ZONES/$view/$zone

    local cmd=($NAMED_CHECKZONE $origin $ROOT/$src)

    # test
    # XXX: checkzone is very specific about the order of arguments, -q must be first
    if $NAMED_CHECKZONE -q $origin $ROOT/$src; then
        log_skip "Check $src ($origin): OK"
    else
        log_error "  Check $src ($origin): Failed:"

        indent "    " "${cmd[@]}"
        
        exit 1
    fi
}

## Deploy
# deploy new zone data to bind
function deploy_zones {
    indent "        rndc: " $RNDC reload
}

# commit data changes
function commit_data {
    local repo=$REPO

    if hg_modified; then
        log_update "Commit changes in $repo:"

        indent "    " hg_diff

        hg_commit "$COMMIT_MSG"
    else
        log_skip "Commit changes in $repo: no changes"
    fi
}

function main {
    # test tty
    [ -t 1 ] && IS_TTY=y
    
    parse_args "$@"

    ## test env
    [ -d $ROOT/$DATA ] || die "Missing data: $ROOT/$DATA"
    ensure_dir  $ZONES
    
    # output dirs
    local views=(internal external)

    for view in "${views[@]}" "common" "hosts" "includes"; do
        ensure_dir $ZONES/$view
    done

    ## hosts
    # test
    log "Testing hosts..."
        check_hosts     $DATA/paivola.txt --check-exempt ufc

    # update
    log "Generating host zones..."
        #                   zone                            base                *args
        update_hosts        $ZONES/hosts/paivola:internal   $DATA/paivola.txt   --forward-zone --forward-txt --forward-mx $FORWARD_MX
        update_hosts        $ZONES/hosts/paivola:external   $DATA/paivola.txt   --forward-zone
        update_hosts        $ZONES/hosts/194.197.235        $DATA/paivola.txt   --reverse-zone $REVERSE_ZONE --reverse-domain $REVERSE_DOMAIN

        
        update_hosts        $ZONES/hosts/10                 $DATA/pvl.txt       --reverse-zone 10 --reverse-domain pvl -q
        update_hosts        $ZONES/hosts/192.168            $DATA/pvl.txt       --reverse-zone 192.168 --reverse-domain pvl -q

        # XXX: unsupported --forward-zone with pvl.txt
        # update_hosts    $ZONES/hosts/pvl                    $DATA/pvl.txt      --forward-zone
        copy_hosts          $ZONES/hosts/pvl                $DATA/pvl.txt

    ## zones
    # parts
    log "Copying zone includes..."
        #                   view            zone                    base
        copy_zone           includes        paivola:internal        paivola.zone.internal
        copy_zone           includes        paivola:external        paivola.zone.external
        copy_zone           includes        paivola.auto            paivola.zone.auto
        copy_zone           includes        paivola.services        paivola.zone.services

    # serials
    log "Updating serials..."

    #                   zone            *deps
    update_serial       pvl             $ZONES/hosts/pvl        $DATA/pvl.zone
    update_serial       10              $ZONES/hosts/10         $DATA/10.zone
    update_serial       192.168         $ZONES/hosts/192.168    $DATA/192.168.zone

    update_serial     paivola           \
        $ZONES/hosts/paivola:*          \
        $DATA/paivola.zone              \
        $ZONES/includes/paivola:*       \
        $ZONES/includes/paivola.*

    update_serial     194.197.235       \
        $ZONES/hosts/194.197.235        \
        $DATA/194.197.235.zone          

    # zones
    log "Updating zones..."
        #                   view        zone            base
        update_zone         internal    pvl
        update_zone         internal    paivola
        update_zone         external    paivola

        update_zone         internal    10
        update_zone         internal    192.168

        update_zone         common      194.197.235
        link_zone           internal    194.197.235
        link_zone           external    194.197.235

    # test
    log "Testing zones..."
        #                   view        zone            origin
        check_zone          internal    paivola         paivola.fi
        check_zone          external    paivola         paivola.fi

        check_zone          internal    10              10.in-addr.arpa
        check_zone          internal    192.168         192.168.in-addr.arpa
        check_zone          common      194.197.235     235.197.194.in-addr.arpa

    # extra zones...
    local base=paivola
    local link_zones=(paivola.fi paivola.net paivola.org paivola.info paivola.mobi xn--pivl-load8j.fi)

    log "Linking zones..."
    for view in "${views[@]}"; do
        for zone in "${link_zones[@]}"; do
            link_zone       $view       $zone           $base
            check_zone      $view       $zone           $zone
        done
    done

    ## deploy
    if [ $DEPLOY_SKIP ]; then
        log "Deploy zones: skipped"

    else
        log "Deploy zones..."

        deploy_zones
    fi

    ## commit
    if [ $COMMIT_SKIP ] && [ ! $COMMIT_FORCE ]; then
        log "Commit data: skipped"

    else
        log "Commit data..."
            commit_data
    fi
}

main "$@"