#!/usr/bin/env bash
# you probably want bash or ksh for this.  Some /bin/sh's may have
# problems with it

# future enhancements:
# 1) The script may need an installboot...

#set -x

# error out if an unitialized variable is used
set -u

# I've used this script on two systems successfully now: one a test
# system, and one for a client.  :)  I feel like this script is
# getting very close to done.

######################################################################
# BACK UP YOUR SYSTEM before using this on a production machine!!!!!
######################################################################

# four parameters to tweak:
# 1) The disk that starts with data on it
SRCDISK=c0t1d0

# 2) The disk that will have data synch'd onto it.  This disk must be
# greater than or equal to in size to $SRCDISK
DSTDISK=c0t2d0

# 3) The slice number of the partition that is to hold metadb's.
# It'll be the same slice on $SRCDISK and $DSTDISK.  It should NOT have
# a filesystem on it, or at least, not a filesystem that you don't mind
# losing all the data in.  :)  I heard that 5M was enough, but when I
# tried to create 3 metadb's in a 10M slice, SVM said there wasn't
# enough room.  Creating 2 metadb's in a 10M slice worked though.
METADEV=7

# do we, or do we not, mirror swap partitions?  If we don't mirror, we
# get twice as much swap space, but you have to boot singleuser and
# comment out a line in /etc/vfstab if there's a disk failure.  Legal
# values are true and false
MIRRORSWAP=true

# Allow the user to specify a list of mount points to NOT MIRROR.
# Filesystems should be separated by whitespace, like blanks or tabs
# Use this if you want to exclude nothing
EXCLUDE_MOUNT_POINTS=""
# use this if you want to exclude /export/home
#EXCLUDE_MOUNT_POINTS="/export/home"

# If you just want a mirror that isn't a system disk, set this to false
# SYSTEM_DISK=false
SYSTEM_DISK=true

# you probably don't need to tune anything below this, unless perhaps
# you have a computer with a *.uci.edu hostname.

if [ -d /usr/opt/SUNWmd ]
then
	PATH=$PATH:/usr/opt/SUNWmd
	export PATH
fi

for prog in metastat metadb metaclear metainit metattach
do
	# note that we're using type, because which didn't give a useful exit
	# status
	if type "$prog" > /dev/null 2>&1
	then
		: Good, we have it
	else
		echo Sorry, you will need "$prog" on your '$PATH' for $0 to work 1>&2
		echo properly.... 1>&2
		exit 1
	fi
done
echo Good, you appear to have all the required programs on your '$PATH'. 1>&2
echo

for prog in metachk dup-label
do
	# note that we're using type, because which didn't give a useful exit
	# status
	if type "$prog" > /dev/null 2>&1
	then
		: Good, we have it
	else
		echo Warning, you do not have "$prog" on your '$PATH'. 1>&2
		echo You probably want it. 1>&2
	fi
done
echo

case "$(uname)-$(uname -r)" in
	@SunOS-5.8)
		# tested with SVM-RAID-5, but not SVM-root-mirror
		echo Solaris 8, checking for patch...
		if [ -d /var/sadm/patch/108693-25 ]
		then
			echo Good, you have 108693-25.  This script was tested with 1>&2
			echo this patch  1>&2
		elif [ -d /var/sadm/patch/108693-* ]
		then
			minor=$(ls /var/sadm/patch/108693-* | head -1 | sed 's#/var/sadm/patch/108693\-\([0-9][0-9]*\)#\1#')
			if [ "$minor" -lt 25 ]
			then
				echo Warning: This script was tested with 108693-25, but you 1>&2
				echo appear to have an earlier version: 108693-$minor 1>&2
			else # $minor > 25
				echo Warning: This script was tested with 108693-25, but you 1>&2
				echo appear to have a later version: 108693-$minor 1>&2
				echo You are probably in good shape. 1>&2
			fi
		fi
		;;
	SunOS-5.9)
		echo Solaris 9, good 1>&2
		;;
	SunOS-*)
		echo Warning, running on a totally untested version of Solaris 1>&2
		;;
	*)
		echo Sorry, you need SunOS to use this script 1>&2
		exit 1
		;;
