#! /bin/bash
# -*- mode: sh; sh-basic-offset: 4; indent-tabs-mode: nil; -*-
#
# SVN version: $Id: metche 176 2006-09-15 15:31:11Z intrigeri $
# $URL: http://poivron.org/dev/svn/metche/upstream/tags/metche-1.1/metche $
#
#  metche: reducing root bus factor
#  Copyright (C) 2004-2006 boum.org collective - property is theft !
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License along
#  with this program; if not, write to the Free Software Foundation, Inc.,
#  59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

set -e
shopt -s nullglob

###
### Auxiliary functions
###

display_usage() {
    ( echo "Usage:"
      echo "       `basename $0` [-h VSERVER] list" 
      echo "       `basename $0` [-h VSERVER] report" \
           "[{stable|testing|unstable}-YYYYMMDDHHMM]"
      echo "       `basename $0` [-h VSERVER] stabilize [testing-YYYYMMDDHHMM]"
      echo ""
      echo "       With -h, the VServer VSERVER is operated on instead of the host system."
      echo ""
      echo "Non-interactive usage:"
      echo "       `basename $0` cron"
      echo ""
    ) >&2
}

fatal() {
    echo -e "Fatal:   $@" >&2
    exit 2
}

warning() {
    echo -e "Warning: $@" >&2
}

debug() {
    [ "$DEBUG" != yes ] || echo -e "Debug:   $@" >&2
}

executable_not_found() {
    local executable="$1"
    local software="$2"
    local dependant_option="$3"
    local solution_option="$4"

    if [ -z "$solution_option" ]; then
        fatal "$executable not found on `current_system`." \
              "Please install $software or turn $dependant_option off."
    else
        fatal "$executable not found on `current_system`." \
              "Please install $software, customize $solution_option" \
              "or turn $dependant_option off."
    fi
}

email() {
    debug "* email $@ to $EMAIL_ADDRESS"
    local subject="`current_system` - $_MAIL_SUBJECT : $1"
    if [ $ENCRYPT_EMAIL = "yes" ]; then
        LC_ALL="$LOCALE" gpg --batch --armor --encrypt \
                             --recipient "$EMAIL_ADDRESS" |
            LC_ALL="$LOCALE" mutt -s "$subject" "$EMAIL_ADDRESS"
    else
        LC_ALL="$LOCALE" mutt -s "$subject" "$EMAIL_ADDRESS"
    fi
}

current_system() {
    $VSERVER_EXEC_PREFIX hostname -f
}

###
### Configuration
###

DEBUG="yes"
WATCHED_DIR="/etc"
BACKUP_DIR="/var/lib/metche"
# if set, activate single changelog mode
#CHANGELOG_FILE="/root/Changelog"
# if set, activate multiple changelogs mode
#CHANGELOG_DIR="/root/changelogs"
DO_PACKAGES="no"
DO_DETAILS="no"
TESTING_TIME="60"
STABLE_TIME="3"
EMAIL_ADDRESS="root@`hostname -f`"
ENCRYPT_EMAIL="no"
EXCLUDES="*.swp #* *~ *.gpg *.key ifstate adjtime ld.so.cache shadow* \
          .gnupg blkid.tab* aumixrc net.enable mtab backup.d \
          vdirbase run.rev vdir run.rev \
          prng_exch smtp_scache.pag smtpd_scache.pag \
          smtp_scache.dir smtpd_scache.dir local.sh"
LOCALE="C"

VSNAMES=""
VSERVERINFO=/usr/sbin/vserver-info
VSERVER=/usr/sbin/vserver

_MAIL_SUBJECT="changes report"
_NO_DEBIAN_PACKAGES_CHANGE="No change in Debian packages state."
_NO_CHANGE="No change."

MAIN_HEADER="
     c h a n g e s   r e p o r t
     ---------------------------

"

CHANGELOGS_HEADER="

Changelogs
==========

"

FILES_HEADER="

Changed files
=============

"

DEBIAN_PACKAGES_HEADER="

Changes in Debian packages
==========================

"

FILES_DETAILS_HEADER="

Details for changed files
=========================

"

