#!/bin/sh
#Copyright (c) 2021-2025 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="EdgeBSD"
PACKAGE="edgebsd-nvmm"
#executables
CAT="cat"
DEBUG="_debug"
EDGEBSD_QEMU="/usr/pkg/libexec/$VENDOR/$PACKAGE/edgebsd-qemu"
MKDIR="mkdir"
NC="nc"
NVMMCTL="/usr/sbin/nvmmctl"
SCREEN="/usr/pkg/bin/screen"
TR="tr"
#settings
CHROOTDIR=
DEVNULL="/dev/null"
DRYRUN=0
PROGNAME="nvmm-xl"
RUNAS=
RUNDIR="/var/run/nvmm"
SCREENDIR="/$HOME/.screen"
SCREENDIR2="/tmp/screens/S-$USER"
SYSCONFDIR="/usr/pkg/etc"
USER="$(id -un)"
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
#debug
_debug()
{
	[ $VERBOSE -ge 3 ] && echo "$@" 1>&2
	[ $DRYRUN -ne 0 ] || "$@"
}


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


#nvmm_xl
_nvmm_xl()
{
	shift $((OPTIND - 1))
	if [ $# -eq 0 ]; then
		_usage
		return $?
	fi
	command="$1"
	shift

	case "$command" in
		"cd-eject" \
		|"console" \
		|"create" \
		|"destroy" \
		|"info" \
		|"list" \
		|"monitor" \
		|"network" \
		|"pause" \
		|"reboot" \
		|"shutdown" \
		|"unpause")
			command=$(echo "$command" | $TR -- - _)

			"_nvmm_xl_$command" "$@"
			return $?
			;;
		*)
			_usage "$command: Unknown command"
			return $?
			;;
	esac
}


#nvmm_xl_cd_eject
_nvmm_xl_cd_eject()
{
	command_args=

	while getopts "fO:qv" name; do
		case "$name" in
			f)
				command_args="$command_args -f"
				;;
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 2 ]; then
		_usage "cd-eject: A domain and a device are required"
		return $?
	fi
	domain="$1"
	device="$2"

	 _nvmm_xl_command "$domain" "eject$command_args $device"
}


#nvmm_xl_command
_nvmm_xl_command()
{(
	if [ $# -lt 2 ]; then
		_usage "command: A domain and a command are required"
		return $?
	fi
	domain="$1"
	socket="$RUNDIR/$domain/socket"
	shift

	$DEBUG echo "$@" | $DEBUG $NC -N -U "$socket" > "$DEVNULL"
)}


#nvmm_xl_command_output
_nvmm_xl_command_output()
{(
	if [ $# -lt 2 ]; then
		_usage "command: A domain and a command are required"
		return $?
	fi
	domain="$1"
	socket="$RUNDIR/$domain/socket"
	shift

	$DEBUG echo "$@" | $DEBUG $NC -N -U "$socket" | (cnt=0
		prevline=
		while read line; do
			cnt=$((cnt + 1))
			[ $cnt -gt 2 ] && echo "$line"
		done)
)}


#nvmm_xl_command_sync
_nvmm_xl_command_sync()
{(
	if [ $# -lt 2 ]; then
		_usage "command: A domain and a command are required"
		return $?
	fi
	domain="$1"
	socket="$RUNDIR/$domain/socket"
	shift

	$DEBUG echo "$@" | $DEBUG $NC -U "$socket" > "$DEVNULL"
)}


#nvmm_xl_console
_nvmm_xl_console()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "console: A domain is required"
		return $?
	fi
	domain="$1"
	session="_nvmm-$domain"

	for s in "$SCREENDIR/"*"._nvmm-"* "$SCREENDIR2/"*"._nvmm-"*; do
		[ -S "$s" ] || continue
		if [ "${s#$SCREENDIR/*.}" = "$session" \
				-o "${s#$SCREENDIR2/*.}" = "$session" ]; then
			$DEBUG $SCREEN -x "$session"
			return $?
		fi
	done
	_error "$domain: Domain not available"
	return $?1
}


#nvmm_xl_create
_nvmm_xl_create()
{
	console=0
	while getopts "cO:qv" name; do
		case "$name" in
			c)
				console=1
				;;
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "create: A configuration file is required"
		return $?
	fi
	filename="$1"

	if [ ! -f "$filename" ]; then
		_error "$filename: Domain not configured"
		return $?
	fi

	domain="${filename##*/}"
	section=
	#variables
	edgebsd_qemu_args=
	[ -n "$CHROOTDIR" ] && edgebsd_qemu_args="$edgebsd_qemu_args -c $CHROOTDIR"
	[ -n "$RUNAS" ] && edgebsd_qemu_args="$edgebsd_qemu_args -u $RUNAS"

	session="_nvmm-$domain"
	screen_args=
	[ $console -eq 0 ] && screen_args="-d -m"

	$DEBUG $SCREEN -S "$session" $screen_args \
		$EDGEBSD_QEMU $edgebsd_qemu_args "$domain" "$filename"
}


#nvmm_xl_destroy
_nvmm_xl_destroy()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "destroy: A domain is required"
		return $?
	fi
	domain="$1"

	_nvmm_xl_command_sync "$domain" "quit"
}