esac

# the stuff in this "case" is just for testing - we delete what we've
# created so we can start over again.  Oh, and also, we override what we
# specified above, for specific machines.  :)
echo
case "`uname -n`" in
	ark.oir.uci.edu)
		if metastat > /dev/null 2>&1
		then
			echo Removing previous SVM setup
			metadb -d /dev/dsk/c0t0d0s5
			metadb -f -d /dev/dsk/c0t2d0s5
#			metaclear -f /dev/md/dsk/d0
#			metaclear -f /dev/md/dsk/d10
#			metaclear -f /dev/md/dsk/d20
#			metaclear -f /dev/md/dsk/d3
#			metaclear -f /dev/md/dsk/d13
#			metaclear -f /dev/md/dsk/d23
#			metaclear -f /dev/md/dsk/d7
#			metaclear -f /dev/md/dsk/d17
#			metaclear -f /dev/md/dsk/d27
			for slice in 0 1 3 6 7
			do
				metaclear -f /dev/md/dsk/d${slice}
				metaclear -f /dev/md/dsk/d1${slice}
				metaclear -f /dev/md/dsk/d2${slice}
			done
			cp /etc/vfstab.SVM.verybeginning /etc/vfstab
			cp /etc/system.SVM.verybeginning /etc/system
		fi
		echo Overriding defaults for sherlock
		SRCDISK=c0t0d0
		DSTDISK=c0t2d0
		METADEV=5
		MIRRORSWAP=true
		EXCLUDE_MOUNT_POINTS="/export/home"
		;;
	sherlock.oas.uci.edu)
		if metastat > /dev/null 2>&1
		then
			echo Removing previous SVM setup
			metadb -d /dev/dsk/c0t0d0s6
			metadb -f -d /dev/dsk/c0t2d0s6
			metaclear -f /dev/md/dsk/d0
			metaclear -f /dev/md/dsk/d10
			metaclear -f /dev/md/dsk/d20
			metaclear -f /dev/md/dsk/d3
			metaclear -f /dev/md/dsk/d13
			metaclear -f /dev/md/dsk/d23
			metaclear -f /dev/md/dsk/d7
			metaclear -f /dev/md/dsk/d17
			metaclear -f /dev/md/dsk/d27
			cp /etc/vfstab.SVM.verybeginning /etc/vfstab
			cp /etc/system.SVM.verybeginning /etc/system
		fi
		echo Overriding defaults for sherlock
		SRCDISK=c0t0d0
		DSTDISK=c0t2d0
		METADEV=6
		MIRRORSWAP=true
		;;
	bingy.nac.uci.edu)
		if metastat > /dev/null 2>&1
		then
			echo Removing previous SVM setup
			set -x
			metadb -d /dev/dsk/c0t1d0s7
			metadb -f -d /dev/dsk/c0t2d0s7
			metaclear -f /dev/md/dsk/d0
			metaclear -f /dev/md/dsk/d10
			metaclear -f /dev/md/dsk/d20
			cp /etc/vfstab.SVM.verybeginning /etc/vfstab
			cp /etc/system.SVM.verybeginning /etc/system
			set +x
		fi
		echo Overriding defaults for bingy
		SRCDISK=c0t1d0
		DSTDISK=c0t2d0
		METADEV=7
		MIRRORSWAP=true
		SYSTEM_DISK=false
		;;
	*)
		echo Using defaults, no host case
		;;
esac

# $1 is a partition
# returns just the slice number (EG 0 for slice 0)
function get_slice
{
	var="$(echo $1 | sed 's/^.*s\([0-9][0-9]*\)$/\1/')"
	echo "$var"
}

# $1 is a partition
# returns just the target number (EG 2 for target 2)
function get_target
{
	var="$(echo $1 | sed 's/^.*t\([0-9][0-9]*\).*$/\1/')"
	echo "$var"
}

