#!/bin/sh
#Copyright (c) 2022-2024 Pierre Pronchery <khorben@EdgeBSD.org>
#This file is part of EdgeBSD NVMM
#This code is derived from software contributed to the EdgeBSD Project
#by Pierre Pronchery <khorben@EdgeBSD.org>
#
#Redistribution and use in source and binary forms, with or without
#modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
# * 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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.



#variables
#essential
VENDOR="@VENDOR@"
PACKAGE="@PACKAGE@"
#executables
BRCONFIG="/sbin/brconfig"
DEBUG="_debug"
ECHO="echo"
case "$(uname -s)" in
	"Darwin")
		ECHO="/bin/echo"
		;;
esac
IFCONFIG="/sbin/ifconfig"
MAKEDEV="/dev/MAKEDEV"
MKDIR="mkdir"
QEMU=
RM="rm -f"
RMDIR="rmdir"
SEQ="seq"
SHA1="sha1"
UNAME="uname"
#settings
BINDIR="@BINDIR@"
DEVNULL="/dev/null"
DRYRUN=0
PROGNAME="edgebsd-qemu"
QEMU_ARGS=
QEMU_CHROOT=
QEMU_CPUS=1
QEMU_DEFAULT_ARGS=
QEMU_EVENT_PANIC=
QEMU_EVENT_REBOOT=
QEMU_EVENT_SHUTDOWN=
QEMU_MACHINE="q35,accel=nvmm"
QEMU_MEMORY=
QEMU_NAME=
QEMU_NETWORK_INTERFACES=
QEMU_NETWORK_MAX=255
QEMU_SOCKET=
QEMU_USER=
RUNDIR="/var/run/nvmm"
SYSCONFDIR="@SYSCONFDIR@"
VERBOSE=1

#load local settings
[ -f "$SYSCONFDIR/$VENDOR/$PACKAGE/$PROGNAME.conf" ] &&
	. "$SYSCONFDIR/$VENDOR/$PACKAGE/$PROGNAME.conf"
[ -n "$HOME" -a -f "$HOME/.config/$VENDOR/$PACKAGE/$PROGNAME.conf" ] &&
	. "$HOME/.config/$VENDOR/$PACKAGE/$PROGNAME.conf"


#functions
#edgebsd_qemu
_edgebsd_qemu()
{
	hostname=
	config=

	while getopts "a:c:O:nqu:v" name; do
		case "$name" in
			a)
				QEMU_ARCH="$OPTARG"
				;;
			c)
				QEMU_CHROOT="$OPTARG"
				;;
			n)
				DRYRUN=1
				;;
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			u)
				QEMU_USER="$OPTARG"
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -eq 1 ]; then
		hostname="$1"
		config="$SYSCONFDIR/$VENDOR/$PACKAGE/hosts/$hostname.conf"
	elif [ $# -eq 2 ]; then
		hostname="$1"
		config="$2"
	fi
	if [ -z "$hostname" ]; then
		_usage "hostname not set"
		return $?
	fi
	if [ -z "$config" ]; then
		_usage "configuration file not set"
		return $?
	fi
	_qemu_parse "$hostname" "$config" &&
		_qemu_run
}

_qemu_cleanup()
{
	#cleanup: network interfaces
	[ -n "$QEMU_NETWORK_INTERFACES" ] && for interface in $QEMU_NETWORK_INTERFACES; do
		$DEBUG $IFCONFIG "$interface" destroy
	done

	#cleanup: UNIX socket
	if [ -n "$QEMU_SOCKET" ]; then
		$DEBUG $RM -- "$QEMU_SOCKET"
	fi

	#cleanup: host directory
	$DEBUG $RMDIR -- "${QEMU_SOCKET%/*}"
}

_qemu_network_interface()
{
	for i in $($SEQ 0 $QEMU_NETWORK_MAX); do
		interface="tap$i"
		device="/dev/$interface"

		if [ ! -c "$device" ]; then
			(cd "${device%/*}" && $DEBUG $MAKEDEV "${device##*/}") 2> "$DEVNULL"
			if [ $? -ne 0 ]; then
				_error "$device: Could not create device node"
				return $?
			fi
		fi
		$DEBUG $IFCONFIG "$interface" create up 2> "$DEVNULL"
		if [ $? -eq 0 ]; then
			$ECHO -n "$interface"
			return 0
		fi
	done
	_error "Could not create network interface"
	return $?
}

_qemu_network_macaddress()
{
	hostname="$1"
	interface="$2"

	hash="$($ECHO -n "$hostname.$interface" | $SHA1)"
	hash1="${hash#??}" 1>&2
	hash0="${hash%$hash1}"
	hash2="${hash1#??}" 1>&2
	hash1="${hash1%$hash2}"
	hash3="${hash2#??}" 1>&2
	hash2="${hash2%$hash3}"
	$ECHO -n "00:0e:22:$hash0:$hash1:$hash2"
}

