#!/bin/sh

# Copyright (C) 2014-2024 Daniel Baumann <daniel.baumann@open-infrastructure.net>
#
# SPDX-License-Identifier: GPL-3.0+
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

set -e

PROJECT="open-infrastructure"
SOFTWARE="compute-tools"
PROGRAM="container"
COMMAND="$(basename ${0})"

CONFIG="/etc/${SOFTWARE}/config"
HOOKS="/etc/${SOFTWARE}/hooks"
MACHINES="/var/lib/machines"

START="false"
SYSTEMCTL="true"

Parameters ()
{
	OPTIONS_ALL=""

	GETOPT_LONGOPTIONS="name:,force,nspawn,start,verbose,"
	GETOPT_OPTIONS="n:,f,v,"

	PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"

	if [ "${?}" != "0" ]
	then
		echo "'${COMMAND}': getopt exit" >&2
		exit 1
	fi

	eval set -- "${PARAMETERS}"

	while true
	do
		case "${1}" in
			-n|--name)
				NAME="${2}"
				shift 2
				;;

			-f|--force)
				FORCE="true"
				shift 1

				OPTIONS_ALL="${OPTIONS_ALL} --force"
				;;

			--nspawn)
				# internal option
				SYSTEMCTL="false"
				shift 1
				;;

			--start)
				# internal option
				START="true"
				SYSTEMCTL="false"
				shift 1
				;;

			-v|--verbose)
				VERBOSE="true"
				shift 1

				OPTIONS_ALL="${OPTIONS_ALL} --verbose"
				;;

			--)
				shift 1
				break
				;;

			*)
				echo "'${COMMAND}': getopt error" >&2
				exit 1
				;;
		esac
	done
}

Usage ()
{
	echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME [-f|--force]" >&2
	echo
	echo "See ${COMMAND}(1), ${PROGRAM}(1) and ${PROJECT}(7) for more information."

	exit 1
}

Parameters "${@}"

if [ -z "${NAME}" ]
then
	Usage
fi

# hooks
export NAME

case "${NAME}" in
	ALL)
		NAMES="$(${PROGRAM} list --format shell --stopped)"

		for NAME in ${NAMES}
		do
			${PROGRAM} start --name ${NAME} ${OPTIONS_ALL} || true
		done

		exit 0
		;;
esac

if [ ! -e "${MACHINES}/${NAME}" ]
then
	echo "'${NAME}': no such container" >&2
	exit 1
fi

# options
if grep -Eqs "^ *cnt.start=" "${CONFIG}/${NAME}.conf" | grep -qs force
then
	FORCE="true"
fi

case "${START}" in
	false)
		STATE="$(machinectl show ${NAME} 2>&1 | awk -FState= '/^State=/ { print $2 }')"

		case "${STATE}" in
			running)
				echo "'${NAME}': container is already started" >&2
				exit 1
				;;
		esac
		;;
esac

if [ -e "${MACHINES}/.#${NAME}.lck" ]
then
	case "${FORCE}" in
		true)
			rm -f "${MACHINES}/.#${NAME}.lck"

			VETHS="$(awk -Fnetwork-veth-extra= '/^network-veth-extra=/ { print $2 }' ${CONFIG}/${NAME}.conf | awk -F: '{ print $1 }')"

			for VETH in ${VETHS}
			do
				ip link delete ${VETH} > /dev/null 2>&1 || true
			done
			;;

		*)
			echo "'${NAME}': container is locked" >&2
			exit 1
			;;
	esac
fi

HOST_ARCHITECTURE="$(dpkg --print-architecture)"
MACHINE_ARCHITECTURE="$(chroot ${MACHINES}/${NAME} dpkg --print-architecture)"

case "${HOST_ARCHITECTURE}" in
	amd64)
		case "${MACHINE_ARCHITECTURE}" in
			i386)
				SETARCH="setarch i686"
				;;

			*)
				SETARCH=""
				;;
		esac
		;;
esac