# $1 is a partition
# returns just the controller number (EG 1 for controller 1)
function get_controller
{
	var="$(echo $1 | sed 's/^.*c\([0-9][0-9]*\).*$/\1/')"
	echo "$var"
}

# $1 is a digit
# returns the digit mapped to the corresponding slice letter, for eeprom's
# boot-device.  IOW, 0 is a, 2 is c, and so on.
function number_to_letter
{
	echo "$1" | tr '[0-9]' '[a-j]'
}

# $1 source partition
# returns corresponding dest partition
function dst_from_src
{
	s=s"$(get_slice $1)"
	echo "$DSTDISK""$s"
}

# useless use of cat, but oh well :)
SRCPARTITIONS="$(cat /etc/vfstab | grep ^/dev/dsk/${SRCDISK}s | \
	while read line
	do
		# need to be able to, for example, exclude /export/home based on this:
		# /dev/dsk/c0t0d0s7       /dev/rdsk/c0t0d0s7      /export/home ufs     2       yes     -
		#
		# this sort of thing is guaranteed to
		# works in bash, and probably ksh, but some bourne shells will
		# choke on it - they will not be able to keep the value of found
		# when the loop terminates
		found=0
		for mtpt in $EXCLUDE_MOUNT_POINTS
		do
			if [ $(echo $line | awk ' { print $3 } ') = $mtpt ]
			then
				found=1
				break
			else
				: found stays 0
			fi
		done
		if [ $found = 1 ]
		then
			echo Excluding line: $line 1>&2
		else
			echo $line
		fi
	done | \
	if [ $MIRRORSWAP = true ]
	then
		# just pass all dsk device files
		awk ' { print $1 } '
	else
		# pass all disk device files but swap partitions
		awk ' { if ($4 != swap) print $1 }'
	fi | \
	sed 's#/dev/dsk/##')"

if [ "$SRCPARTITIONS" = "" ]
then
	echo 'Sorry, no source partitions found.  Do you have vfstab entries for your'
	echo 'source partitions?  It may seem pointless, but it lets me know what'
	echo 'you need in terms of SVM.'
	exit 1
fi

if [ "$SYSTEM_DISK" = true ]
then
	SRCROOTPARTITION="$(mount | grep /${SRCDISK}s | \
		awk ' { if ($1 == "/") print $3 }' | \
		sed 's#/dev/dsk/##')"

	DSTROOTPARTITION="$(dst_from_src $SRCROOTPARTITION)"
fi

echo SRCDISK is "$SRCDISK"
echo DSTDISK is "$DSTDISK"
echo METADEV is "$METADEV"
echo MIRRORSWAP is "$MIRRORSWAP"
echo SYSTEM_DISK is "$SYSTEM_DISK"
# we want this one rewritten to all one line...
echo SRCPARTITIONS is $SRCPARTITIONS
if [ "$SYSTEM_DISK" = true ]
then
	echo SRCROOTPARTITION is "$SRCROOTPARTITION"
	echo DSTROOTPARTITION is "$DSTROOTPARTITION"
fi

# method by which to copy the partition table.  s2 is the entire disk, by
# convention, starting with the disk label.  Use dup-label if available,
# otherwise use dd
method=1
if type dup-label > /dev/null 2>&1
then
	echo Good, found dup-label
else
	echo Sorry, dup-label not found, so we will just use dd instead
	method=3
fi

echo
echo Hit interrupt now if you do not want these settings!
echo Sleeping for 10 seconds...
sleep 10

echo
echo 'We begin!  :)'
echo

# save a copy of the DSTDISK's previous partitioning, in case there's
# lost disk space
prtvtoc /dev/rdsk/"$DSTDISK"s2 > /var/tmp/${DSTDISK}s2

