#!/bin/sh
################################################################################
# xmclone by Bob Brandt                                                        #
# based on XenClone by Glen Davis                                              #
# Clone a SLES10 domU                                                          #
################################################################################

################################################################################
# Defaults                                                                     #
#                                                                              #
VERSION=0.3
XEN_CONFIGS=/etc/xen/vm/
XEN_BASE=/xen/
SOURCE=
DESTINATION=
DUPLICATE=0
HOSTNAME=
IP=
MAC=
################################################################################


################################################################################
# Subroutines used in this script                                              #
#                                                                              #
# Display the usage information for this script                                #
usage()
{
	echo -e "\nUsage: ${0##*/} [-h|?|--help] [-v|--version] [-c dir] [-b dir] [-d]"
	echo -e "                    [-n hostname] [-i address] [-m address]"
	echo -e "                    SourcedomU NewdomU\n"
	echo -e "Clones a domU, and gives a new Host name, MAC address and possibly IP address."
	echo -e "Once finished the new domU should boot without any additional configuration."
	echo -e "Currently works with single NIC, and basic bridge setup. Tested with cloning"
	echo -e "a SLES10 install created from the SLES10 YaST Xen module.\n"
	echo -e "  -h, -?, --help       Display this help message."
	echo -e "  -v, --version		Display the version of this program."
	echo -e "  -c                   XEN configuration directory which defaults to:"
	echo -e "                       $XEN_CONFIGS"
	echo -e "  -b                   XEN image base directory which defaults to:"
	echo -e "                       $XEN_BASE"
	echo -e "  -d                   Only Duplicate, do not modify attributes."	
	echo -e "  -n                   Hostname to be used, if not specified the NewdomU name"
	echo -e "                       will be used."	
	echo -e "  -i                   IP address to be used, if not specified the IP address"
	echo -e "                       will not be changed."
	echo -e "  -m                   MAC address to be used, if not specified a psuedo-random"
	echo -e "                       address will be used based on the ip address with the"
	echo -e "                       format: 00:16:AA:BB:CC:DD"
	echo -e "                       Where AA,BB,CC,DD are the Hex octals of the IP address."
	echo -e "From XENSource Networking WIKI (http://wiki.xensource.com/xenwiki/XenNetworking)"
	echo -e "Virtualised network interfaces in domains are given Ethernet MAC addresses. When"
	echo -e "choosing MAC addresses to use, ensure you choose a unicast address. That is, one"
	echo -e "with the low bit of the first octet set to zero. For example, an address"
	echo -e "starting aa: is OK but ab: is not."
	echo -e 'It is best to keep to the range of addresses declared to be "locally assigned"'
	echo -e "(rather than allocated globally to hardware vendors). These have the second"
	echo -e "lowest bit set to one in the first octet. For example, aa: is OK, a8: isn't.\n"
	echo -e "Exit status is 0 if OK, 1 if minor problems, 2 if serious trouble.\n"
}
# Display the version information for this script                              #
version()
{
	echo -e "${0##*/} (XEN vm clone utility) $VERSION"
	echo -e "This is free software.  You may redistribute copies of it under the terms of"
	echo -e "the GNU General Public License <http://www.gnu.org/licenses/gpl.html>."
	echo -e "There is NO WARRANTY, to the extent permitted by law.\n"
	echo -e "Written by Bob Brandt, based on work by Glen Davis."
}
# Find/Replace text within a file                                              #
replace()
{
	SearchText=$1
	ReplaceText=$2
	File=$3
	if sed "s/$SearchText/$ReplaceText/g" "$File" > "$File.temporary" && sleep 1
	then
		if mv -f "$File.temporary" "$File" && sleep 1
		then
			return 0
		fi
	fi
	return 1
}
################################################################################


################################################################################
# Make sure the user is root                                                   #
#                                                                              #
if [ `id -u` -ne 0 ]; then
	echo -e "You must be root to run this script!\n"
    exit 1
fi
################################################################################


################################################################################
# Process the parameters                                                       #
#                                                                              #
# Must look for double -- arguments before getopts
if [ "$1" = "--version" ]; then
	version
	exit 0;
fi
if [ "$1" = "--help" ]; then
	usage
	exit 0;
