bin/update
author Tero Marttila <terom@paivola.fi>
Mon, 19 Mar 2012 17:32:32 +0200
changeset 36 3208cd6540dc
parent 32 694dab02a6b0
child 37 f28db5535b10
permissions -rwxr-xr-x
update: restructure zones/, manage .pvl
#!/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)
PROCESS_ZONE=$BIN/process-zone
EXPAND_ZONE=$BIN/expand-zone
UPDATE_SERIAL=$BIN/update-serial

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

# 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_INFO=
LOG_DEBUG=
LOG_CMD=

UPDATE_FORCE=
UPDATE_NOOP=
UPDATE_DIFF=
SERIAL_NOUPDATE=
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
    -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:qvDVpFSnCcm:' opt "$@"; do
        case $opt in
            h)  
                help_args $0
                exit 0
            ;;

            d)  ROOT="$OPTARG" ;;

            q)  
                LOG= 
                LOG_WARN=
                ;;

            v)  LOG_INFO=y ;;
            D)  
                LOG_DEBUG=y
                LOG_INFO=y
                ;;
            V)  LOG_CMD=y ;;

            p)  UPDATE_DIFF=y ;;
            F)  UPDATE_FORCE=y ;;
            S)  SERIAL_NOUPDATE=y ;;
 
            n)  
                UPDATE_NOOP=y 
                # implies -Sp
                UPDATE_DIFF=y
                SERIAL_NOUPDATE=y
                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[0;'${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_info {
    [ $LOG_INFO ] && log_color 36 "  $*" || true
}

function log_debug {
    [ $LOG_DEBUG ] && log_color 32 "    $*" || true
}

function log_cmd {
    [ $LOG_CMD ] && log_color 35 "        \$ $*" || true
}

# XXX: broken
function log_stack {
    local level=1

    while info=$(caller $level); do
        echo $info | read line sub file

        log_msg "$file:$lineno $sub()"

        level=$(($level + 1))
    done
}

function fail {
    func=$(caller 1 | cut -d ' ' -f 2)
    
    log_error "$func: $*"

    exit 2
}

function die {
    log_error "$*"
    exit 1
}

function cmd {
    log_cmd "$@"

    "$@" || die "Failed"
}

function run_cmd {
    local msg=$1; shift

    log_info "$msg... "

    cmd "$@"
}

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

    log_cmd "$@"

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

    return ${PIPESTATUS[0]}
}

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

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

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

function hg_modified {
    local repo=$1

    hg $repo id | grep -q '+'
}

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

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

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

function hg_diff {
    local repo=$1

    hg $repo diff
}

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


## functions
function check_update {
    # target
    local dst=$1; shift

    log_debug "check_update: $dst"

    # need update?
    local update=

    if [ ! -e $dst ] || [ $UPDATE_FORCE ]; then
        log_debug "  update forced"
        update=y
    fi

    # check deps
    for dep in "$@"; do
        # don't bother checking if already figured out
        [ $update ] && continue

        # check

        if [ $ROOT/$dst -ot $ROOT/$dep ]; then
            log_debug "  changed: $dep"
            update=y
        fi
    done

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

    # return
    [ $update ]
}

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

    log_debug "update: $dst"
    cmd "$@" > $ROOT/$tmp

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

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

        cmd rm $ROOT/$tmp
    else
        # commit
        log_debug "  update"

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

function update {
    local dst=$1; shift;

    local sep=
    local dep=()
    local cmd=()

    for arg in "$@"; do
        if [ $arg == '--' ]; then
            sep=y
        fi

        if [ $sep ]; then
            cmd=("${cmd[@]:-}" "$arg")
        else
            dep=("${dep[@]:-}" "$arg")
        fi
    done

    [ ! $sep ] && fail "Invalid args given: $@"

    check_update $dst "${dep[@]}" && do_update $dst "${cmd[@]}" || true
}

## actions
function update_zone_serial {
    local name=$1; shift
    local file=$SERIALS/$name.serial
    
    local old=$(test -e $ROOT/$file && cat $ROOT/$file || echo '')

    log_info "Updating $file..."

    cmd $UPDATE_SERIAL $* $ROOT/$file
    
    local new=$(cat $ROOT/$file)
        
    log_debug "  $old -> $new"
}

