#!/bin/bash
# SPDX-FileCopyrightText: 2023 SUSE LLC
# SPDX-License-Identifier: GPL-2.0-or-later
set -euo pipefail

config_mount="/run/combustion/mount"
# Use /dev/shm for data exchange
exchangedir="/dev/shm/combustion/"
config_dir="${exchangedir}/config"

get_combustion_url() {
	(set +eu; . /lib/dracut-lib.sh; getarg combustion.url)
}

if [ "${1-}" = "--prepare" ]; then
	rm -rf "${exchangedir}"
	mkdir "${exchangedir}"

	enable_network() {
		sh -s <<'EOF'
			. /lib/dracut-lib.sh
			# Set rd.neednet if not already done and reevaluate it (module-specific)
			getargbool 0 'rd.neednet' && exit 0
			echo rd.neednet=1 > /etc/cmdline.d/40-combustion-neednet.conf
			if [ -e "${hookdir}/pre-udev/60-net-genrules.sh" ]; then
				# Wicked
				. "${hookdir}/pre-udev/60-net-genrules.sh"
				# Re-trigger generation of network rules and apply them
				udevadm control --reload
				udevadm trigger --subsystem-match net --action add
			elif [ -e "${hookdir}/cmdline/99-nm-config.sh" ]; then
				# NetworkManager
				. "${hookdir}/cmdline/99-nm-config.sh"
				if [ -e /usr/lib/systemd/system/NetworkManager-config-initrd.service ]; then
					systemctl restart NetworkManager-config-initrd.service
				fi
			else
				echo "ERROR: unknown network framework" >&2
				exit 1
			fi
EOF
	}

	# Look at combustion.url first
	if [ -n "$(get_combustion_url)" ]; then
		echo "URL provided, downloading config later"
		enable_network
		exit 0
	fi

	# Try fw_cfg next
	if [ -e "/sys/firmware/qemu_fw_cfg/by_name/opt/org.opensuse.combustion" ]; then
		mkdir -p "${config_dir}"
		if ! cp /sys/firmware/qemu_fw_cfg/by_name/opt/org.opensuse.combustion/script/raw \
		        "${config_dir}/script"; then
			echo "Failed to copy script from fw_cfg!"
			exit 1
		fi
		# TODO: Support other files, e.g. with a tarball or fs image?
	fi

	# Try VMware guestinfo next
	if ! [ -d "${config_dir}" ] && [ "$(systemd-detect-virt)" = "vmware" ]; then
		mkdir -p "${config_dir}"
		# The exit code isn't really useful or documented. Print all errors and only treat
		# a zero exit code + nonempty stdout as success.
		echo "Trying vmware-rpctool"
		if ! vmware-rpctool "info-get guestinfo.combustion.script" > "${config_dir}/script.gz.b64" \
		   || ! [ -s "${config_dir}/script.gz.b64" ] \
		   || ! base64 -d < "${config_dir}/script.gz.b64" | gzip -cd > "${config_dir}/script" \
		   || ! [ -s "${config_dir}/script" ]; then
			# vmware-rpctool failed or the script is empty
			rm -f "${config_dir}/script.gz.b64" "${config_dir}/script"
			rmdir "${config_dir}"
		fi
		rm -f "${config_dir}/script.gz.b64"
	fi

	# Try disks next - both lower and upper case
	# "INSTALL" is used as label for KIWI selfinstall .isos. Try those as fallback.
	for label in combustion COMBUSTION ignition IGNITION install INSTALL; do
		[ -d "${config_dir}" ] && break
		[ -e "/dev/disk/by-label/${label}" ] || continue

		mkdir -p "${config_mount}"
		if ! mount -o ro /dev/disk/by-label/${label} "${config_mount}"; then
			echo "Failed to mount config drive!"
			rmdir "${config_mount}"
			exit 1
		fi

		if [ -d "${config_mount}/combustion" ]; then
			if ! cp -R "${config_mount}/combustion" "${config_dir}"; then
				echo "Failed to copy config!"
				rm -rf "${config_dir}"
				umount "${config_mount}"
				rmdir "${config_mount}"
				exit 1
			fi
		else
			echo "No config found on drive."
		fi

		umount "${config_mount}"
		rmdir "${config_mount}"
	done

	if ! [ -d "${config_dir}" ]; then
		echo "No config source found"
		exit 0
	fi

	if ! [ -e "${config_dir}/script" ]; then
		echo "No config script found!"
		exit 1
	fi

	chmod a+x "${config_dir}/script"

	# Check for the magic flag "# combustion: prepare" in the script
	if grep -qE '^# combustion:(.*)\<prepare\>' "${config_dir}/script"; then
		# Run the script with the --prepare option
		if ! (cd "${config_dir}"; exec ./script --prepare); then
			echo "script --prepare failed with $?"
			exit 1
		fi
	fi
	# Note: In case ^ creates a NM config by writing to
	# /etc/NetworkManager/system-connections/, nm-initrd-generator must not generate a
	# default configuration anymore. This happens automatically since 1.36.0 (79885656d3).

	# Check for the magic flag "# combustion: network" in the script
	if grep -qE '^# combustion:(.*)\<network\>' "${config_dir}/script"; then
		enable_network
	fi

	exit 0
fi

# Set by combustion.service but not ignition-kargs-helper.
# Controls whether config is actually completed by this step,
# triggering deletion of /var/lib/YaST2/reconfig_system
complete=0

if [ "${1-}" = "--complete" ]; then
	complete=1
fi

delete_resolv_conf=0

cleanup() {
	rm -rf "${exchangedir}" || true

	if [ "${delete_resolv_conf}" -eq 1 ]; then
		rm -f /sysroot/etc/resolv.conf || true
	fi

	if findmnt /sysroot >/dev/null; then
		# zypper (both from t-u's selfupdate as well as the script)
		# uses gpg which starts gpg-agent in the background. This keeps
		# /sysroot busy, so we'll have to stop those.
		while pkill -x gpg-agent; do sleep 0.5; done

		# umount and remount so that the new default subvol is used
		umount -R /sysroot
		# Manual umount confuses systemd sometimes because it's async and the
		# .mount unit might still be active when the "start" is queued, making
		# it a noop, which ultimately leaves /sysroot unmounted
		# (https://github.com/systemd/systemd/issues/20329). To avoid that,
		# wait until systemd processed the umount events. In a chroot (or with
		# SYSTEMD_OFFLINE=1) systemctl always succeeds, so avoid an infinite loop.
		if ! systemctl --quiet is-active does-not-exist.mount; then
			while systemctl --quiet is-active sysroot.mount; do sleep 0.5; done
		fi
	fi

	if grep -w /sysroot /proc/*/mountinfo 2>/dev/null; then
		echo "Warning: /sysroot still mounted somewhere"
	fi

	systemctl start sysroot.mount
}

# Note: The /sysroot remounting during cleanup happens unconditionally.
# This is needed as ignition-mount.service's ExecStop is also disabled unconditionally.
trap cleanup EXIT

# Compatibility for ignition-kargs-helper, which drops a script into
# "/run/combustion/mount/combustion" and then calls combustion
if [ -d "${config_mount}/combustion" ]; then
	rm -rf "${config_dir}"
	mkdir -p "${exchangedir}"
	cp -R "${config_mount}/combustion" "${config_dir}"
	chmod a+x "${config_dir}/script"
fi

# Download and use a script from the given URL
combustion_url="$(get_combustion_url)" || :
if ! [ -d "${config_dir}" ] && [ -n "${combustion_url}" ]; then
	echo "Downloading config..."
	mkdir -p "${config_dir}"
	curl --globoff --location --retry 3 --retry-connrefused --fail --show-error --output "${config_dir}/script" -- "${combustion_url}"
	chmod a+x "${config_dir}/script"

	# Check for the magic flag "# combustion: prepare" in the script
	if grep -qE '^# combustion:(.*)\<prepare\>' "${config_dir}/script"; then
		# Run the script with the --prepare option
		if ! (cd "${config_dir}"; exec ./script --prepare); then
			echo "script --prepare failed with $?"
			exit 1
		fi
	fi
fi

[ -d "${config_dir}" ] || exit 0

# Make sure /sysroot is mounted
systemctl start sysroot.mount

# Same for /sysroot/usr if it exists (e.g. through mount.usr)
if systemctl cat sysroot-usr.mount &>/dev/null; then
	systemctl start sysroot-usr.mount
fi

# Have to take care of x-initrd.mount first and from the outside, but skip /etc
# if it's the new nested subvolume mechanism
# Note: ignition-kargs-helper calls combustion but already mounted those itself.
awk '$1 !~ /^#/ && $4 ~ /(\<|,)x-initrd\.mount(\>|,)/ && ! ( $2 == "/etc" && $3 == "none" ) { if(system("findmnt /sysroot/" $2 " >/dev/null || mount --target-prefix /sysroot --fstab /sysroot/etc/fstab " $2) != 0) exit 1; }' /sysroot/etc/fstab

# Make sure the old snapshot is relabeled too, otherwise syncing its /etc fails.
(
	set +eu
	. /lib/dracut-lib.sh
	set -eu
	if [ -e "${hookdir}/pre-pivot/50-selinux-microos-relabel.sh" ]; then
		NEWROOT=/sysroot bash -c '. /lib/dracut-lib.sh; . "${hookdir}/pre-pivot/50-selinux-microos-relabel.sh"'
	elif [ -e /sysroot/.autorelabel ] || [ -e /sysroot/etc/selinux/.autorelabel ]; then
		echo "ERROR: Relabel (probably) needed, but selinux-microos-relabel not found."
		exit 1
	fi
)

# Prepare chroot.
# Ideally this would just be --rbind /dev /sysroot/dev etc., but that has subtle downsides:
# By default, umount -R /sysroot would then propagate into the main /dev etc. and try to
# unmount /dev/shm/ etc. which must be avoided. Disabling mount propagation with
# --make-rslave interacts badly with other mount namespaces (used for PrivateMounts=true
# e.g. by systemd-udevd.service): The mounts propagate, but not all unmounts, leaving e.g.
# /sysroot/dev/shm/ mounted in udev's mnt context. To avoid all that, just bind mount
# everything individually, then it can be unmounted without caring about propagation.
# Alternatives are
# a) mount --bind --make-private /sysroot /sysroot on itself, but then e.g. udev doesn't
#    see the same /sysroot as us
# b) Run combustion in its own mount namespace, but the needed redesign breaks some
#    subtle interactions with the outside.
findmnt -nrvo TARGET | grep -E '^/(sys|proc|dev)(/|$)' | while read i; do
	mount --bind "$i" "/sysroot/$i"
done

# Mount everything we can, errors deliberately ignored
chroot /sysroot mount -a || true
# t-u needs writable /var/run and /tmp
findmnt /sysroot/run >/dev/null || mount -t tmpfs tmpfs /sysroot/run
findmnt /sysroot/tmp >/dev/null || mount -t tmpfs tmpfs /sysroot/tmp

# Need to write to /sysroot/etc for possibly creating resolv.conf
# and touching /etc/selinux/.autorelabel.
if ! [ -w /sysroot/etc ]; then
	# Transactional systems have a separate mount for /etc.
	# In the nested subvolume setup there's a bind mount that is read-only at first.
	if findmnt /sysroot/etc >/dev/null; then
		mount -o remount,rw /sysroot/etc
	else
		mount -o remount,rw /sysroot
	fi
fi

# Fake a netconfig setup
if [ -r /etc/resolv.conf ]; then
	mkdir -p /sysroot/run/netconfig/
	cp /etc/resolv.conf /sysroot/run/netconfig/resolv.conf

	if ! [ -e /sysroot/etc/resolv.conf ]; then
		if ln -sf /run/netconfig/resolv.conf /sysroot/etc/resolv.conf; then
			delete_resolv_conf=1
		fi
	fi
fi

if [ -x /sysroot/usr/sbin/transactional-update ]; then
	# t-u doesn't allow running arbitrary commands and
	# also ignores the shell's exit code, so DIY.
	if ! chroot /sysroot transactional-update shell <<EOF; then
		cd "${config_dir}"
		./script
		echo \$? > "${exchangedir}/retval"
		# Snapshot got touched while the policy isn't active, needs relabeling again.
		if [ -e /etc/selinux/.relabelled ]; then >> /etc/selinux/.autorelabel; fi
EOF
		echo "transactional-update failed"
		exit 1
	fi

	if ! [ -e "${exchangedir}/retval" ] || [ "$(cat "${exchangedir}/retval")" -ne 0 ]; then
		echo "Command failed, rolling back"
		chroot /sysroot transactional-update --no-selfupdate rollback
		exit 1
	fi

	# If the mount options for /sysroot include subvol(id)=...,
	# it would mount the old snapshot again. Override it.
	# At this point, /sysroot is mounted so systemctl show returns the active mount options,
	# not the configured ones. Use cat to get the configured ones instead.
	if systemctl cat sysroot.mount | grep -q '\<subvol\(id\)\?='; then
		# systemctl cat can't query a specific property, so work with the active options instead here...
		rootopts="$(systemctl show -P Options sysroot.mount | sed -Ee 's/(^|,)subvol=[^,]*(,|$)//g;s/(^|,)subvolid=[^,]*(,|$)//g')"
		# Apparently there is no better API?
		newdefault=$(btrfs subvolume get-default /sysroot | sed -e 's/^.* path \([^[:space:]]*\).*$/\1/')
		# Drop-ins in /etc have precedence over generated files in /run.
		mkdir -p /etc/systemd/system/sysroot.mount.d/
		cat > /etc/systemd/system/sysroot.mount.d/combustion-newsubvol.conf <<-EOF
			[Mount]
			Options=${rootopts},subvol=${newdefault}
		EOF
		systemctl daemon-reload # Unfortunately required for this :-/
	fi
else
	if ! chroot /sysroot sh -e -c "cd '${config_dir}'; ./script"; then
		echo "Command failed"
		exit 1
	fi
	chroot /sysroot snapper --no-dbus create -d "After combustion configuration" --userdata "important=yes" || :
fi

if [ "${complete}" -eq 1 ]; then
	rm -f /sysroot/var/lib/YaST2/reconfig_system
fi

exit 0
