#!/bin/ksh -u
# Backup everything to DAT/SCSI tape.
#
#      /\
#     /  \            (C) Copyright 1997, 2008 Parliament Hill Computers Ltd.
#     \  /            All rights reserved.
#      \/
#       .             Author: Alain Williams, <addw@phcomp.co.uk> Jan 1997, March 2008
#       .
#        .            SCCS: @(#)TapeBackup	1.23 07/30/10 00:11:04
#          .
#
# ADDW 

cd /

PATH=$PATH:/etc:/sbin:/usr/sbin			export PATH

# Tape: default control and no rewind devices.
tape_ctl=/dev/st0
tape_rw=/dev/st0
tape_nr=/dev/nst0

# **************** You should not need to change anything below this line ****************

PROGNAME=${0##*/}

machine=$( hostname )
logdir=/var/log/tapebackups/	# for help message
logfile=		# Filled in later
FullTapeBackup=$logdir/DateLastFileTapeBackup	# Where we record date last backed up
MultiVolume=		# tar multi volume option
CpioMulti=		# cpio prompt for multi volume
TarSuc=y		# Set n if there is some error
DoCopyLog=n		# Copy output to stderr as well as $logdir
Eject=y			# Eject the tape at end
ShowLabel=n		# Show the tape tabel & exit
Directories=		# Directories to back up
incr=n			# Incremental backup
Mount=-mount		# Do not cross mount points
verbose=		# Tar option
MailTo=			# Who to email results to
OKstatus=success	# Change to 'warning' if basically OK, but warning
ArchProg=tar		# What to use to archive
CpioOpts='-aB -H crc'	# Cpio Options when writing the tape
CpioReadOpts='-B'	# Cpio Options needed for reading
Label=			# Comment/label to put on the tape & in messages
SOURCE_LOCATION=http://www.phcomp.co.uk/Packages/TapeBackup.html
GPL_Comment="This is free software licensed under the GNU GENERAL PUBLIC LICENSE"

function Usage {
	cat <<-!
	Backup this machine to a DAT/SCSI tape
	-a prog	Archive program, tar or cpio, default $ArchProg
	-C dev	tape Control device, default $tape_ctl
	-d dirs	space separated list of directories to back up
	-I	Incremental backup since last complete tape backup
	-l lab	Tape Label contains the comment string: lab
	-L	Show the tape tabel & exit
	-m	cross Mount points, default is to not back up mounted file systems below directories listed
	-M addr Mail a success/fail message and the log file to here
	-N dev	tape device (no rewind on close), default $tape_nr
	-o	Copy output to the current stderr as well as something in $logdir
	-t	prompt for more Tapes if the tape becomes full
	-T dev	tape device (rewind on close), default $tape_rw
	-v	Tar/cpio verbose option
	-U	don't Unload (eject) the tape at end (default is to do so)
	-x	eXplain, help message
	--help	eXplain, help message
	Version: 1.23 07/30/10, latest at: $SOURCE_LOCATION
	$GPL_Comment
	!
}

# echo the argument message, send a failure to whoever we email
# Exit with fail code.
function Die {
	echo "$*" >&2
	MailStatus fail "$*"
	exit 2
}

# Output the argument(s) as a warning, change the OKstatus to warning
function Warn {
	OKstatus=warning
	echo "$*" >&2
}

# Mail a success/fail message to whoever we email to;
# Args:
# 1	'fail' 'success' 'warning'
# 2	A message
function MailStatus {
	typeset code="$1"
	typeset msg="$2"

	[[ -z $MailTo ]] && return

	# For the mail Subject: line
	[[ -n $Label ]] &&
		lab=" label $Label" ||
		lab=

	( [[ -n $Label ]] &&
		echo "Backup idenfication: $Label" &&
		echo ""

	  echo "Ends with message: $msg"
	  echo ""

	  [[ $code = warning ]] &&
	  	echo "Note that there are warnings, please check them" &&
		echo ""

	  if [[ -n $logfile && -f $logfile ]]
	  then	echo "Contents of log file: $logfile"
		echo ""
	  	cat $logfile
	  fi
	) | mail -s "$PROGNAME: $code$lab" "$MailTo"
}

# This prints simple instructions on how to restore from the tape:
function PrintRestoreInstructions {
	echo ""
	echo "Tape contents (as separate tape-files, each of which contains an archive containing many files from disk):"
	echo " 0	This label"

	# See discussion on 'set -f' below.
	set -f
	n=1
	for dir in $Directories
	do	echo " $n	Archive for: $dir"
		(( n++ ))
	done
	echo ""
	set +f

	if [[ $ArchProg = tar ]]
	then	echo "Each tape-file is a (gnu) tar archive"
	else	echo "Each tape-file is a cpio archive, options: $CpioOpts"
	fi
	echo ""

	echo "To restore files from this tape, do something like:"
	echo " 1) Identify the tape-file number from the list above; the label is tape-file 0"
	echo " 2) Rewind the tape:"
	echo "    mt -f $tape_ctl rewind"
	echo " 3) Forward the take to the tape-file identified in step 1, this example uses tape-file 3:"
	echo "    mt -f $tape_nr fsf 3"
	echo " 4) Move to a temporary directory where you will extract the files in an archive:"
	echo "    cd /tmp/restore"
	echo " 5) Extract the files:"
	if [[ $ArchProg = tar ]]
	then	echo "    tar xvf $tape_rw"
	else	echo "    cpio -ivdum $CpioReadOpts < $tape_rw"
	fi

	echo ""
}

# Create Label - this is somewhat Linux specific:
function CreateTapeLabel {
	echo "Backup ($Type) for $( hostname ) on $HumanDate"
	echo ""

	[[ -n $Label ]] &&
		echo "Backup idenfication: $Label" &&
		echo ""

	[[ $incr = y ]] &&
		echo "Incremental tape write is for files changed since: $( find $FullTapeBackup -printf '%Tc' )" &&
		echo "(Files where permissions, etc, have changed will not be backed up.)" &&
		echo "This is a full tape write"

	PrintRestoreInstructions

	echo "The information that follows is for the machine that wrote this tape"
	echo ""

	echo "System information:"
	uname -a
	echo

	echo "Memory & Swap:"
	free
	swapon -s

	echo
	echo "Hard disk parameters:"

	for hd in $(sed -n -e '/[hs]d.$/s/^.* //p' /proc/partitions )
	do	hdparm -i /dev/$hd
	done

	echo
	echo "Disk parititioning:"
	fdisk -l 2>/dev/null

	# If mdadm is installed:
	if type mdadm > /dev/null 2>&1
	then	echo
		echo "Possible mirror config:"
		mdadm --examine --verbose --scan --config=partitions

		# Try to give a bit more info:
		for md in $( mdadm --examine --verbose --scan --config=partitions | awk '/^ARRAY/ { print $2 } ' )
		do	echo
			echo "Mirror device: $md"
			mdadm --misc --detail $md
		done
	fi

	# If LVM is installed:
	if type pvdisplay > /dev/null 2>&1
	then	echo
		echo "LVM config:"
		pvdisplay --verbose 2> /dev/null
		lvdisplay 2> /dev/null
		vgdisplay --verbose 2> /dev/null
	fi

	echo
	echo "Mounted File Systems:"
	mount -v
	echo

	# Don't inspect over NFS - we could hang as a result
	echo "Disk Usage:"
	FSys="$( mount | awk '$1 ~ /:/ {next} {print $3}' )"
	df -Pk  $FSys
	echo
	df -Pki $FSys

	# How are they mounted ?
	echo
	echo "Contents of /etc/fstab:"
	cat /etc/fstab
	echo

	# Lilo information
	if type lilo > /dev/null 2>&1
	then	echo
		echo "Lilo information:"
		lilo -q -v -v -v
		echo
	fi

	echo "Boot info:"
	echo "ls -l /boot"
	ls -l /boot
	echo

	# This might get some duplicates:
	for file in grub.conf menu.lst
	do	if [[ -r /boot/grub/$file ]]
		then	echo
			echo "Contents of: /boot/grub/$file" 
			cat /boot/grub/$file
			echo
		fi
	done

	if [[ -r /etc/Hacks ]]
	then	echo
		echo "Contents of /etc/Hacks, last modified: $( find /etc/Hacks -printf '%Tc' )"
		echo "================================"
		cat /etc/Hacks
		echo "================================"
		echo
	fi

	echo "User information:"
	cat /etc/passwd
	echo ""

	if type ifconfig > /dev/null 2>&1
	then	echo
		echo "Network config: ifconfig"
		ifconfig
	fi

	if type netstat > /dev/null 2>&1
	then	echo
		echo "Network config: routing tables"
		netstat -rn
	fi

	if type rpm > /dev/null 2>&1
	then	echo
		echo "Installed packages (rpm):"
		rpm -qa
	fi

	if type dpkg > /dev/null 2>&1
	then	echo
		echo "Installed packages (dpkg):"
		dpkg --list
	fi

}

# Parse options, recognise --help
while	[[ $# -ge $OPTIND ]] && eval A1=\$$OPTIND || A1=
	if [[ $A1 = --help ]]
	then	opt=x	# --help is -x
	else	getopts a:C:d:Il:LomM:N:T:vUx opt
	fi
do	case "$opt" in
	a)	ArchProg=$OPTARG ;;
	C)	tape_ctl=$OPTARG ;;
	d)	Directories="$OPTARG" ;;
	I)	incr=y ;;
	l)	Label=$OPTARG ;;
	L)	ShowLabel=y ;;
	m)	Mount= ;;
	M)	MailTo="$OPTARG" ;;
	N)	tape_nr=$OPTARG ;;
	o)	DoCopyLog=y ;;
	t)	MultiVolume=M
		CpioMulti=--message='End of tape, please insert tape %d' ;;
	T)	tape_rw=$OPTARG ;;
	U)	Eject=n ;;
	v)	verbose=v ;;
	x)	Usage
		exit;;
	*)	echo "$PROGNAME: Unknown option '$1'" >&2
		Usage >&2
		exit 2;;
	esac