if [ "$1" = "-h" ]; then
    VSNAME="$2"
    CMD="$3"
    MILESTONE="$4"
else
    CMD="$1"
    MILESTONE="$2"
fi

if [ -f /etc/metche.conf ]; then
    . /etc/metche.conf
else
    display_usage
    fatal "Config file not found."
fi

PATH="/bin:/usr/bin"
unset LC_ALL
unset LC_CTYPE
unset LANGUAGE
unset LANG
umask 077

# Manage deprecated configuration files and options
test ! -d /etc/metche || fatal "An old configuration directory (/etc/metche/)" \
                               "was found, please upgrade your configuration."
test -z "$TAR_OPTS"   || fatal "TAR_OPTS is deprecated, use EXCLUDES instead."

# Backup various configuration values: these non-underscored variable
# names will be re-initialized in context_config() and re-used everywhere.
_WATCHED_DIR="$WATCHED_DIR"
_BACKUP_DIR="$BACKUP_DIR"
_CHANGELOG_DIR="$CHANGELOG_DIR"
_CHANGELOG_FILE="$CHANGELOG_FILE"
_DO_PACKAGES="$DO_PACKAGES"
_EMAIL_ADDRESS="$EMAIL_ADDRESS"

if [ -n "$VSNAMES" ]; then
    # check VSERVERINFO and VSERVER availability
    test -x "$VSERVERINFO" || \
        executable_not_found "vserver-info" "util-vserver" "VSNAMES" "VSERVERINFO"
    test -x "$VSERVER"     || \
        executable_not_found "vserver" "util-vserver" "VSNAMES" "VSERVER"
    # check VROOTDIR availability
    test -n "$VROOTDIR"    || \
        VROOTDIR="`$VSERVERINFO info SYSINFO \
                   | grep '^ *vserver-Rootdir' | awk '{print $2}'`"
    test -n "$VROOTDIR"    || \
        fatal "VSNAMES is not empty, but VROOTDIR could not be guessed." \
              "Please set VROOTDIR in /etc/metche.conf."
    test -d "$VROOTDIR"    || \
        fatal "VSNAMES is not empty, but VROOTDIR ($VROOTDIR) does not exist." \
              "Please set VROOTDIR in /etc/metche.conf."
    # expand VSNAMES if it is set to "all"
    if [ "$VSNAMES" = all ]; then
        VSNAMES=`ls $VROOTDIR | grep -E -v "lost\+found|ARCHIVES" | tr "\n" " "`
        if [ -z "$VSNAMES" ]; then
            warning "VSNAMES is set to \"all\", but no VServer could be found" \
                    "in VROOTDIR ($VROOTDIR)."
        fi
    fi
fi

if [ "$ENCRYPT_EMAIL" = "yes" ]; then
    which gpg > /dev/null ||
        executable_not_found "gpg" "GnuPG" "ENCRYPT_EMAIL"
fi

DATE=`date "+%Y%m%d%H%M"`

# How to use $TAR_OPTS:
#    - $TAR_OPTS should be used unquoted
#    - 'set -o noglob' has to be run before any $TAR_OPTS use
#    - 'set +o noglob' has to be run after any $TAR_OPTS use
TAR_OPTS=""
set -o noglob
for pattern in $EXCLUDES; do
    TAR_OPTS="$TAR_OPTS --exclude=$pattern"
done
set +o noglob

# How to use $FIND_OPTS:
#    - $FIND_OPTS should appear unquoted between:
#       . the (optional) target files and directories
#       . the (compulsory) action, such as -print or -exec
#    - 'set -o noglob' has to be run before any $FIND_OPTS use
#    - 'set +o noglob' has to be run after any $FIND_OPTS use
FIND_OPTS=""
set -o noglob
# DO NOT fix me: the final -or at the end of $FIND_OPTS is really needed
for pattern in $EXCLUDES; do
    FIND_OPTS="$FIND_OPTS -path */$pattern -prune -or"
done
set +o noglob


###
### A few functions to do the real work
###