fi
while getopts ":hvc:b:dn:i:m:" opt; do
	case $opt in
		h | \? )
			usage
			exit 0;;
		v )
			version
			exit 0;;
		c ) XEN_CONFIGS=$OPTARG;;
		b ) XEN_BASE=$OPTARG;;
		d ) DUPLICATE=1;;
		n ) HOSTNAME=$OPTARG;;
		i ) IP=$OPTARG;;
		m ) MAC=$OPTARG;;
	esac
done
shift $(($OPTIND-1))

SOURCE=$1
DESTINATION=$2
################################################################################


################################################################################
# Verify the Source and Destination parameters                                 #
#                                                                              #
# The source and destination should be relative directory names without trailing /'s
# If the source does have a full path, use that path as the XEN_BASE
# Otherwise remove all but the last part of the path for both source and destination
SOURCEDIR=${SOURCE%/*}
SOURCEBASE=${SOURCE##*/}
if [ "$SOURCEDIR" != "$SOURCEBASE" ]; then
	XEN_BASE=$SOURCEDIR"/"
	SOURCE=$SOURCEBASE
fi
SOURCE=${SOURCE##*/}
DESTINATION=${DESTINATION##*/}
################################################################################


################################################################################
# Verify the XEN Config and Source parameters                                  #
#                                                                              #
# Directories should have a / after them                                       #
if [ "$XEN_CONFIGS" != "" ]; then
	XEN_CONFIGS="${XEN_CONFIGS%/}/"
fi
if [ "$XEN_BASE" != "" ]; then
	XEN_BASE="${XEN_BASE%/}/"
fi
# Verify the validity of each agrument ask the user if there is a problem
while [ ! -d "$XEN_CONFIGS" ]; do
	echo -e "\nThe $XEN_CONFIGS directory does not exist. Please enter a valid directory."
	read -p "XEN Configuration Directory? " XEN_CONFIGS
done
while [ ! -d "$XEN_BASE" ]; do
	echo -e "\nThe $XEN_BASE directory does not exist. Please enter a valid directory."
	read -p "XEN Image Base Directory? " XEN_BASE
done
################################################################################


################################################################################
# Verify that actual image and configuration file exist                        #
#                                                                              #
while [ ! -d "$XEN_BASE$SOURCE" ] || [ ! -f "$XEN_CONFIGS$SOURCE" ]; do
	if [ ! -d "$XEN_BASE$SOURCE" ]; then
		echo -e "The directory $XEN_BASE$SOURCE is invalid, please select another."
		FILES=
		tmpFILES=`ls $XEN_BASE`

		for FILE in $tmpFILES; do
			# If the Entry is a Directory
			if [ -d "$XEN_BASE$FILE" ]; then
				# And if that Directory is not empty
				if [ "`ls $XEN_BASE$FILE`" != "" ]; then
					FILES="$FILES $XEN_BASE$FILE"
				fi
			fi
		done
		if [ "$FILES" = "" ]; then
			echo -e "There are no directories beneath $XEN_BASE"
			exit 1
		else
			echo -e "Directories beneath $XEN_BASE"
			select FILE in $FILES; do
				if [ "$FILE" ]; then
					SOURCE=${FILE##*/}
					break
				else
					echo -e "Invalid Selection."
				fi
			done
		fi
	fi

	if [ ! -f "$XEN_CONFIGS$SOURCE" ]; then
		echo -e "\nThe $XEN_CONFIGS$SOURCE file does not exist. Please select a valid file."
		FILES=
		tmpFILES=`ls $XEN_CONFIGS`

		for FILE in $tmpFILES; do
			# If the Entry is a File
			if [ -f "$XEN_CONFIGS$FILE" ]; then
				FILES="$FILES $XEN_CONFIGS$FILE"
			fi
		done
		if [ "$FILES" = "" ]; then
			echo -e "There are no files beneath $XEN_CONFIGS"
			exit 1
		else
			echo -e "Files beneath $XEN_CONFIGS"
			select FILE in $FILES; do
				if [ "$FILE" ]; then
					SOURCE=${FILE##*/}			
					break
				else
					echo -e "Invalid Selection."
				fi
			done
		fi
	fi
done
################################################################################


################################################################################
# That the destination location does not already have a image or config file   #
#                                                                              #
while [ "$DESTINATION" == "" ]; do
	echo -e "\nYou have not specified a Destination."
	read -p "New Destination? " DESTINATION
done

while [ -d "$XEN_BASE$DESTINATION" ] || [ -f "$XEN_CONFIGS$DESTINATION" ]; do
	if [ -d "$XEN_BASE$DESTINATION" ]; then
		echo -e "The image location $XEN_BASE$DESTINATION already exists!"
		read -p "Please select a new Destination? " DESTINATION
	fi
	if [ -f "$XEN_CONFIGS$DESTINATION" ]; then
		echo -e "The configuration file $XEN_CONFIGS$DESTINATION already exists!"
		read -p "Please select a new Destination? " DESTINATION	
	fi
done
################################################################################


################################################################################
# Verify the network parameters (if Duplicate Only was not selected)           #
#                                                                              #
if [ "$DUPLICATE" == "0" ]; then
	if [ "$HOSTNAME" == "" ]; then
		echo -e "\nYou have not entered a Host Name.  If you wish to, enter one now."
		read -p "New Host Name? (Default: $DESTINATION) " HOSTNAME
	fi
	if [ "$HOSTNAME" == "" ]; then
		HOSTNAME=$DESTINATION
	fi
	
	if [ "$IP" == "" ]; then
		echo -e "\nYou have not specified an IP Address.  If you wish to change the IP address, enter one now."
		read -p "New IP Address? " IP
	fi
	while [ "$IP" != "" ] && [ "${IP/*.*.*.*/ok}" != "ok" ]; do
		echo -e "\nThe IP Address you specified is invalid.  If you wish, enter a new one now."
		read -p "New IP Address? " IP
		if [ "$IP" == "" ]; then
			break
		fi
	done

	if [ "$MAC" == "" ]; then
		newMAC=""
		newMACtext="(format 01:23:45:67:89:AB)"
		# If the IP Address is specified and the MAC isn't, generate one.
		if [ "$IP" != "" ]; then
			octal1=${IP%%.*}
			IP=${IP#*.}
			octal2=${IP%%.*}
			IP=${IP#*.}
			octal3=${IP%%.*}
			octal4=${IP#*.}
			IP="$octal1.$octal2.$octal3.$octal4"
			octal1="00"`echo $octal1 16 o p | dc`
			octal2="00"`echo $octal2 16 o p | dc`
			octal3="00"`echo $octal3 16 o p | dc`
			octal4="00"`echo $octal4 16 o p | dc`
			newMAC="00:16:"${octal1:(-2)}":"${octal2:(-2)}":"${octal3:(-2)}":"${octal4:(-2)}
			newMACtext="(default $newMAC)"
		fi
		echo -e "\nYou have not specified a MAC Address.  If you wish to change the MAC address, enter one now."
		read -p "New MAC Address? $newMACtext " MAC
		if [ "$MAC" == "" ]; then
			MAC=$newMAC
		fi
	fi
	
	while [ "$MAC" != "" ] && [ "${MAC/[0123456789abcdefABCDEF][0123456789abcdefABCDEF]:[0123456789abcdefABCDEF][0123456789abcdefABCDEF]:[0123456789abcdefABCDEF][0123456789abcdefABCDEF]:[0123456789abcdefABCDEF][0123456789abcdefABCDEF]:[0123456789abcdefABCDEF][0123456789abcdefABCDEF]:[0123456789abcdefABCDEF][0123456789abcdefABCDEF]/ok}" != "ok" ]; do
		echo -e "\nThe MAC Address you specified is invalid.  If you wish, enter a new one now."
		read -p "New MAC Address? (format 01:23:45:67:89:AB) " MAC	
		if [ "$MAC" == "" ]; then
			break
		fi
	done
else
	HOSTNAME=
	IP=
	MAC=
fi
################################################################################


################################################################################
# Make sure that the source VM is not running                                  #
#                                                                              #
xmid=`xm list | grep $SOURCE | cut -c34-36`
if [ "$xmid" != "" ]; then
	echo -e "domU $SOURCE is currently running on Xen, please shutdown before cloning."
	echo -e "The command \"xm shutdown $xmid -w\" will shutdown the domU"
	exit 1
fi
################################################################################


################################################################################
# Copy the XEN Config file                                                     #
#                                                                              #
SOURCECONFIG="$XEN_CONFIGS$SOURCE"
DESTCONFIG="$XEN_CONFIGS$DESTINATION"
echo -e "Copying Configuration files"
if ! cp -fv "$SOURCECONFIG" "$DESTCONFIG"
then
	echo -e "The Config file $SOURCECONFIG was unable to be copied to $DESTCONFIG"
	exit 1
fi
################################################################################


################################################################################
# Edit newly copied configuration file                                         #
#                                                                              #
echo -e "Editing config file ($DESTCONFIG), correcting the new domU Name."
if ! replace $SOURCE $DESTINATION $DESTCONFIG
then
	echo -e "Unable to change the domU name in $DESTCONFIG from $SOURCE to $DESTINATION"
	exit 1
fi

if [ "$DUPLICATE" == "0" ] && [ "$MAC" != "" ]; then
	# Get the vif line in the config file
	oldMAC=`grep "vif = " $DESTCONFIG`
	# extract everything between the square brackets
	oldMAC=${oldMAC#*[}
	oldMAC=${oldMAC%*]}
	# using the single quotes as delimiters, get the second field (this script can only deal with one adapter!)
	oldMAC=`echo "$oldMAC" | cut -f2 -d\'`
	# remove the mac= from the beginning
	oldMAC=${oldMAC#mac=*}

	if ! replace $oldMAC $MAC $DESTCONFIG
	then
		echo -e "Unable to change the MAC address in $DESTCONFIG from ($oldMAC) to ($MAC)"
		exit 1
	fi
fi
################################################################################


################################################################################
# Create and Copy image directory                                              #
#                                                                              #
SOURCEXEN="$XEN_BASE$SOURCE/"
DESTXEN="$XEN_BASE$DESTINATION/"
echo -e "Creating the new image directory $DESTXEN"
if ! mkdir -pv --mode=775 "$DESTXEN"
then
	echo -e "Unable to create the directory $DESTXEN"
	exit 1
fi
echo -e "Copying complete image.  (This may take a few minutes!)"

if ! cp -fv --sparse=never $SOURCEXEN* $DESTXEN
then
	echo -e "Unable to copy the images from $SOURCEXEN to $DESTXEN"
	exit 1
fi
################################################################################


# The rest of the document only applies if we are change the values within the image
if [ "$DUPLICATE" == "0" ] && [ "$MAC" != "" ]; then
	############################################################################
	# Mount the newly copied image file                                        #
	#                                                                          #
	# Find a temporary directory name
	tmpdir="/mnt/xentmp"
	declare -i a=0
	while [ -d "$tmpdir$a" ]; do
		a=a+1	
	done
	tmpdir="$tmpdir$a"
	if ! mkdir -pv "$tmpdir"
	then
		echo -e "Unable to create $tmpdir Directory."
		exit 1
	fi
	
	# Get the vif line in the config file
	DISKIMAGE=`grep "disk = " $DESTCONFIG`
	# extract everything between the square brackets
	DISKIMAGE=${DISKIMAGE#*[}
	DISKIMAGE=${DISKIMAGE%*]}
	# extract the first entry that is a file
	DISKIMAGE=${DISKIMAGE#*file:*}
	# remove the end of the entry
	DISKIMAGE=${DISKIMAGE%,w*}
	# using the comma as delimiter, get the second field (this script assumes that the first file entry is the boot disk!)
	DISKIMAGE=`echo "$DISKIMAGE" | cut -f2 -d\,`
	
	# Get the vif line in the config file
	PARTITION=`grep "bootentry = " $DESTCONFIG`
	# using the single quotes as delimiters, extract the data
	PARTITION=`echo "$PARTITION" | cut -f2 -d\'`
	# using the comma as delimiter, extra the boot partition information
	PARTITION=`echo "$PARTITION" | cut -f1 -d\,`
	PARTITION=${PARTITION%:*}
	PARTITION=${PARTITION#$DISKIMAGE*}
	
	if ! lomount -diskimage "$DESTXEN$DISKIMAGE" -partition $PARTITION "$tmpdir"
	then
		echo -e "Unable to mount newly copied image $DESTXEN$DISKIMAGE at $tmpdir"
		exit 1
	fi
	############################################################################


	############################################################################
	# Change the Network Configuration in the mounted image file               #
	#                                                                          #
	sleep 1
	pushd "$tmpdir"
	if [ "$MAC" != "" ]; then
		echo -e "Changing the Network configuration in the newly copied image."
		cd "$tmpdir/etc/sysconfig/network/"
		sleep 1
		# Make a backup of the old eth config files in ./othether directory
		if mkdir -pv ./oldether/
		then
			cp -fv ifcfg-eth* ./oldether/
		fi
		
		# Find the ifcfg-ethMACADDRESS file in the newly copied image
		ETH0=`ls | grep ifcfg-eth | cut -f1`
		if [ "$ETH0" = "" ]; then
			echo -e "Unable to find ethernet file in image file"
			exit 1
		fi
		# Rename the file to ifcfg-eth0 (if it has not already been done.
		# Then delete all the other config files
		if [ "$ETH0" != "ifcfg-eth0" ]; then
			if mv -f "$ETH0" ifcfg-eth0
			then
				rm -fv ifcfg-eth-id*
			fi
		fi

		# Make sure that the ifcfg-eth0 file exists, since everything depends on it!	
		if [ ! -f "ifcfg-eth0" ]; then
			echo -e "Unable to create /etc/sysconfig/network/ifcfg-eth0 file!"
			exit 1
		fi

		# The 30-net_persistent_names.rules file controls which interface to use.
		# Be removing the SUBSYSTEM line, we force the system to recreate it.
		cd "$tmpdir/etc/udev/rules.d/"
		cp -fv 30-net_persistent_names.rules 30-net_persistent_names.rules.old
		grep -v SUBSYSTEM 30-net_persistent_names.rules > 30-net_persistent_names.rules.tmp
		if ! mv -f 30-net_persistent_names.rules.tmp 30-net_persistent_names.rules
		then
			echo -e "Unable to modify the /etc/udev/rules.d/30-net_persistent_names.rules file."
			exit 1
		fi
	fi
	############################################################################


	############################################################################
	# Change the IP Address in the mounted image file                          #
	#                                                                          #
	if [ "$IP" != "" ]; then

		echo -e "Modify the IP Address of the new domU."
		cd "$tmpdir/etc/sysconfig/network/"
		sleep 1

		# Make sure that the ifcfg-eth0 file exists, since everything depends on it!	
		if [ ! -f "ifcfg-eth0" ]; then
			echo -e "Unable to find /etc/sysconfig/network/ifcfg-eth0 file!"
			exit 1
		fi

		# Make a copy every line of the ifcfg-eth0 file except the IPADDR line.
		grep -v IPADDR ifcfg-eth0 > ifcfg-eth0.xentmp
		sleep 1

		# Then add a new IPADDR line with the new IP address
		echo 'IPADDR="'$IP'"' >> ifcfg-eth0.xentmp
		sleep 1

		if ! mv ifcfg-eth0.xentmp ifcfg-eth0
		then
			echo -e "unable to modify the IP address in /etc/sysconfig/network/ifcfg-eth0"
			exit 1
		fi

	fi
	############################################################################


	############################################################################
	# Change the HOSTNAME and hosts files in the mounted image file            #
	#                                                                          #
	if [ "$HOSTNAME" != "" ]; then
		echo -e "Changing HOSTNAME file to $HOSTNAME."
		cd "$tmpdir/etc"
		# using the period as a delimter, select the first column for the hostname
		oldHOSTNAME=`cat "$tmpdir/etc/HOSTNAME" | cut -f1 -d\.`
		sleep 1
		
		if ! replace $oldHOSTNAME $HOSTNAME HOSTNAME
		then
			echo -e "Unable to change the HOSTNAME from $oldHOSTNAME to $HOSTNAME"
			exit 1
		fi
		FQDN=`cat HOSTNAME`

		echo -e "Changing hosts file."
		
		cp -fv hosts hosts.old
		if grep -v $oldHOSTNAME hosts > hosts.xentmp
		then
			echo "$IP\t$FQDN\t$HOSTNAME" >> hosts.xentmp
		fi
		
		if [ ! -f hosts.xentmp ]; then
			echo -e "Unable to change the hosts file"
			exit 1
		fi
		if ! mv -f hosts.xentmp hosts
		then
			echo -e "Unable to change the hosts file"
			exit 1
		fi
	fi
	############################################################################


	############################################################################
	# Umount image file and remove temporary directory                         #
	#                                                                          #
	popd > /dev/null
	sleep 1
	umount "$tmpdir"
	sleep 1
	rm -r "$tmpdir"
	############################################################################
fi

echo -e "Clone is complete. domU $DESTCONFIG is ready to start!"
exit 0