done
shift $((OPTIND - 1))

[[ $incr = n ]] &&
	Type=full ||
	Type=incremental

# Check option:
[[ $ArchProg = tar || $ArchProg = cpio ]] ||
	Die "The archive program (-a option) must be 'tar' or 'cpio', not: $ArchProg"

# Have a nominal time for the start of the backup, use this everywhere
# for consistency - even if it is slightly early for some operations:
eval $( date '+LabelName=DATE=%Y.%m.%d-%H:%M:%S TouchDate=%Y''%m%d%H''%M.%S HumanDate="%c" yyyymmdd=%Y''%m%d hhmm=%H''%M' )
logfile=$logdir$yyyymmdd-$hhmm

# Just display what the label would contain 
if [[ $ShowLabel = y ]]
then	CreateTapeLabel
	exit
fi

[[ -z $Directories ]] &&
	echo "Directories not specified (-d)" >&2 &&
	Usage &&
	exit 2

# If we don't know when the last full backup was done -- complain and abandon
[[ ! -f $FullTapeBackup && $incr = y ]] &&
	Die "Incremental backup requested, but have not done a full one yet - abandon backup for $machine"

# Increment option for tar
if [[ $incr = n ]]
then	FindIncrement=
	TarIncrement=
else	FindIncrement="-newer $FullTapeBackup"
	TarIncrement="--newer $FullTapeBackup"
