#!/bin/sh
#
# Wrapper script to manage KVM virtual machines.
# Licensed using the 2-clause BSD license (below)
#
# Copyright 2008 Freddie Cash
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
#    in the documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY FREDDIE CASH ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FREDDIE CASH BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Commands used in this script, just in case they are located elsewhere on your system
# Alphabetised for convenience
awk="/usr/bin/awk"
basename="/usr/bin/basename"
brctl="/usr/sbin/brctl"
cat="/bin/cat"
cp="/bin/cp"
grep="/bin/grep"
kill="/bin/kill"
kvm="/usr/bin/kvm"
pgrep="/usr/bin/pgrep"
pkill="/usr/bin/pkill"
rm="/bin/rm"
sleep="/bin/sleep"
sudo="/usr/bin/sudo"
vncviewer="/usr/bin/vncviewer"

# Directories used in this script
piddir="/var/run/kvm"
confdir="/etc/kvm"

# The file to use as a template when creating new VM configs
template="template.kvm"

# What to use as the base of the MAC address
# This allows for 32 VMs to run on this host (01 through ff)
# The id number from the config is appended to this
macbase="00:11:EC:00:00"

# Adjust these as needed.  These are the min and max amount
# of RAM to assign to a VM, in MB
minmem="128"
maxmem="4096"

# How long to sleep at certain points in the script
sleeptime="3"


##### Unless you want to change the default values in load_defaults() #####
#####      NOTHING BELOW THIS LINE SHOULD NEED TO BE MODIFIED         #####


# What are we called?
scriptname=$( ${basename} $0 )
scriptversion="2.0.2"

# Functions used in the script
load_defaults()
{
	# How much RAM to associate with the VM.
	defmem="512"

	# The number of virtual CPUs to assign to the VM.
	# Stable values are 1-4
	defcpus="1"

	# Which mouse device to use
	# Values:  mouse, tablet
	defmouse="tablet"

	# The network chipset to use in the VM.
	# Values:  rtl1389, e1000, virtio
	defnic="virtio"

	# Which virtual block device to boot from
	# Values:  a=floppy0, b=floppy1, c=disk0, d=disk1/disk2
	defboot="c"

	# Values  for disktype: ide, scsi, virtio
	defdisktype="ide"

	# Values  for media: disk, cdrom
	defmedia="disk"

	# Values for acpi:  no, "blank"
	# no disables ACPI support in the VM
	defacpi=""
}

load_configfile()
{
	# Check if the host config file exists, and suck in the contents if it does
	if [ -r ${confdir}/${1}.kvm ]; then
		. ${confdir}/${1}.kvm
	else
		echo "Config file for ${1} (${confdir}/${1}.kvm) doesn't exist or is not readable."
		exit 1
	fi
}

check_host()
{
	# Check if a VM host name was given on the commandline
	if [ -z "${1}" ]; then
		echo "Missing host to work on."
		do_usage
		exit 1
	fi

	# Check if the host name given corresponds to a configured VM
	if [ ! -e ${confdir}/${1}.kvm ]; then
		echo "The host you want to manage (${1}) doesn't have a config file.  ${confdir}/${1}.kvm doesn't exist."
		do_usage
		exit 1
	fi
}

