File combustion of Package combustion

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

config_mount="/run/combustion/mount"

if [ "${1-}" = "--prepare" ]; then
	# Mount config drive
	mkdir -p "${config_mount}"

	config_drive_found=0

	# Try fw_cfg first
	if [ -e "/sys/firmware/qemu_fw_cfg/by_name/opt/org.opensuse.combustion" ]; then
		mkdir -p "${config_mount}/combustion"
		if ! cp /sys/firmware/qemu_fw_cfg/by_name/opt/org.opensuse.combustion/script/raw \
		        "${config_mount}/combustion/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?

		config_drive_found=1
	fi

	# Try disks next
	for label in combustion ignition; do
		[ "${config_drive_found}" = "1" ] && break
		[ -e "/dev/disk/by-label/${label}" ] || continue

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

                config_drive_found=1
	done

	if [ "${config_drive_found}" = "0" ]; then
		echo "No config drive found"
		exit 0
	fi

	# Check for the magic flag "# combustion: network" in the script
	if [ -e "${config_mount}/combustion/script" ] \
	   && grep -qE '^# combustion:(.*)\<network\>' "${config_mount}/combustion/script"; then
		sh -s <<EOF
			. /lib/dracut-lib.sh
			getargbool 0 'rd.neednet' && exit 0
			echo rd.neednet=1  > /etc/cmdline.d/40-combustion-neednet.conf
			# Re-trigger generation of network rules and apply them
			. /lib/dracut/hooks/pre-udev/60-net-genrules.sh
			udevadm control --reload
			udevadm trigger --subsystem-match net --action add
EOF
	fi

	exit 0
fi

# Use /dev/shm for data exchange
exchangedir="/dev/shm/combustion/"
delete_resolv_conf=0

cleanup() {
	if findmnt "${config_mount}" >/dev/null; then
		umount "${config_mount}" || true
		rmdir "${config_mount}" || true
	else
		rm -rf "${config_mount}" || true
	fi

	rm -rf "${exchangedir}" || true

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

	# 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 is-active does-not-exist.mount; then
		while systemctl is-active sysroot.mount; do sleep 0.5; done
	fi
	systemctl start sysroot.mount
}

trap cleanup EXIT

# ignition-mount.service mounts stuff below /sysroot in ExecStart and umounts
# it on ExecStop, failing if umounting fails. This conflicts with the
# mounts/umounts done by combustion. Ignition is already done, so just stop it.
if systemctl --quiet is-active ignition-mount.service; then
	systemctl stop ignition-mount.service
fi

if ! [ -d "${config_mount}/combustion" ]; then
	echo "No config found - doing nothing."
	exit 0
fi

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

# Copy config
mkdir "${exchangedir}"
config_dir="${exchangedir}/config"
cp -R "${config_mount}/combustion" "${config_dir}"

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

# Have to take care of x-initrd.mount first and from the outside
awk '$4 ~ /x-initrd.mount/ { system("findmnt /sysroot" $2 " >/dev/null || mount -t " $3 " -o " $4 " " $1 " /sysroot" $2) }' /sysroot/etc/fstab

# Make sure the old snapshot is relabeled too, otherwise syncing its /etc fails.
if [ -e /sysroot/etc/selinux/.autorelabel ]; then
	NEWROOT=/sysroot bash -c '. /lib/dracut-lib.sh; . /lib/dracut/hooks/pre-pivot/50-selinux-microos-relabel.sh'
fi

# Prepare chroot
for i in proc sys dev; do
	mount --rbind /$i /sysroot/$i
done
mount --make-rslave /sysroot

# 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

# 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}"
		chmod a+x script
		./script
		echo \$? > "${exchangedir}/retval"
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

	# Snapshot got touched while the policy isn't active, needs relabeling again.
	[ -e /sysroot/etc/selinux/.relabelled ] && >> /sysroot/etc/selinux/.autorelabel
else
	mount -o remount,rw /sysroot
	if ! chroot /sysroot sh -e -c "cd '${config_dir}'; chmod a+x script; ./script"; then
		echo "Command failed"
		exit 1
	fi
	chroot /sysroot snapper --no-dbus create -d "After combustion configuration" || :
fi

exit 0
openSUSE Build Service is sponsored by