# Tiny Cloud - Init Functions
# vim:set filetype=sh:
# shellcheck shell=sh

# set defaults
: "${PREFIX:=/usr}"
: "${LIBDIR:=$PREFIX/lib}"
. "$LIBDIR/tiny-cloud/common"

: "${SKIP_INIT_ACTIONS:=}"

### default phase actions (without leading 'init__')

DEFAULT_ACTIONS_BOOT="
	expand_root
	set_ephemeral_network
	set_default_interfaces
	enable_sshd
"
DEFAULT_ACTIONS_EARLY="
	save_userdata
"
DEFAULT_ACTIONS_MAIN="
	create_default_user
	set_hostname
	set_ssh_keys
"
DEFAULT_ACTIONS_FINAL=""

: "${INIT_ACTIONS_BOOT=$DEFAULT_ACTIONS_BOOT}"
: "${INIT_ACTIONS_EARLY=$DEFAULT_ACTIONS_EARLY}"
: "${INIT_ACTIONS_MAIN=$DEFAULT_ACTIONS_MAIN}"
: "${INIT_ACTIONS_FINAL=$DEFAULT_ACTIONS_FINAL}"

### standard boot phase functions...

init__expand_root() {
	local dev=$(awk '$2 == "/" {print $1}' "$PROC"/mounts 2>/dev/null)
	local filesystem=$(awk '$2 == "/" {print $3}' "$PROC"/mounts 2>/dev/null)
	local partition=$(cat "$SYS/class/block/${dev#/dev/}/partition" 2>/dev/null)

	# only support ext2/ext3/ext4 for now
	case "$filesystem" in
		ext*) ;;
		*) return;;
	esac

	if [ -n "$partition" ]; then
		# it's a partition, resize it
		local volume=$(readlink -f "$SYS/class/block/${dev#/dev/}/..")
		volume="/dev/${volume##*/}"
		echo ", +" | $MOCK sfdisk -q --no-reread -N "$partition" "$volume"
		$MOCK partx -u "$volume"
	fi
	# resize filesystem
	if [ -e "$dev" ] || [ -n "$MOCK" ]; then
		$MOCK resize2fs "$dev"
	fi
}

# collect ethernet interfaces, sorted by index
ethernets() {
	for i in "$SYS/class/net/"*; do
		local iface="${i##*/}"
		case "$iface" in
			eth*) echo "$(cat "$i/ifindex") $iface";;
		esac
	done | sort -n | awk '{print $2}'
}

# find the interface that is has operstate up
find_first_interface_up() {
	local n=0
	[ $# -eq 0 ] && return
	while [ $n -le ${TINY_CLOUD_LINK_WAIT_MAX:-10} ]; do
		for i in "$@"; do
			if [ "$(cat "$SYS/class/net/$i/operstate")" = "up" ]; then
				echo "$i"
				return
			fi
		done
		sleep 0.1
		n=$((n+1))
	done
}

# auto detect which network interface to auto configure
# check which is connected or fallback to first
# This will set link to down to all eth* except the found
auto_detect_ethernet_interface() {
	local ifaces="$(ethernets)"
	[ -z "$ifaces" ] && return

	# find first connected interface
	for i in $ifaces; do
		$MOCK ip link set dev $i up >/dev/null
	done
	local iface="$(find_first_interface_up $ifaces)"

	# use first if all are disconnected
	if [ -z "$iface" ]; then
		set -- $ifaces
		iface="$1"
	fi

	# we will use the found interface later so lets keep it up
	for i in $ifaces; do
		if [ "$i" != "$iface" ]; then
			$MOCK ip link set dev $i down >/dev/null
		fi
	done
	echo "$iface"
}

# may be overridded by provider
want_ephemeral_network() {
	false
}

init__set_ephemeral_network() {
	if ! want_ephemeral_network; then
		return
	fi
	local iface="$(auto_detect_ethernet_interface)"
	if [ -z "$iface" ]; then
		return
	fi
	$MOCK udhcpc -i "$iface" -f -q
}

init__set_default_interfaces() {
	if [ -f "$ETC"/network/interfaces ]; then
		log -i -t "$phase" info "$ACTION: already set up"
		return
	fi

	mkdir -p "$ETC/network"
	printf "%s\n%s\n\n" \
		"auto lo" \
		"iface lo inet loopback" \
		> "$ETC/network/interfaces"

	local iface="$(auto_detect_ethernet_interface)"
	if [ -z "$iface" ]; then
		# TODO: message/log?
		return
	fi
	printf "%s\n%s\n\t%s\n\n" \
		"auto $iface" \
		"iface $iface" \
		"use dhcp"  >> "$ETC/network/interfaces"
}

init__create_default_user() {
	local user="$CLOUD_USER"
	if [ "$user" = "none" ] || [ -z "$user" ]; then
		log -i -t "$phase" info "$ACTION: skip"
		return
	fi
	# don't do anything if it already exists
	if getent passwd "$user" >/dev/null; then
		log -i -t "$phase" info "$ACTION: already exists"
		return
	fi

	$MOCK addgroup "$user"
	$MOCK adduser -D \
		-h "${CLOUD_USER_HOMEDIR:-/home/$user}" \
		-s "${CLOUD_USER_SHELL:-/bin/sh}" \
		-G "${CLOUD_USER_PRIMARY_GROUP:-$user}" \
		${CLOUD_USER_GECOS:+-g "$CLOUD_USER_GECOS"} \
		"$user"
	$MOCK addgroup "$user" wheel
	echo "$user:*" | $MOCK chpasswd -e

	# setup sudo and/or doas
	if [ -d "$ETC/sudoers.d" ]; then
		echo '%wheel ALL=(ALL) NOPASSWD: ALL' > "$ETC/sudoers.d/wheel"
	fi
	if [ -d "$ETC/doas.d" ]; then
		echo 'permit nopass :wheel' > "$TARGET/etc/doas.d/wheel.conf"
	elif [ -f "$ETC/doas.conf" ]; then
		add_once "$TARGET/etc/doas.conf" "permit nopass :wheel"
	fi
}

init__enable_sshd() {
	$MOCK rc-update add sshd default
	# in case something else has enabled/disabled dservices
	$MOCK rc-update --update
}

### standard early phase functions

init__save_userdata() {
	local userdata="$TINY_CLOUD_VAR/user-data"
	local tmpfile=$(mktemp "$userdata.XXXXXX")

	imds -e @userdata > "$tmpfile"
	if printf '\037\213\010' | cmp -s -n 3 "$tmpfile"; then
		gzip -dc "$tmpfile" > "$userdata"
	elif printf 'BZh' | cmp -s -n 3 "$tmpfile"; then
		bzip2 -dc "$tmpfile" > "$userdata"
	elif printf '\375\067\172\130\132\000' | cmp -s -n 6 "$tmpfile"; then
		unxz -c "$tmpfile" > "$userdata"
	elif printf '\135\000\000' | cmp -s -n 3 "$tmpfile"; then
		lzma -dc "$tmpfile" > "$userdata"
	elif printf '\211\114\132' | cmp -s -n 3 "$tmpfile"; then
		lzop -dc "$tmpfile" > "$userdata"
	elif printf '\004\042\115\030' | cmp -s -n 4 "$tmpfile"; then
		lz4 -dc "$tmpfile" > "$userdata"
	elif printf '(\265/\375' | cmp -s -n 4 "$tmpfile"; then
		zstd -dc "$tmpfile" > "$userdata"
	else
		cp "$tmpfile" "$userdata"
	fi
	rm "$tmpfile"
}


### standard main phase functions

init__set_hostname() {
	local fqdn=$(imds @hostname)
	if [ -z "$fqdn" ]; then
		log -i -t "$phase" info "$ACTION: no hostname set"
		return
	fi
	if [ ${#fqdn} -gt 255 ]; then
		log -i -t "$phase" warning "$ACTION: hostname exceeds 255 chars '$fqdn'"
		return 1
	fi
	local host="${fqdn%%\.*}"
	if [ -z "$host" ]; then
		log -i -t "$phase" warning "$ACTION: unable to extract short hostname from '$fqdn'"
		return 1
	fi
	if [ ${#host} -gt 63 ]; then
		log -i -t "$phase" warning "$ACTION: short hostname exceeds 63 chars '$host'"
		return 1
	fi
	mkdir -p "$ETC"
	echo "$host" > "$ETC"/hostname
	$MOCK hostname -F "$ETC"/hostname
	echo -e "127.0.1.1\t$fqdn $host" >> "$ETC"/hosts
}

init__set_ssh_keys() {
	local sshkeys="$(imds @ssh-keys)"
	if [ -z "$sshkeys" ]; then
		log -i -t "$phase" info "$ACTION: no ssh key found"
		return
	fi
	local user="$CLOUD_USER"
	local pwent="$(getent passwd "$user")"
	if [ -z "$pwent" ]; then
		log -i -t "$phase" err "$ACTION: failed to find user $user"
		return 1
	fi
	local group=$(echo "$pwent" | cut -d: -f4)
	local ssh_dir="${ROOT}$(echo "$pwent" | cut -d: -f6)/.ssh"
	local keys_file="$ssh_dir/authorized_keys"

	if [ ! -d "$ssh_dir" ]; then
		mkdir -p "$ssh_dir"
		chmod 700 "$ssh_dir"
	fi

	touch "$keys_file"
	chmod 600 "$keys_file"
	$MOCK chown -R "$user:$group" "$ssh_dir"
	echo "$sshkeys" > "$keys_file"
}


### standard final phase functions would be here, if there were any


### load cloud-specific init functions / vars (potentially overriding)

if [ "$CLOUD" = "alpine" ]; then
	log -i -t "$phase" warning "CLOUD provider alpine is deprecated. Use nocloud"
	CLOUD="nocloud"
fi

if [ -f "$LIBDIR/tiny-cloud/cloud/$CLOUD/init" ]; then
	. "$LIBDIR/tiny-cloud/cloud/$CLOUD/init"
fi


### load user-data type-specific init functions / vars (potentially overriding)

userdata_type() {
	if [ ! -f "$TINY_CLOUD_VAR/user-data" ]; then
		echo missing
		return
	fi
	header=$(head -n1 "$TINY_CLOUD_VAR/user-data" | sed -e 's/[[:space:]].*//g')
	case "$header" in
		'#!'*)  echo script;;
		'#'*)   echo ${header#\#};;
		*)	  echo unknown;;
	esac
}

USERDATA_TYPE="$(userdata_type)"
if [ -f "$LIBDIR/tiny-cloud/user-data/$USERDATA_TYPE" ]; then
	. "$LIBDIR/tiny-cloud/user-data/$USERDATA_TYPE"
fi