do_start()
{
	# Check if a virtual host was given on the commandline
	# Will exit if none was given
	check_host "${1}"

	# Load the default values for all the config options
	load_defaults "${1}"

	# Try to load the config file for the named host
	# Will exit if the config file doesn't exist
	load_configfile "${1}"

	# Check that an ID is set
	if [ -z ${id} ]; then
		echo "Error! ID number for this host has not been set."
		exit 1
	fi

	# Check that a host name is set
	if [ -z ${host} ]; then
		echo "Error! Host name not set in the config file."
		exit 1
	fi

	# Check if there is a primary virtual block device in the config
	if [ ! -z ${disk0} ]; then
		# Check if the primary block device exists in the host.
		if [ ! -e ${disk0} ]; then
			echo "Error!  Primary virtual drive (${disk0}) doesn't exist or isn't readable."
			exit 1
		else
			confdisk0=${disk0}
		fi

		# Check what kind of media to use for the virtual drive
		case "${media0}" in
			disk)
				confmedia0="disk"
				;;
			cdrom)
				confmedia0="cdrom"
				;;
			*)
				confmedia0=${defmedia}
				;;
		esac

		# Check what kind of interface to use for the virtual drive
		case "${disktype0}" in
			ide)
				confdisktype0="ide"
				;;
			scsi)
				confdisktype0="scsi"
				;;
			virtio)
				confdisktype0="virtio"
				;;
			*)
				confdisktype0=${defdisktype}
				;;
		esac

		# Check whether to use extboot support for the virtual drive
		if [ ${confdisktype0} = "scsi" -o ${confdisktype0} = "virtio" ]; then
			confextboot=",boot=on"
		else
			confextboot=""
		fi

		# Build the drive entry that will be passed to kvm
		confdrive0="-drive index=0,media=${confmedia0},if=${confdisktype0}${confextboot},file=${confdisk0}"
		if [ "x$diskopts0" != "x" ]; then
		    # Append the extra options
		    confdrive0="${confdrive0},${diskopts0}"
		fi
	else
		echo "Error!  disk0 not set in config file.  Can't boot without a primary hard drive."
		exit 1
	fi

	# Check if there is a second virtual block device in the config
	if [ ! -z ${disk1} ]; then
		# Check if the primary block device exists in the host.
		if [ ! -e ${disk1} ]; then
			echo "Error!  Second virtual drive (${disk1}) doesn't exist or isn't readable."
			exit 1
		else
			confdisk1=${disk1}
		fi

		# Check what kind of media to use for the virtual drive
		case "${media1}" in
			disk)
				confmedia1="disk"
				;;
			cdrom)
				confmedia1="cdrom"
				;;
			*)
				confmedia1=${defmedia}
				;;
		esac

		# Check what kind of interface to use for the virtual drive
		case "${disktype1}" in
			ide)
				confdisktype1="ide"
				;;
			scsi)
				confdisktype1="scsi"
				;;
			virtio)
				confdisktype1="virtio"
				;;
			*)
				confdisktype1=${defdisktype}
				;;
		esac

		# Build the drive entry that will be passed to kvm
		confdrive1="-drive index=1,media=${confmedia1},if=${confdisktype1},file=${confdisk1}"
		if [ "x$diskopts1" != "x" ]; then
		    # Append the extra options
		    confdrive1="${confdrive1},${diskopts1}"
		fi
	fi

	# Check if there is a third virtual block device in the config
	if [ ! -z ${disk2} ]; then
		# Check if the primary block device exists in the host.
		if [ ! -e ${disk2} ]; then
			echo "Error!  Third virtual drive (${disk2}) doesn't exist or isn't readable."
			exit 1
		else
			confdisk2=${disk2}
		fi

		# Check what kind of media to use for the virtual drive
		case "${media2}" in
			disk)
				confmedia2="disk"
				;;
			cdrom)
				confmedia2="cdrom"
				;;
			*)
				confmedia2=${defmedia}
				;;
		esac

		# Check what kind of interface to use for the virtual drive
		case "${disktype2}" in
			ide)
				confdisktype2="ide"
				;;
			scsi)
				confdisktype2="scsi"
				;;
			virtio)
				confdisktype2="virtio"
				;;
			*)
				confdisktype2=${defdisktype}
				;;
		esac

		# Build the drive entry that will be passed to kvm
		confdrive2="-drive index=2,media=${confmedia2},if=${confdisktype2},file=${confdisk2}"
		if [ "x$diskopts2" != "x" ]; then
		    # Append the extra options
		    confdrive2="${confdrive2},${diskopts2}"
		fi
	fi

	# Check if there is a fourth virtual block device in the config
	if [ ! -z ${disk3} ]; then
		# Check if the primary block device exists in the host.
		if [ ! -e ${disk3} ]; then
			echo "Error!  Fourth virtual drive (${disk3}) doesn't exist or isnt' readable."
			exit 1
		else
			confdisk3=${disk3}
		fi

		# Check what kind of media to use for the virtual drive
		case "${media3}" in
			disk)
				confmedia3="disk"
				;;
			cdrom)
				confmedia3="cdrom"
				;;
			*)
				confmedia3=${defmedia}
				;;
		esac

		# Check what kind of interface to use for the virtual drive
		case "${disktype3}" in
			ide)
				confdisktype3="ide"
				;;
			scsi)
				confdisktype3="scsi"
				;;
			virtio)
				confdisktype3="virtio"
				;;
			*)
				confdisktype3=${defdisktype}
				;;
		esac

		# Build the drive entry that will be passed to kvm
		confdrive3="-drive index=3,media=${confmedia3},if=${confdisktype3},file=${confdisk3}"
		if [ "x$diskopts3" != "x" ]; then
		    # Append the extra options
		    confdrive3="${confdrive3},${diskopts3}"
		fi
	fi

	# Check which device to boot from
	if [ ! -z ${boot} ]; then
		case "${boot}" in
			a)
				confboot="a"
				;;
			b)
				confboot="b"
				;;
			c)
				confboot="c"
				;;
			d)
				confboot="d"
				;;
			*)
				confboot=${defboot}
				;;
		esac
	fi

	# If booting off a CD-ROM, make "reboot" command act like "poweroff" command
	if [ ! -z ${confmedia2} ]; then
		if [ ${confboot} = "d" -a ${confmedia2} = "cdrom" ]; then
			confreboot="-no-reboot"
		else
			confreboot=""
		fi
	fi

	# Check which virtual NIC chipset to use
	case "${nic}" in
		rtl8139)
			confnic="rtl8139"
			;;
		e1000)
			confnic="e1000"
			;;
		virtio)
			confnic="virtio"
			;;
		*)
			confnic=${defnic}
			;;
	esac

	# Check which virtual mouse chipset to use
	case "${mouse}" in
		mouse)
			confmouse="mouse"
			;;
		tablet)
			confmouse="tablet"
			;;
		*)
			confmouse=${defmouse}
			;;
	esac

	# Check whether to disable ACPI
	case "${acpi}" in
		no)
			confacpi="-no-acpi"
			;;
		*)
			confacpi=${defacpi}
			;;
	esac

	# Check number of virtual CPUs to use
	case "${cpus}" in
		1)
			confcpus="1"
			;;
		2)
			confcpus="2"
			confacpi=${defacpi}
			;;
		3)
			confcpus="3"
			confacpi=${defacpi}
			;;
		4)
			confcpus="4"
			confacpi=${defacpi}
			;;
		*)
			confcpus=${defcpus}
			confacpi=${defacpi}
			;;
	esac

	# Check the amount of RAM to assign to the VM
	if [ ${mem} -lt ${minmem} -o ${mem} -gt ${maxmem} ]; then
		echo "Virtual RAM is not between ${minmem} and ${maxmem}.  Setting virtual RAM to ${defmem}."
		confmem=${defmem}
	else
		confmem=${mem}
	fi

	# Start the VM
	echo "Attempting to start VM for ${1} ..."
	${kvm} \
		-name ${host} \
		-smp ${confcpus} \
		-m ${confmem} \
		-vnc :${id} \
		-daemonize \
		-localtime \
		-usb \
		-usbdevice ${confmouse} \
		-net nic,macaddr=${macbase}:${id},model=${confnic} \
		-net tap,ifname=tap${id} \
		-pidfile ${piddir}/${host}.pid \
		-boot ${confboot} \
		${confacpi} \
		${confreboot} \
		${confdrive0} \
		${confdrive1} \
		${confdrive2} \
		${confdrive3}
	echo "VM for ${1} has started."

	# Show the VNC port assigned to the VM.
	do_whichvnc "${1}"
}