_qemu_parse()
{
	hostname="$1"
	config="$2"
	context=

	#set default values
	if [ -z "$QEMU_DEFAULT_ARGS" ]; then
		[ -n "$QEMU_SOCKET" ] || QEMU_SOCKET="$RUNDIR/$hostname/socket"
		QEMU_DEFAULT_ARGS="-nodefaults -nographic\
 -chardev stdio,id=s1,signal=off\
 -serial none -device isa-serial,chardev=s1\
 -chardev socket,id=mon0,path=$QEMU_SOCKET,server=on,wait=off\
 -mon chardev=mon0,mode=readline\
 -object rng-random,filename=/dev/urandom,id=viornd0\
 -device virtio-rng-pci,rng=viornd0"
	fi
	[ -z "$QEMU_NAME" ] && QEMU_NAME="$hostname"

	drive_driver=
	drive_file=
	drive_format=
	drive_id=
	drive_id_new=

	network_bridge=
	network_driver=
	network_id=
	network_id_new=
	network_interface=
	network_mac=
	network_on_down="no"
	network_on_up="no"
	network_type=

	#parse the configuration file
	cnt=0
	while read line; do
		cnt=$((cnt + 1))
		case "$line" in
			"#"*)
				#ignore comments
				;;
			"["*"]")
				#change of section
				context="${line%]*}"
				context="${context#[}"

				#signal the new context
				_qemu_set "$context" "" ""
				[ $? -eq 0 ] || return 2
				;;
			*=*)
				#variable definition
				name="${line%%=*}"
				value="${line#*=}"
				_qemu_set "$context" "$name" "$value"
				[ $? -eq 0 ] || return 2
				;;
			*)
				[ -z "$line" ] && continue
				_error "$config: parse error at line $cnt"
				return $?
				;;
		esac
	done < "$config"

	#wrap ongoing parsing
	_qemu_set "" "" ""
}

_qemu_run()
{
	#determine the path to QEMU
	if [ -z "$QEMU" ]; then
		[ -z "$QEMU_ARCH" ] && QEMU_ARCH="$($UNAME -m)"
		case "$QEMU_ARCH" in
			amd64)
				QEMU_ARCH="x86_64"
				;;
		esac
		QEMU="$BINDIR/qemu-system-$QEMU_ARCH"
	fi

	#determine QEMU parameters
	[ -n "$QEMU_NAME" ] && QEMU="$QEMU -name $QEMU_NAME"
	[ -n "$QEMU_MACHINE" ] && QEMU="$QEMU -machine $QEMU_MACHINE"
	[ -n "$QEMU_USER" ] && QEMU="$QEMU -run-with user=$QEMU_USER"
	[ -n "$QEMU_CHROOT" ] && QEMU="$QEMU -run-with chroot=$QEMU_CHROOT"
	[ $QEMU_CPUS -gt 1 ] && QEMU="$QEMU -smp $QEMU_CPUS"
	[ -n "$QEMU_MEMORY" ] && QEMU="$QEMU -m $QEMU_MEMORY"
	[ -n "$QEMU_EVENT_PANIC" -a "$QEMU_EVENT_PANIC" != "poweroff" ] &&
		QEMU="$QEMU -action panic=$QEMU_EVENT_PANIC"
	[ -n "$QEMU_EVENT_REBOOT" -a "$QEMU_EVENT_REBOOT" != "reboot" ] &&
		QEMU="$QEMU -action reboot=$QEMU_EVENT_REBOOT"
	[ -n "$QEMU_EVENT_SHUTDOWN" -a "$QEMU_EVENT_SHUTDOWN" != "poweroff" ] &&
		QEMU="$QEMU -action shutdown=$QEMU_EVENT_SHUTDOWN"

	#lock if necessary
	if [ -n "$QEMU_SOCKET" ]; then
		$DEBUG $MKDIR -p -m 0700 "${QEMU_SOCKET%/*/*}" &&
			$DEBUG $MKDIR -m 0700 "${QEMU_SOCKET%/*}"
		if [ $? -ne 0 ]; then
			_error "$QEMU_SOCKET: Could not create the socket"
			return $?
		fi
	fi

	#run QEMU
	$DEBUG $QEMU $QEMU_DEFAULT_ARGS $QEMU_ARGS
	ret=$?

	_qemu_cleanup

	return $ret
}