fi

# Fiddle with where the output of this program goes to.
# The point is that under normal operation we want to capture/redirect
# everything to a log file, when run manually we might want to do that
# but still have everything come to the terminal as well.
[[ -d $logdir ]] || mkdir -p $logdir || Die "Cannot make directory $logdir"
exec 3>&2
if [[ $DoCopyLog = y ]]
then	tee /proc/$$/fd/3 > $logfile |&
	exec 1>&p 2>&1
else
	# If being run non interactively, send output to a log file:
	[[ ! -t 2 ]] && exec > $logfile 2>&1
fi

echo "Tape backup ($Type) starting at $( date )"
echo "Written by $PROGNAME, version 1.23, get it from $SOURCE_LOCATION"
echo "$GPL_Comment"
echo ""

[[ -n $Label ]] &&
	echo "Backup idenfication: $Label" &&
	echo ""

cd / || Die "Cannot cd to /"

# $df is a date named file that also contains label/system information
# This file gets overwritten for each machine archive

df=tmp/$LabelName
fifo=/tmp/backupfifo.$$
trap "rm -f /$df /$df.start /$df.end $fifo" 0
trap 'Die "Trapped interrupt. Code $?"' 2 3

CreateTapeLabel > $df

# Have 2 so that on restore we can tell that we have everything.
ln $df $df.start
ln $df.start $df.end

# The point of this is that it will show if a tape has not been put in
# or that the device does not exist, or something.
echo "Get tape status at $( date )"
mt -f $tape_ctl status || Die "Cannot get tape status"
echo ""