if systemctl status systemd-networkd > /dev/null 2>&1
then
	NETWORK_SUBSYSTEM="systemd-networkd"
else
	NETWORK_SUBSYSTEM="ifupdown"
fi

case "${START}" in
	start)
		;;

	*)
		# Pre hooks
		for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
		do
			if [ -x "${FILE}" ]
			then
				"${FILE}"
			fi
		done
		;;
esac

# config
if [ -e "${CONFIG}/${NAME}.conf" ]
then
	CNT_OVERLAY="$(awk -Fcnt.overlay= '/^cnt.overlay=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
	CNT_OVERLAY_OPTIONS="$(awk -Fcnt.overlay-options= '/^cnt.overlay-options=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${CNT_OVERLAY}" ]
	then
		CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')"

		COUNT="0"
		for CNT_OVERLAY in ${CNT_OVERLAYS}
		do
			DIRECTORY_LOWER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $1 }')"
			DIRECTORY_UPPER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $2 }')"
			DIRECTORY_WORK="$(echo ${CNT_OVERLAY} | awk -F: '{ print $3 }')"
			DIRECTORY_MERGED="$(echo ${CNT_OVERLAY} | awk -F: '{ print $4 }')"

			COUNT="$((${COUNT} + 1))"
			CNT_OVERLAY_OPTION="$(echo ${CNT_OVERLAY_OPTIONS} | awk -F ';' "{ print \$${COUNT} }")"

			for DIRECTORY in "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}"
			do
				mkdir -p "${DIRECTORY}"
			done

			if ! findmnt -n -o SOURCE "${DIRECTORY_MERGED}" | grep -qs '^cnt.overlay-'
			then
				if [ -n "${CNT_OVERLAY_OPTION}" ]
				then
					CNT_OVERLAY_OPTION="-o ${CNT_OVERLAY_OPTION}"
				fi

				mount cnt.overlay-${NAME} -t overlay ${CNT_OVERLAY_OPTION} -olowerdir="${DIRECTORY_LOWER}",upperdir="${DIRECTORY_UPPER}",workdir="${DIRECTORY_WORK}",default_permissions "${DIRECTORY_MERGED}"
			fi
		done
	fi

	BIND="$(awk -Fbind= '/^bind=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${BIND}" ]
	then
		BINDS="$(echo ${BIND} | sed -e 's|;| |g')"

		for BIND in ${BINDS}
		do
			DIRECTORY="$(echo ${BIND} | awk -F: '{ print $1 }')"

			if [ ! -e "${DIRECTORY}" ]
			then
				echo "'${DIRECTORY}': creating non-existing directory for bind mounting"
				mkdir -p "${DIRECTORY}"
			fi
		done

		BIND=""

		for DIRECTORIES in ${BINDS}
		do
			BIND="${BIND} --bind ${DIRECTORIES}"
		done
	fi

	BIND_RO="$(awk -Fbind-ro= '/^bind-ro=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${BIND_RO}" ]
	then
		BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')"

		for BIND_RO in ${BINDS_RO}
		do
			DIRECTORY="$(echo ${BIND_RO} | awk -F: '{ print $1 }')"

			if [ ! -e "${DIRECTORY}" ]
			then
				echo "'${DIRECTORY}': creating non-existing directory for bind-ro mounting"
				mkdir -p "${DIRECTORY}"
			fi
		done

		BIND_RO=""

		for DIRECTORIES in ${BINDS_RO}
		do
			BIND_RO="${BIND_RO} --bind-ro ${DIRECTORIES}"
		done
	fi

	BOOT="$(awk -Fboot= '/^boot=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo yes)"

	case "${BOOT}" in
		yes)
			BOOT="--boot"
			;;

		*)
			BOOT=""
			;;
	esac

	CAPABILITY="$(awk -Fcapability= '/^capability=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	case "${CAPABILITY}" in
		"")
			CAPABILITY=""
			;;

		*)
			CAPABILITY="--capability=${CAPABILITY}"
			;;
	esac

	DIRECTORY="$(awk -Fdirectory= '/^directory=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo ${MACHINES}/${NAMES})"
	DIRECTORY="--directory ${DIRECTORY}"

	DROP_CAPABILITY="$(awk -Fdrop-capability= '/^drop-capability=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	case "${DROP_CAPABILITY}" in
		"")
			DROP_CAPABILITY=""
			;;

		*)
			DROP_CAPABILITY="--drop-capability=${DROP_CAPABILITY}"
			;;
	esac

	LINK_JOURNAL="$(awk -Flink-journal= '/^link-journal=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo no)"

	case "${LINK_JOURNAL}" in
		yes)
			LINK_JOURNAL="--link-journal=yes"
			;;

		*)
			LINK_JOURNAL="--link-journal=no"
			;;
	esac

	MACHINE="--machine=${NAME}"

	NETWORK_VETH_EXTRA=""

	VETHS="$(awk -Fnetwork-veth-extra= '/^network-veth-extra=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	case "${VETHS}" in
		"")
			;;

		*)
			for VETH in ${VETHS}
			do
				NETWORK_VETH_EXTRA="${NETWORK_VETH_EXTRA} --network-veth-extra=${VETH}"
				INTERFACE="$(echo ${VETH} | awk -F: '{ print $1 }')"

				if [ "$(echo ${INTERFACE} | wc -c)" -gt 16 ]
				then
					echo "'${INTERFACE}': name exceeds maximum of 15 characters, network might be not working."
				fi
			done
			;;
	esac

	NETWORK_BRIDGES="$(awk -Fcnt.network-bridge= '/^cnt.network-bridge=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	case "${NETWORK_BRIDGES}" in
		"")
			;;

		*)
			for BRIDGE_DEFINITION in ${NETWORK_BRIDGES}
			do
				INTERFACE="$(echo ${BRIDGE_DEFINITION} | awk -F: '{ print $1 }')"
				BRIDGE="$(echo ${BRIDGE_DEFINITION} | awk -F: '{ print $2 }')"

				if [ "$(echo ${INTERFACE} | wc -c)" -gt 16 ]
				then
					echo "'${INTERFACE}': name exceeds maximum of 15 characters, network might be not working."
				fi

				if [ -n "${BRIDGE}" ] && [ -n "${INTERFACE}" ]
				then

					case "${NETWORK_SUBSYSTEM}" in
						ifupdown)