# Check and mangle the context-dependant configuration variables,
# i.e. the parameters specific to the host system or to a given VServer.
# If $1 is empty, operate on the host system.
# Else, operate on a VServer and return with exit-code:
#   - 2 if $1 is not an existing VServer
#   - 3 if $1 is not a VServer listed in VSNAMES
#   - 4 if $1 is not a running VServer
# Anyway, return with exit-code:
#   - 5 if the GnuPG public key can not be found
context_config() {
    local vsname="$1"
    local res
    debug "-------- Operating on" \
          "`if [ -z $vsname ]; then echo 'the host system'; else echo VServer $vsname; fi`"
    debug "* context_config"

    #
    # Variables & VServer
    #

    if [ -z "$vsname" ]; then
        WATCHED_DIR="$_WATCHED_DIR"
        BACKUP_DIR="$_BACKUP_DIR"
        VSERVER_EXEC_PREFIX=""
        EMAIL_ADDRESS="$_EMAIL_ADDRESS"
    else
        WATCHED_DIR="$VROOTDIR/$vsname/$_WATCHED_DIR"
        BACKUP_DIR="$_BACKUP_DIR/$vsname"
        VSERVER_EXEC_PREFIX="$VSERVER $vsname exec"
        EMAIL_ADDRESS="root@`current_system`"
        # does the current VServer exist ?
        if [ ! -d "$VROOTDIR/$vsname" ]; then
            warning "  VServer $vsname does not exist (error 2)."
            return 2
        fi
        # is the current VServer listed in VSNAMES ?
        local found="no";
        for i in $VSNAMES; do
            if [ "$vsname" = "$i" ]; then
                found=yes
                break
            fi
        done
        if [ $found = no ]; then
            warning "  VServer $vsname is not listed in VSNAMES (error 3)."
            return 3
        fi
        # is the current VServer running ?
        res=""
        $VSERVERINFO -q "$vsname" RUNNING || res=failed
        if [ "$res" = failed ]; then
            warning "  VServer $vsname is not running (error 4)."
            return 4
        fi
    fi

    # E-mail encryption
    if [ $ENCRYPT_EMAIL = "yes" ]; then
        gpg --batch --list-public-keys $EMAIL_ADDRESS >/dev/null 2>&1
        res=$?
        if [ $res -ne 0 ]; then
            warning "  GnuPG public key for $EMAIL_ADDRESS not found."
            return 5
        fi
    fi

    #
    # Files and directories
    #

    # Check the existence of WATCHED_DIR
    test -d "$WATCHED_DIR" || \
        fatal "$WATCHED_DIR directory (built from WATCHED_DIR) does not exist."

    # Initialize WATCHED_PARENT
    WATCHED_PARENT=`dirname $WATCHED_DIR`
    if [ "$WATCHED_PARENT" != '/' ]; then
        WATCHED_PARENT="$WATCHED_PARENT/"
    fi

    # Check the existence of the resulting BACKUP_DIR, creating it if needed.
    if [ ! -d "$BACKUP_DIR" ]; then
        debug "  Creating $BACKUP_DIR directory for `current_system`."
        if mkdir -p "$BACKUP_DIR"; then
            debug "  Successfully created $BACKUP_DIR directory."
        else
            fatal "  Failed to create $BACKUP_DIR directory."
        fi
    fi

    #
    # Modules enabling/disabling
    #

    # DO_CHANGELOGS
    DO_CHANGELOGS="no"
    if [ -n "$_CHANGELOG_DIR" ]; then
        if [ -z "$vsname" ]; then
            CHANGELOG_DIR="$_CHANGELOG_DIR"
        else
            CHANGELOG_DIR="$VROOTDIR/$vsname$_CHANGELOG_DIR"
        fi
        if [ -d "$CHANGELOG_DIR" ]; then
            DO_CHANGELOGS="dir"
        else
            warning "  The directory $CHANGELOG_DIR (built from CHANGELOG_DIR)" \
                    "  does not exist. Changelogs file monitoring thereferore" \
                    "  cannot be performed this time for `current_system`."
        fi
    elif [ -n "$CHANGELOG_FILE" ]; then
        if [ -z "$vsname" ]; then
            CHANGELOG_FILE="$_CHANGELOG_FILE"
        else
            CHANGELOG_FILE="$VROOTDIR/$vsname$_CHANGELOG_FILE"
        fi
        if [ -f "$CHANGELOG_FILE" ]; then
            DO_CHANGELOGS="file"
        else
            warning "  The file $CHANGELOG_FILE (built from CHANGELOG_FILE)"
            warning "  does not exist. Changelog file monitoring thereferore"
            warning "  cannot be performed this time for `current_system`."
        fi
    fi

    # DO_PACKAGES
    DO_PACKAGES="$_DO_PACKAGES"
    if [ "$DO_PACKAGES" = "yes" ]; then
        $VSERVER_EXEC_PREFIX which apt-show-versions > /dev/null
        res=$?
        if [ $res -ne 0 ]; then
            warning "  apt-show-versions not found on `current_system`."
            warning "  Please install it or turn DO_PACKAGES off."
            warning "  DO_PACKAGES therefore cannot be performed this time"
            warning "  for `current_system`."
            DO_PACKAGES=no
        fi
    fi

    #
    # Final steps to get a coherent initial status
    #

    # Make sure we've got at least one testing and one stable
    milestone_exists testing-latest || save_state "testing"
    milestone_exists stable-latest || stabilize_state "testing-latest"
}