echo "Let the tape settle for 10 seconds at $( date )"
echo ""
sleep 10

echo "Rewinding tape at $( date )"
mt -f $tape_ctl rewind || Die "Cannot rewind tape"
echo "Tape rewound, let it settle for 30 seconds at $( date )"
echo ""
sleep 30

# Write a tape label:
cd /tmp || Die "Cannot cd to /tmp"
echo "Write tape label & pause 5 seconds, starting at $( date )"

if [[ $ArchProg = tar ]]
then	tar c${verbose}${MultiVolume}f $tape_nr $LabelName       || Die "Write of initial tape label failed"
else	echo $LabelName | cpio -o${verbose} $CpioOpts > $tape_nr || Die "Write of initial tape label failed"
fi
sleep 5

# Put to the log file some instructions:
PrintRestoreInstructions

# Something so that find can talk to tar:
if [[ $ArchProg = tar ]]
then	mkfifo $fifo || Die "Cannot create fifo $fifo"
fi

cd / || Die "Cannot cd to /"

echo ""
echo "Preparing for write to tape of backup for $machine at $( date )"


echo "Starting $Type tape write of $machine at $( date )"
[[ $incr = y ]] &&
	echo "Incremental tape write is for files changed since: $( find $FullTapeBackup -printf '%Tc' )"

# Ensure that the pipelines fail if anything in the pipeline fails.
[[ -z ${KSH_VERSION:-} ]] && set -o pipefail

# Prevent filename expansion in $Directories below, this is so that a pattern like
# /home/[a-m]* is put up as one file on the tape.
# Switch on expansion again later so that these will be seen as separate arguments to find.
set -f

for dir in $Directories
do	# Allow filename expansion again.
	# When $dir is expanded in the find commands below spaces are preserved, ie if
	# an expanded name contains spaces one (or more) arguments to find will be a
	# name containing spaces.
	set +f

	echo ""
	echo "****"
	echo "Starting backup for: $dir at $( date )"

	if [[ $ArchProg = tar ]]
	then
		# Put the label at the start AND end of tape, under different names.
		# Use find 'cos we can exclude old files/...
		# Need to double up any '\' else tar will interpret '\123' as 'S'.
		find $df.start $dir $df.end $Mount -depth $FindIncrement | sed -e 's/\\/\\\\/' > $fifo &

		# Read from the fifo rather than pipe, else tar gets confused when trying to prompt for tape change.
		# Tar --newer-mtime still outputs directories (even unmodified ones).
		# Tar also does not have a -mount option.
		tar c${verbose}${MultiVolume}f $tape_nr --files-from $fifo --no-recursion --atime-preserve &&
			Errs=no ||
			Errs=yes
	else
		# Cpio doesn't have the same file name problems as tar
		find $df.start $dir $df.end $Mount -depth $FindIncrement -print0 |
				cpio -o${verbose} $CpioOpts --null $CpioMulti > $tape_nr &&
			Errs=no ||
			Errs=yes
	fi

	[[ $Errs = yes ]] && TarSuc=n && OKstatus=warning	# Note any problems this dir as overall error.

	echo "Completed backup for: $dir at $( date ). Errors=$Errs"
done

echo ""
echo "****"
echo "Ended tape write for backup of $machine at $( date )"

# If a full backup & it worked: note that the time of the backup is the time of the
# start of the backup. The point is that if we do a subsequent incremental backup we
# don't want to miss a file that was modified after this backup was started but before
# it was completed.
if [[ $incr = n && $TarSuc = y ]]
then	( echo "Date started  : $HumanDate"
	  echo "Date completed: $( date '+%c' )" ) > $FullTapeBackup
	touch -t $TouchDate $FullTapeBackup
fi

echo "Let the tape settle for 30 seconds at $( date )"
sleep 30

echo "Rewinding tape at $( date )"
mt -f $tape_ctl rewind || Die "Cannot rewind tape"
echo "Tape rewound, let it settle for 30 seconds at $( date )"
sleep 30

if [[ $Eject = y ]]
then	echo "Ejecting tape at $( date )"
	mt -f $tape_ctl eject || Die "Cannot eject tape"
else	Warn "Tape NOT ejected"
fi

echo "Tape backup completed at $( date ) - started at $HumanDate"

MailStatus $OKstatus "Tape backup ending normally."

exit

# end