_qemu_set()
{
	context="$1"
	name="$2"
	value="$3"

	if [ -z "$context" ]; then
		case "$name" in
			arch)
				[ -z "$QEMU_ARCH" ] && QEMU_ARCH="$value"
				;;
			args)
				QEMU_ARGS="$QEMU_ARGS $value"
				;;
			chroot)
				[ -z "$QEMU_CHROOT" ] && QEMU_CHROOT="$value"
				;;
			cpus)
				QEMU_CPUS="$value"
				;;
			machine)
				QEMU_MACHINE="$value"
				;;
			memory)
				QEMU_MEMORY="$value"
				;;
			name)
				QEMU_NAME="$value"
				;;
			on_panic)
				QEMU_EVENT_PANIC="$value"
				;;
			on_reboot)
				QEMU_EVENT_REBOOT="$value"
				;;
			on_shutdown)
				QEMU_EVENT_SHUTDOWN="$value"
				;;
			user)
				[ -z "$QEMU_USER" ] && QEMU_USER="$value"
				;;
			"")
				#XXX ignore
				;;
			*)
				_error "$name: unsupported global parameter"
				return $?
				;;
		esac
	elif [ "${context%%::*}" = "drive" ]; then
		drive_id_new="${context#*::}"
		case "$name" in
			driver)
				drive_driver="$value"
				;;
			file)
				drive_file="$value"
				;;
			format)
				drive_format="$value"
				;;
			id)
				drive_id="$value"
				;;
			"")
				#XXX ignore
				;;
			*)
				_error "$name: unsupported parameter in [drive::$drive_id]"
				return $?
				;;
		esac
	elif [ "${context%%::*}" = "network" ]; then
		network_id_new="${context#*::}"
		case "$name" in
			bridge)
				network_bridge="$value"
				;;
			driver)
				network_driver="$value"
				;;
			id)
				network_id="$value"
				;;
			interface)
				network_interface="$value"
				;;
			mac)
				network_mac="$value"
				;;
			on_down)
				network_on_down="$value"
				;;
			on_up)
				network_on_up="$value"
				;;
			type)
				network_type="$value"
				;;
			"")
				#XXX ignore
				;;
			*)
				_error "$name: unsupported parameter in [network::$network_id]"
				return $?
				;;
		esac
	else
		_error "$context: unsupported section"
		return $?
	fi
	if [ "$oldcontext" != "$context" ]; then
		case "${oldcontext%%::*}" in
			drive)
				if [ -z "$drive_id_new" ]; then
					#XXX should never happen
					_error "ID not set for drive"
					return $?
				fi
				if [ -z "$drive_file" ]; then
					_error "file not set in [drive::$drive_id]"
					return $?
				fi
				[ -n "$drive_driver" ] || drive_driver="virtio-blk-pci"
				[ -n "$drive_format" ] || drive_format="raw"
				QEMU_ARGS="$QEMU_ARGS -drive file=$drive_file,if=none,format=$drive_format,id=$drive_id -device $drive_driver,drive=$drive_id"
				drive_driver=
				drive_file=
				drive_format=
				drive_id="$drive_id_new"
				drive_id_new=
				;;
			network)
				if [ -z "$network_id_new" ]; then
					#XXX should never happen
					_error "ID not set for network"
					return $?
				fi
				[ -n "$network_driver" ] || network_driver="virtio-net"
				if [ -z "$network_interface" ]; then
					network_interface="$(_qemu_network_interface)"
					[ $? -eq 0 ] || return 2
				fi
				[ -n "$network_mac" ] || network_mac="$(_qemu_network_macaddress "$hostname" "$network_id")"
				[ -n "$network_type" ] || network_type="tap"
				QEMU_ARGS="$QEMU_ARGS -netdev $network_type,id=$network_id,ifname=$network_interface,script=$network_on_up,downscript=$network_on_down -device $network_driver,netdev=$network_id,mac=$network_mac"
				#TODO report errors (here it is too late)
				[ -n "$network_bridge" ] && $DEBUG $BRCONFIG "$network_bridge" add "$network_interface"
				QEMU_NETWORK_INTERFACES="$QEMU_NETWORK_INTERFACES $network_interface"
				network_bridge=
				network_driver=
				network_id="$network_id_new"
				network_id_new=
				network_interface=
				network_mac=
				network_type=
				network_on_down=
				network_on_up=
				;;
		esac
	fi
	oldcontext="${context}"
}


#debug
_debug()
{
	[ $VERBOSE -ge 3 ] && echo "$@" 1>&2
	[ $DRYRUN -ne 0 ] || "$@"
}


#error
_error()
{
	echo "$PROGNAME: $@" 1>&2
	return 2
}


#info
_info()
{
	[ $VERBOSE -ge 2 ] && echo "$@" 1>&2
}


#usage
_usage()
{
	[ $# -ge 1 ] && _error "$@"
	echo "Usage: $PROGNAME [-nqv][-a arch][-u user] hostname [filename]" 1>&2
	return 1
}


#main
_edgebsd_qemu "$@"