function link_zone_serial {
    local name=$1
    local base=$2

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

    if [ -e $ROOT/$lnk ] && [ $(readlink $ROOT/$lnk) == $ROOT/$tgt ]; then
        log_info "Linking $lnk -> $tgt: not changed"

    else
        log_info "Linking $lnk -> $tgt..."

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

function update_zone_part {
    local zone=$1
    local part=$2

    local name=$zone.zone.$part
    local src=$DATA/$name
    local dst=$ZONES/$name


    if check_update $dst $src; then
        log_info "Copying zones/$name..."

        do_update $dst cat $ROOT/$src
    else
        log_info "Copying zones/$name: not changed"
    fi
}

function update_zone {
    local zone=$1

    local out=$ZONES/$zone
    local in=$DATA/$zone.zone
    local serial=$SERIALS/$zone.serial

    if check_update $out $in $serial; then
        log_info "Generating $out..." 

        do_update $out \
            $EXPAND_ZONE $ROOT/$DATA/$zone.zone   \
                --serial $ROOT/$SERIALS/$zone.serial  \
                --expand zones=$(abspath $ZONES)
    else
        log_info "Generating $out: not changed" 
    fi
}

function update_zone_view {
    local zone=$1
    local view=$2

    local out=$ZONES/$view/$zone
    local in=$DATA/$zone.zone
    local serial=$SERIALS/$zone.serial

    if check_update $out $in $serial; then
        log_info "Generating $out..."

        do_update $out \
            $EXPAND_ZONE $ROOT/$DATA/$zone.zone   \
                --serial $ROOT/$SERIALS/$zone.serial  \
                --expand zones=$(abspath $ZONES)    \
                --expand view=$view
    else
        log_info "Generating $out: not changed"
    fi
}

function link_zone_view {
    local zone=$1
    local view=$2
    local base=$3
    
    local lnk=$ZONES/$view/$zone
    local tgt=

    # find tgt
    for path in "$ZONES/$view/$base" "$ZONES/$base"; do
        if [ -e "$ROOT/$path" ]; then
            log_debug "$view/$zone: base $base from $path"
            tgt="$path"

            break
        fi
    done

    if [ ! $tgt ]; then
        die "$view/$zone: base $base not found!"
    fi
   
    # link
    if [ -e $ROOT/$lnk ] && [ $(readlink $ROOT/$lnk) == $ROOT/$tgt ]; then
        log_info "Linking $lnk -> $tgt: not changed"

    else
        log_info "Linking $lnk -> $tgt..."

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

function update_hosts {
    local dst=$1; shift
    local src=$1; shift


    if check_update $dst $src; then
        log_info "Generating $dst..."

        do_update $dst $PROCESS_ZONE $PROCESS_ARGS $ROOT/$src "$@"
    else
        log_info "Generating $dst: not changed"
    fi
}

# feed `hg annotate -qd`'d version of input to process-zone --input-line-date
# only applies commit'd data
function update_hosts_meta {
    local dst=$1; shift
    local src=$1; shift

    if check_update $dst $src; then
        log_info "Generating $dst..."
        
        # via stdin
        $hg $DATA annotate -qd $ROOT/$src | \
            do_update $dst $PROCESS_ZONE $PROCESS_ARGS --input-line-date --meta-zone "$@"
    else
        log_info "Generating $dst: not changed"
    fi
}

function check_hosts {
    local hosts=$1; shift 1

    local cmd=($PROCESS_ZONE $PROCESS_ARGS $ROOT/$hosts --check-hosts "$@")

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

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

        exit 1
    fi
}

function check_zone {
    local name=$1
    local file=$2

    local cmd=($NAMED_CHECKZONE $name $ROOT/$file)

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

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

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

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

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

        indent "    " hg_diff $repo

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

function ensure_dir {
    local dir=$1

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

function main {
    # test tty
    [ -t 1 ] && IS_TTY=y
    
    local views=(internal external)

    parse_args "$@"

    ## test env
    [ -d $ROOT/$DATA ] || die "Missing data: $ROOT/$DATA"
    ensure_dir  $ZONES
    
    # views
    for view in "${views[@]}" "common" "hosts"; do
        ensure_dir $ZONES/$view
    done

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

    # update
    log "Generating host zones..."
        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

        
        # XXX: foward
        update_hosts    $ZONES/hosts/pvls                   $DATA/pvl.zone      --forward-zone
        update_hosts    $ZONES/hosts/10                     $DATA/pvl.zone      --reverse-zone 10 --reverse-domain pvl -q
        update_hosts    $ZONES/hosts/192.168                $DATA/pvl.zone      --reverse-zone 192.168 --reverse-domain pvl -q


    ## zones
    # parts
    log "Copying zone parts..."
        update_zone_part    paivola         auto
        update_zone_part    paivola         services
        update_zone_part    paivola         internal
        update_zone_part    paivola         external

    # serials
    if [ $SERIAL_NOUPDATE ]; then
        log "Updating serials: skipped"

    else
        log "Updating serials..."

        update_zone_serial  pvl
        link_zone_serial    10          pvl
        link_zone_serial    192.168     pvl

        update_zone_serial  paivola
        update_zone_serial  194.197.235
    fi

    # headers
    log "Updating zones headers..."
        update_zone_view    pvl                 internal
        update_zone_view    paivola             internal
        update_zone_view    paivola             external

        update_zone_view    10                  internal
        update_zone_view    192.168             internal

        update_zone_view    194.197.235         common

    # test
    log "Testing zones..."
        check_zone          paivola.fi                  $ZONES/external/paivola
        check_zone          paivola.fi                  $ZONES/internal/paivola

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

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

    log "Linking extra zones..."
    for view in "${views[@]}"; do
        for zone in "${link_zones[@]}"; do
            link_zone_view  $zone   $view   $base_zone
            check_zone      $zone   $ZONES/$view/$zone
        done

        # reverse
        link_zone_view      194.197.235     $view   common/194.197.235
    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 $REPO
    fi
}

main "$@"