#! /bin/bash
#########################################################################
#									#
#	lqvms-backups is automatically generated,			#
#		please do not modify!					#
#									#
#########################################################################

#########################################################################
#									#
# Author: Copyright (C) 2021-2025  Mark Grant				#
#									#
# Released under the GPLv3 only.					#
# SPDX-License-Identifier: GPL-3.0-only					#
#									#
# Purpose:								#
# A script to start VMs in a user-supplied list and perform either	#
# daily or weekly backups.						#
#                                                            	  	#
# Syntax:      See usage()						#
#									#
# Exit codes used:-							#
# Bash standard Exit Codes:	0 - success				#
#				1 - general failure			#
# User-defined exit code range is 64 - 113				#
#	C/C++ Semi-standard exit codes from sysexits.h range is 64 - 78	#
#		EX_USAGE	64	command line usage error	#
#		EX_DATAERR	65	data format error		#
#		EX_NOINPUT	66	cannot open input		#
#		EX_NOUSER	67	addressee unknown		#
#		EX_NOHOST	68	host name unknown		#
#		EX_UNAVAILABLE	69	service unavailable		#
#		EX_SOFTWARE	70	internal software error		#
#		EX_OSERR	71	system error (e.g., can't fork)	#
#		EX_OSFILE	72	critical OS file missing	#
#		EX_CANTCREAT	73	can't create (user) output file	#
#		EX_IOERR	74	input/output error		#
#		EX_TEMPFAIL	75	temp failure; user is invited	#
#					to retry			#
#		EX_PROTOCOL	76	remote error in protocol	#
#		EX_NOPERM	77	permission denied		#
#		EX_CONFIG	78	configuration error		#
#	User-defined (here) exit codes range 79 - 113:			#
#		None							#
#									#
# Further Info:								#
#									#
#########################################################################


set -uo pipefail


##################
# Init variables #
##################
readonly version=1.2.2				# Script version
readonly packageversion=1.3.4		# Package version

readonly etclocation=/etc		# Path to config directory

declare -i maxconcbackups=1
declare -a passwords
script_name=""
declare -i status=0
declare -a usernames
declare vms


#############
# Functions #
#############

# -h --help output.
# No parameters
# No return value
usage()
{
cat << EOF
Usage is:-
$(basename "$0") {-h|-V}
$(basename "$0") [-b MAXCONCBACKUPS] -s SCRIPT_NAME [-u CONNECTION] VM_NAME ...
Usage is:-
$(basename "$0") [options]
	-b or --maxconcbackups Maximum number of concurrent backups to run
	-h or --help Display this help information
	-s or --script-name Script to be run
	-u or --connect-uri Use the named connection URI
	-V or --version Print script version information
EOF
}

# Standard function to emit messages depending on various parameters.
# Parameters -	$1 What:-	The message to emit.
#		$2 Where:-	stdout == 0
#				stderr == 1
# No return value.
output()
{
	if (( !$2 )); then
		printf "%s\n" "$1"
	else
		printf "%s\n" "$1" 1>&2
	fi
}

# Standard function to tidy up and return exit code.
# Parameters - 	$1 is the exit code.
# No return value.
script_exit()
{
	exit "$1"
}

# Standard function to test command error and exit if non-zero.
# Parameters - 	$1 is the exit code, (normally $? from the preceeding command).
# No return value.
std_cmd_err_handler()
{
	if (( $1 )); then
		script_exit "$1"
	fi
}

# Standard trap exit function.
# No parameters.
# No return value.
# shellcheck disable=SC2317  # Do not warn about unreachable commands in trap
# functions, they are legitimate.
trap_exit()
{
	local -i exit_code=$?
	local msg

	msg="Script terminating with exit code $exit_code due to trap received."
	output "$msg" 1
	script_exit "$exit_code"
}

# Setup trap
trap trap_exit SIGHUP SIGINT SIGQUIT SIGTERM

# Process the config file just for the parameters of interest.
# Parameters - None
# No return value.
proc_config_file()
{
	local input=()
	local oldIFS=$IFS

	IFS="="

	exec 3<"$etclocation/lqvm.conf"
	std_cmd_err_handler $?
	while read -u3 -ra input; do
		std_cmd_err_handler $?
		case ${input[0]-} in
		connecturi)
			connect_uri=${input[1]}
			;;
		maxconcbackups)
			if (( input[1] <= 0 )); then
				msg="maxconcbackups in config file must be"
				msg+=" greater than zero, defaulting to"
				msg+=" $maxconcbackups"
				output "$msg" 1
			else
				maxconcbackups=${input[1]}
			fi
			;;
		esac
	done
	exec 3<&-

	IFS=$oldIFS
}

# Process command line arguments with GNU getopt.
# Parameters -	$1 is the command line.
# No return value.
proc_CL()
{
	local GETOPTTEMP
	local msg
	local tmp

	tmp="getopt -o b:hs:u:V --long maxconcbackups:,help,script-name:"
	tmp+=",connect-uri:,version"
	GETOPTTEMP=$($tmp -n "$0" -- "$@")
	std_cmd_err_handler $?

	eval set -- "$GETOPTTEMP"
	std_cmd_err_handler $?

	while true; do
		case "$1" in
		-b|--maxconcbackups)
			maxconcbackups=$2
			shift 2
			;;
		-h|--help)
			usage
			shift
			script_exit 0
			;;
		-s|--script-name)
			script_name=$2
			shift 2
			;;
		-u|--connect-uri)
			connect_uri=$2
			shift 2
			;;
		-V|--version)
			printf "%s Script version %s\n" "$0" $version
			printf "%s Package version %s\n" "$0" $packageversion
			shift
			script_exit 0
			;;
		--)	shift
			break
			;;
		*)	output "Internal error." 1
			script_exit 64
			;;
		esac
	done

	# One option has to be selected.
	if [[ $script_name == "" ]]; then
		msg="Script name must be specified"
		output "$msg" 1
		script_exit 64
	fi

	if (( maxconcbackups <= 0 )); then
		msg="maxconcbackups must be greater than zero"
		output "$msg" 1
		script_exit 64
	fi

	# Remaining arguments are VM names
	vms=$*
}