#nvmm_xl_info
_nvmm_xl_info()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -lt 1 ]; then
		_usage "info: A domain must be provided"
		return $?
	elif [ $# -gt 2 ]; then
		_usage "info: A sub-command may be provided"
		return $?
	fi
	domain="$1"
	subcommand="$2"

	_nvmm_xl_command_output "$domain" "info $subcommand"
}


#nvmm_xl_list
_nvmm_xl_list()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 0 ]; then
		_usage "list: Parameters are not supported"
		return $?
	fi

	#XXX output more information about each domain
	echo "Name"
	echo "NVMM"
	for domain in "$SCREENDIR/"*"._nvmm-"* "$SCREENDIR2/"*"._nvmm-"*; do
		[ -S "$domain" ] || continue
		echo "${domain##*._nvmm-}"
	done
}


#nvmm_xl_monitor
_nvmm_xl_monitor()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "monitor: A domain is required"
		return $?
	fi
	domain="$1"
	socket="$RUNDIR/$domain/socket"

	$DEBUG $NC -U "$socket"
}


#nvmm_xl_network
_nvmm_xl_network()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -lt 3 ]; then
		_usage "network: A domain, an interface, a sub-command are required"
		return $?
	fi
	domain="$1"
	interface="$2"
	subcommand="$3"

	#XXX provide this help more easily
	case "$subcommand" in
		"connect")
			subcommand="set_link $interface on"
			;;
		"disconnect")
			subcommand="set_link $interface off"
			;;
		"help"|*)
			_usage "network: $subcommand: Unsupported sub-command
Sub-commands supported:
  connect
  disconnect
"
			;;
	esac
	_nvmm_xl_command "$domain" "$subcommand"
}


#nvmm_xl_pause
_nvmm_xl_pause()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "pause: A domain is required"
		return $?
	fi
	domain="$1"

	_nvmm_xl_command "$domain" "stop"
}


#nvmm_xl_reboot
_nvmm_xl_reboot()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "reboot: A domain is required"
		return $?
	fi
	domain="$1"

	_nvmm_xl_command "$domain" "system_reset"
}


#nvmm_xl_shutdown
_nvmm_xl_shutdown()
{
	fallback=0
	while getopts "FO:qv" name; do
		case "$name" in
			F)
				#XXX implement
				fallback=1
				;;
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "shutdown: A domain is required"
		return $?
	fi
	domain="$1"

	_nvmm_xl_command_sync "$domain" "system_powerdown"
}


#nvmm_xl_unpause
_nvmm_xl_unpause()
{
	while getopts "O:qv" name; do
		case "$name" in
			O)
				eval "${OPTARG%%=*}"="${OPTARG#*=}"
				;;
			q)
				VERBOSE=0
				;;
			v)
				VERBOSE=$((VERBOSE + 1))
				;;
			*)
				_usage
				return $?
				;;
		esac
	done
	shift $((OPTIND - 1))
	if [ $# -ne 1 ]; then
		_usage "unpause: A domain is required"
		return $?
	fi
	domain="$1"

	_nvmm_xl_command "$domain" "cont"
}


#usage
_usage()
{
	[ $# -ge 1 ] && echo "$PROGNAME: $@" 1>&2
	echo "Usage: $PROGNAME command [options...]" 1>&2
	echo 1>&2
	echo "Commands supported:" 1>&2
	echo "  cd-eject	Eject a CD from a removable device" 1>&2
	echo "  console	Attach the console of a Virtual Machine instance" 1>&2
	echo "  create [-c]	Start a Virtual Machine instance" 1>&2
	echo "  destroy	Power off a Virtual Machine instance" 1>&2
	echo "  info		Print information about a Virtual Machine instance" 1>&2
	echo "  list		List the Virtual Machine instances available" 1>&2
	echo "  monitor	Attach the monitor of a Virtual Machine instance" 1>&2
	echo "  network	Manage the network interfaces of a Virtual Machine instance" 1>&2
	echo "  pause	Pause a Virtual Machine instance" 1>&2
	echo "  reboot	Reboot a Virtual Machine instance" 1>&2
	echo "  shutdown [-F]	Stop a Virtual Machine instance gracefully" 1>&2
	echo "  unpause	Unpause a Virtual Machine instance" 1>&2
	return 1
}


#main
_nvmm_xl "$@"
