#!/bin/sh
# Copyright (c) 2013 International Business Machines
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Authors: Vasant Hegde <hegdevasant@linux.vnet.ibm.com>
#
# Simple script for code update on "PowerNV (Non-Virtualized)" platform.
# This is a simple wrapper script to pass the image.
# - On FSP based system, the Linux kernel and FW does the real work
#   during system reboot.
# - On OpenPower system, we use IPMI interface to pass image to service
#   processor and service processor will update the image. Then we make
#   reboot command to reboot the host.
#
# This script has minimal dependencies so it can operate in a
# rescue environment.

#set -x

# Error codes
E_SUCCESS=0	# Success
E_UNSUPPORTED=1	# Firmware update is not supported
E_USAGE=3	# Usage error
E_PERM=4	# Permission error
E_IMAGE=5	# Image file error
E_SYS_FS=6	# Firmware update related sysfs file doesn't exist
E_MODULE=7	# Error loading module
E_OPAL=8	# OPAL call failed
E_USER=9	# User aborted operation
E_OVERWRITE=10	# Auto overwrite permanent side image
E_WRNTY=15	# Update Access Key Expired
E_KEXEC=16	# Kexec service stop error
E_IPMI=17	# ipmitool is not installed

# Firmware update related files on FSP based platform
SYS_IMAGE_FILE=/sys/firmware/opal/image
SYS_VALIDATE_FLASH=/sys/firmware/opal/validate_flash
SYS_MANAGE_FLASH=/sys/firmware/opal/manage_flash
SYS_UPDATE_FLASH=/sys/firmware/opal/update_flash

# Device tree path
DT_PATH=/proc/device-tree

# Current firmware version files on FSP based platform
DT_FW_MI_FILE=${DT_PATH}/ibm,opal/firmware/mi-version
DT_FW_ML_FILE=${DT_PATH}/ibm,opal/firmware/ml-version

# Firmware versions device tree node
DT_PATH_FW_NODE=${DT_PATH}/ibm,firmware-versions

# Code update status values
FLASH_SUCCESS=0			# Success
FLASH_PARAM_ERR=-1		# Parameter error
FLASH_BUSY=-2			# OPAL busy
FLASH_HW_ERR=-6			# Hardware error
FLASH_INTERNAL_ERR=-11		# Internal error
FLASH_NO_OP=-1099		# No operation initiated by user
FLASH_NO_AUTH=-9002		# Inband firmware update is not allowed

# Validate image status values
FLASH_IMG_READY=-1001		# Image ready for validation
FLASH_IMG_INCOMPLETE=-1002	# User copied < VALIDATE_BUF_SIZE

# Manage image status values
FLASH_ACTIVE_ERR=-9001		# Cannot overwrite active img

# Flash image status values
FLASH_IMG_READY=0		# Image ready for flash on reboot
FLASH_INVALID_IMG=-1003		# Flash image shorter than expected
FLASH_IMG_NULL_DATA=-1004	# Bad data
FLASH_IMG_BAD_LEN=-1005		# Bad length

# Validate image update result tokens
#
# T side will be updated
VALIDATE_TMP_UPDATE=0
#
# Partition does not have authority
VALIDATE_FLASH_AUTH=1
#
# Candidate image is not valid for this platform
VALIDATE_INVALID_IMG=2
#
# Current fixpack level is unknown
VALIDATE_CUR_UNKNOWN=3
#
# Current T side will be committed to P side before being replace
# with new image, and the new image is downlevel from current image
VALIDATE_TMP_COMMIT_DL=4
#
# Current T side will be committed to P side before being replaced
# with new image
VALIDATE_TMP_COMMIT=5
#
# T side will be updated with a downlevel image
VALIDATE_TMP_UPDATE_DL=6
#
# The candidate image's release date is later than the system's Update
# Access Key Expiration date - service warranty period has expired
VALIDATE_OUT_OF_WRNTY=7

error() {
	local exit_code=$1

	if [ $# -lt 1 ]; then
		echo "error(): usage." >&2
		return $E_USAGE
	fi

	shift;
	echo update_flash: $* >&2
	exit $exit_code
}

usage() {
	local exit_code;

	if [ "$1" = $E_SUCCESS ]; then
		exit_code=$E_SUCCESS
	else
		exit_code=$E_USAGE
	fi

	echo "USAGE: update_flash {-h | -s | -r | -c | -d | [-v|-n] -f <image filename>}" >&2
	echo "	-h		Print this message." >&2
	echo "	-s		Determine if partition has access to" >&2
	echo "			perform flash image management." >&2
	echo "	-r		Reject temporary image." >&2
	echo "	-c		Commit temporary image." >&2
	echo "	-d		Display current firmware version." >&2
	echo "	-v		Validate the given image file." >&2
	echo "	-n		Do not overwrite Permanent side" >&2
	echo "			image automatically." >&2
	echo "	-f <filename>	Update with given image file. If possible," >&2
	echo "			the image is automatically validated prior" >&2
	echo "			to update." >&2
	echo "" >&2
	exit $exit_code
}

# Validate sysfs interface
fsp_validate_sysfs_file() {
	local file="$1"
	if [ -r "$file" ]; then
		return $E_SUCCESS
	fi

	error $E_SYS_FS "sysfs interface for firmware update does not exists."
}

# Check kexec service status
check_kexec_service() {
	# check systemctl command
	which systemctl >/dev/null 2>&1
	if [ $? -ne 0 ]; then
		return 1
	fi

	# kexec service is running
	systemctl status kexec.service | grep -w "active" >/dev/null 2>&1
	if [ $? -eq 0 ]; then
		return 0
	fi

	return 1
}

# Stop kexec service
stop_kexec_service() {
	systemctl stop kexec.service >/dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "update_flash: Failed to stop kexec service."
		error $E_KEXEC "Please stop kexec service and retry."
	fi

	echo
	echo "info: kexec service stopped."
}

# Copy image to sysfs file
fsp_copy_candidate_image() {
	local img_file=$1

	[ $# -eq 1 ] || error $E_USAGE "fsp_copy_candidate_image(): usage."

	[ -r "$img_file" ] || error $E_IMAGE "Cannot read ${img_file}."

	# Copy candidate image
	dd if=$img_file of=$SYS_IMAGE_FILE 2>/dev/null
	if [ $? -ne 0 ]; then
		echo "update_flash: Error copying firmware image."
		error $E_IMAGE "Please retry with valid firmware image."
	fi
}

fsp_echo_opal_return_status() {
	case "$1" in
	$FLASH_PARAM_ERR)
		error $E_OPAL "Parameter Error.";;
	$FLASH_BUSY)
		error $E_OPAL "OPAL Busy.";;
	$FLASH_HW_ERR)
		error $E_OPAL "Hardware error.";;
	$FLASH_INTERNAL_ERR)
		error $E_OPAL "OPAL internal error.";;
	$FLASH_NO_AUTH)
		error $E_PERM "System does not have authority to perform firmware update.";;
	$FLASH_IMG_INCOMPLETE)
		error $E_IMAGE "Invalid candidate image.";;
	$FLASH_ACTIVE_ERR)
		error $E_OVERWRITE "Cannot Overwrite the Active Firmware Image.";;
	$FLASH_INVALID_IMG)
		error $E_IMAGE "Invalid candidate image.";;
	$FLASH_IMG_NULL_DATA)
		error $E_IMAGE "Bad data value in flash list block.";;
	$FLASH_IMG_BAD_LEN)
		error $E_IMAGE "Bad length value in flash list block.";;
	*)	error $E_OPAL "Unknown return status.";;
	esac
}

# Determine if partition has access to perform flash image management
fsp_query_flash_support() {
	# Validate sysfs interface
	fsp_validate_sysfs_file $SYS_IMAGE_FILE

	# By default PowerNV host supports firmware management
	echo "update_flash: Firmware image management is supported."

	exit $E_SUCCESS
}

fsp_echo_validate_buf() {
	local output="$1"
	local cur_t="$(echo "$output" | grep "^MI" | head -n 1 | awk ' { print $2 } ')"
	local cur_p="$(echo "$output" | grep "^MI" | head -n 1 | awk ' { print $3 } ')"
	local new_t="$(echo "$output" | grep "^MI" | tail -n 1 | awk ' { print $2 } ')"
	local new_p="$(echo "$output" | grep "^MI" | tail -n 1 | awk ' { print $3 } ')"

	echo "Projected Flash Update Results:"
	echo "Current T Image: $cur_t"
	echo "Current P Image: $cur_p"
	echo "New T Image:     $new_t"
	echo "New P Image:     $new_p"
}

fsp_echo_validate_return_status() {
	local output="$1"
	local rc="$(echo "$output" | head -n 1)"
	local opal_buf="$(echo "$output" | tail -n +2)"

	[ $# -eq 1 ] || error $E_USAGE "fsp_echo_validate_return_status(): usage."

	if [ $rc -lt 0 ]; then
		fsp_echo_opal_return_status $rc
	fi

	# Validation result
	case "$rc" in
	$VALIDATE_TMP_UPDATE)
		echo -n "info: Temporary side will be updated with a newer or"
		echo " identical image.";;
	$VALIDATE_FLASH_AUTH)
		error $E_OPAL "System does not have authority.";;
	$VALIDATE_INVALID_IMG)
		error $E_OPAL "Invalid candidate image for this platform.";;
	$VALIDATE_CUR_UNKNOWN)
		echo "info: Current fixpack level is unknown.";;
	$VALIDATE_TMP_COMMIT_DL)
		echo "info: Current Temporary image will be committed to"
		echo "Permanent side before being replaced with new image,"
		echo "and the new image is downlevel from current image.";;
	$VALIDATE_TMP_COMMIT)
		echo "info: Current Temporary side will be committed to"
		echo "Permanent side before being replaced with the new"
		echo "image.";;
	$VALIDATE_TMP_UPDATE_DL)
		echo "info: Temporary side will be updated with a downlevel image.";;
	*)	error $E_OPAL "Unknown return status."
	esac

	echo
	fsp_echo_validate_buf "$opal_buf"

	# Do not commit T side image to P side
	if [ $no_overwrite_opt -eq 1 ]; then
		if [ $rc -eq $VALIDATE_TMP_COMMIT_DL ] ||
			[ $rc -eq $VALIDATE_TMP_COMMIT ]; then
			echo ""
			echo "update_flash: Run without -n option to flash new image."
			exit $E_OVERWRITE
		fi
	fi
}

fsp_validate_flash() {
	local output=""

	# Validate candidate image
	echo 1 > $SYS_VALIDATE_FLASH 2>/dev/null

	# Display appropriate message, exiting if necessary
	output="$(cat $SYS_VALIDATE_FLASH)"
	fsp_echo_validate_return_status "$output"
}

fsp_validate_flash_from_file() {
	local img_file=$1

	[ $# -eq 1 ] || error $E_USAGE "fsp_validate_flash_from_file(): usage."

	# Validate sysfs interface
	fsp_validate_sysfs_file $SYS_VALIDATE_FLASH

	# Copy candiadate image
	fsp_copy_candidate_image $img_file

	# Validate candidate image
	fsp_validate_flash

	# Check kexec service status
	check_kexec_service
	if [ $? -eq 0 ]; then
		echo
		echo "info: kexec service is running. It will be stopped before flashing."
	fi

	exit $E_SUCCESS
}

fsp_echo_update_return_status() {
	local rc="$1"

	[ $# -eq 1 ] || error $E_USAGE "fsp_echo_update_return_status(): usage."

	if [ $rc -lt 0 ]; then
		fsp_echo_opal_return_status $rc
	elif [ $rc -eq $FLASH_IMG_READY ]; then
		echo
		echo "FLASH: Image ready...rebooting the system..."
		echo "FLASH: This will take several minutes."
		echo "FLASH: Do not power off!"
	else
		error $E_SYS_FS "Unknown return status."
	fi
}

fsp_update_flash_from_file() {
	local img_file=$1
	local output=""

	[ $# -eq 1 ] || error $E_USAGE "fsp_update_flash_from_file(): usage."

	# Validate sysfs interface
	fsp_validate_sysfs_file $SYS_UPDATE_FLASH

	# Copy candidate image
	fsp_copy_candidate_image $img_file

	# Validate candidate image
	fsp_validate_flash

	# Update image
	echo 1 > $SYS_UPDATE_FLASH 2>/dev/null
	output="$(cat $SYS_UPDATE_FLASH)"
	fsp_echo_update_return_status "$output"

	# Stop kexec service
	check_kexec_service
	if [ $? -eq 0 ]; then
		stop_kexec_service
	fi

	# Reboot system, so that we can flash new image
	reboot

	exit $E_SUCCESS
}

fsp_echo_manage_return_status() {
	local is_commit=$1
	local output=$2
	local rc=$(echo $output)

	[ $# -eq 2 ] || error $E_USAGE "fsp_echo_manage_return_status(): usage."

	if [ $rc -lt 0 ]; then
		fsp_echo_opal_return_status $rc
	elif [ $rc -eq $FLASH_SUCCESS ]; then
		if [ $is_commit -eq 0 ]; then
			echo "Success: Rejected temporary firmware image."
		else
			echo "Success: Committed temporary firmware image."
		fi
	else
		error $E_OPAL "Unknown return status."
	fi
}

fsp_manage_flash() {
	local is_commit=$1
	local commit_str="1"
	local reject_str="0"
	local output=""

	[ $# -eq 1 ] || error $E_USAGE "fsp_manage_flash(): usage."

	# Validate sysfs interface
	fsp_validate_sysfs_file $SYS_MANAGE_FLASH

	# Commit operation
	if [ $is_commit -eq 1 ]; then
		echo $commit_str > $SYS_MANAGE_FLASH
	else
		echo $reject_str > $SYS_MANAGE_FLASH
	fi

	# Result
	output=$(cat $SYS_MANAGE_FLASH)
	fsp_echo_manage_return_status $is_commit "$output"

	exit $E_SUCCESS
}

fsp_display_current_fw_version() {

	if [ ! -r "$DT_FW_MI_FILE" ] || [ ! -r "$DT_FW_ML_FILE" ]; then
		error $E_SYS_FS "Firmware version information is not available"
	fi

	echo "Current firwmare version :"

	# P side
	local ml_ver=`cat $DT_FW_ML_FILE | head -n 1 | awk ' { print $3 }'`
	local mi_ver=`cat $DT_FW_MI_FILE | head -n 1 | awk ' { print $3 }'`
	echo "  P side    : $ml_ver ($mi_ver)"

	# T side
	local ml_ver=`cat $DT_FW_ML_FILE | head -n 1 | awk ' { print $2 }'`
	local mi_ver=`cat $DT_FW_MI_FILE | head -n 1 | awk ' { print $2 }'`
	echo "  T side    : $ml_ver ($mi_ver)"

	# Boot side
	local ml_ver=`cat $DT_FW_ML_FILE | head -n 1 | awk ' { print $4 }'`
	local mi_ver=`cat $DT_FW_MI_FILE | head -n 1 | awk ' { print $4 }'`
	echo "  Boot side : $ml_ver ($mi_ver)"

	exit $E_SUCCESS
}


# Check for ipmitool command
check_ipmitool() {
	which ipmitool >/dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "update_flash: ipmitool command not found."
		error $E_IPMI "Please install the 'ipmitool' package and retry."
	fi

	# Is BMC alive ?
	ipmitool -I usb mc info >/dev/null 2>&1 && break;
	if [ $? -ne 0 ]; then
		echo "update_flash: Failed to connect to BMC."
		error $E_IPMI "Please try again."
	fi
}

opp_query_flash_support() {
	check_ipmitool

	# By default PowerNV host supports firmware management
	echo "update_flash: Firmware image management is supported."

	exit $E_SUCCESS
}

opp_validate_flash_from_file() {
	local img_file=$1

	[ $# -eq 1 ] || error $E_USAGE "opp_validate_flash_from_file(): usage."

	# Check ipmitool
	check_ipmitool

	# Check file exists
	[ -r "$img_file" ] || error $E_IMAGE "Cannot read ${img_file}."

	ipmitool -z 30000 -I usb hpm check ${img_file} >/dev/null 2>&1
	if [ $? -ne 0 ]; then
		error $E_IPMI "Invalid candidate image for this platform."
	fi

	echo "info: Boot side firmware will be updated."
	echo
	ipmitool -z 30000 -I usb hpm check ${img_file}

	exit $E_SUCCESS
}

bmc_reset() {
	local timeout=120

	# We need to discard the errors as the USB device is reseted
	# along with the BMC.
	ipmitool -I usb mc reset cold >/dev/null 2>&1

	# The restart should take more or less 80 seconds
	for retry_count in 1 2; do
		echo "Waiting for BMC to cold reset. Sleeping $timeout seconds"
		sleep $timeout

		ipmitool -I usb mc info >/dev/null 2>&1 && break;

		retry_count=0
	done

	if [ $retry_count -eq 0 ]; then
		echo "update_flash: Failed to reset BMC."
		error $E_IPMI "Please try again."
	fi
}

opp_update_flash_from_file() {
	local img_file=$1

	[ $# -eq 1 ] || error $E_USAGE "opp_update_flash_from_file(): usage."

	# Check ipmitool
	check_ipmitool

	# -n option is not applicable for BMC based system
	if [ $no_overwrite_opt -eq 1 ]; then
		error $E_IMAGE "Option -n is not applicable on BMC based system."
	fi

	# Check file presence
	[ -r "$img_file" ] || error $E_IMAGE "Cannot read ${img_file}."

	# Start with a clean state: require a cold reset of the BMC.
	bmc_reset

	# Protect BMC memory content, avoid loosing network settings
	ipmitool -I usb raw 0x32 0xba 0x18 0x00 >/dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "update_flash: Failed to protect BMC memory content."
		error $E_IPMI "Please try again."
	fi

	# Start firmware update
	# XXX We have -v option to validate the firmware image. Also BMC
	#     validates image before upgrading. Hence I think we do not
	#     need user confirmation again. Lets pass the confirmation
	#     message here.
	echo "y" | ipmitool -z 30000 -I usb hpm upgrade $img_file all
	if [ $? -ne 0 ]; then
		echo "update_flash: Firmware update failed."
		# XXX Looks like BMC expects host reboot to
		#     retry firmware update.
		error $E_IPMI "Please reboot and try again."
	fi

	# BMC should reset it self after the update of component 1. As
	# we need to be sure or reboot will fail, force a reset.
	bmc_reset

	# Reboot system
	echo
	echo "Firmware update complete."
	echo "Rebooting the system..."
	reboot

	exit $E_SUCCESS
}

opp_manage_flash() {
	echo "Commit/Reject operation is not supported on this platform."
	exit $E_SUCCESS
}

# Use device tree to get firmware version
opp_display_dt_firmware_version() {
	local prop_prod=""

	echo
	echo "Firmware version:"

	# Different BMCs uses different device tree property to display
	# product name. On P9 system OPAL firmware takes care of parsing
	# and it will add version property under device tree. But on older
	# systems will have IBM or open-power or buildroot depending on
	# who builds firmware image.
	if [ -f $DT_PATH_FW_NODE/version ]; then
		prop_prod=version
	elif [ -f $DT_PATH_FW_NODE/IBM ]; then
		prop_prod=IBM
	elif [ -f $DT_PATH_FW_NODE/open-power ]; then
		prop_prod=open-power
	elif [ -f $DT_PATH_FW_NODE/buildroot ]; then
		prop_prod=buildroot
	fi

	if [ "$prop_prod" != "" ]; then
		echo " Product Version       : $(tr -d '\0' < $DT_PATH_FW_NODE/$prop_prod)"
	fi

	for i in `ls $DT_PATH_FW_NODE`
	do
		if [ "$i" = "$prop_prod" ]; then
			continue
		fi
		if [ "$i" = "name" ]; then
			continue
		fi
		if [ "$i" = "phandle" ] || [ "$i" = "linux,phandle" ]; then
			continue
		fi
		echo " Product Extra         : $i-$(tr -d '\0' < $DT_PATH_FW_NODE/$i)"
	done
}

# Use inband ipmi interface to get firmware version
opp_display_ipmi_fw_version() {
	which ipmitool >/dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "update_flash: ipmitool command not found."
		error $E_IPMI "Please install ipmitool and retry."
	fi

	# Use inband interface to get firmware version
	ipmitool mc info >/dev/null 2>&1 && break;
	if [ $? -ne 0 ]; then
		echo "update_flash: ipmi inband interface is not working."
		error $E_IPMI "Check 'ipmi_devintf' kernel module loaded or not."
	fi

	echo
	id=`ipmitool fru 2>/dev/null |grep "System Firmware" | cut -d " " -f8 | cut -d ")" -f1`
	if [ "x$id" = "x" ]; then
		error $E_IPMI "Failed to get firmware version."
	fi

	echo "Firmware version:"
	ipmitool fru print $id
}

opp_display_current_fw_version() {
	if [ -d $DT_PATH_FW_NODE ]; then
		opp_display_dt_firmware_version
	else
		opp_display_ipmi_fw_version
	fi

	exit $E_SUCCESS
}


file=""
check_opt=0
display_opt=0
commit_opt=0
reject_opt=0
validate_opt=0
no_overwrite_opt=0
file_opt=0

# Only root user can perform firmware update
[ "`whoami`" = "root" ] || error $E_PERM "Must be root to execute this command."

# Check for platform and call appropriate functions
if [ -d ${DT_PATH}/fsps ]; then
	platform="fsp"
elif [ -d ${DT_PATH}/bmc ]; then
	platform="opp"	# OpenPower platform
else
	echo "update_flash: Unknown platform"
	exit $E_UNSUPPORTED
fi

# Parse command line options
while [ -n "$1" ]; do
	arg="$1"
	shift
	case "$arg" in
	  -q|-l|-D|-S) error $E_USAGE "The $arg option is not implemented.";;
	  -h) usage $E_SUCCESS;;
	  -s) check_opt=1;;
	  -d) display_opt=1;;
	  -c) commit_opt=1;;
	  -r) reject_opt=1;;
	  -v) validate_opt=1;;
	  -n) no_overwrite_opt=1;;
	  -f) file_opt=1; file="$1"; shift;;
	  *) error $E_USAGE "Unknown option ${arg}."
	esac
done

if [ -n "$file" ]; then
	if [ $commit_opt -eq 1 ] || [ $reject_opt -eq 1 ] ||
		[ $display_opt -eq 1 ] || [ $check_opt -eq 1 ]; then
		usage
	elif [ $validate_opt -eq 1 ] && [ $no_overwrite_opt -eq 1 ]; then
		usage
	elif [ $validate_opt -eq 1 ]; then
		${platform}_validate_flash_from_file $file
	else
		${platform}_update_flash_from_file $file
	fi
else
	if [ $check_opt -eq 1 ]; then
		if [ $commit_opt -eq 1 ] || [ $reject_opt -eq 1 ] ||
			[ $display_opt -eq 1 ]; then
			usage
		else
			${platform}_query_flash_support
		fi
	fi

	# Display current firmware version
	if [ $display_opt -eq 1 ]; then
		if [ $commit_opt -eq 1 ] || [ $reject_opt -eq 1 ]; then
			usage
		else
			${platform}_display_current_fw_version
		fi
	fi

	[ $commit_opt -eq 0 ] && [ $reject_opt -eq 0 ] && usage
	[ $commit_opt -eq 1 ] && [ $reject_opt -eq 1 ] && usage
	${platform}_manage_flash $commit_opt
fi