do_stop()
{
	# Check if a virtual host was given on the commandline
	# Will exit if none was given
	check_host "${1}"

	echo "Attempting to stop VM for ${1} ..."
	if [ -r ${piddir}/${1}.pid ]; then
		${kill} -TERM $( ${cat} ${piddir}/${1}.pid )

		${sleep} ${sleeptime}

		if [ -d /proc/$( ${cat} ${piddir}/${1}.pid ) ]; then
			echo "Something is wrong ... couldn't stop the VM."
			do_forcekill "${1}"
		fi

		${rm} ${piddir}/${1}.pid
	else
		echo "PID file missing or not readable."
		do_forcekill "${1}"
	fi

	echo "VM for ${1} has stopped."
}

do_forcekill()
{
        echo "Forcibly killing the KVM process for ${1}"
	${pkill} -f -- "kvm.*-name ${1}"
}

do_status()
{
        pids=$(pgrep -x kvm)
	if [ -z ${1} ]; then
		# Called with blank argument, show just the names of the running VMs
		echo "The following VMs are running:"
		for pid in $pids; do
		        ps_info_for $pid | perl -n -e '/-name ([\S]+) / && print "$1\n"'
		done
	elif [ ${1} = "kvm" ]; then
		# Called with "kvm" argument, show full details of all kvm processes
		echo "The following VMs are running:"
		for pid in $pids; do
		        ps_info_for $pid
		done
	elif [ ${1} = "pids" ]; then
	        # Called with "pids" argument, show the PIDs for the kvm processes
	        echo $pids
	elif [ -e ${confdir}/${1}.kvm ]; then
		# Called with a VM host name, show details for just that kvm process
		${pgrep} -lf -- "-name ${1} " | ${grep} -v ${scriptname}
	else
		echo "Don't understand the command."
		do_usage
		exit 1
	fi
}

ps_info_for()
{
        ps ax | $grep "^ *$1"
}