# Returns 0 if, and only if, specified milestone exists.
milestone_exists() {
    local milestone="$1"
    if [ -f "${BACKUP_DIR}/${milestone}.tar.bz2" -o \
         -L "${BACKUP_DIR}/${milestone}.tar.bz2" ]; then
        return 0
    else
        return 1
    fi
}

# Echoes the given milestone's version (i.e. "stable", "testing", "unstable")
# if it has a valid version, else "none".
# The given milestone can be inexistant.
milestone_version() {
    local milestone="$1"
    local version="`echo $milestone | sed 's/-.*$//'`"
    case $version in
      stable|testing|unstable)
          echo $version;;
      *)
          echo "none";;
    esac
}

# Echoes given milestone's date.
# Symlinks (e.g.: *-latest) are dereferenced if needed.
# The given milestone can be inexistant.
milestone_date() {
    local milestone="$1"

    if [ -L "${BACKUP_DIR}/${milestone}.tar.bz2" ]; then
        milestone="`readlink ${BACKUP_DIR}/${milestone}.tar.bz2`"
    fi
    echo `basename $milestone` | sed 's/.*-//' | sed 's/\..*$//'
}

# Returns 0 if, and only if, the given milestone ($1) is the latest one
# of its type.
# The given milestone can be inexistant.
is_latest() {
    local file milestone ref_milestone ref_date ref_version
    
    ref_milestone="$1"
    ref_date="`milestone_date $ref_milestone`"
    ref_version="`milestone_version $ref_milestone`"
    for file in "${BACKUP_DIR}/${ref_version}-"*.tar.bz2; do
        milestone=`basename $file | sed 's/\.tar\.bz2$//'`
        if [ "`milestone_date $milestone`" -gt "$ref_date" ]; then
            return 1
        fi
    done
    return 0
}

# This will save an archive of the watched directory with the given prefix
save_files() {
    debug "    - save_files $@"
    set -o noglob
    tar jcf "$BACKUP_DIR/$1-$DATE".tar.bz2 \
        -C "$WATCHED_PARENT" $TAR_OPTS `basename "$WATCHED_DIR"`
    set +o noglob
    ln -sf "$1-$DATE".tar.bz2 "$BACKUP_DIR/$1"-latest.tar.bz2
}

# This will save packages list with the given prefix
save_packages() {
    debug "    - save_packages $@"
    $VSERVER_EXEC_PREFIX apt-show-versions -i
    $VSERVER_EXEC_PREFIX apt-show-versions |
        sort > "$BACKUP_DIR/$1-$DATE".packages
    ln -sf "$1-$DATE".packages "$BACKUP_DIR/$1"-latest.packages
}