cat > "/etc/network/interfaces.d/${INTERFACE}" << EOF
allow-hotplug ${INTERFACE}
iface ${INTERFACE} inet manual
	pre-up ip link set ${INTERFACE} up
	post-up ip link set ${INTERFACE} master ${BRIDGE}
	pre-down ip link set ${INTERFACE} nomaster
	post-down ip link set ${INTERFACE} down
EOF

							;;

						systemd-networkd)
							mkdir -p /run/systemd/network

cat > "/run/systemd/network/${INTERFACE}.network" << EOF
[Match]
Name=${INTERFACE}

[Network]
Bridge=${BRIDGE}
EOF

							networkctl reload || true
							;;
					esac
				else
					echo "Warning bridge definition '${BRIDGE_DEFINITION}' not recognized (expected <bridge>:<interface>): Ignoring"
				fi
			done
			;;
	esac

	PRIVATE_USERS="$(awk -Fprivate-users= '/^private-users=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo no)"

	case "${PRIVATE_USERS}" in
		yes)
			PRIVATE_USERS="--private-users=yes"
			;;

		*)
			PRIVATE_USERS="--private-users=no"
			;;
	esac

	REGISTER="$(awk -Fregister= '/^register=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo yes)"

	case "${REGISTER}" in
		yes)
			REGISTER="--register=yes"
			;;

		*)
			REGISTER="--register=no"
			;;
	esac

	BLOCK_IO_DEVICE_WEIGHT="$(awk -FBlockIODeviceWeight= '/^BlockIODeviceWeight=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${BLOCK_IO_DEVICE_WEIGHT}" ]
	then
		BLOCK_IO_DEVICE_WEIGHT="BlockIODeviceWeight=${BLOCK_IO_DEVICE_WEIGHT}"
		SET_PROPERTY="true"
	fi

	BLOCK_IO_READ_BANDWIDTH="$(awk -FBlockIOReadBandwidth= '/^BlockIOReadBandwidth=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${BLOCK_IO_READ_BANDWIDTH}" ]
	then
		BLOCK_IO_READ_BANDWIDTH="BlockIOReadBandwidth=${BLOCK_IO_READ_BANDWIDTH}"
		SET_PROPERTY="true"
	fi

	BLOCK_IO_WEIGHT="$(awk -FBlockIOWeight= '/^BlockIOWeight=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${BLOCK_IO_WEIGHT}" ]
	then
		BLOCK_IO_WEIGHT="BlockIOWeight=${BLOCK_IO_WEIGHT}"
		SET_PROPERTY="true"
	fi

	BLOCK_IO_WRITE_BANDWIDTH="$(awk -FBlockIOWriteBandwidth=/= '/^BlockIOWriteBandwidth=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${BLOCK_IO_WRITE_BANDWIDTH}" ]
	then
		BLOCK_IO_WRITE_BANDWIDTH="BlockIOWriteBandwidth=${BLOCK_IO_WRITE_BANDWIDTH}"
		SET_PROPERTY="true"
	fi

	CPU_QUOTA="$(awk -FCPUQuota= '/^CPUQuota=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${CPU_QUOTA}" ]
	then
		CPU_QUOTA="CPUQuota=${CPU_QUOTA}"
		SET_PROPERTY="true"
	fi

	CPU_SHARES="$(awk -FCPUShares= '/^CPUShares=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${CPU_SHARES}" ]
	then
		CPU_SHARES="CPUShares=${CPU_SHARES}"
		SET_PROPERTY="true"
	fi

	MEMORY_LIMIT="$(awk -FMemoryLimit= '/^MemoryLimit=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${MEMORY_LIMIT}" ]
	then
		MEMORY_LIMIT="MemoryLimit=${MEMORY_LIMIT}"
		SET_PROPERTY="true"
	fi

	TASKS_MAX="$(awk -FTasksMax= '/^TasksMax=/ { print $2 }' ${CONFIG}/${NAME}.conf)"

	if [ -n "${TASKS_MAX}" ]
	then
		TASKS_MAX="TasksMax=${TASKS_MAX}"
		SET_PROPERTY="true"
	fi
