#! /bin/bash
#
# metche: reducing root bus factor
# Copyright (C) 2004-2005 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
###
function display_usage {
( echo "Usage: `basename $0` [-h HOST] list"
echo " `basename $0` [-h HOST] report [{stable|testing|unstable}-YYYYMMDDHHMM]"
echo " `basename $0` [-h HOST] cron"
echo " `basename $0` [-h HOST] stabilize [testing-YYYYMMDDHHMM]"
echo "If -h is specified, /etc/metche/HOST.conf is used instead of /etc/metche.conf."
echo ""
) >&2
}
function fatal() {
echo -e "$@" >&2
exit 2
}
function debug() {
[ $DEBUG -eq 0 ] || echo -e "debug: $@" >&2
}
function email() {
debug "email $@"
local SUBJECT="$_MAIL_SUBJECT : $1"
if [ $ENCRYPT_EMAIL == "yes" ]; then
cat |
(LC_ALL="$LOCALE" gpg --batch --armor --encrypt --recipient "$EMAIL_ADDRESS") |
(LC_ALL="$LOCALE" mutt -s "$SUBJECT" "$EMAIL_ADDRESS")
else
cat |
(LC_ALL="$LOCALE" mutt -s "$SUBJECT" "$EMAIL_ADDRESS")
fi
}
###
### Configuration
###
DEBUG=1
WATCHED_DIR="/etc"
BACKUP_DIR="/var/lib/metche"
CHANGELOG_DIR="/root/changelogs"
DO_PACKAGES="yes"
TESTING_TIME="60"
STABLE_TIME="3"
EMAIL_ADDRESS="root@`hostname -f`"
ENCRYPT_EMAIL="no"
TAR_OPTS="--exclude *.swp --exclude #* --exclude *~ --exclude=*.key --exclude=ifstate"
LOCALE="C"
VSERVER_EXEC_PREFIX=""
_MAIL_SUBJECT="`hostname -f` - 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
if [ -f /etc/metche/$2.conf ]; then
. /etc/metche/$2.conf
CMD="$3"
MILESTONE="$4"
else
display_usage
fatal "Config file /etc/metche/$2.conf does not exist."
fi
elif [ -f /etc/metche.conf ]; then
. /etc/metche.conf
CMD="$1"
MILESTONE="$2"
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
test -d "$WATCHED_DIR" || fatal "WATCHED_DIR ($WATCHED_DIR) does not exist."
test -d "$BACKUP_DIR" || fatal "BACKUP_DIR ($BACKUP_DIR) does not exist."
if [ $ENCRYPT_EMAIL == "yes" ]; then
gpg --batch --list-public-keys $EMAIL_ADDRESS >/dev/null 2>&1 \
|| fatal "GnuPG public key for $EMAIL_ADDRESS not found."
fi
DATE=`date "+%Y%m%d%H%M"`
WATCHED_PARENT=`dirname $WATCHED_DIR`
if [ "$WATCHED_PARENT" != '/' ]; then
WATCHED_PARENT="$WATCHED_PARENT/"
fi
###
### Modules enabling/disabling
###
# Changelogs
DO_CHANGELOGS="yes"
test -d "$CHANGELOG_DIR" || DO_CHANGELOGS="no"
# Debian packages
# Enabled/disabled by $DO_PACKAGES, initialized to "yes", can be
# overriden by the sourced conf file.
###
### A few functions to do the real work
###
# Returns 0 if, and only if, specified milestone exists.
function 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.
function 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.
function milestone_date() {
local MILESTONE="$1"
[ ! -L "${BACKUP_DIR}/${MILESTONE}.tar.bz2" ] || MILESTONE="`readlink ${BACKUP_DIR}/${MILESTONE}.tar.bz2`"
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.
function is_latest() {
local FILE MILESTONE
local REF_MILESTONE="$1"
local REF_DATE="`milestone_date $REF_MILESTONE`"
local 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
function save_files() {
debug " - save_files $@"
tar jcf "$BACKUP_DIR/$1-$DATE".tar.bz2 \
-C "$WATCHED_PARENT" $TAR_OPTS `basename "$WATCHED_DIR"`
ln -sf "$1-$DATE".tar.bz2 "$BACKUP_DIR/$1"-latest.tar.bz2
}
# This will save packages list with the given prefix
function 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
function save_changelogs() {
debug " - save_changelogs $@"
local CHANGELOG DOMAIN file
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
}
# Save whatever reflect the current state with the given prefix
function 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
function report_changes() {
debug "report_changes $@"
local RULER TMP TMPDIR CHANGELOG DOMAIN DIFF TAR_DIFF DIFF_DIFF FILES OLD NEW TMP_PACKAGES file
# Just for fancy graphics
RULER=`echo "$WATCHED_DIR" | tr '[:print:]' '='`
# 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 == "yes" ]; 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"` || (echo "$DOMAIN:" ; echo "$DIFF" |
grep -v '^[0-9-]\|^\\') >> "$TMP"
done
fi
echo "$FILES_HEADER" >> "$TMP"
# Find differences with tar
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="$TAR_DIFF$DIFF_DIFF"
if [ "$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 -wEbB "$BACKUP_DIR/$1".packages "$TMP_PACKAGES"; then
echo "$_NO_DEBIAN_PACKAGES_CHANGE"
fi | grep -v '^[0-9-]' >> "$TMP"
fi
echo "$FILES_DETAILS_HEADER" >> "$TMP"
# Just diff it!
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"
# Put report 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.
function 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
is_latest $STABLE && \
ln -sf "`basename $DST`" "${BACKUP_DIR}/`basename $DST | sed 's/-[0-9]*\./-latest\./'`"
done
}
###
### Main
###
# 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"
case "$CMD" in
report)
[ "$MILESTONE" == "" ] && { report_changes "testing-latest"; exit 0; }
milestone_exists "$MILESTONE" && { report_changes "$MILESTONE"; exit 0; }
display_usage
fatal "The specified state does not exist."
;;
list)
for file in "$BACKUP_DIR"/*.tar.bz2; do
echo `basename ${file%%.tar.bz2}`
done
exit 0
;;
cron)
### 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
if [ "`find "$WATCHED_DIR" -cmin "-$TESTING_TIME" | head -1`" == "" ]; then
if [ "`find "$WATCHED_DIR" -newer "$BACKUP_DIR"/testing-latest.tar.bz2 | head -1`" != "" ]; then
report_changes "testing-latest" | email "testing-$DATE"
save_state "testing"
debug "removing all saved unstable states."
find "$BACKUP_DIR" -name 'unstable-*' -exec rm "{}" \;
elif [ "`find "$WATCHED_DIR" -ctime "-$STABLE_TIME" | head -1`" = "" ]; then
if [ "`find "$WATCHED_DIR" -newer "$BACKUP_DIR"/stable-latest.tar.bz2 | head -1`" != "" ]; then
save_state "stable"
echo "metche saved a new stable state : stable-${DATE}." | email "stable-$DATE"
debug "removing all saved testing states older that STABLE_TIME ($STABLE_TIME)."
find "$BACKUP_DIR" -name 'testing-*' -ctime +"$STABLE_TIME" -exec rm "{}" \;
fi
fi
exit 0
fi
milestone_exists unstable-latest && \
{
if [ "`find "$WATCHED_DIR" -newer "$BACKUP_DIR"/unstable-latest.tar.bz2 | head -1`" != "" ]; then
save_state "unstable"
fi;
exit 0;
}
save_state "unstable"
;;
stabilize)
[ "$MILESTONE" == "" ] && { stabilize_state "testing-latest"; exit 0; }
"`milestone_version $MILESTONE`" == "testing" \
&& milestone_exists $MILESTONE \
&& stabilize_state "$MILESTONE"
display_usage
fatal "The specified state is not an existing testing state."
exit 0
;;
test)
milestone_version "stable-200507040202"
milestone_version "testing-latest"
milestone_version "testing-200507030047"
milestone_version "testing-200507030047qsfd"
milestone_date "stable-200507040202"
milestone_date "testing-latest"
milestone_date "testing-200507030047"
milestone_date "testing-200507030047qsfd"
(is_latest testing-latest && echo oui) || echo non
(is_latest testing-200507031821 && echo oui) || echo non
(is_latest stable-200507031831 && echo oui) || echo non
(is_latest stable-200507040202 && echo oui) || echo non
;;
*)
display_usage
exit 1
;;
esac