# This will save Changelogs with the given prefix
save_changelogs() {
    debug "    - save_changelogs $@"
    local changelog domain file

    if [ "$DO_CHANGELOGS" = "dir" ]; then
        for file in "$CHANGELOG_DIR"/*/Changelog; do
            changelog="${file##$CHANGELOG_DIR/}"
            domain="${changelog%%/Changelog}"
            cat "$file" > "$BACKUP_DIR/$1-$DATE.$domain.Changelog"
            ln -sf "$1-$DATE.$domain.Changelog" \
                "$BACKUP_DIR/$1-latest.$domain.Changelog"
        done
    elif [ "$DO_CHANGELOGS" = "file" ]; then
        cat "$CHANGELOG_FILE" > "$BACKUP_DIR/$1-$DATE.Changelog"
        ln -sf "$1-$DATE.Changelog" "$BACKUP_DIR/$1-latest.Changelog"
    fi
}

# Save whatever reflect the current state with the given prefix
save_state() {
    debug "* save_state $@"
    save_files "$1"
    [ $DO_PACKAGES = "no" ] || save_packages "$1"
    [ $DO_CHANGELOGS = "no" ] || save_changelogs "$1"
}

# Report changes against given version to standard output
report_changes() {
    debug "* report_changes $@"    
    local tmp tmpdir changelog domain diff tar_diff diff_diff
    local files old new tmp_packages file

    # File to store results
    tmp=`mktemp -q`
    # We need to diff against given version, so extract it
    tmpdir=`mktemp -d -q`
    tar jxf "$BACKUP_DIR/$1".tar.bz2 -C "$tmpdir"

    echo "$MAIN_HEADER" >> "$tmp"

    if [ $DO_CHANGELOGS = "dir" ]; then
        echo "$CHANGELOGS_HEADER" >> "$tmp"
        for file in "$CHANGELOG_DIR"/*/Changelog; do
            changelog="${file##$CHANGELOG_DIR/}"
            domain="${changelog%%/Changelog}"
            diff=`LC_ALL=$LOCALE \
                  diff -wEbBN "$BACKUP_DIR/$1.$domain.Changelog" \
                              "$file"` ||
                # diff returns false when files differ
                (echo "$domain:" ; echo "$diff" |
                     grep -v '^[0-9-]\|^\\') >> "$tmp"
        done
    fi
    if [ $DO_CHANGELOGS = "file" ]; then
        echo "$CHANGELOGS_HEADER" >> "$tmp"
        diff=`LC_ALL=$LOCALE \
              diff -wEbBN "$BACKUP_DIR/$1.Changelog" "$CHANGELOG_FILE"` ||
            # diff returns false when files differ
            (echo "$diff" | grep -v '^[0-9-]\|^\\') >> "$tmp"
    fi

    echo "$FILES_HEADER" >> "$tmp"

    # Find differences with tar
    set -o noglob
    tar_diff=$(tar jdf "$BACKUP_DIR/$1".tar.bz2 \
                   -C "$WATCHED_PARENT" $TAR_OPTS 2>&1 |
       # transform:
       #   etc/issue: Gid differs   -> etc/issue
       #   tar: etc/irssi.conf: ... -> etc/irssi.conf
       sed -e 's/\(tar: \)\?\([^:]*\):.*/\2/')
    # Get new files
    diff_diff=$(diff -qr $TAR_OPTS "$tmpdir"/`basename "$WATCHED_DIR"` \
                                   "$WATCHED_DIR" 2>/dev/null |
       # Only in test/etc: issue -> test/etc/issue
       sed -n -e "s,^Only in $WATCHED_PARENT\([^:]*\): \(.*\),\1/\2,p")
    files="`echo "$tar_diff$diff_diff" | sort -u`"
    set +o noglob
    if [ -z "$files" ]; then
        echo "$_NO_CHANGE" >> "$tmp"
    else
        for file in $files; do
            old="$tmpdir"/"$file"
            new="$WATCHED_PARENT$file"
            if [ -e "$old" -a -e "$new" ]; then
                echo -n '< '
                ls -ld "$old" | sed -e "s;$tmpdir/;;"
                echo -n '> '
                ls -ld "$new" | sed -e "s;$WATCHED_PARENT;;"
            elif [ -e "$old" ]; then
                echo -n '- '
                ls -ld "$old" | sed -e "s;$tmpdir/;;"
            elif [ -e "$new" ]; then
                echo -n '+ '
                ls -ld "$new" | sed -e "s;$WATCHED_PARENT;;"
            fi
        done >> "$tmp"
    fi
 
    if [ "$DO_PACKAGES" = "yes" ]; then
        echo "$DEBIAN_PACKAGES_HEADER" >> "$tmp"

        tmp_packages=`mktemp -q`
        $VSERVER_EXEC_PREFIX apt-show-versions -i
        $VSERVER_EXEC_PREFIX apt-show-versions | sort > "$tmp_packages"
        if diff -wEbBN "$BACKUP_DIR/$1".packages "$tmp_packages"; then
            echo "$_NO_DEBIAN_PACKAGES_CHANGE"
        fi | grep -v '^[0-9-]' >> "$tmp"
    fi

    if [ "$DO_DETAILS" = "yes" ]; then
        echo "$FILES_DETAILS_HEADER" >> "$tmp"

        # Just diff it!
        set -o noglob
        if (LC_ALL=$LOCALE diff -urBN $TAR_OPTS \
                --minimal "$tmpdir"/`basename "$WATCHED_DIR"` \
                "$WATCHED_DIR" 2>/dev/null); then
            echo "$_NO_CHANGE" 
        fi | grep -v '^--- \|diff ' |
             sed -e "s;^+++ $WATCHED_PARENT\([^ ]*\)    .*;+++ \1;" \
             >> "$tmp"
        set +o noglob
    fi

    # Put  on standard output
    cat "$tmp"

    # Clean temporaries
    rm -rf "$tmp" "$tmpdir"
}