fi

case "${SYSTEMCTL}" in
	true)
		systemctl start ${PROGRAM}@${NAME}.service

		# Post hooks
		for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
		do
			if [ -x "${FILE}" ]
			then
				"${FILE}"
			fi
		done

		exit 0
		;;
esac

case "${START}" in
	true)
		case "${SET_PROPERTY}" in
			true)
				systemctl --runtime set-property ${NAME} ${BLOCK_IO_DEVICE_WEIGHT} ${BLOCK_IO_READ_BANDWIDTH} ${BLOCK_IO_WEIGHT} ${BLOCK_IO_WRITE_BANDWIDTH} ${CPU_QUOTA} ${CPU_SHARES} ${MEMORY_LIMIT} ${TASKS_MAX}
				;;
		esac
		;;

	*)
		# Run

		case "${VERBOSE}" in
			true)
				echo -n "Starting container ${NAME}..."
				;;
		esac

		mkdir -p "/var/lib/${SOFTWARE}/state"
		echo "start" > "/var/lib/${SOFTWARE}/state/${NAME}.run"

		${SETARCH} systemd-nspawn --keep-unit ${BIND} ${BIND_RO} ${BOOT} ${CAPABILITY} ${DIRECTORY} ${DROP_CAPABILITY} ${MACHINE} ${NETWORK_VETH_EXTRA} ${LINK_JOURNAL} ${REGISTER}

		case "${VERBOSE}" in
			true)
				echo " done."
				;;
		esac
		;;
esac