do_usage()
{
	${cat} <<-end-of-help
	${scriptname} ${scriptversion}
	Licensed under BSDL  Copyright:      2008

	${scriptname} is a management and control script for KVM-based virtual machines.

	Usage:  ${scriptname} start    host    - start the named VM
	        ${scriptname} startvnc host    - start the named VM, and then connect to console via VNC
	        ${scriptname} stop     host    - stop  the named VM (only use if the guest is hung)
	        ${scriptname} restart  host    - stop and then start the named VM (only use if the guest is hung)

	        ${scriptname} vnc      host    - connect via VNC to the console of the named VM
	        ${scriptname} whichvnc host    - show which VNC display port is assigned to the named VM
	        ${scriptname} killvnc  host    - kills any running vncviewer processes attached to the named VM

	        ${scriptname} edit     host    - open config file for host using \$EDITOR, or create a new config file based on a template

	        ${scriptname} status           - show the names of all running VMs
	        ${scriptname} status   kvm     - show full details for all running kvm processes
	        ${scriptname} status   pids    - show only the PIDs for running kvm processes
	        ${scriptname} status   host    - show full details for the named kvm process

	        ${scriptname} help             - show this usage blurb

	** Using stop is the same as pulling the power cord on a physical system. Use with caution.

	end-of-help
}

do_whichvnc()
{
	# Check if a virtual host was given on the commandline
	# Will exit if none was given
	check_host ${1}

	echo -n "The VNC port for ${1} is "
	${pgrep} -lf -- "-name ${1}" | ${grep} -v ${scriptname} | ${awk} '{ print $10 }'
	echo ""
}

do_vnc()
{
	if [ ${UID} -eq 0 ];then
		echo -n "Do you really want to run vncviewer as root? (y/n) "
		read yesno
		case "${yesno}" in
			[yY]*)
				break
				;;
			*)
				exit 1
				;;
		esac
	fi

	${vncviewer} localhost$( ${pgrep} -lf -- "-name ${1}" | ${grep} -v ${scriptname} | ${awk} '{ print $10 }' ) > /dev/null 2>&1 &
}

do_killvnc()
{
	# Check if a virtual host was given on the commandline
	# Will exit if none was given
	check_host ${1}

	# Get the VNC port used by the VM
	vncport=$( ${pgrep} -lf -- "-name ${1}" | ${grep} -v ${scriptname} | ${awk} '{ print $10 }' ) 

	echo -n "Do you really want to kill the vncviewer for ${1}? (y/n) "
	read yesno
	case "${yesno}" in
		[yY]*)
			# Kill the associated vncviewer process
			${pkill} -f -- "vnc.*localhost${vncport}"
			echo "vncviewer process for ${1} has been terminated."
			;;
		*)
			exit 0
			;;
	esac
}

check_uid()
{
	# If not being run as root, try to use sudo for start/stop
	if [ ${UID} -ne 0 ]; then
		cat="${sudo} ${cat}"
		kvm="${sudo} ${kvm}"
		kill="${sudo} ${kill}"
		pkill="${sudo} ${pkill}"
		rm="${sudo} ${rm}"
	fi
}

do_edit()
{
	# If the config file exists, and is writable by the user, then load it using $EDITOR
	if [ -e ${confdir}/${1}.kvm ]; then
		if [ -w ${confdir}/${1}.kvm ]; then
			${EDITOR} ${confdir}/${1}.kvm
		else
			echo "You don't have write permission for ${confdir}/${1}.kvm"
			exit 1
		fi
	else
		echo -n "${confdir}/${1}.kvm does not exist.  Would you like to create one from the template? (y/n) "
		read yesno
		case "${yesno}" in
			[yY]*)
				if [ -r ${confdir}/${template} ]; then
					if [ -w ${confdir} ]; then
						${cp} ${confdir}/${template} ${confdir}/${1}.kvm
						${EDITOR} ${confdir}/${1}.kvm
					else
						echo "You don't have write permission for ${confdir}"
						exit 1
					fi
				else
					echo "The template config file (${confdir}/${template}) doesn't exist or isn't readable."
					exit 1
				fi
				;;
			*)
				echo "As you wish."
				;;
		esac
	fi
}

# Main script
case "${1}" in
	start)
		check_uid
		do_start "${2}"
		;;
	startvnc)
		check_uid
		do_start "${2}"
		do_vnc "${2}"
		;;
	stop)
		check_uid
		do_stop "${2}"
		;;
	restart)
		check_uid
		do_stop "${2}"
		${sleep} ${sleeptime}
		do_start "${2}"
		;;
	status)
		do_status "${2}"
		;;
	vnc)
		do_vnc "${2}"
		;;
	whichvnc)
		do_whichvnc "${2}"
		;;
	killvnc)
		do_killvnc "${2}"
		;;
	edit)
		do_edit "${2}"
		;;
	help)
		do_usage
		;;
	*)
		do_usage
		;;
esac
