Yet Another Sysadmin Script

(WORK IN PROGRESS)

I wanted to implement change control on dustpuppy so I got used to doing the Right Thing and not just randomly dorking around with production config files.  But there weren’t any decent open source packages out there that did what I wanted.  So I started writing a script to do it.  The deploy function works but is missing a key feature, and the rollback function fails ($OLDFILE is getting clobbered somewhere).

#!/bin/bash
#
# syschange.sh - makes system changes safely
#
###################UNIX Bourne Again Shell Script, 80 Columns##################

# The idea is to take a target file and source file from the command line and
# copy both files to /tmp, diff them, confirm with the user, make a log entry,
# and then copy in the new file, saving the old file in a backup folder.
#
# In rollback mode, the new file will be copied to /tmp, the original location
# looked up, the old file copied back from the backup folder, and the new file
# put back in its original location.  A log entry will be made.
#

# options
CHANGE_DB="/var/local/lib/syschange"
CHANGE_LOG="/var/log/syschange.log"

# check to see if we're root
user=`whoami`

if [[ $user != 'root' ]]
then
	echo "This command must be run as root." >&2
	exit 77 # EX_NOPERM
fi

# check for the required number of args
if [[ $# -lt 2 ]]
then
	echo "You need to specify a mode and at least one argument." >&2
	exit 64 # EX_USAGE
fi

# The first arg should be the mode identifier
MODE=$1

if [[ $MODE == 'deploy' ]]
then
	OLDFILE=$2
	NEWFILE=$3
		
	# check absolute paths
	old_slash=`expr index "$OLDFILE" /`
	new_slash=`expr index "$NEWFILE" /`

	if [[ "$old_slash" -ne 1 ]] || [[ "$new_slash" -ne 1 ]]
	then
		echo "Paths must be absolute!" >&2
		exit 1
	fi

	changeserial="$(echo $OLDFILE | sed s#/#_#g).$(date +%s)"
	old_name=`basename $OLDFILE`
	new_name=`basename $NEWFILE`
	old_tmp="/tmp/$old_name"
	new_tmp="/tmp/$new_name"
	diff_file="$CHANGE_DB/$changeserial.diff"

	cp "$OLDFILE" /tmp
	cp "$NEWFILE" /tmp

	diff "$old_tmp" "$new_tmp" > "$diff_file"

	# less the output
	less "$diff_file"

	# make sure this is what the user wants
	echo "Are you sure you want to make this change to $OLDFILE? (y/n)"
	read response

	if [[ $response != 'y' ]] && [[ $response != 'Y' ]]
	then
		exit 0
	fi

	# gzip the diff
	gzip "$diff_file"

	# make the log entries
	echo "$(date --rfc-3339=seconds) $changeserial deploy \
		$OLDFILE $NEWFILE" >> "$CHANGE_LOG"

	# copy the files
	cp "$OLDFILE" "$CHANGE_DB/$old_name"
	gzip "$CHANGE_DB/$old_name"
	cp -f "$NEWFILE" "$OLDFILE"
		
	# exit
	exit 0
	
elif [[ "$MODE" == 'rollback' ]]
then
	OF_COLUMN=5 # the column number for the old file in the log

	# argument should be change serial or filename
	slash_index=`expr index "$2" /`

	if [[ $slash_index -eq 0 ]] # expr index prints 0 if the substring isn't found
	then
		# assume it's a changeserial
		changeserial=$2
		spec="changeserial"

	elif [[ $slash_index -eq 1 ]] # if the string starts with a /
	then
		# assume it's a filename
		OLDFILE=$2
		spec="filename"

	else # it has slashes but doesn't start with one
		# so it must be a relative path, which isn't supported
		echo "You must specify an absolute path to a file." >&2
		exit 1
	fi

	case "$spec" in
		'changeserial')
			# find the transaction
			entry=`grep "$changeserial" $CHANGE_LOG`
			echo "Found entry\n$entry\nIs this what you want to roll back?"
			read rollback_response

			if [[ $rollback_response != 'Y' ]] && \
				[[ $rollback_response != 'y' ]]
			then
				# abort
				exit 1 # EXIT_FAILURE
			fi

			# do the rollback
			OLDFILE=`echo $entry | awk '{ print $5 }'`
			;;

		'filename')
			# list the transactions for this file
			tranlist=`grep "$OLDFILE" $CHANGE_LOG`

			select tran in "$tranlist"
			do
				changeserial=`echo "$tran" | awk '{ print $3 }'`
				break
			done
				
			if [[ $changeserial == 'rollback' ]]
			then
				echo "This change has already been rolled back." >&2
				exit 0
			fi

			entry=$tran
			;;

		*)
			# abort
			exit 70 # EX_SOFTWARE
			;;
	esac

	of_stored="$CHANGE_DB/$(basename $OLDFILE).gz"
	
	# ensure the old file exists in the cache directory
	if [[ ! -e $of_stored ]]
	then
		# abort
		echo "File not found in change dataset." >&2
		exit 66 # EX_NOINPUT
	fi

	# gunzip the file
	gunzip "$of_stored"
	of_stored="$CHANGE_DB/$(basename $OLDFILE)"

	# make the log entry
	echo `date --rfc-3339=seconds` " rollback $changeserial" \
		>> "$CHANGE_LOG"

	# copy the file
	orig_lcn=`echo $entry | awk '{ print $6 }'`
	cp -i "$OLD_FILE" "$orig_lcn"
	cp -f "$of_stored" "$OLD_FILE"

	# done
	exit 0

#else	
	# there's a problem, bail out
#	exit 64 # EX_USAGE
fi
Advertisements

  1. #1 by Andrew D. on July 8, 2010 - 3:40 PM

    Sounds like you need something like dispatch-conf or etc-update.

    http://www.gentoo.org/doc/en/handbook/handbook-x86.xml?part=3&chap=4

    I don’t know if they have a flavor-agnostic version, but you might be able to steal some useful stuff from them. It was indispensable on Gentoo.

    • #2 by Joshua on July 8, 2010 - 6:38 PM

      I love Gentoo and that’s one of the reasons. And yes, dispatch-conf would totally rock on Red Hat. Solaris has the new SMF system that handles that but it’s rather more labyrinthine than Gentoo’s etc-update or dispatch-conf.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: