#!/bin/sh
# oci-uefi-boot-order: manage the persistent UEFI BootOrder of a machine.
#
# openstack-cluster-installer uses this to make sure a machine boots from
# the network (the OCI live environment) when it (re)installs, and from
# the installed disk once the installation is over. Contrary to the
# one-shot "efibootmgr -n" (BootNext), reordering the BootOrder is
# persistent across reboots, which is what we want, as the firmware may
# boot the machine several times before we reorder it again.
#
# Usage:
#   oci-uefi-boot-order live     Persistently boot from the network (PXE) first.
#   oci-uefi-boot-order hdd      Persistently boot from the installed OS first.
#   oci-uefi-boot-order list    Print how entries are classified, change nothing.
#
# Entries are classified from the device path reported by "efibootmgr -v",
# independently of the firmware vendor:
#   network: contains MAC( together with IPv4(, IPv6( or Uri(
#   os:      disk device path (HD(, SCSI(, NVMe(, Sata( or Ata() plus a
#            File( loader path, eg: a Debian or Windows boot entry
#   disk:    disk device path without a File( loader
#   rest:    firmware volumes (FvVol/FvFile), USB, CDROM, and everything
#            else without a matching device path
# "live" puts network entries first, preferring IPv4 entries for one of
# this machine's NICs. "hdd" puts OS entries first, falling back to disk
# entries. The relative ordering of everything else is kept from the
# current BootOrder, and entries are never added to or removed from it.

TARGET="$1"

case "${TARGET}" in
	"live"|"hdd"|"list")
	;;
	*)
		echo "Usage: oci-uefi-boot-order live|hdd|list" >&2
		exit 1
	;;
esac

# A machine not booted in UEFI mode (ie: legacy BIOS) has no NVRAM
# BootOrder to manage: exiting with 0 means it is not an error, as this
# script is called on any machine whatever its firmware.
if [ ! -d /sys/firmware/efi ] ; then
	echo "Not booted in UEFI mode: nothing to do."
	exit 0
fi

if ! command -v efibootmgr >/dev/null 2>&1 ; then
	echo "efibootmgr isn't installed: cannot manage the BootOrder." >&2
	exit 1
fi

LIST_OUT=$(efibootmgr -v 2>/dev/null)
CURRENT_ORDER=$(echo "${LIST_OUT}" | sed -n 's/^BootOrder: *//p')
if [ -z "${CURRENT_ORDER}" ] ; then
	echo "No BootOrder found in efibootmgr output:" >&2
	echo "${LIST_OUT}" >&2
	exit 1
fi

# MAC addresses of this machine's NICs (no colon, lowercase), so we can
# prefer the PXE entries that will be answered by the OCI installer.
LOCAL_MACS=$(cat /sys/class/net/*/address 2>/dev/null | tr -d ':' | tr '[:upper:]' '[:lower:]')

PXE_LOCAL=""
PXE_OTHER=""
OS_LIST=""
DISK_LIST=""
REST_LIST=""

for ID in $(echo "${CURRENT_ORDER}" | tr ',' ' ') ; do
	LINE=$(echo "${LIST_OUT}" | grep "^Boot${ID}[ *]" | head -n 1)
	CLASS="rest"
	case "${LINE}" in
		*"MAC("*"IPv4("*|*"MAC("*"IPv6("*|*"MAC("*"Uri("*)
			CLASS="network"
		;;
		*"HD("*"File("*|*"SCSI("*"File("*|*"NVMe("*"File("*|*"Sata("*"File("*|*"Ata("*"File("*)
			CLASS="os"
		;;
		*"HD("*|*"SCSI("*|*"NVMe("*|*"Sata("*|*"Ata("*)
			CLASS="disk"
		;;
	esac
	if [ "${CLASS}" = "network" ] ; then
		# Prefer IPv4 PXE on a NIC of this machine.
		LINE_NORM=$(echo "${LINE}" | tr -d ':' | tr '[:upper:]' '[:lower:]')
		for MAC in ${LOCAL_MACS} ; do
			case "${LINE_NORM}" in
				*"mac(${MAC}"*"ipv4("*)
					CLASS="pxe_local"
				;;
			esac
		done
	fi
	case "${CLASS}" in
		"pxe_local")	PXE_LOCAL="${PXE_LOCAL} ${ID}" ;;
		"network")	PXE_OTHER="${PXE_OTHER} ${ID}" ;;
		"os")		OS_LIST="${OS_LIST} ${ID}" ;;
		"disk")		DISK_LIST="${DISK_LIST} ${ID}" ;;
		*)		REST_LIST="${REST_LIST} ${ID}" ;;
	esac
	if [ "${TARGET}" = "list" ] ; then
		echo "Boot${ID} [${CLASS}] ${LINE}"
	fi
done

if [ "${TARGET}" = "list" ] ; then
	echo "BootOrder: ${CURRENT_ORDER}"
	exit 0
fi

if [ "${TARGET}" = "live" ] ; then
	if [ -z "${PXE_LOCAL}" ] && [ -z "${PXE_OTHER}" ] ; then
		echo "No network (PXE) boot entry found in the BootOrder." >&2
		exit 1
	fi
	NEW_ORDER="${PXE_LOCAL} ${PXE_OTHER}"
	TARGET_SET=" ${PXE_LOCAL} ${PXE_OTHER} "
elif [ "${TARGET}" = "hdd" ] ; then
	if [ -z "${OS_LIST}" ] && [ -z "${DISK_LIST}" ] ; then
		echo "No OS or disk boot entry found in the BootOrder." >&2
		exit 1
	fi
	NEW_ORDER="${OS_LIST} ${DISK_LIST}"
	TARGET_SET=" ${OS_LIST} ${DISK_LIST} "
fi

# Keep everything else in the current relative order, after the target.
for ID in $(echo "${CURRENT_ORDER}" | tr ',' ' ') ; do
	case "${TARGET_SET}" in
		*" ${ID} "*)
		;;
		*)
			NEW_ORDER="${NEW_ORDER} ${ID}"
		;;
	esac
done
NEW_ORDER=$(echo ${NEW_ORDER})

if [ "${NEW_ORDER}" = "$(echo "${CURRENT_ORDER}" | tr ',' ' ')" ] ; then
	echo "BootOrder is already ordered for ${TARGET}: $(echo ${NEW_ORDER} | tr ' ' ',')"
	exit 0
fi

if ! efibootmgr -o "$(echo ${NEW_ORDER} | tr ' ' ',')" >/dev/null ; then
	echo "Failed to set BootOrder with efibootmgr." >&2
	exit 1
fi
echo "BootOrder is now: $(echo ${NEW_ORDER} | tr ' ' ',') (was: ${CURRENT_ORDER})"
