From: dsb@world.std.com (David Boyce)
Subject: Keeping track of Linux system configurations
Organization: The World Public Access UNIX, Brookline, MA
Date: Sun, 16 Aug 1992 00:48:34 GMT

    I posted an article a little while ago in which I basically
said we should pay more attention to Linux configuration issues,
meaning things like where files go, what their modes are, etc.
Just to speak of devices: half of the existing users have
serial devices starting at ttys0; the other half start
at ttys1. Some of them use /dev/sd01 and some /dev/sda1.
Some /dev/hd0, some /dev/hda. I myself know that I should
be using /dev/fd0, but I don't know what its major and minor
are. It's not mentioned in the FAQ; I could download the
entire new mcc-interim release to see what fd0 is but I don't
want to do that. Or I could post a note asking, but I don't want
to waste hundreds or even thousands of dollars. Anyway, this
kind of thing is irritating now and it will get worse as
users proliferate and versions accumulate.
    So, in the spirit of Doing Something About It, I've written
a script which I think could be useful for passing configuration
info around. It's called "treestat" and I've included it below.
Basically, it takes a snapshot of a set of files on your
system and contains that snapshot within itself. Then if you move it
to a different machine it will tell you how that system differs.
    There is a usage message (run it with no arguments to see it)
and some additional info at the top of the script, but the basic usage
of treestat is exceedingly simple. User A, wanting to record the
state of his system, runs "cd /; treestat -record". He then gives
treestat (which has just modified itself) to user B.
If user B runs "treestat -compare" he will see on
stdout the ways in which his files differ from those recorded
by user A. Note that treestat is a configuration tool, not a versioning
tool. Thus it does not check whether files are identical via checksums
or the like. It cares only about the presence or absence of each
pathname and whether it has the correct owner/group/mode/major/minor.
Also note that it ignores any additional files user B might have
that aren't part of the canonical set defined by user A.

    I'd appreciate it if (at least) the people in charge of mcc-interim,
MJ, TAMU, etc. would give it a look and let me know if you find it
useful, tell me of any improvements you'd like to see, bugs found, etc.

    My idea is that people making "standard" releases, such as
mcc-interim, MJ, and TAMU would

-> add treestat to their release
-> run treestat -record just before making the release
-> place it alone on their ftp site as "treestat.mcc.98.Z" or whatever.

    Then users who are running an old and maybe handmade version of linux
could simply download treestat.mcc.98 and run it with -compare to see how
they've diverged from mcc 0.98.

---------------- Cut Here ---------------------------------------------
#!/bin/sh
# Copyright (c) 1992 David Boyce
# You may distribute this under the terms of the GNU General Public License,
# version 2.

# Run this with the -compare option to check your system.

#   This script was designed for Linux but should work on most *ixes,
# modulo the awk and -g issues mentioned below.
#   The format used for describing files is the SVR4 package format. This was
# chosen simply because it's common and well-known. The format is pretty
# self-explanatory. The output from -compare is simply a normal diff between
# two SVR4 package format files.
#  This might have been more elegant as a perl script, but the whole point
# requires assuming as few tools and as few nonstandard features as possible.
# As far as I know, it requires only sh, awk, ls, cat, xargs, and diff to run
# on the local system. Generating a profile requires a few more but then you
# expect everything to be in place there. As for special features, it requires
# that the version of awk be either GNU awk (gawk) or the "new awk" (aka nawk)
# shipped with SVR4. Also, the sense of the -g flag to ls is inverted between
# SysV and BSD. I set it up in the BSD way, which also works with GNU ls and
# thus Linux. On SysV machines you might have to remove the -g flag.

usage()
{
    cat <<CATEOF
Usage: $ARG0 -compare|-fixscript|-record|-version|-printcanon
    Exactly one of the above options must be used.
-compare compares the system description contained in this script against the
    system it's running on. Only files included in the canonical list within
    the script are compared; extra files on the local system are ignored.
    NOTE: Use "$ARG0 -compare" to check your system.
-fixscript is like -compare but its output is in the form of a shell script
    to bring local modes into line with the canonical. I.e.
    "$ARG0 -fixscript | sh -xe" will normalize your local file modes.
-record causes $ARG0 TO REPLACE ITSELF with a new version that describes
    the current system. By default the list of files is derived via an
    internal "find \`pwd\` -print | egrep -v '^/tmp|^/usr/tmp|^/usr/src|...'"
    but you can generate any file list you like by any method you like and
    pipe it to stdin, and that will be used instead. Note that supplied
    filenames must be rooted. Typical usage: "cd /; $ARG0 -record".
    Only people creating a canonical system description need this.
-version simply prints out the name of the system currently described.
-printcanon causes the canonical system description within the script to be
    printed to stdout.
CATEOF
    exit 1
}