# Turns into stable the given testing.
# NB: argument validity is supposed to have been already checked.
stabilize_state() {
    debug "* stabilize_state $@"
    local testing stable file dst

    testing="$1"
    # follow symlink if needed
    if [ -L "${BACKUP_DIR}/$testing".tar.bz2 ]; then
        testing="`readlink ${BACKUP_DIR}/${testing}.tar.bz2`"
        testing="`basename $testing | sed 's/\..*//'`"
    fi
    stable="`echo $testing | sed 's/^testing/stable/'`"
    for file in "${BACKUP_DIR}/${testing}"*; do
        dst="`echo $file | sed 's/\/testing-/\/stable-/'`"
        cp "$file" "$dst"
        # create/change stable-latest* links if, and only if,
        # it's really the latest    
        if is_latest $stable; then
            ln -sf "`basename $dst`" "${BACKUP_DIR}/`basename $dst |
                sed 's/-[0-9]*\./-latest\./'`"
        fi
    done
}

# Print watched directory and files separated by spaces
# (suitable for find)
# Note: this function needs pathname expansion, but is called from places where
#       it is disabled; that's why we need to save the pathname expansion status
#       in the beginning and reset it to end with.
print_watched_files() {
    local files
    local reset_noglob_status_cmd

    files="$WATCHED_DIR"
    reset_noglob_status_cmd="`set +o | grep 'set .o noglob'`"
    set +o noglob
    if [ "$DO_CHANGELOGS" = "dir" ]; then
        files="$files `echo "$CHANGELOG_DIR"/*/Changelog`"
    elif [ "$DO_CHANGELOGS" = "file" ]; then
        files="$files $CHANGELOG_FILE"
    fi
    $reset_noglob_status_cmd
    echo "$files"
}

# Return true if watched files has not changed since $1 minutes
no_change_since() {
    local time
    
    time="$1"
    set -o noglob
    if [ -z "$(find $(print_watched_files) $FIND_OPTS -cmin "-$time" -print | head -1)" ]; then
        set +o noglob
        return 0
    else
        set +o noglob
        return 1
    fi
}

# Return true if watched files has changed since file $1 last modification
changed_from() {
    local ref_file
    
    ref_file="$1"
    set -o noglob
    if [ "$(find $(print_watched_files) $FIND_OPTS -newer "$ref_file" -print | head -1)" ]; then
        set +o noglob
        return 0
    else
        set +o noglob
        return 1
    fi
}

###
### Main
###

case "$CMD" in

    report)
        context_config "$VSNAME" || fatal "Aborting (error $?)."
        DO_DETAILS="yes"
        if [ -z "$MILESTONE" ]; then
            report_changes "testing-latest"
        elif milestone_exists "$MILESTONE"; then
            report_changes "$MILESTONE"
        else
            display_usage
            fatal "The specified state does not exist."
        fi
        ;;

    list)
        context_config "$VSNAME" || fatal "Aborting (error $?)."
        for file in "$BACKUP_DIR"/*.tar.bz2; do
            echo `basename ${file%%.tar.bz2}`
        done
        ;;

    cron)
        STABLE_TIME_MIN=`expr 24 '*' 60 '*' "$STABLE_TIME"`
        if [ -n "$VSNAME" ]; then
            display_usage
            fatal "-h option not available for 'metche cron'"
        fi

        for i in "" $VSNAMES; do
            res=0
            context_config "$i" || res=$?
            if [ $res -ne 0 ]; then
                warning "-------- Ignoring" \
                        "`if [ -z $i ]; then \
                              echo 'the host system'; \
                          else \
                              echo VServer $i; fi` (error $res)"
                continue
            fi

            ### Algorithm
            #
            # if (no change happened for TESTING_TIME) then
            #     if (something has changed since the last testing) then
            #       send a report against last testing
            #       save a new testing state
            #       delete all saved unstable states
            #     elif (no change happened for STABLE_TIME) then
            #       if (something has changed since the last stable) then
            #           save a new stable state and notify EMAIL_ADDRESS
            #           delete all saved testing states older than STABLE_TIME
            #       fi
            #     fi
            # elif (last unstable exists) then
            #     if (something has changed since the last unstable) then
            #         save a new unstable state
            #     fi
            # else
            #     save a new unstable state
            # fi

            debug "* main algorithm"

            if no_change_since "$TESTING_TIME"; then
                debug "  no change since TESTING_TIME"
                if changed_from "$BACKUP_DIR"/testing-latest.tar.bz2; then
                    debug "  changed from testing-latest"
                    report_changes "testing-latest" | email "testing-$DATE"
                    save_state "testing"
                    debug "  removing all saved unstable states."
                    find "$BACKUP_DIR" -name 'unstable-*' -exec rm "{}" \;
                elif no_change_since "$STABLE_TIME_MIN"; then
                    if changed_from "$BACKUP_DIR"/stable-latest.tar.bz2; then
                        save_state "stable"
                        echo "metche saved a new stable state: stable-${DATE}." |
                        email "stable-$DATE"
                        debug "  removing all saved testing states older" \
                              "than STABLE_TIME ($STABLE_TIME)."
                        find "$BACKUP_DIR" -name 'testing-*' \
                            -ctime +"$STABLE_TIME" -exec rm "{}" \;
                    fi
                fi
            elif milestone_exists unstable-latest; then
                if changed_from "$BACKUP_DIR"/unstable-latest.tar.bz2; then
                    debug "  changed from unstable-latest"
                    save_state "unstable"
                else
                    debug "  not changed from unstable-latest"
                fi
            else
                save_state "unstable"
            fi
            
        done
        
        ;;

    stabilize)
        context_config "$VSNAME" || fatal "Aborting (error $?)."
        if [ -z "$MILESTONE" ]; then
            stabilize_state "testing-latest"
        elif [ "`milestone_version $MILESTONE`" = "testing" -a \
               milestone_exists $MILESTONE ]; then
            stabilize_state "$MILESTONE"
        else
            display_usage
            fatal "The specified state is not an existing testing state."
        fi
        ;;

    test)
        for i in "" $VSNAMES; do
            res=0
            context_config "$i" || res=$?
            if [ $res -ne 0 ]; then
                warning "-------- Ignoring" \
                        "`if [ -z $i ]; then \
                              echo 'the host system'; \
                          else \
                              echo VServer $i; fi` (error $res)"
                continue
            fi
            milestone_version "stable-200507040202"
            milestone_version "testing-latest"
            milestone_date "testing-latest"
            #report_changes "testing-latest" | email "testing-$DATE"
        done
        ;;

    *)
        display_usage
        exit 1
        ;;
esac

# vim: et sw=4