case "$method" in
	1)
		# copy partition table.  s2 is the entire disk, by convention, starting
		# with the disk label
		if ! dup-label /dev/dsk/"$SRCDISK"s2 /dev/dsk/"$DSTDISK"s2
		then
			echo dup-label "$SRCDISK"s2 "$DSTDISK"s2 failed 1>&2
			exit 1
		fi
		;;
	2)
		# this complains about cylinder boundaries if your disks aren't the
		# same.
		prtvtoc /dev/rdsk/"$SRCDISK"s2 | fmthard -s - /dev/rdsk/"$DSTDISK"s2
		;;
	3)
		# this works, but it also has the disadvantage of copying the disk
		# type to the other disk.  So a 200G maxtor could end up telling
		# admins it's a 100G seagate, or something like that.  However, sun
		# format's autoconfigure option can restore the label.
		#
		# Does this cause bad magic number errors in dmesg sometimes?
		dd if=/dev/dsk/"$SRCDISK"s2 of=/dev/dsk/"$DSTDISK"s2 count=16
		;;
esac

set -x
# Set up metadb's.  Writing this to an actual filesystem is a no-no
if ! metadb -a -f -c2 "$SRCDISK"s$METADEV "$DSTDISK"s$METADEV
then
	echo error "metadb'ing" "$SRCDISK"s$METADEV and "$DSTDISK"s$METADEV 1>&2
	exit 1
fi
set +x

(
	echo '#!/bin/sh'
	echo 'set -x'
) > /var/tmp/finish-up
chmod 755 /var/tmp/finish-up

# set up meta devices.
# dx's are mirror devices
# d1x's are source devices
# d2x's are destination devices
for srcpart in $SRCPARTITIONS
do
	slice="$(get_slice $srcpart)"
	srcmetadev=d1"$slice"
	dstmetadev=d2"$slice"
	# need "force" -f flag for mounted partitions
	metainit -f "$srcmetadev" 1 1 /dev/dsk/"$srcpart"
	dstpart="$(dst_from_src $srcpart)"
	metainit "$dstmetadev" 1 1 /dev/dsk/"$dstpart"
	# set up mirror with source submirror
	metainit d"$slice" -m d1"$slice"
	echo metattach d"$slice" d2"$slice" >> /var/tmp/finish-up
done

# backup vfstab and system.  mostrecent is changed every time we run
# this script.  verybeginning is only created the first time we run
# this script.
touch /etc/lvm/md.tab
for file in vfstab system lvm/md.tab
do
	cp /etc/"$file" /etc/"$file".SVM.mostrecent
	if [ -f /etc/"$file".SVM.verybeginning ]
	then
		:
	else
		cp /etc/"$file" /etc/"$file".SVM.verybeginning
	fi
done

# Tell SVM to get root from the new root mirror.  This also modifies
# /etc/vfstab, but only for the root filesystem.
if [ "$SYSTEM_DISK" = true ]
then
	metaroot d"$(get_slice $SRCROOTPARTITION)"
fi