# Convert "ls -dilsg" output to SVR4 "package" format.
pkgfmt()
{
    awk "
	function mode(perms) {
	    extra = 0;
	    perms = substr(perms, 2);
	    gsub(\"S\", \"s\", perms);
	    gsub(\"t\", \"s\", perms);
	    gsub(\"T\", \"s\", perms);
	    if(substr(perms, 3, 1) == \"s\") extra += 4;
	    if(substr(perms, 6, 1) == \"s\") extra += 2;
	    if(substr(perms, 9, 1) == \"s\") extra += 1;
	    gsub(\"s\", \"x\", perms);
	    gsub(\"rwx\", \"7\", perms);
	    gsub(\"rw-\", \"6\", perms);
	    gsub(\"r-x\", \"5\", perms);
	    gsub(\"r--\", \"4\", perms);
	    gsub(\"-wx\", \"3\", perms);
	    gsub(\"-w-\", \"2\", perms);
	    gsub(\"--x\", \"1\", perms);
	    gsub(\"---\", \"0\", perms);
	    return extra perms;
	}
	function ftype(perms, links, inode, fname) {
	    type = substr(perms,1,1);
	    sub(\"-\",\"f\",type);
	    sub(\"l\",\"s\",type);
	    if(links > 1) {
		if(inodemap[inode]) {
		    type = \"l\";
		    symval = inodemap[inode];
		} else {
		    inodemap[inode] = fname;
		}
	    }
	    return type;
	}
	{
	    inode = \$1;
	    fld3 = \$3;
	    links = \$4;
	    owner = \$5;
	    group = \$6;
	    devval  = \$7 \$8;
	    fname = substr(\$11, 2);
	    if(length(fname) < 1) next;
	    devname = substr(\$12, 2);
	    symval = \$13
	}
	{
	    type = ftype(fld3, links, inode, fname);
	    if((type == \"s\") || (type == \"l\")) {
		print type,\"none\",fname \"=\" symval,owner,group,mode(fld3);
	    } else if((type == \"b\") || (type == \"c\")) {
		print type,devval,devname,owner,group,mode(fld3);
	    } else {
		print type,\"none\",fname,owner,group,mode(fld3);
	    }
	}
    "
}

# Query user for data.
resp() { echo "$1"; read $2; }

record()
{
    set -e
    NEWVERS=$RMTMPDIR/$ARG0
    PWD=${PWD:-`pwd`}
    resp "Enter an identifying name/version for this system's $PWD: \c" \
	    SNAPSHOT </dev/tty
    chmod u+w $0
    cp $0 $TMPDIR
    awk "/^exit 0/{print; exit} {print}" < $0 > $NEWVERS
    echo "$SYSMARK $SNAPSHOT" >> $NEWVERS
    if tty -s ; then
	find $PWD -print					|\
	    egrep -v "$ARG0|^/tmp/|^/usr/tmp/|^/usr/src/|^/usr/spool/"	|\
	    egrep -v "^/usr/man/cat"					|\
	    sort							|\
	    xargs ls -dilsg						|\
	    pkgfmt >> $NEWVERS
    else
	sort | xargs ls -dilsg | pkgfmt >> $NEWVERS
    fi
    if cat $NEWVERS > $0; then
	rm -f $NEWVERS
    fi
}

printcanon()
{
    awk <$0 '{if(data) print} /^exit 0/{data=1}'
}

compare()
{
    printcanon > $CANON
    echo "$SYSMARK local" > $LOCAL
    awk 'NR>1 {split($3,a,"="); print "/" a[1]}' < $CANON		|\
	xargs ls -dilsg 2>/dev/null					|\
	pkgfmt >> $LOCAL
    diff $CANON $LOCAL
}

fixscript()
{
    while read FIRST TYPE MM FNAME OWNER GROUP MODE; do
	case $FIRST=-=$TYPE in
	    \<=-=[fdbc])
		case $TYPE=-=$MM in
		    [bc]=-=*,*)
			echo rm -f /$FNAME '&&' mknod /$FNAME $TYPE \
			    `echo $MM | awk -F, '{print $1,$2}'`
			;;
		esac
		if [ -f /$FNAME -o "$MM" != "none" ]; then
		    echo chown $OWNER /$FNAME
		    echo chgrp $GROUP /$FNAME
		    echo chmod $MODE  /$FNAME
		else
		    echo ": Missing: /$FNAME"
		fi
		;;
	esac
    done
}

ARG0=`basename $0`
TMPDIR=${TMPDIR:-/tmp}
RMTMPDIR=$TMPDIR/tmp.$$
LOCAL=$RMTMPDIR/local.$ARG0
CANON=$RMTMPDIR/canon.$ARG0
SYSMARK="System:"

mkdir -p $RMTMPDIR 2>/dev/null

case "$1" in
    -c*) compare ;;
    -f*) compare | fixscript ;;
    -p*) printcanon ;;
    -r*) record ;;
    -v*) egrep "^$SYSMARK" $0 ;;
    *) usage ;;
esac

rm -rf $RMTMPDIR
exit 0 # This must remain as the last line!
-- 
David Boyce	dsb@world.std.com	617-576-1540