# Validate the VM name.
# Parameters	1 - VM name
# Return value - 0 on success, 77 on error.
validate_vm_name()
{
	local -i name_matches

	name_matches=$(virsh -c "$connect_uri" list --all \
		| { grep "\<$1\>" || test $?=1; } | wc -l)
	std_cmd_err_handler $?
	if (( ! name_matches )); then
		output "No such VM $1" 1
		return 77
	else
		return 0
	fi
}

# Validate VM names.
# Parameters - None.
# No return value.
validate_vm_names()
{
	local ok=true

	for vm in $vms; do
		if ! validate_vm_name "$vm"; then
			ok=false
		fi
	done
	if ! $ok; then
		script_exit 77
	fi
}

# Determine if the VM is running.
# Parameters	1 - VM.
#		2 - Variable to receive the result.
# Return value - true or false in the variable specified above.
is_vm_running()
{
	local __result=$2
	local tmp_result
	local -i vm_running_matches

	vm_running_matches=$(virsh -c "$connect_uri" list \
		| { grep "\<$1\>" || test $?=1; } | wc -l)
	std_cmd_err_handler $?
	if (( vm_running_matches )); then
		tmp_result="true"
	else
		tmp_result="false"
	fi
	# shellcheck disable=SC2086 # $__result is OK without ""
	eval $__result="'$tmp_result'"
}

# Ensure relevant VMs are not running.
# Parameters - None.
# No return value.
ensure_vms_not_running()
{
	local status
	local vm

	for vm in $vms; do
		is_vm_running "$vm" status
		if $status; then
			output "Please shutdown all relevant VMs." 1
			script_exit 77
		fi
	done
}

# Start VMs.
# Parameters - None.
# No return value.
start_vms()
{
	local vm

	for vm in $vms; do
		/usr/bin/lqvm-start -w -u "$connect_uri" "$vm"
		std_cmd_err_handler $?
		sleep 90
	done
}

# Iterate over VM names getting sudo username and password and validating them.
# Parameters - None.
# No return value.
get_usernames_and_passwords()
{
	local -i count=0
	local msg
	local ok
	local password
	local username
	local vm

	for vm in $vms; do
		ok=false
		while ! $ok; do
			echo "Enter sudo username for $vm:"
			read -r username
			usernames[count]="$username"
			echo "Enter sudo password for $vm:"
			read -rs password
			passwords[count]="$password"
			ssh -o NumberOfPasswordPrompts=0 \
				-t "${usernames[$count]}""@$vm" \
				'echo '"${passwords[$count]}" \
				'| sudo -S bash -c "ls > /dev/null"'
			# shellcheck disable=SC2181 # Above cmd too long for if.
			if (( $? )); then
				msg="Login test failed for "
				# Words of the form $'string' are treated
				# specially. The word expands to "string",
				# with backslash-escaped characters replaced
				# as specified by the ANSI C standard.
				msg+="${usernames[$count]} on $vm."$'\n'
				output "$msg" 1
			else
				msg="Login test passed for "
				msg+="${usernames[$count]} on $vm."$'\n'
				output "$msg" 0
				(( count+=1 ))
				ok=true
			fi
		done
	done
}

# Iterate over VMs performing backup.
# Parameters - None.
# No return value.
perform_vms_backups()
{
	local backups_WIP=()
	local -i count=0
	local -i i
	local -i status
	local status_string
	local vm

	for vm in $vms; do
		ssh -t "${usernames[$count]}@$vm" \
			"echo ${passwords[$count]} | sudo -S bash backup-at-scripts-launcher.sh -t \"now\" \"$script_name\""
		status=$?
		if (( status )); then
			output "Backup failed for $vm."$'\n' 1
		else
			output "Backup started for $vm."$'\n' 0
		fi

		(( count+=1 ))
		backups_WIP+=("$vm")

		# Wait for spare capacity in number of concurrent backups
		# allowed before attempting to start the next backup.
		while (( ${#backups_WIP[@]} >= maxconcbackups )); do
			sleep 45
			for i in "${!backups_WIP[@]}"; do
				is_vm_running "${backups_WIP[i]}" status_string
				if ! $status_string; then
					unset "backups_WIP[i]"
				fi
			done
		done
	done
	# Backups to start is exhausted, wait for remaining VMs to shutdown
	while (( ${#backups_WIP[@]} )); do
		sleep 45
		for i in "${!backups_WIP[@]}"; do
			is_vm_running "${backups_WIP[i]}" status_string
			if ! $status_string; then
				unset "backups_WIP[i]"
			fi
		done
	done
}


########
# Main #
########

proc_config_file

proc_CL "$@"

validate_vm_names

ensure_vms_not_running

start_vms

get_usernames_and_passwords

perform_vms_backups

# And exit.
script_exit 0