# transform this:
#/dev/dsk/c1t0d0s5 /dev/rdsk/c1t0d0s5 /export/home ufs 2 yes logging
# ...into:
#/dev/md/dsk/d05 /dev/md/rdsk/d05 /export/home ufs 2 yes logging
# ...just for example
while read line
do
	if echo "$line" | grep '^#' > /dev/null
	then
		# preserve comments
		echo "$line"
	else
		set $line
		# a little sanity checking
		if [ "$#" = 7 ] && \
			echo "$1" | grep "^/dev/dsk/${SRCDISK}s" > /dev/null &&
			echo "$2" | grep "^/dev/rdsk/${SRCDISK}s" > /dev/null
		then
			echo "# $line"
			DSK=/dev/md/dsk/d"$(get_slice $1)"
			RDSK=/dev/md/rdsk/d"$(get_slice $2)"
			echo "$DSK" "$RDSK" "$3" "$4" "$5" "$6" "$7"
		elif [ "$#" = 7 ] && \
			echo "$4" | egrep "^swap$" > /dev/null && \
			echo "$1" | egrep "^/dev/dsk/${SRCDISK}s" > /dev/null
		then
			if [ "$MIRRORSWAP" = true ]
			then
				echo "# Swap is mirrored"
				echo "/dev/md/dsk/d$(get_slice $1) $2 $3 $4 $5 $6 $7"
			else
				# set up swap on both disks.  We intentionally do not mirror
				# them.  $SRCDISK will most likely already have one or more
				# swap partitions.  We set up identical partitions on the
				# $DSTDISK, and swap on all of them.  Be careful not to do
				# swap -files-, only swap partitions
				echo "# swap partitions are not mirrored, but we swap on both"
				echo "# disks"
				echo "$1 $2 $3 $4 $5 $6 $7"
				# a swap line looks like:
				# /dev/dsk/c0t0d0s1 - - swap - no -
				echo "/dev/dsk/${DSTDISK}s$(get_slice $1) $2 $3 $4 $5 $6 $7"
			fi
		elif [ "$#" = 7 ] && \
			echo "$1" | egrep "/dev/md/dsk/|/dev/md/rdsk" > /dev/null
		then
			# Echo any other md lines, because root should already have
			# been set up by metaroot.
			# Someday we may want to throw out everything but the root
			# filesystem though.
			echo "$line"
		else
			# and just copy through anything else, unadorned
			echo "$line"
		fi
			
	fi
done < /etc/vfstab > /etc/vfstab.new

mv /etc/vfstab.new /etc/vfstab

if [ "$SYSTEM_DISK" = true ]
then
	if grep '^set md:mirrored_root_flag=1$' /etc/system > /dev/null
	then
		echo Not modifying /etc/system
	else
		echo Modifying /etc/system
		(
			echo
			echo
			echo '# Tell SVM to ignore quorum for the mirrored root'
			echo 'set md:mirrored_root_flag=1'
			# this is useful because if you're mirroring two disks, and you
			# lose half your metadb's, there is no way to get a quorum if you
			# have an equal number of metadb's on each disk.  50% isn't a
			# majority.  However, if this script is ever extended to handle
			# mirroring across 3 or more disks, then we can turn this off.
		) >> /etc/system
	fi
else
	echo This is not a system disk, I am not changing /etc/system
fi

if [ "$SYSTEM_DISK" = true ]
then
	srccontroller="$(get_controller $SRCROOTPARTITION)"
	dstcontroller="$(get_controller $DSTROOTPARTITION)"
	case "$srccontroller.$dstcontroller" in
		0.0)
			eeprom "boot-device=disk$(get_target $SRCROOTPARTITION):$(number_to_letter $(get_slice $SRCROOTPARTITION)) disk$(get_target $DSTROOTPARTITION):$(number_to_letter $(get_slice $DSTROOTPARTITION))"
			eeprom boot-device
			;;
		*)
			echo "Sorry, this script cannot set up your boot device for disks"
			echo "that are not on controller 0"
			;;
	esac
fi

# this is similar to sync, but more effective.  It doesn't return until
# buffers are flushed, and it also rolls data out of the logs into the
# filesystem proper on log structured filesystems
echo Running lockfs to sync up buffered/logged data...
lockfs -fa

# this appeases metachk
metastat -p > /etc/lvm/md.tab

echo
echo "Done."
echo
echo "You should now inspect /etc/vfstab, /etc/system, and run the"
echo "metastat command, and if all looks well, you should reboot."
echo
echo "Setup is -NOT- complete.  You only have half of your mirror disks"
echo "added at this point.  After you reboot, you need to run"
echo "/var/adm/finish-up.  This should complete the configuration of SVM"
if [ "$SYSTEM_DISK" = true ]
then
	echo "for root disk mirroring."
else
	echo "for data disk mirroring."
fi
echo
echo "A copy of your DSTDISK's previous partitioning is in"
echo "/var/tmp/${DSTDISK}s2".  If your DSTDISK is larger than your
echo "SRCDISK, you may be able to recover some (unmirrored) disk space"
echo "on $DSTDISK with some fancy partitioning, but that's beyond the"
echo "scope of this program, at least at this time."