meta-asrock: Add bios-update infrastructure

On all supported ASRock platforms the BIOS (host firmware) update
mechanism involves setting some GPIOs, binding a driver for an MTD flash
device, writing the image to the flash, and then undoing the setup steps
in reverse order.  The exact GPIOs involved (and some parameters of the
firmware image) vary a bit between systems; the platform-specific
differences are distilled out into some config parameters loaded from
/etc/default/bios-update.

Signed-off-by: Zev Weiss <zev@bewilderbeest.net>
Co-developed-by: Olivier Faurax <olivier.faurax@eu.equinix.com>
Change-Id: I4ecf93a73359063e3639895b078057a637dff452
diff --git a/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager/bios-update.sh b/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager/bios-update.sh
new file mode 100644
index 0000000..4697392
--- /dev/null
+++ b/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager/bios-update.sh
@@ -0,0 +1,209 @@
+#!/bin/bash
+
+die() { logger -s -t bios-update "Error: $*"; exit 1; }
+info() { logger -s -t bios-update "$*"; }
+
+# shellcheck disable=SC1091
+. /etc/default/bios-update || die "Failed: unable to load /etc/default/bios-update"
+
+[ -n "$BIOS_UPDATE_MAGIC_OFFSET" ] || die "BIOS_UPDATE_MAGIC_OFFSET not set"
+[ -n "$BIOS_UPDATE_MAGIC" ] || die "BIOS_UPDATE_MAGIC not set"
+[ -n "$BIOS_UPDATE_SIZE" ] || die "BIOS_UPDATE_SIZE not set"
+
+declare -A prep_gpios_pids
+
+bios_flash_spidev="1e630000.spi"
+smc_drvdir="/sys/bus/platform/drivers/spi-aspeed-smc"
+
+hoststate_svc="xyz.openbmc_project.State.Host"
+hoststate_path="/xyz/openbmc_project/state/host0"
+hoststate_intf="xyz.openbmc_project.State.Host"
+hoststate_prop="CurrentHostState"
+hoststate_off="xyz.openbmc_project.State.Host.HostState.Off"
+
+check_host_off()
+{
+	local state
+	state="$(busctl get-property "$hoststate_svc" "$hoststate_path" \
+			"$hoststate_intf" "$hoststate_prop")"
+	if [ "$state" != "s \"$hoststate_off\"" ]; then
+		die "host must be off before performing BIOS update"
+	fi
+}
+
+# sets variables (gpioset background PIDs and bios flash mtd chardev,
+# commented as "global") for later use
+attach_bios_flash()
+{
+	for gpio in "${BIOS_UPDATE_PREP_GPIOS[@]}" ; do
+		read -ra kv <<<"${gpio/=/ }"
+		info "Setting ${kv[0]} to ${kv[1]}..."
+		gpio="$(gpiofind "${kv[0]}")" || die "Failed to find ${kv[0]} GPIO"
+		# shellcheck disable=SC2086
+		gpioset -m signal ${gpio}="${kv[1]}" &
+		prep_gpios_pids[${kv[0]}]=$! # global
+		sleep 1
+	done
+
+	info "Attaching BIOS flash..."
+	echo "$bios_flash_spidev" > "$smc_drvdir/bind" || die "failed to attach aspeed-smc driver to BIOS SPI flash"
+
+	local tmp
+	tmp="$(grep -xl bios /sys/class/mtd/*/name)"
+	tmp="${tmp%/name}"
+	tmp="${tmp##*/}"
+	bios_mtd_dev="/dev/$tmp" # global
+	[ -c "$bios_mtd_dev" ] || die "bios mtd chardev not found"
+}
+
+detach_bios_flash()
+{
+	info "Detaching BIOS flash..."
+	echo "$bios_flash_spidev" > "$smc_drvdir/unbind" || die "failed to detach aspeed-smc driver from BIOS SPI flash"
+
+	# Detach in reverse order
+	for ((i = ${#BIOS_UPDATE_PREP_GPIOS[@]} - 1; i >= 0; i--)) ; do
+		read -ra kv <<<"${BIOS_UPDATE_PREP_GPIOS[i]/=/ }"
+		notvalue=$((! kv[1]))
+		info "Resetting ${kv[0]} to ${notvalue}..."
+		kill -INT "${prep_gpios_pids[${kv[0]}]}"
+		wait "${prep_gpios_pids[${kv[0]}]}"
+		gpio="$(gpiofind "${kv[0]}")" || die "Failed to find ${kv[0]} GPIO"
+		# shellcheck disable=SC2086
+		gpioset -m exit ${gpio}="$notvalue"
+		sleep 1
+	done
+}
+
+check_bios_image()
+{
+	[ -r "$1" ] || die "can't read BIOS image $1"
+
+	local imgsize magic
+	imgsize="$(stat -c %s "$1")"
+	[ "$imgsize" = "${BIOS_UPDATE_SIZE}" ] || die "invalid BIOS image (wrong size)"
+
+	magic="$(dd if="$1" bs=1 count=4 skip="${BIOS_UPDATE_MAGIC_OFFSET}" 2>/dev/null | hexdump -e '3/1 "%02x" "%02x\n"')"
+	[ "$magic" = "${BIOS_UPDATE_MAGIC}" ] || die "invalid BIOS image (magic number mismatch)"
+}
+
+flash_bios_image()
+{
+	local bios_img="$1"
+
+	info "Checking BIOS image..."
+	check_bios_image "$bios_img"
+
+	info "Checking host state..."
+	check_host_off
+
+	attach_bios_flash
+
+	info "Writing BIOS image to SPI flash..."
+	if flashcp -v "$bios_img" "$bios_mtd_dev"; then
+		info "Flash update successful"
+		local status=0
+	else
+		info "Error updating flash! (proceeding with detach)"
+		local status=1
+	fi
+
+	detach_bios_flash
+
+	return "$status"
+}
+
+# HACK: for unknown reasons, on e3c246d4i, the host seems to refuse to power on
+# after we switch the BIOS SPI flash to the BMC and back to the host,
+# but it recovers if we hold the POWER_OUT GPIO as in a press-and-hold
+# of the front-panel power button (even though the host is already
+# powered off).  I don't really know what's going on here.
+do_power_hack()
+{
+	# power-control holds the POWER_OUT gpio, so we need to stop it if it's on
+	local powerctl_svc="xyz.openbmc_project.Chassis.Control.Power.service"
+	local powerhack_time=8
+	local psout_gpio
+
+	psout_gpio="$(gpiofind "${BIOS_UPDATE_POWER_GPIO}")"
+
+	prev_powerctl_state="$(systemctl show --property=ActiveState "$powerctl_svc")"
+	if [ "$prev_powerctl_state" = "ActiveState=active" ]; then
+		systemctl stop "$powerctl_svc" || info "Warning: failed to stop $powerctl_svc"
+	fi
+
+	info "Holding host power line for $powerhack_time seconds..."
+
+	# shellcheck disable=SC2086
+	gpioset -m time -s "$powerhack_time" ${psout_gpio}=0 || die "Failed to assert ${BIOS_UPDATE_POWER_GPIO}) GPIO"
+	# shellcheck disable=SC2086
+	gpioset ${psout_gpio}=1 || die "Failed to release ${BIOS_UPDATE_POWER_GPIO} GPIO"
+
+	info "Host power line released..."
+
+	# if the power-control service was for some reason not running to
+	# start with, leave it that way.
+	if [ "$prev_powerctl_state" = "ActiveState=active" ]; then
+		systemctl start "$powerctl_svc" || info "Warning: failed to restore $powerctl_svc"
+	fi
+}
+
+# Find the image file within a /tmp/images/$IMGHASH directory (should
+# be the one file not named MANIFEST).  We could be a little more
+# automagic and run check_bios_image on each candidate in case there's
+# more than one (discarding any that fail), but for now we'll keep it
+# simple and not try to handle anything unexpected.
+find_imgfile()
+{
+	[ -d "$1" ] || die "$1: not a directory"
+	local img='' path name
+	for path in "$1"/*; do
+		name="$(basename "$path")"
+		if [ "$name" = "MANIFEST" ]; then
+			# ignore MANIFEST file
+			continue
+		elif [ -n "$img" ]; then
+			# if we've already hit a non-MANIFEST file, bail
+			die "multiple potential image files in $1"
+		else
+			img="$path"
+		fi
+	done
+	[ -n "$img" ] || die "no image file found in $1"
+	echo "$img"
+}
+
+# when invoked by the systemd unit as part of the web-UI machinery we
+# get passed /tmp/images/$IMGHASH (directory containing the BIOS
+# image), but for manual use it's nice to be able to just pass the raw
+# image file directly, so we support both, differentiated by a '-d'
+# flag.
+imgdir_mode=false
+
+while getopts d opt; do
+	case "$opt" in
+	d) imgdir_mode=true;;
+	*) exit 1;;
+	esac
+done
+
+shift $((OPTIND-1))
+[ $# = 1 ] || die "usage: $0 [ BIOS_IMAGE | -d IMAGE_DIR ]"
+
+if $imgdir_mode; then
+	imgfile="$(find_imgfile "$1")"
+else
+	imgfile="$1"
+fi
+
+if flash_bios_image "$imgfile"; then
+	info "BIOS update complete."
+else
+	die "BIOS update failed!"
+fi
+
+if [ -n "$BIOS_UPDATE_POWER_GPIO" ]; then
+	do_power_hack
+fi
+
+info "Done."
diff --git a/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager/obmc-flash-host-bios@.service b/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager/obmc-flash-host-bios@.service
new file mode 100644
index 0000000..6ae7b66
--- /dev/null
+++ b/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager/obmc-flash-host-bios@.service
@@ -0,0 +1,7 @@
+[Unit]
+Description=Flash Host Bios image %I
+
+[Service]
+Type=oneshot
+RemainAfterExit=no
+ExecStart=/usr/sbin/bios-update.sh -d /tmp/images/%i
diff --git a/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager_%.bbappend b/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager_%.bbappend
new file mode 100644
index 0000000..a61d306
--- /dev/null
+++ b/meta-asrock/meta-common/recipes-phosphor/flash/phosphor-software-manager_%.bbappend
@@ -0,0 +1,14 @@
+FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
+SRC_URI += "file://bios-update.sh"
+
+PACKAGECONFIG:append = " flash_bios"
+RDEPENDS:${PN} += "bash libgpiod"
+
+do_install:append() {
+    install -d ${D}/${sbindir}
+    install -m 0755 ${WORKDIR}/bios-update.sh ${D}/${sbindir}/
+    if [ -e ${WORKDIR}/bios-update ]; then
+        install -d ${D}${sysconfdir}/default
+        install -m 0644 ${WORKDIR}/bios-update ${D}${sysconfdir}/default
+    fi
+}