From e5d9077c395e09010023db36d0e6d6dac9d13241 Mon Sep 17 00:00:00 2001
From: Kyle MacLeod <kyle.macleod@windriver.com>
Date: Tue, 20 Jun 2023 17:47:11 -0400
Subject: [PATCH] Update gen iso scripts for CentOS support in Debian

Update existing dynamic ISO generation scripts to target the last
CentOS release from within a Debian environment.

Changes are applied for gen-prestaged-iso-centos.sh for support
within a Debian instance.

The existing Debian-targeting files (gen-bootloader-iso.sh,
gen-prestaged-iso.sh) are updated for minor fixes in logging,
consistency, and overall code structure for ease of support and
maintenance.

This commit aligns recent changes of gen-bootloader-iso.sh into
gen-bootloader-iso-centos.sh, for easier support going forward.

And finally, unit tests are added for the gen-prestaged-iso* scripts.
The unit tests use shunit2 framework, which is dynamically pulled in as
required to run the tests. At this point, tests are only run manually;
they are not part of the loadbuild. Unit tests may be added for
gen-bootloader-iso.sh in a further commit; however, this is of lesser
priority since this script is employed frequently on subcloud remote
installations.

Test Plan

PASS:
- Test gen-prestaged-iso.sh for generating a prestaged ISO targeting a
  local Debian install. An ISO is generated and a full subcloud
  deployment is successful
- Test gen-prestaged-iso-centos.sh for generating a prestaged ISO
  targeting a local CentOS install. An ISO is generated and a full
  subcloud deployment is successful
- Run the two new unit tests, gen-prestaged-iso-test.sh, and
  gen-prestaged-iso-centos-test.sh, both in standalone and via
  run-tests.sh. Verify with both an empty and populated input directory.
- Verify full remote/redfish Debian subcloud install and deployment.
  Targeted at gen-bootloader-iso.sh changes.
- Verify full remote/redfish CentOS subcloud install and deployment.
  Targeted at gen-bootloader-iso-centos.sh changes.

Story: 2010611
Task: 48267
Depends-On: https://review.opendev.org/c/starlingx/metal/+/886662

Signed-off-by: Kyle MacLeod <kyle.macleod@windriver.com>
Change-Id: I31e76ed107f589b1196320b3c8d7243fb15d3491
---
 .../scripts/gen-bootloader-iso-centos.sh      | 925 +++++++++---------
 .../scripts/gen-bootloader-iso.sh             | 131 +--
 .../scripts/gen-prestaged-iso-centos.sh       | 351 +++----
 .../scripts/gen-prestaged-iso.sh              | 319 +++---
 .../scripts/stx-iso-utils-centos.sh           | 187 ++--
 .../platform-util/scripts/stx-iso-utils.sh    |  22 +-
 .../platform-util/scripts/test/.gitignore     |   3 +
 .../platform-util/scripts/test/README.md      |  24 +
 .../test/gen-prestaged-iso-centos-test.sh     | 300 ++++++
 .../scripts/test/gen-prestaged-iso-test.sh    | 302 ++++++
 .../platform-util/scripts/test/run-tests.sh   |  24 +
 .../scripts/test/shunit2_helper.sh            |  68 ++
 12 files changed, 1760 insertions(+), 896 deletions(-)
 create mode 100644 utilities/platform-util/scripts/test/.gitignore
 create mode 100644 utilities/platform-util/scripts/test/README.md
 create mode 100755 utilities/platform-util/scripts/test/gen-prestaged-iso-centos-test.sh
 create mode 100755 utilities/platform-util/scripts/test/gen-prestaged-iso-test.sh
 create mode 100755 utilities/platform-util/scripts/test/run-tests.sh
 create mode 100644 utilities/platform-util/scripts/test/shunit2_helper.sh

diff --git a/utilities/platform-util/scripts/gen-bootloader-iso-centos.sh b/utilities/platform-util/scripts/gen-bootloader-iso-centos.sh
index 76ed431d..ecda8d8f 100755
--- a/utilities/platform-util/scripts/gen-bootloader-iso-centos.sh
+++ b/utilities/platform-util/scripts/gen-bootloader-iso-centos.sh
@@ -1,4 +1,5 @@
 #!/bin/bash
+# vim: filetype=sh shiftwidth=4 expandtab
 #
 # Copyright (c) 2020-2023 Wind River Systems, Inc.
 #
@@ -12,43 +13,119 @@
 # under an http/https served directory
 #
 #
+readonly SCRIPTDIR=$(readlink -m "$(dirname "$0")")
+readonly SCRIPTNAME=$(basename "$0")
 
 # Source shared utility functions
-source "$(dirname "$0")"/stx-iso-utils-centos.sh
+# shellcheck disable=SC1090 # ignore source warning
+source "$SCRIPTDIR"/stx-iso-utils-centos.sh
 
-declare LOG_TAG=$(basename $0)
+ADDON=
+BASE_URL=
+BOOT_ARGS_COMMON=
+BOOT_GATEWAY=
+BOOT_HOSTNAME=
+BOOT_INTERFACE=
+BOOT_IP=
+BOOT_NETMASK=
+CLEAN_NODE_DIR="no"
+CLEAN_SHARED_DIR="no"
+DEFAULT_GRUB_ENTRY=
+DEFAULT_SYSLINUX_ENTRY=
+DELETE="no"
+GRUB_TIMEOUT=-1
+INPUT_ISO=
+INSTALL_TYPE=
+ISO_VERSION=
+KS_NODETYPE=
+LOCK_FILE=/var/run/.gen-bootloader-iso.lock
+LOCK_TMOUT=600  # Wait up to 10 minutes, by default
+LOG_TAG=$SCRIPTNAME
+NODE_ID=
+NODE_URL=
+OUTPUT_ISO=
+PATCHES_FROM_HOST="yes"
+SCRATCH_DIR=${SCRATCH_DIR:-/scratch}
+TIMEOUT=0
+UPDATE_TIMEOUT="no"
+VERBOSE=${VERBOSE:-}
+VERBOSE_LOG_DIR=/var/log/dcmanager/miniboot
+VERBOSE_OVERRIDE_FILE=/tmp/gen-bootloader-verbose  # turn on verbose if this file is present
+WWW_ROOT_DIR=
 
-function log_error {
-    logger -i -s -t ${LOG_TAG} -- "$@"
+declare -a PARAMS
+
+# Initialized via initialize_and_lock:
+BUILDDIR=
+NODE_DIR=
+NODE_DIR_BASE=
+VERBOSE_RSYNC=
+WORKDIR=
+
+# Initialized by stx-iso-utils-centos.sh:mount_efiboot_img
+EFI_MOUNT=
+
+# Set this to a directory path containing kickstart *.cfg script(s) for testing:
+KICKSTART_OVERRIDE_DIR=${KICKSTART_OVERRIDE_DIR:-/var/miniboot/kickstart-override-centos}
+
+function log_verbose {
+    if [ -n "$VERBOSE" ]; then
+        echo "$@"
+    fi
 }
 
-declare ADDON=
-declare BASE_URL=
-declare BOOT_GATEWAY=
-declare BOOT_HOSTNAME=
-declare BOOT_INTERFACE=
-declare BOOT_IP=
-declare BOOT_NETMASK=
-declare CLEAN_NODE_DIR="no"
-declare CLEAN_SHARED_DIR="no"
-declare DEFAULT_GRUB_ENTRY=
-declare DEFAULT_LABEL=
-declare DEFAULT_SYSLINUX_ENTRY=
-declare DELETE="no"
-declare GRUB_TIMEOUT=-1
-declare INPUT_ISO=
-declare ISO_VERSION=
-declare KS_NODETYPE=
-declare -i LOCK_TMOUT=600 # Wait up to 10 minutes, by default
-declare NODE_ID=
-declare ORIG_PWD=$PWD
-declare OUTPUT_ISO=
-declare -a PARAMS
-declare PATCHES_FROM_HOST="yes"
-declare -i TIMEOUT=0
-declare UPDATE_TIMEOUT="no"
-declare WORKDIR=
-declare WWW_ROOT_DIR=
+function log_info {
+    echo "$@"
+}
+
+function log_error {
+    logger -i -s -t "${LOG_TAG}" -- "ERROR: $*"
+}
+
+function log_warn {
+    logger -i -s -t "${LOG_TAG}" -- "WARN: $*"
+}
+
+function fatal_error {
+    logger -i -s -t "${LOG_TAG}" -- "FATAL: $*"
+    exit 1
+}
+
+function check_rc_exit {
+    local rc=$1
+    shift
+    if [ "$rc" -ne 0 ]; then
+        logger -i -s -t "${LOG_TAG}" -- "FATAL: $* [exit: $rc]"
+        exit "$rc"
+    fi
+}
+
+function get_os {
+    local os
+    os=$(awk -F '=' '/^ID=/ { print $2; }' /etc/os-release)
+    case "$os" in
+        *debian*)
+            echo debian
+            ;;
+        *centos*)
+            echo centos
+            ;;
+        *)
+            echo "$os"
+            ;;
+    esac
+}
+
+function get_path_size {
+    local path=$1
+    du -hs "$path" | awk '{print $1}'
+}
+
+function log_path_size {
+    local path=$1
+    local msg=$2
+    log_info "$msg: $(get_path_size "$path")"
+}
 
 function usage {
     cat <<ENDUSAGE
@@ -118,155 +195,240 @@ EOF
 ENDUSAGE
 }
 
-#
-# Parse cmdline arguments
-#
-LONGOPTS="input:,addon:,param:,default-boot:,timeout:,lock-timeout:,patches-from-iso"
-LONGOPTS="${LONGOPTS},base-url:,www-root:,id:,delete"
-LONGOPTS="${LONGOPTS},boot-gateway:,boot-hostname:,boot-interface:,boot-ip:,boot-netmask:"
-LONGOPTS="${LONGOPTS},help"
-
-OPTS=$(getopt -o h --long "${LONGOPTS}" --name "$0" -- "$@")
-
-if [ $? -ne 0 ]; then
-    usage
-    exit 1
-fi
-
-eval set -- "${OPTS}"
-
-while :; do
-    case "$1" in
-        --input)
-            INPUT_ISO=$2
-            shift 2
-            ;;
-        --addon)
-            ADDON=$2
-            shift 2
-            ;;
-        --boot-gateway)
-            BOOT_GATEWAY=$2
-            shift 2
-            ;;
-        --boot-hostname)
-            BOOT_HOSTNAME=$2
-            shift 2
-            ;;
-        --boot-interface)
-            BOOT_INTERFACE=$2
-            shift 2
-            ;;
-        --boot-ip)
-            BOOT_IP=$2
-            shift 2
-            ;;
-        --boot-netmask)
-            BOOT_NETMASK=$2
-            shift 2
-            ;;
-        --param)
-            PARAMS+=($2)
-            shift 2
-            ;;
-        --default-boot)
-            DEFAULT_LABEL=$2
-            shift 2
-
-            case ${DEFAULT_LABEL} in
-                0)
-                    DEFAULT_SYSLINUX_ENTRY=0
-                    DEFAULT_GRUB_ENTRY="serial"
-                    KS_NODETYPE='controller'
-                    ;;
-                1)
-                    DEFAULT_SYSLINUX_ENTRY=1
-                    DEFAULT_GRUB_ENTRY="graphical"
-                    KS_NODETYPE='controller'
-                    ;;
-                2)
-                    DEFAULT_SYSLINUX_ENTRY=0
-                    DEFAULT_GRUB_ENTRY="serial"
-                    KS_NODETYPE='smallsystem'
-                    ;;
-                3)
-                    DEFAULT_SYSLINUX_ENTRY=1
-                    DEFAULT_GRUB_ENTRY="graphical"
-                    KS_NODETYPE='smallsystem'
-                    ;;
-                4)
-                    DEFAULT_SYSLINUX_ENTRY=0
-                    DEFAULT_GRUB_ENTRY="serial"
-                    KS_NODETYPE='smallsystem_lowlatency'
-                    ;;
-                5)
-                    DEFAULT_SYSLINUX_ENTRY=1
-                    DEFAULT_GRUB_ENTRY="graphical"
-                    KS_NODETYPE='smallsystem_lowlatency'
-                    ;;
-                *)
-                    log_error "Invalid default boot menu option: ${DEFAULT_LABEL}"
-                    usage
-                    exit 1
-                    ;;
-            esac
-            ;;
-        --timeout)
-            let -i timeout_arg=$2
-            shift 2
-
-            if [ ${timeout_arg} -gt 0 ]; then
-                let -i TIMEOUT=${timeout_arg}*10
-                GRUB_TIMEOUT=${timeout_arg}
-            elif [ ${timeout_arg} -eq 0 ]; then
-                GRUB_TIMEOUT=0.001
-            fi
-
-            UPDATE_TIMEOUT="yes"
-            ;;
-        --www-root)
-            WWW_ROOT_DIR=$2
-            shift 2
-            ;;
-        --base-url)
-            BASE_URL=$2
-            shift 2
-            ;;
-        --id)
-            NODE_ID=$2
-            shift 2
-            ;;
-        --lock-timeout)
-            LOCK_TMOUT=$2
-            shift 2
-            if [ $LOCK_TMOUT -le 0 ]; then
-                echo "Lock timeout must be greater than 0" >&2
-                exit 1
-            fi
-            ;;
-        --delete)
-            DELETE="yes"
-            shift
-            ;;
-        --patches-from-iso)
-            PATCHES_FROM_HOST="no"
-            shift
-            ;;
-        --)
-            shift
-            break
-            ;;
-        *)
-            usage
-            exit 1
-            ;;
-    esac
-done
-
 #
 # Functions
 #
 
+function parse_arguments {
+    # Parse cmdline arguments
+    local longopts opts
+    longopts="input:,addon:,param:,default-boot:,timeout:,lock-timeout:,patches-from-iso"
+    longopts="${longopts},base-url:,www-root:,id:,delete"
+    longopts="${longopts},boot-gateway:,boot-hostname:,boot-interface:,boot-ip:,boot-netmask:"
+    longopts="${longopts},verbose,help"
+
+    opts=$(getopt -o h --long "${longopts}" --name "$0" -- "$@")
+    # shellcheck disable=SC2181 # prefer to check exit code:
+    if [ $? -ne 0 ]; then
+        usage
+        exit 1
+    fi
+
+    eval set -- "${opts}"
+
+    while :; do
+        case "$1" in
+            --input)
+                INPUT_ISO=$2
+                shift 2
+                ;;
+            --addon)
+                ADDON=$2
+                shift 2
+                ;;
+            --boot-gateway)
+                BOOT_GATEWAY=$2
+                shift 2
+                ;;
+            --boot-hostname)
+                BOOT_HOSTNAME=$2
+                shift 2
+                ;;
+            --boot-interface)
+                BOOT_INTERFACE=$2
+                shift 2
+                ;;
+            --boot-ip)
+                BOOT_IP=$2
+                shift 2
+                ;;
+            --boot-netmask)
+                BOOT_NETMASK=$2
+                shift 2
+                ;;
+            --param)
+                PARAMS+=($2)
+                shift 2
+                ;;
+            --default-boot)
+                INSTALL_TYPE=$2
+                shift 2
+                case ${INSTALL_TYPE} in
+                    0)
+                        DEFAULT_SYSLINUX_ENTRY=0
+                        DEFAULT_GRUB_ENTRY=serial
+                        KS_NODETYPE='controller'
+                        ;;
+                    1)
+                        DEFAULT_SYSLINUX_ENTRY=1
+                        DEFAULT_GRUB_ENTRY=graphical
+                        KS_NODETYPE='controller'
+                        ;;
+                    2)
+                        DEFAULT_SYSLINUX_ENTRY=0
+                        DEFAULT_GRUB_ENTRY=serial
+                        KS_NODETYPE='smallsystem'
+                        ;;
+                    3)
+                        DEFAULT_SYSLINUX_ENTRY=1
+                        DEFAULT_GRUB_ENTRY=graphical
+                        KS_NODETYPE='smallsystem'
+                        ;;
+                    4)
+                        DEFAULT_SYSLINUX_ENTRY=0
+                        DEFAULT_GRUB_ENTRY=serial
+                        KS_NODETYPE='smallsystem_lowlatency'
+                        ;;
+                    5)
+                        DEFAULT_SYSLINUX_ENTRY=1
+                        DEFAULT_GRUB_ENTRY=graphical
+                        KS_NODETYPE='smallsystem_lowlatency'
+                        ;;
+                    *)
+                        log_error "Invalid default boot menu option: ${INSTALL_TYPE}"
+                        usage
+                        exit 1
+                        ;;
+                esac
+                ;;
+            --timeout)
+                local -i timeout_arg=$2
+                shift 2
+                if [ ${timeout_arg} -gt 0 ]; then
+                    TIMEOUT=$(( timeout_arg * 10 ))
+                    GRUB_TIMEOUT=${timeout_arg}
+                elif [ ${timeout_arg} -eq 0 ]; then
+                    GRUB_TIMEOUT=0.001
+                fi
+                UPDATE_TIMEOUT="yes"
+                ;;
+            --www-root)
+                WWW_ROOT_DIR=$2
+                shift 2
+                ;;
+            --base-url)
+                BASE_URL=$2
+                shift 2
+                ;;
+            --id)
+                NODE_ID=$2
+                shift 2
+                ;;
+            --lock-timeout)
+                local -i LOCK_TMOUT=$2
+                shift 2
+                if [ "${LOCK_TMOUT}" -le 0 ]; then
+                    echo "Lock timeout must be greater than 0" >&2
+                    exit 1
+                fi
+                ;;
+            --delete)
+                DELETE="yes"
+                shift
+                ;;
+            --patches-from-iso)
+                PATCHES_FROM_HOST="no"
+                shift
+                ;;
+            --verbose)
+                VERBOSE=1
+                shift
+                ;;
+            --)
+                shift
+                break
+                ;;
+            *)
+                usage
+                exit 1
+                ;;
+        esac
+    done
+}
+
+function get_lock {
+    # Grab the lock, to protect against simultaneous execution
+    # Open $LOCK_FILE for reading, with assigned file handle 200
+    exec 200>${LOCK_FILE}
+    flock -w "${LOCK_TMOUT}" 200
+    check_rc_exit $? "Failed waiting for lock: ${LOCK_FILE}"
+}
+
+function initialize_and_lock {
+    check_requirements
+
+    # Check mandatory parameters
+    check_required_param "--id" "${NODE_ID}"
+    check_required_param "--www-root" "${WWW_ROOT_DIR}"
+    [ -d "${WWW_ROOT_DIR}" ] || fatal_error "Root directory ${WWW_ROOT_DIR} does not exist"
+
+    [ -f "${VERBOSE_OVERRIDE_FILE}" ] && VERBOSE=1
+    if [ -n "${VERBOSE}" ]; then
+        VERBOSE_RSYNC="--verbose"
+
+        # log all output to file
+        if [ ! -d "$(dirname "${VERBOSE_LOG_DIR}")" ]; then
+            # For testing: the base directory does not exist - use /tmp instead
+            VERBOSE_LOG_DIR=/tmp/miniboot
+        fi
+        [ -d "${VERBOSE_LOG_DIR}" ] || mkdir -p "${VERBOSE_LOG_DIR}"
+        local logfile="${VERBOSE_LOG_DIR}/gen-bootloader-iso-centos-${NODE_ID}.log"
+        [ -f "${logfile}" ] && rm -f "${logfile}"
+        touch "${logfile}"
+        echo "Verbose: logging output to ${logfile}"
+        echo "$(date) Starting $0"
+        printenv >> "${logfile}"
+        exec > >(tee --append "${logfile}") 2>&1
+    fi
+
+    # Initialize dynamic variables
+    NODE_DIR_BASE="${WWW_ROOT_DIR}/nodes"
+    NODE_DIR="${NODE_DIR_BASE}/${NODE_ID}"
+    SHARED_DIR="${WWW_ROOT_DIR}/shared"
+
+    if [ ! -d "$SCRATCH_DIR" ]; then
+        log_warn "SCRATCH_DIR does not exist, using /tmp"
+        SCRATCH_DIR=/tmp
+    fi
+
+    get_lock
+
+    # Check for deletion
+    if [ ${DELETE} = "yes" ]; then
+        handle_delete
+        exit 0
+    fi
+
+    # Handle extraction and setup
+    check_required_param "--input" "${INPUT_ISO}"
+    check_required_param "--default-boot" "${DEFAULT_GRUB_ENTRY}"
+    check_required_param "--base-url" "${BASE_URL}"
+    check_required_param "--boot-ip" "${BOOT_IP}"
+    check_required_param "--boot-interface" "${BOOT_INTERFACE}"
+
+    NODE_URL="${BASE_URL%\/}/nodes/${NODE_ID}"
+
+    if [ ! -f "${INPUT_ISO}" ]; then
+        fatal_error "Input file does not exist: ${INPUT_ISO}"
+    fi
+    if [ -d "${NODE_DIR}" ]; then
+        fatal_error "Output dir already exists: ${NODE_DIR}"
+    fi
+
+    # Run cleanup on any exit
+    trap cleanup_on_exit EXIT
+
+    BUILDDIR=$(mktemp -d -p "${SCRATCH_DIR}" gen_bootloader_build_XXXXXX)
+    if [ -z "${BUILDDIR}" ] || [ ! -d "${BUILDDIR}" ]; then
+        fatal_error "Failed to create builddir: ${BUILDDIR}"
+    fi
+
+    WORKDIR=$(mktemp -d -p "${SCRATCH_DIR}" gen_bootloader_workdir_XXXXXX)
+    if [ -z "${WORKDIR}" ] || [ ! -d "${WORKDIR}" ]; then
+        fatal_error "Failed to create WORKDIR directory: $WORKDIR"
+    fi
+}
+
 function generate_boot_cfg {
     local isodir=$1
 
@@ -277,14 +439,16 @@ function generate_boot_cfg {
     local KS_URL="${NODE_URL}/miniboot_${KS_NODETYPE}.cfg"
     local BOOT_IP_ARG="${BOOT_IP}::${BOOT_GATEWAY}:${BOOT_NETMASK}:${BOOT_HOSTNAME}:${BOOT_INTERFACE}:none"
 
-    local COMMON_ARGS="inst.text inst.gpt boot_device=sda rootfs_device=sda"
-    COMMON_ARGS="${COMMON_ARGS} biosdevname=0 usbcore.autosuspend=-1"
-    COMMON_ARGS="${COMMON_ARGS} security_profile=standard user_namespace.enable=1"
-    COMMON_ARGS="${COMMON_ARGS} inst.repo=${NODE_URL} inst.stage2=${NODE_URL} inst.ks=${KS_URL}"
-    COMMON_ARGS="${COMMON_ARGS} ip=${BOOT_IP_ARG}"
+    BOOT_ARGS_COMMON="inst.text inst.gpt boot_device=sda rootfs_device=sda"
+    BOOT_ARGS_COMMON="${BOOT_ARGS_COMMON} biosdevname=0 usbcore.autosuspend=-1"
+    BOOT_ARGS_COMMON="${BOOT_ARGS_COMMON} security_profile=standard user_namespace.enable=1"
+    BOOT_ARGS_COMMON="${BOOT_ARGS_COMMON} inst.repo=${NODE_URL} inst.stage2=${NODE_URL} inst.ks=${KS_URL}"
+    BOOT_ARGS_COMMON="${BOOT_ARGS_COMMON} ip=${BOOT_IP_ARG}"
 
-    for f in ${isodir}/isolinux.cfg ${isodir}/syslinux.cfg; do
-        cat <<EOF > ${f}
+    log_info "Using boot parameters: ${BOOT_ARGS_COMMON}"
+    log_verbose "Generating isolinux.cfg/syslinux.cfg, default: $DEFAULT_SYSLINUX_ENTRY, timeout: $TIMEOUT"
+    for f in "${isodir}/isolinux.cfg" "${isodir}/syslinux.cfg"; do
+        cat <<EOF > "${f}"
 display splash.cfg
 timeout ${TIMEOUT}
 F1 help.txt
@@ -304,49 +468,54 @@ menu begin
         menu label Serial Console
         kernel vmlinuz
         initrd initrd.img
-        append rootwait ${COMMON_ARGS} console=ttyS0,115200 serial
+        append rootwait ${BOOT_ARGS_COMMON} console=ttyS0,115200 serial
 
     # Graphical Console submenu
     label 1
         menu label Graphical Console
         kernel vmlinuz
         initrd initrd.img
-        append rootwait ${COMMON_ARGS} console=tty0
+        append rootwait ${BOOT_ARGS_COMMON} console=tty0
 menu end
 
 EOF
     done
 
-    for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg; do
-        cat <<EOF > ${f}
+    log_verbose "Generating grub.cfg, install_type: ${INSTALL_TYPE}, default: ${DEFAULT_GRUB_ENTRY}, timeout: ${GRUB_TIMEOUT}"
+    for f in "${isodir}/EFI/BOOT/grub.cfg" "${EFI_MOUNT}/EFI/BOOT/grub.cfg"; do
+        cat <<EOF > "${f}"
 default=${DEFAULT_GRUB_ENTRY}
 timeout=${GRUB_TIMEOUT}
 search --no-floppy --set=root -l 'oe_iso_boot'
 
-menuentry "${NODE_ID}" {
+menuentry "CentOS Miniboot Install ${NODE_ID}" {
     echo " "
 }
 
 menuentry 'Serial Console' --id=serial {
-    linuxefi /vmlinuz ${COMMON_ARGS} console=ttyS0,115200 serial
+    linuxefi /vmlinuz ${BOOT_ARGS_COMMON} console=ttyS0,115200 serial
     initrdefi /initrd.img
 }
 
 menuentry 'Graphical Console' --id=graphical {
-    linuxefi /vmlinuz ${COMMON_ARGS} console=tty0
+    linuxefi /vmlinuz ${BOOT_ARGS_COMMON} console=tty0
     initrdefi /initrd.img
 }
 EOF
-
     done
 }
 
-function cleanup {
+function cleanup_on_exit {
+    # This is invoked from the trap handler.
+    # The last exit code is used to determine if we are exiting
+    # in failed state (non-zero exit), in which case we do the
+    # full cleanup. Disable the warning here since we are
+    # invoked as a trap handler
+    # shellcheck disable=SC2181 # Check exit code directly...
     if [ $? -ne 0 ]; then
-        # Clean up from failure
+        log_info "Cleanup on failure"
         handle_delete
     fi
-
     common_cleanup
 }
 
@@ -356,21 +525,23 @@ function check_requirements {
 
 function handle_delete {
     # Remove node-specific files
-    if [ -d ${NODE_DIR} ]; then
-        rm -rf ${NODE_DIR}
+    if [ -d "${NODE_DIR}" ]; then
+        rm -rf "${NODE_DIR}"
     fi
 
     # If there are no more nodes, cleanup everything else
-    if [ $(ls -A ${NODE_DIR_BASE} 2>/dev/null | wc -l) = 0 ]; then
-        if [ -d ${NODE_DIR_BASE} ]; then
-            rmdir ${NODE_DIR_BASE}
+    # shellcheck disable=SC2012
+    if [ "$(ls -A "${NODE_DIR_BASE}" 2>/dev/null | wc -l)" = 0 ]; then
+        if [ -d "${NODE_DIR_BASE}" ]; then
+            rmdir "${NODE_DIR_BASE}"
         fi
 
-        if [ -d ${SHARED_DIR} ]; then
-            rm -rf ${SHARED_DIR}
+        if [ -d "${SHARED_DIR} "]; then
+            rm -rf "${SHARED_DIR}"
         fi
     fi
 
+    # TODO(kmacleod): do we need this?
     # Mark the DNF cache expired
     dnf clean expire-cache
 }
@@ -378,48 +549,33 @@ function handle_delete {
 function get_patches_from_host {
     local host_patch_repo=/var/www/pages/updates/rel-${ISO_VERSION}
 
-    if [ ! -d ${host_patch_repo} ]; then
+    if [ ! -d "${host_patch_repo}" ]; then
         log_error "Patch repo not found: ${host_patch_repo}"
         # Don't fail, as there could be scenarios where there's nothing on
         # the host related to the release on the ISO
         return
     fi
 
-    mkdir -p ${SHARED_DIR}/patches
-    if [ $? -ne 0 ]; then
-        log_error "Failed to create directory: ${SHARED_DIR}/patches"
-        exit 1
-    fi
+    mkdir -p "${SHARED_DIR}/patches"
+    check_rc_exit $? "Failed to create directory: ${SHARED_DIR}/patches"
 
-    rsync -a ${host_patch_repo}/repodata ${SHARED_DIR}/patches/
-    if [ $? -ne 0 ]; then
-        log_error "Failed to copy ${host_patch_repo}/repodata"
-        exit 1
-    fi
+    rsync -a "${host_patch_repo}/repodata" "${SHARED_DIR}/patches/"
+    check_rc_exit $? "Failed to copy ${host_patch_repo}/repodata"
 
-    if [ -d ${host_patch_repo}/Packages ]; then
-        rsync -a ${host_patch_repo}/Packages ${SHARED_DIR}/patches/
-        if [ $? -ne 0 ]; then
-            log_error "Failed to copy ${host_patch_repo}/Packages"
-            exit 1
-        fi
-    elif [ ! -d ${SHARED_DIR}/patches/Packages ]; then
+    if [ -d "${host_patch_repo}/Packages" ]; then
+        rsync -a "${host_patch_repo}/Packages" "${SHARED_DIR}/patches/"
+        check_rc_exit $? "Failed to copy ${host_patch_repo}/Packages"
+    elif [ ! -d "${SHARED_DIR}/patches/Packages" ]; then
         # Create an empty Packages dir
-        mkdir ${SHARED_DIR}/patches/Packages
-        if [ $? -ne 0 ]; then
-            log_error "Failed to create ${SHARED_DIR}/patches/Packages"
-            exit 1
-        fi
+        mkdir "${SHARED_DIR}/patches/Packages"
+        check_rc_exit $? "Failed to create ${SHARED_DIR}/patches/Packages"
     fi
 
     mkdir -p \
-        ${SHARED_DIR}/patches/metadata/available \
-        ${SHARED_DIR}/patches/metadata/applied \
-        ${SHARED_DIR}/patches/metadata/committed
-    if [ $? -ne 0 ]; then
-        log_error "Failed to create directory: ${SHARED_DIR}/patches/metadata/${state}"
-        exit 1
-    fi
+        "${SHARED_DIR}/patches/metadata/available" \
+        "${SHARED_DIR}/patches/metadata/applied" \
+        "${SHARED_DIR}/patches/metadata/committed"
+    check_rc_exit $? "Failed to create director(ies): ${SHARED_DIR}/patches/metadata/..."
 
     local metadata_to_copy=
     for state in applied committed; do
@@ -429,11 +585,8 @@ function get_patches_from_host {
 
         metadata_to_copy=$(find /opt/patching/metadata/${state} -type f -exec grep -q "<sw_version>${ISO_VERSION}</sw_version>" {} \; -print)
         if [ -n "${metadata_to_copy}" ]; then
-            rsync -a ${metadata_to_copy} ${SHARED_DIR}/patches/metadata/${state}/
-            if [ $? -ne 0 ]; then
-                log_error "Failed to copy ${state} patch metadata"
-                exit 1
-            fi
+            rsync -a "${metadata_to_copy}" "${SHARED_DIR}/patches/metadata/${state}/"
+            check_rc_exit $? "Failed to copy ${state} patch metadata"
         fi
     done
 }
@@ -459,49 +612,34 @@ function extract_pkg_to_workdir {
         return
     fi
 
-    if [ ! -f "${pkgfile}" ]; then
-        log_error "File doesn't exist, unable to extract: ${pkgfile}"
-        exit 1
-    fi
+    [ -f "${pkgfile}" ] || fatal_error "File doesn't exist, unable to extract: ${pkgfile}"
 
-    pushd ${WORKDIR} >/dev/null
-    echo "Extracting files from ${pkgfile}"
+    pushd "${WORKDIR}" >/dev/null
+    log_info "Extracting files from ${pkgfile}"
     rpm2cpio ${pkgfile} | cpio -idmv
-    if [ $? -ne 0 ]; then
-        log_error "Failed to extract files from ${pkgfile}"
-        exit 1
-    fi
+    check_rc_exit $? "Failed to extract files from ${pkgfile}"
     popd >/dev/null
 }
 
 function extract_shared_files {
-    if [ -d ${SHARED_DIR} ]; then
+    if [ -d "${SHARED_DIR}" ]; then
         # If the shared dir already exists, assume we don't need to re-extract
         return
     fi
 
-    mkdir -p ${SHARED_DIR}
-    if [ $? -ne 0 ]; then
-        log_error "Failed to create directory: ${SHARED_DIR}"
-        exit 1
-    fi
+    mkdir -p "${SHARED_DIR}"
+    check_rc_exit $? "Failed to create directory: ${SHARED_DIR}"
 
     # Check ISO content
-    if [ ! -f ${MNTDIR}/LiveOS/squashfs.img ]; then
-        log_error "squashfs.img not found on ${INPUT_ISO}"
-        exit 1
-    fi
+    [ -f "${MNTDIR}/LiveOS/squashfs.img" ] || fatal_error "squashfs.img not found on ${INPUT_ISO}"
 
     # Setup shared patch data
-    if [ ${PATCHES_FROM_HOST} = "yes" ]; then
+    if [ "${PATCHES_FROM_HOST}" = "yes" ]; then
         get_patches_from_host
     else
-        if [ -d ${MNTDIR}/patches ]; then
-            rsync -a ${MNTDIR}/patches/ ${SHARED_DIR}/patches/
-            if [ $? -ne 0 ]; then
-                log_error "Failed to copy patches repo from ${INPUT_ISO}"
-                exit 1
-            fi
+        if [ -d "${MNTDIR}/patches" ]; then
+            rsync -a "${MNTDIR}/patches/" "${SHARED_DIR}/patches/"
+            check_rc_exit $? "Failed to copy patches repo from ${INPUT_ISO}"
         fi
     fi
 
@@ -509,63 +647,53 @@ function extract_shared_files {
     dnf clean expire-cache
 
     local squashfs_img_file=${MNTDIR}/LiveOS/squashfs.img
-    if [ ${PATCHES_FROM_HOST} = "yes" ]; then
+    if [ "${PATCHES_FROM_HOST}" = "yes" ]; then
         extract_pkg_to_workdir 'pxe-network-installer'
 
         local patched_squashfs_img_file=${WORKDIR}/var/www/pages/feed/rel-${ISO_VERSION}/LiveOS/squashfs.img
-        if [ -f ${patched_squashfs_img_file} ]; then
+        if [ -f "${patched_squashfs_img_file}" ]; then
             # Use the patched squashfs.img
             squashfs_img_file=${patched_squashfs_img_file}
         fi
     fi
 
-    mkdir ${SHARED_DIR}/LiveOS
-    rsync -a ${squashfs_img_file} ${SHARED_DIR}/LiveOS/
-    if [ $? -ne 0 ]; then
-        log_error "Failed to copy rootfs: ${patched_squashfs_img_file}"
-        exit 1
+    mkdir "${SHARED_DIR}/LiveOS"
+    rsync -a "${squashfs_img_file}" "${SHARED_DIR}/LiveOS/"
+    check_rc_exit $? "Failed to copy rootfs: ${patched_squashfs_img_file}"
+
+    # The CentOS kickstart files are on the system controller in their own directory.
+    # Copy them into miniboot ISO.
+    [ -f /etc/build.info ] || fatal_error "File /etc/build.info does not exist. Cannot determine software version."
+    source /etc/build.info
+    [ -n "$SW_VERSION" ] || fatal_error "SW_VERSION is not in /etc/build.info. Cannot determine software version."
+    local kickstart_files_dir=/var/www/pages/feed/rel-${SW_VERSION}/kickstart/centos
+
+    mkdir "${SHARED_DIR}/kickstart" || fatal_error "mkdir ${SHARED_DIR}/kickstart failed"
+    rsync -a "${kickstart_files_dir}"/miniboot_*.cfg "${SHARED_DIR}"/kickstart
+    check_rc_exit $? "Failed to copy kickstart files from ${kickstart_files_dir}"
+
+    # Any files in $KICKSTART_OVERRIDE_DIR are used in place of the files from above:
+    if [ -d "${KICKSTART_OVERRIDE_DIR}" ] && \
+            [ "$(echo "${KICKSTART_OVERRIDE_DIR}"/miniboot_*.cfg)" != \
+                "${KICKSTART_OVERRIDE_DIR}/miniboot_*.cfg" ]; then
+        log_info "Copying override cfg files from ${KICKSTART_OVERRIDE_DIR}"
+        cp "${KICKSTART_OVERRIDE_DIR}"/miniboot_*.cfg "${SHARED_DIR}"/kickstart
+        check_rc_exit $? "Failed to copy override kickstart files from ${KICKSTART_OVERRIDE_DIR}"
     fi
 
-    local kickstart_files_dir=${MNTDIR}/
-    if [ ${PATCHES_FROM_HOST} = "yes"  ]; then
-        extract_pkg_to_workdir 'platform-kickstarts'
+    rsync -a "${MNTDIR}/isolinux.cfg" "${SHARED_DIR}/"
+    check_rc_exit $? "Failed to copy isolinux.cfg from ${INPUT_ISO}"
 
-        local patched_kickstart_files_dir=${WORKDIR}/var/www/pages/feed/rel-${ISO_VERSION}
-        if [ -f ${patched_kickstart_files_dir}/miniboot_controller_ks.cfg ]; then
-            # Use the patched kickstart files
-            kickstart_files_dir=${patched_kickstart_files_dir}
-        fi
-    fi
+    rsync -a "${MNTDIR}/Packages/" "${SHARED_DIR}/Packages/"
+    check_rc_exit $? "Failed to copy base packages from ${INPUT_ISO}"
 
-    mkdir ${SHARED_DIR}/kickstart/
-    rsync -a ${kickstart_files_dir}/miniboot_*.cfg ${SHARED_DIR}/kickstart
-    if [ $? -ne 0 ]; then
-        log_error "Failed to copy kickstart files from ${kickstart_files_dir}"
-        exit 1
-    fi
-
-    rsync -a ${MNTDIR}/isolinux.cfg ${SHARED_DIR}/
-    if [ $? -ne 0 ]; then
-        log_error "Failed to copy isolinux.cfg from ${INPUT_ISO}"
-        exit 1
-    fi
-
-    rsync -a ${MNTDIR}/Packages/ ${SHARED_DIR}/Packages/
-    if [ $? -ne 0 ]; then
-        log_error "Failed to copy base packages from ${INPUT_ISO}"
-        exit 1
-    fi
-
-    rsync -a ${MNTDIR}/repodata/ ${SHARED_DIR}/repodata/
-    if [ $? -ne 0 ]; then
-        log_error "Failed to copy base repodata from ${INPUT_ISO}"
-        exit 1
-    fi
+    rsync -a "${MNTDIR}/repodata/" "${SHARED_DIR}/repodata/"
+    check_rc_exit $? "Failed to copy base repodata from ${INPUT_ISO}"
 }
 
 function extract_node_files {
     # Copy files for mini ISO build
-    rsync -a \
+    rsync ${VERBOSE_RSYNC} -a \
         --exclude LiveOS/ \
         --exclude Packages/ \
         --exclude repodata/ \
@@ -576,44 +704,34 @@ function extract_node_files {
         --exclude '*_ks.cfg' \
         --exclude ks.cfg \
         --exclude ks \
-        ${MNTDIR}/ ${BUILDDIR}/
-    rc=$?
-    if [ $rc -ne 0 ]; then
-        log_error "Call to rsync ISO content. Aborting..."
-        exit $rc
-    fi
+        "${MNTDIR}/" "${BUILDDIR}/"
+    check_rc_exit $? "Failed to rsync ISO content from ${MNTDIR} to ${BUILDDIR}"
 
-    if [ ${PATCHES_FROM_HOST} = "yes" ]; then
+    if [ "${PATCHES_FROM_HOST}" = "yes" ]; then
         local patched_initrd_file=${WORKDIR}/pxeboot/rel-${ISO_VERSION}/installer-intel-x86-64-initrd_1.0
         local patched_vmlinuz_file=${WORKDIR}/pxeboot/rel-${ISO_VERSION}/installer-bzImage_1.0
 
         # First, check to see if pxe-network-installer is already extracted.
         # If this is the first setup for this ISO, it will have been extracted
         # during the shared setup, and we don't need to do it again.
-        if [ ! -f ${patched_initrd_file} ]; then
+        if [ ! -f "${patched_initrd_file}" ]; then
             extract_pkg_to_workdir 'pxe-network-installer'
         fi
 
         # Copy patched files, as appropriate
-        if [ -f ${patched_initrd_file} ]; then
-            rsync -a ${patched_initrd_file} ${BUILDDIR}/initrd.img
-            if [ $? -ne 0 ]; then
-                log_error "Failed to copy ${patched_initrd_file}"
-                exit 1
-            fi
+        if [ -f "${patched_initrd_file}" ]; then
+            rsync -a "${patched_initrd_file}" "${BUILDDIR}"/initrd.img
+            check_rc_exit $? "Failed to copy ${patched_initrd_file}"
         fi
 
-        if [ -f ${patched_vmlinuz_file} ]; then
-            rsync -a ${patched_vmlinuz_file} ${BUILDDIR}/vmlinuz
-            if [ $? -ne 0 ]; then
-                log_error "Failed to copy ${patched_vmlinuz_file}"
-                exit 1
-            fi
+        if [ -f "${patched_vmlinuz_file}" ]; then
+            rsync -a "${patched_vmlinuz_file}" "${BUILDDIR}"/vmlinuz
+            check_rc_exit $? "Failed to copy ${patched_vmlinuz_file}"
         fi
     fi
 
     # Setup syslinux and grub cfg files
-    generate_boot_cfg ${BUILDDIR}
+    generate_boot_cfg "${BUILDDIR}"
 
     # Set/update boot parameters
     if [ ${#PARAMS[@]} -gt 0 ]; then
@@ -621,17 +739,13 @@ function extract_node_files {
             param=${p%%=*} # Strip from the first '=' on
             value=${p#*=}  # Strip to the first '='
 
-            update_parameter ${BUILDDIR} "${param}" "${value}"
+            update_parameter "${BUILDDIR}" "${param}" "${value}"
         done
     fi
 
     unmount_efiboot_img
 
-    mkdir -p ${NODE_DIR}
-    if [ $? -ne 0 ]; then
-        log_error "Failed to create ${NODE_DIR}"
-        exit 1
-    fi
+    mkdir -p "${NODE_DIR}" || fatal_error "Failed to create ${NODE_DIR}"
 
     # Setup symlinks to the shared content, which lighttpd can serve
     pushd ${NODE_DIR} >/dev/null
@@ -640,7 +754,8 @@ function extract_node_files {
 
     # Rebuild the ISO
     OUTPUT_ISO=${NODE_DIR}/bootimage.iso
-    mkisofs -o ${OUTPUT_ISO} \
+    log_info "Creating ${OUTPUT_ISO} from BUILDDIR: ${BUILDDIR}"
+    mkisofs -o "${OUTPUT_ISO}" \
         -R -D -A 'oe_iso_boot' -V 'oe_iso_boot' \
         -quiet \
         -b isolinux.bin -c boot.cat -no-emul-boot \
@@ -648,19 +763,19 @@ function extract_node_files {
         -eltorito-alt-boot \
         -e images/efiboot.img \
         -no-emul-boot \
-        ${BUILDDIR}
-
-    isohybrid --uefi ${OUTPUT_ISO}
-    implantisomd5 ${OUTPUT_ISO}
+        "${BUILDDIR}"
+    check_rc_exit $? "mkisofs failed"
 
+    isohybrid --uefi "${OUTPUT_ISO}"
+    check_rc_exit $? "isohybrid failed"
+    implantisomd5 "${OUTPUT_ISO}"
+    check_rc_exit $? "implantisomd5 failed"
+    log_path_size "$OUTPUT_ISO" "Size of bootimage.iso"
     # Setup the kickstart
     local ksfile=${SHARED_DIR}/kickstart/miniboot_${KS_NODETYPE}_ks.cfg
 
-    cp ${ksfile} ${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg
-    if [ $? -ne 0 ]; then
-        log_error "Failed to copy ${ksfile} to ${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg"
-        exit 1
-    fi
+    cp "${ksfile}" "${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg"
+    check_rc_exit $? "Failed to copy ${ksfile} to ${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg"
 
     # Number of dirs in the NODE_URL: Count the / characters, subtracting 2 for http:// or https://
     DIRS=$(($(grep -o "/" <<< "$NODE_URL" | wc -l) - 2))
@@ -671,118 +786,56 @@ function extract_node_files {
     sed -i "s#xxxHTTP_URLxxx#${NODE_URL_SED}#g;
             s#xxxHTTP_URL_PATCHESxxx#${NODE_URL_SED}/patches#g;
             s#NUM_DIRS#${DIRS}#g" \
-        ${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg
+        "${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg"
 
     # Append the custom addon
     if [ -n "${ADDON}" ]; then
-        cat <<EOF >>${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg
+        cat <<EOF >> "${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg"
 
 %post --erroronfail
 
 # Source common functions
 . /tmp/ks-functions.sh
 
-$(cat ${ADDON})
+$(cat "${ADDON}")
 
 %end
 EOF
     fi
 }
 
+function create_miniboot_iso {
+    # Determine release version from ISO
+    [ -f ${MNTDIR}/upgrades/version ] || fatal_error "Version info not found on input ISO: ${INPUT_ISO}"
+    ISO_VERSION=$(source ${MNTDIR}/upgrades/version && echo ${VERSION})
+    if [ -z "${ISO_VERSION}" ]; then
+        fatal_error "Failed to determine version of installation ISO from ${MNTDIR}/upgrades/version"
+    fi
+
+    # Copy the common files from the ISO, if needed
+    extract_shared_files
+
+    # Extract/generate the node-specific files
+    extract_node_files
+}
+
 #
 # Main
 #
-
-# Check script dependencies
-check_requirements
-
-# Validate parameters
-
-# Check mandatory parameters
-
-check_required_param "--id" "${NODE_ID}"
-check_required_param "--www-root" "${WWW_ROOT_DIR}"
-
-declare NODE_DIR_BASE="${WWW_ROOT_DIR}/nodes"
-declare NODE_DIR="${NODE_DIR_BASE}/${NODE_ID}"
-declare SHARED_DIR="${WWW_ROOT_DIR}/shared"
-
-if [ ! -d "${WWW_ROOT_DIR}" ]; then
-    log_error "Root directory ${WWW_ROOT_DIR} does not exist"
-    exit 1
-fi
-
-# Grab the lock, to protect against simultaneous execution
-LOCK_FILE=/var/run/.gen-bootloader-iso.lock
-exec 200>${LOCK_FILE}
-flock -w ${LOCK_TMOUT} 200
-if [ $? -ne 0 ]; then
-    log_error "Failed waiting for lock: ${LOCK_FILE}"
-    exit 1
-fi
-
-# Check for deletion
-if [ ${DELETE} = "yes" ]; then
-    handle_delete
+function main {
+    # if [ "$(get_os)" != centos ]; then
+    #     fatal_error "This script must be invoked on CentOS only"
+    # fi
+    parse_arguments "$@"
+    initialize_and_lock
+    mount_iso "${INPUT_ISO}" "${SCRATCH_DIR}"
+    create_miniboot_iso
+    unmount_iso
     exit 0
+}
+
+# Execute main if script is executed directly (not sourced):
+# This allows for shunit2 testing
+if [[ "${BASH_SOURCE[0]}" = "$0" ]]; then
+    main "$@"
 fi
-
-# Handle extraction and setup
-
-check_required_param "--input" "${INPUT_ISO}"
-check_required_param "--default-boot" "${DEFAULT_GRUB_ENTRY}"
-check_required_param "--base-url" "${BASE_URL}"
-check_required_param "--boot-ip" "${BOOT_IP}"
-check_required_param "--boot-interface" "${BOOT_INTERFACE}"
-
-declare NODE_URL="${BASE_URL%\/}/nodes/${NODE_ID}"
-
-if [ ! -f ${INPUT_ISO} ]; then
-    log_error "Input file does not exist: ${INPUT_ISO}"
-    exit 1
-fi
-
-if [ -d ${NODE_DIR} ]; then
-    log_error "Output dir already exists: ${NODE_DIR}"
-    exit 1
-fi
-
-# Run cleanup on any exit
-trap cleanup EXIT
-
-BUILDDIR=$(mktemp -d -p /scratch gen_bootloader_build_XXXXXX)
-if [ -z "${BUILDDIR}" -o ! -d ${BUILDDIR} ]; then
-    log_error "Failed to create builddir. Aborting..."
-    exit 1
-fi
-
-WORKDIR=$(mktemp -d -p /scratch gen_bootloader_workdir_XXXXXX)
-if [ -z "${WORKDIR}" -o ! -d ${WORKDIR} ]; then
-    log_error "Failed to create builddir. Aborting..."
-    exit 1
-fi
-
-mount_iso ${INPUT_ISO} /scratch
-
-# Determine release version from ISO
-if [ ! -f ${MNTDIR}/upgrades/version ]; then
-    log_error "Version info not found on ${INPUT_ISO}"
-    exit 1
-fi
-
-ISO_VERSION=$(source ${MNTDIR}/upgrades/version && echo ${VERSION})
-if [ -z "${ISO_VERSION}" ]; then
-    log_error "Failed to determine version of installation ISO"
-    exit 1
-fi
-
-# Copy the common files from the ISO, if needed
-extract_shared_files
-
-# Extract/generate the node-specific files
-extract_node_files
-
-unmount_iso
-
-exit 0
-
diff --git a/utilities/platform-util/scripts/gen-bootloader-iso.sh b/utilities/platform-util/scripts/gen-bootloader-iso.sh
index affdccc4..0bbbdb69 100755
--- a/utilities/platform-util/scripts/gen-bootloader-iso.sh
+++ b/utilities/platform-util/scripts/gen-bootloader-iso.sh
@@ -29,18 +29,17 @@ BOOT_HOSTNAME=
 BOOT_INTERFACE=
 BOOT_IP=
 BOOT_NETMASK=
-MINIBOOT_INITRD_FILE=/var/miniboot/initrd-mini  # populated by the loadbuild at this location
 DEFAULT_GRUB_ENTRY=
 DEFAULT_SYSLINUX_ENTRY=
 DELETE="no"
 GRUB_TIMEOUT=-1
 INITRD_FILE=
-INSTALL_TYPE=
-REPACK=no     # REPACK initrd is disabled until signing is fixed
 INPUT_ISO=
+INSTALL_TYPE=
 LOCK_FILE=/var/run/.gen-bootloader-iso.lock
 LOCK_TMOUT=600  # Wait up to 10 minutes, by default
 LOG_TAG=$SCRIPTNAME
+MINIBOOT_INITRD_FILE=/var/miniboot/initrd-mini  # populated by the loadbuild at this location
 NODE_ID=
 OUTPUT_ISO=
 REPACK=yes  # Repack/trim the initrd and kernel images by default
@@ -49,7 +48,6 @@ TIMEOUT=100
 VERBOSE=${VERBOSE:-}
 VERBOSE_LOG_DIR=/var/log/dcmanager/miniboot
 VERBOSE_OVERRIDE_FILE=/tmp/gen-bootloader-verbose  # turn on verbose if this file is present
-WORKDIR=
 WWW_ROOT_DIR=
 XZ_ARGS="--threads=0 -9 --format=lzma"
 
@@ -86,17 +84,6 @@ function log_warn {
     logger -i -s -t "${LOG_TAG}" -- "WARN: $*"
 }
 
-function get_path_size {
-    local path=$1
-    du -hs "$path" | awk '{print $1}'
-}
-
-function log_path_size {
-    local path=$1
-    local msg=$2
-    log_info "$msg: $(get_path_size "$path")"
-}
-
 function fatal_error {
     logger -i -s -t "${LOG_TAG}" -- "FATAL: $*"
     exit 1
@@ -127,6 +114,17 @@ function get_os {
     esac
 }
 
+function get_path_size {
+    local path=$1
+    du -hs "$path" | awk '{print $1}'
+}
+
+function log_path_size {
+    local path=$1
+    local msg=$2
+    log_info "$msg: $(get_path_size "$path")"
+}
+
 function usage {
     cat <<ENDUSAGE
 Description: Sets up a mini bootimage.iso that includes the minimum required to
@@ -224,7 +222,6 @@ function parse_arguments {
                 INPUT_ISO=$2
                 shift 2
                 ;;
-            # TODO: do we need to support --addon for debian?
             --addon)
                 ADDON=$2
                 shift 2
@@ -307,12 +304,12 @@ function parse_arguments {
                 esac
                 ;;
             --timeout)
-                timeout_arg=$2
+                local -i timeout_arg=$2
                 shift 2
-                if [ $(( timeout_arg )) -gt 0 ]; then
+                if [ ${timeout_arg} -gt 0 ]; then
                     TIMEOUT=$(( timeout_arg * 10 ))
                     GRUB_TIMEOUT=${timeout_arg}
-                elif [ $(( timeout_arg )) -eq 0 ]; then
+                elif [ ${timeout_arg} -eq 0 ]; then
                     GRUB_TIMEOUT=0.001
                 fi
                 ;;
@@ -341,9 +338,9 @@ function parse_arguments {
                 shift 2
                 ;;
             --lock-timeout)
-                LOCK_TMOUT=$2
+                local -i LOCK_TMOUT=$2
                 shift 2
-                if [ "$LOCK_TMOUT" -le 0 ]; then
+                if [ "${LOCK_TMOUT}" -le 0 ]; then
                     echo "Lock timeout must be greater than 0" >&2
                     exit 1
                 fi
@@ -387,22 +384,25 @@ function initialize_and_lock {
     check_required_param "--id" "${NODE_ID}"
     check_required_param "--www-root" "${WWW_ROOT_DIR}"
     [ -d "${WWW_ROOT_DIR}" ] || fatal_error "Root directory ${WWW_ROOT_DIR} does not exist"
-    [ -d "${WWW_ROOT_DIR}/iso" ] || mkdir "${WWW_ROOT_DIR}/iso"
 
-    [ -f "$VERBOSE_OVERRIDE_FILE" ] && VERBOSE=1
-    if [ -n "$VERBOSE" ]; then
+    [ -f "${VERBOSE_OVERRIDE_FILE}" ] && VERBOSE=1
+    if [ -n "${VERBOSE}" ]; then
         VERBOSE_RSYNC="--verbose"
         XZ_ARGS="--verbose $XZ_ARGS"
 
         # log all output to file
-        if [ ! -d "$(dirname "$VERBOSE_LOG_DIR")" ]; then
+        if [ ! -d "$(dirname "${VERBOSE_LOG_DIR}")" ]; then
             # For testing: the base directory does not exist - use /tmp instead
             VERBOSE_LOG_DIR=/tmp/miniboot
         fi
-        [ -d "$VERBOSE_LOG_DIR" ] || mkdir -p "$VERBOSE_LOG_DIR"
+        [ -d "${VERBOSE_LOG_DIR}" ] || mkdir -p "${VERBOSE_LOG_DIR}"
         local logfile="${VERBOSE_LOG_DIR}/gen-bootloader-iso-${NODE_ID}.log"
-        echo "Verbose: logging output to $logfile"
-        exec > >(tee "$logfile") 2>&1
+        [ -f "${logfile}" ] && rm -f "${logfile}"
+        touch "${logfile}"
+        echo "Verbose: logging output to ${logfile}"
+        echo "$(date) Starting $0"
+        printenv >> "${logfile}"
+        exec > >(tee --append "${logfile}") 2>&1
     fi
 
     # Initialize dynamic variables
@@ -439,14 +439,14 @@ function initialize_and_lock {
     # Run cleanup on any exit
     trap cleanup_on_exit EXIT
 
-    BUILDDIR=$(mktemp -d -p "$SCRATCH_DIR" gen_bootloader_build_XXXXXX)
+    BUILDDIR=$(mktemp -d -p "${SCRATCH_DIR}" gen_bootloader_build_XXXXXX)
     if [ -z "${BUILDDIR}" ] || [ ! -d "${BUILDDIR}" ]; then
-        fatal_error "Failed to create builddir: $BUILDDIR"
+        fatal_error "Failed to create builddir: ${BUILDDIR}"
     fi
 
-    WORKDIR=$(mktemp -d -p "$SCRATCH_DIR" gen_bootloader_initrd_XXXXXX)
+    WORKDIR=$(mktemp -d -p "${SCRATCH_DIR}" gen_bootloader_initrd_XXXXXX)
     if [ -z "${WORKDIR}" ] || [ ! -d "${WORKDIR}" ]; then
-        fatal_error "Failed to create initrd extract directory: $WORKDIR"
+        fatal_error "Failed to create WORKDIR directory: $WORKDIR"
     fi
 }
 
@@ -468,12 +468,17 @@ function generate_boot_cfg {
         for p in "${PARAMS[@]}"; do
             param=${p%%=*}
             value=${p#*=}
-            # Pull the boot device out of PARAMS and convert to instdev
-            if [ "$param" = "boot_device" ]; then
-                log_info "Setting instdev=$value from boot_device param"
-                instdev=$value
+            if [ "${param}" = "${p}" ]; then
+                # there is no '=' in the parameter; include it directly:
+                PARAM_LIST="${PARAM_LIST} ${param}"
+            else
+                # Pull the boot device out of PARAMS and convert to instdev
+                if [ "$param" = "boot_device" ]; then
+                    log_info "Setting instdev=$value from boot_device param"
+                    instdev=$value
+                fi
+                PARAM_LIST="${PARAM_LIST} ${param}=${value}"
             fi
-            PARAM_LIST="${PARAM_LIST} ${param}=${value}"
         done
     fi
     log_verbose "Parameters: ${PARAM_LIST}"
@@ -557,8 +562,8 @@ LABEL 1
 EOF
     done
 
-    log_verbose "Generating grub.cfg, install_type: $INSTALL_TYPE, default: $DEFAULT_GRUB_ENTRY, timeout: $GRUB_TIMEOUT"
-    for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg; do
+    log_verbose "Generating grub.cfg, install_type: ${INSTALL_TYPE}, default: ${DEFAULT_GRUB_ENTRY}, timeout: ${GRUB_TIMEOUT}"
+    for f in "${isodir}/EFI/BOOT/grub.cfg" "${EFI_MOUNT}/EFI/BOOT/grub.cfg"; do
         cat <<EOF > "${f}"
 default=${DEFAULT_GRUB_ENTRY}
 timeout=${GRUB_TIMEOUT}
@@ -581,13 +586,13 @@ menuentry 'UEFI Graphical Console' --id=graphical {
 }
 EOF
     done
-if [ -n "$VERBOSE" ]; then
-    log_verbose "Contents of ${isodir}/EFI/BOOT/grub.cfg"
-    cat "${isodir}/EFI/BOOT/grub.cfg"
-    log_verbose ""
-    log_verbose "Contents of ${EFI_MOUNT}/EFI/BOOT/grub.cfg"
-    cat "${EFI_MOUNT}/EFI/BOOT/grub.cfg"
-fi
+    if [ -n "$VERBOSE" ]; then
+        log_verbose "Contents of ${isodir}/EFI/BOOT/grub.cfg"
+        cat "${isodir}/EFI/BOOT/grub.cfg"
+        log_verbose ""
+        log_verbose "Contents of ${EFI_MOUNT}/EFI/BOOT/grub.cfg"
+        cat "${EFI_MOUNT}/EFI/BOOT/grub.cfg"
+    fi
 }
 
 function cleanup_on_exit {
@@ -624,28 +629,28 @@ function handle_delete {
 }
 
 function create_miniboot_iso {
-    log_info "Creating minitboot ISO"
+    log_info "Creating miniboot ISO"
     # Copy files for mini ISO build
-    rsync $VERBOSE_RSYNC -a \
+    rsync ${VERBOSE_RSYNC} -a \
           --exclude ostree_repo \
           --exclude pxeboot \
         "${MNTDIR}/" "${BUILDDIR}"
     check_rc_exit $? "Failed to rsync ISO from $MNTDIR to $BUILDDIR"
 
-    if [ "$REPACK" = yes ]; then
+    if [ "${REPACK}" = yes ]; then
         # Use default initrd-mini location if none specified
         # This picks up the initrd-mini file if it is available
         # (included in ISO by the loadbuild). Otherwise we warn
         # and continue without repacking initrd - instead using
         # the original from the ISO.
-        if [ -z "$INITRD_FILE" ]; then
-            INITRD_FILE="$MINIBOOT_INITRD_FILE"
+        if [ -z "${INITRD_FILE}" ]; then
+            INITRD_FILE="${MINIBOOT_INITRD_FILE}"
         fi
-        if [ -f "$INITRD_FILE" ]; then
+        if [ -f "${INITRD_FILE}" ]; then
             if [ -f "${INITRD_FILE}.sig" ]; then
                 # Overwrite the original ISO initrd file:
                 log_info "Repacking miniboot ISO using initrd: ${INITRD_FILE} and ${INITRD_FILE}.sig"
-                cp "$INITRD_FILE" "${BUILDDIR}/initrd"
+                cp "${INITRD_FILE}" "${BUILDDIR}/initrd"
                 check_rc_exit $? "copy initrd failed"
                 cp "${INITRD_FILE}.sig" "${BUILDDIR}/initrd.sig"
                 check_rc_exit $? "copy initrd.sig failed"
@@ -653,10 +658,10 @@ function create_miniboot_iso {
                 log_error "No initrd.sig found at: ${INITRD_FILE}.sig ...skipping initrd repack"
             fi
         else
-            log_warn "Could not find initrd file at $INITRD_FILE ...skipping initrd repack"
+            log_warn "Could not find initrd file at ${INITRD_FILE} ...skipping initrd repack"
         fi
         log_info "Trimming miniboot ISO content"
-        log_path_size "$BUILDDIR" "Size of extracted miniboot before trim"
+        log_path_size "${BUILDDIR}" "Size of extracted miniboot before trim"
         # Remove unused kernel images:
         rm "${BUILDDIR}"/{bzImage,bzImage-rt}
         check_rc_exit $? "failed to trim miniboot iso files"
@@ -668,18 +673,18 @@ function create_miniboot_iso {
     # where any .cfg files are now copied into the /kickstart directory in the ISO
     # Any files in this override directory can replace the files from the ISO copied
     # from the rsync above.
-    if [ -n "$KICKSTART_OVERRIDE_DIR" ] \
-        && [ -d "$KICKSTART_OVERRIDE_DIR" ] \
-        && [ "$(echo "$KICKSTART_OVERRIDE_DIR/"*.cfg)" != "$KICKSTART_OVERRIDE_DIR/*.cfg" ]; then
-        log_info "Copying .cfg files from KICKSTART_OVERRIDE_DIR=$KICKSTART_OVERRIDE_DIR to $BUILDDIR/kickstart"
-        cp "$KICKSTART_OVERRIDE_DIR/"*.cfg "$BUILDDIR/kickstart"
+    if [ -n "${KICKSTART_OVERRIDE_DIR}" ] \
+        && [ -d "${KICKSTART_OVERRIDE_DIR}" ] \
+        && [ "$(echo "${KICKSTART_OVERRIDE_DIR}/"*.cfg)" != "${KICKSTART_OVERRIDE_DIR}/*.cfg" ]; then
+        log_info "Copying .cfg files from KICKSTART_OVERRIDE_DIR=${KICKSTART_OVERRIDE_DIR} to ${BUILDDIR}/kickstart"
+        cp "${KICKSTART_OVERRIDE_DIR}/"*.cfg "${BUILDDIR}/kickstart"
     fi
 
     # Setup syslinux and grub cfg files
     if [ -z "${EFI_MOUNT}" ]; then
         mount_efiboot_img "${BUILDDIR}"
         check_rc_exit $? "failed to mount EFI"
-        log_info "Using EFI_MOUNT=$EFI_MOUNT"
+        log_info "Using EFI_MOUNT=${EFI_MOUNT}"
     fi
     generate_boot_cfg "${BUILDDIR}"
     unmount_efiboot_img
@@ -688,7 +693,7 @@ function create_miniboot_iso {
 
     # Rebuild the ISO
     OUTPUT_ISO=${NODE_DIR}/bootimage.iso
-    log_info "Creating $OUTPUT_ISO from BUILDDIR: ${BUILDDIR}"
+    log_info "Creating ${OUTPUT_ISO} from BUILDDIR: ${BUILDDIR}"
     mkisofs -o "${OUTPUT_ISO}" \
         -A 'instboot' -V 'instboot' \
         -quiet -U -J -joliet-long -r -iso-level 2 \
@@ -718,7 +723,7 @@ function main {
     fi
     parse_arguments "$@"
     initialize_and_lock
-    mount_iso "$INPUT_ISO" "$SCRATCH_DIR"
+    mount_iso "${INPUT_ISO}" "${SCRATCH_DIR}"
     create_miniboot_iso
     unmount_iso
     exit 0
diff --git a/utilities/platform-util/scripts/gen-prestaged-iso-centos.sh b/utilities/platform-util/scripts/gen-prestaged-iso-centos.sh
index 23d61011..431d44c7 100755
--- a/utilities/platform-util/scripts/gen-prestaged-iso-centos.sh
+++ b/utilities/platform-util/scripts/gen-prestaged-iso-centos.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 #
-# Copyright (c) 2021-2022 Wind River Systems, Inc.
+# Copyright (c) 2021-2023 Wind River Systems, Inc.
 #
 # SPDX-License-Identifier: Apache-2.0
 #
@@ -14,26 +14,30 @@
 # exceed 4 GB. Multiple archives can be provided.  All archives
 # must have the suffix 'tar.gz'.
 
-function log_error {
-    echo "$@" >&2
+# shellcheck disable=1091    # don't warn about following 'source <file>'
+# shellcheck disable=2164    # don't warn about pushd/popd failures
+# shellcheck disable=2181    # don't warn about using rc=$?
+
+function log_fatal {
+    echo "$(date "+%F %H:%M:%S") FATAL: $*" >&2
+    exit 1
 }
 
-# Source shared utility functions
-DIR_NAME=$(dirname $0)
-if [ ! -f ${DIR_NAME}/stx-iso-utils-centos.sh ]; then
-    log_error "Unable to find required utility: stx-iso-utils-centos.sh"
-    exit 1
-fi
+function log_error {
+    echo "$(date "+%F %H:%M:%S"): ERROR: $*" >&2
+}
 
-source $(dirname $0)/stx-iso-utils-centos.sh
+function log_info {
+    echo "$(date "+%F %H:%M:%S"): INFO: $*" >&2
+}
 
+# Usage manual.
 function usage {
     cat <<ENDUSAGE
-Utility to convert a StarlingX installation iso into a prestaged subcloud
-installation iso.
+Utility to convert a StarlingX installation iso into a CentOS prestaged subcloud installation iso.
 
 Usage:
-   $(basename $0) --input <input bootimage.iso>
+   $(basename "$0") --input <input bootimage.iso>
                   --output <output bootimage.iso>
                   [ --images <images.tar.gz> ]
                   [ --patch <patch-name.patch> ]
@@ -83,6 +87,7 @@ ENDUSAGE
 }
 
 function cleanup {
+    # This is invoked from the trap handler.
     common_cleanup
 }
 
@@ -94,31 +99,31 @@ function set_default_label {
     local isodir=$1
 
     if [ -z "${EFI_MOUNT}" ]; then
-        mount_efiboot_img ${isodir}
+        mount_efiboot_img "${isodir}"
     fi
 
-    for f in ${isodir}/isolinux.cfg ${isodir}/syslinux.cfg; do
+    for f in "${isodir}/isolinux.cfg" "${isodir}/syslinux.cfg"; do
         if [ "${DEFAULT_LABEL}" = "NULL" ]; then
             # Remove default, if set
-            grep -q '^default' ${f}
+            grep -q '^default' "${f}"
             if [ $? -eq 0 ]; then
-                sed -i '/^default/d' ${f}
+                sed -i '/^default/d' "${f}"
             fi
         else
-            grep -q '^default' ${f}
+            grep -q '^default' "${f}"
             if [ $? -ne 0 ]; then
-                cat <<EOF >> ${f}
+                cat <<EOF >> "${f}"
 
 default ${DEFAULT_SYSLINUX_ENTRY}
 EOF
             else
-                sed -i "s/^default.*/default ${DEFAULT_SYSLINUX_ENTRY}/" ${f}
+                sed -i "s/^default.*/default ${DEFAULT_SYSLINUX_ENTRY}/" "${f}"
             fi
         fi
     done
 
     for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg; do
-        sed -i "s/^default=.*/default=\"${DEFAULT_GRUB_ENTRY}\"/" ${f}
+        sed -i "s/^default=.*/default=\"${DEFAULT_GRUB_ENTRY}\"/" "${f}"
     done
 }
 
@@ -126,27 +131,27 @@ function set_timeout {
     local isodir=$1
 
     if [ -z "${EFI_MOUNT}" ]; then
-        mount_efiboot_img ${isodir}
+        mount_efiboot_img "${isodir}"
     fi
 
     for f in ${isodir}/isolinux.cfg ${isodir}/syslinux.cfg; do
-        sed -i "s/^timeout.*/timeout ${TIMEOUT}/" ${f}
+        sed -i "s/^timeout.*/timeout ${TIMEOUT}/" "${f}"
     done
 
     for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg; do
-        sed -i "s/^timeout=.*/timeout=${GRUB_TIMEOUT}/" ${f}
+        sed -i "s/^timeout=.*/timeout=${GRUB_TIMEOUT}/" "${f}"
 
-        grep -q "^  set timeout=" ${f}
+        grep -q "^  set timeout=" "${f}"
         if [ $? -eq 0 ]; then
             # Submenu timeout is already added. Update the value
-            sed -i -e "s#^  set timeout=.*#  set timeout=${GRUB_TIMEOUT}#" ${f}
+            sed -i -e "s#^  set timeout=.*#  set timeout=${GRUB_TIMEOUT}#" "${f}"
             if [ $? -ne 0 ]; then
                 log_error "Failed to update grub timeout"
                 exit 1
             fi
         else
             # Parameter doesn't exist. Add it to the cmdline
-            sed -i -e "/^submenu/a \ \ set timeout=${GRUB_TIMEOUT}" ${f}
+            sed -i -e "/^submenu/a \ \ set timeout=${GRUB_TIMEOUT}" "${f}"
             if [ $? -ne 0 ]; then
                 log_error "Failed to add grub timeout"
                 exit 1
@@ -171,13 +176,14 @@ function normalized_path {
     local path="${1}"
     local default_fn="${2}"
 
-    local path_name="$(basename "${path}")"
-    local path_dir="$(dirname "${path}")"
+    local path_name path_dir
+    path_name="$(basename "${path}")"
+    path_dir="$(dirname "${path}")"
 
     # If 'path' ends in '/' then path was intended to be a directory
-    if [ "${path:(-1):1}" == "/" ]; then
+    if [ "${path: -1:1}" = "/" ]; then   # Note: space is required after : to distinguish from ${path:-...}
         # Drop the trailing '/'
-        path_dir="${path:0:(-1)}"
+        path_dir="${path:0:-1}"
         path_name="${default_fn}"
     fi
 
@@ -198,7 +204,6 @@ function normalized_path {
     fi
 }
 
-
 function copy_to_iso {
     local src="${1}"
     local dest="${2}"
@@ -208,9 +213,7 @@ function copy_to_iso {
     local default_dest=
     local final_dest=
     local final_dest_dir=
-    local default_md5=
     local final_md5=
-    local final_md5_dir=
 
     if [ -z "${src}" ] || [ -z "${dest}" ]; then
         log_error "Error: copy_to_iso: missing argument"
@@ -227,9 +230,7 @@ function copy_to_iso {
     final_dest="${BUILDDIR}/${dest}"
     final_dest_dir="$(dirname "${final_dest}")"
 
-    if [ ! -z "${md5}" ]; then
-        default_md5="$(basename "${dest}.md5")"
-
+    if [ -n "${md5}" ]; then
         case "${md5}" in
             y | Y | yes | YES )
                 # Use a default name, in same dir as dest
@@ -237,9 +238,7 @@ function copy_to_iso {
                 ;;
         esac
 
-        md5="$(normalized_path "${md5}" "${final_md5}")"
         final_md5="${BUILDDIR}/${md5}"
-        final_md5_dir="$(dirname "${final_md5}")"
     fi
 
     if [ -z "${overwrite}" ] || [ "${overwrite}" == 'n' ]; then
@@ -254,21 +253,14 @@ function copy_to_iso {
         exit 1
     fi
 
-    if [ ! -z "${final_md5_dir}" ]; then
-        if [ ! -d "${final_md5_dir}" ]; then
-            log_error "Error: copy_to_iso: md5 destination directory does not exist '${final_md5_dir}'"
-            exit 1
-        fi
-    fi
-
-    \cp -f "${src}" "${final_dest}"
+    cp -f "${src}" "${final_dest}"
     if [ $? -ne 0 ]; then
         log_error "Error: Failed to copy '${src}' to '${final_dest}'"
         exit 1
     fi
 
-    if [ ! -z "${final_md5}" ]; then
-        pushd ${final_dest_dir} > /dev/null
+    if [ -n "${final_md5}" ]; then
+        pushd "${final_dest_dir}" > /dev/null
             md5sum "$(basename "${final_dest}")" >> "${final_md5}"
         popd > /dev/null
     fi
@@ -371,12 +363,13 @@ function find_in_patch {
     # make sure patch is an absolute path
     patch="$(abspath "${patch}")"
 
-    patchdir=$(mktemp -d -p $PWD updateiso_build_patch_XXXXXX)
+    patchdir=$(mktemp -d -p "${PWD}" updateiso_build_patch_XXXXXX)
     pushd "${patchdir}" > /dev/null
         extract_patch "${patch}"
+        # shellcheck disable=2044
         for rpm in $(find . -name '*.rpm'); do
             if path="$(find_in_rpm "${rpm}" "${target}")"; then
-                found_rpm="$(basename ${rpm})"
+                found_rpm="$(basename "${rpm}")"
                 break
             fi
         done
@@ -411,14 +404,14 @@ function copy_rpm_file_to_iso {
     # make sure patch is an absolute path
     rpm="$(abspath "${rpm}")"
 
-    patchdir=$(mktemp -d -p $PWD updateiso_build_rpm_XXXXXX)
+    patchdir=$(mktemp -d -p "${PWD}" updateiso_build_rpm_XXXXXX)
     pushd "${patchdir}" > /dev/null
-        rpm2cpio "${rpm}" | cpio -imdv ${src}
+        rpm2cpio "${rpm}" | cpio -imdv "${src}"
         if [ $? -ne 0 ]; then
-            log_error "Error: copy_rpm_file_to_iso: extraction error from rpm '$(basename ${rpm})'"
+            log_error "copy_rpm_file_to_iso: extraction error from rpm '$(basename "${rpm}")'"
             rc=1
-        elif [ ! -e ${src} ]; then
-            log_error "Error: copy_rpm_file_to_iso: file '${src}' not found in rpm '$(basename {rpm})'"
+        elif [ ! -e "${src}" ]; then
+            log_error "copy_rpm_file_to_iso: file '${src}' not found in rpm '$(basename "${rpm}")'"
             rc=1
         else
             # we do not need an md5 here, so leaving third argument empty
@@ -453,11 +446,11 @@ function copy_patch_file_to_iso {
     # make sure patch is an absolute path
     patch="$(abspath "${patch}")"
 
-    rpmdir=$(mktemp -d -p $PWD updateiso_build_patch_XXXXXX)
+    rpmdir=$(mktemp -d -p "${PWD}" updateiso_build_patch_XXXXXX)
     pushd "${rpmdir}" > /dev/null
         extract_patch "${patch}"
         if [ ! -f "${rpm}" ]; then
-            log_error "Error: copy_patch_file_to_iso: rpm '${rpm}' not found in patch '$(basename ${patch})'"
+            log_error "copy_patch_file_to_iso: rpm '${rpm}' not found in patch '$(basename "${patch}")'"
             rc=1
         else
             copy_rpm_file_to_iso "${rpm}" "${src}" "${dest}" "${overwrite}"
@@ -474,28 +467,31 @@ function copy_patch_file_to_iso {
 }
 
 function generate_boot_cfg {
+    log_info "Generating boot config"
     local isodir=$1
 
     if [ -z "${EFI_MOUNT}" ]; then
-        mount_efiboot_img ${isodir}
+        mount_efiboot_img "${isodir}"
     fi
 
     local COMMON_ARGS="inst.text inst.gpt boot_device=sda rootfs_device=sda"
     COMMON_ARGS="${COMMON_ARGS} biosdevname=0 usbcore.autosuspend=-1"
     COMMON_ARGS="${COMMON_ARGS} security_profile=standard user_namespace.enable=1"
     COMMON_ARGS="${COMMON_ARGS} inst.stage2=hd:LABEL=${VOLUME_LABEL} inst.ks=hd:LABEL=${VOLUME_LABEL}:/${PRESTAGED_KICKSTART}"
-    if [[ "${FORCE_INSTALL}" == true ]]; then
+    if [ -n "${FORCE_INSTALL}" ]; then
         COMMON_ARGS="${COMMON_ARGS} force_install"
     fi
+    log_info "COMMON_ARGS: ${COMMON_ARGS}"
 
-    for f in ${isodir}/isolinux.cfg ${isodir}/syslinux.cfg; do
-        cat <<EOF > ${f}
+    for f in "${isodir}/isolinux.cfg" "${isodir}/syslinux.cfg"; do
+        cat <<EOF > "${f}"
 display splash.cfg
 timeout ${TIMEOUT}
 F1 help.txt
 F2 devices.txt
 F3 splash.cfg
 serial 0 115200
+
 ui vesamenu.c32
 menu background   #ff555555
 default ${DEFAULT_SYSLINUX_ENTRY}
@@ -521,7 +517,7 @@ menu end
 EOF
     done
     for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg; do
-        cat <<EOF > ${f}
+        cat <<EOF > "${f}"
 default=${DEFAULT_GRUB_ENTRY}
 timeout=${GRUB_TIMEOUT}
 search --no-floppy --set=root -l '${VOLUME_LABEL}'
@@ -540,34 +536,46 @@ menuentry 'Graphical Console' --id=graphical {
     initrdefi /initrd.img
 }
 EOF
-
     done
 }
 
+# Source shared utility functions
+DIR_NAME=$(dirname "$0")
+if [ ! -e "${DIR_NAME}"/stx-iso-utils-centos.sh ]; then
+    echo  "${DIR_NAME}/stx-iso-utils-centos.sh does not exist" >&2
+    exit 1
+else
+    source "${DIR_NAME}"/stx-iso-utils-centos.sh
+fi
+
+# Required variables
 declare INPUT_ISO=
 declare OUTPUT_ISO=
 declare -a IMAGES
-declare ORIG_PWD=$PWD
 declare KS_SETUP=
 declare KS_ADDON=
+declare UPDATE_TIMEOUT="no"
+declare -i FOREVER_GRUB_TIMEOUT=-1
+declare -i DEFAULT_GRUB_TIMEOUT=30
+declare -i DEFAULT_TIMEOUT=$(( DEFAULT_GRUB_TIMEOUT*10 ))
+declare -i TIMEOUT=${DEFAULT_TIMEOUT}
+declare -i GRUB_TIMEOUT=${DEFAULT_GRUB_TIMEOUT}
 declare -a PARAMS
 declare -a PATCHES
 declare -a KICKSTART_PATCHES
 declare DEFAULT_LABEL=
 declare DEFAULT_SYSLINUX_ENTRY=1
 declare DEFAULT_GRUB_ENTRY="graphical"
-declare UPDATE_TIMEOUT="no"
-declare FOREVER_GRUB_TIMEOUT=-1
-declare DEFAULT_GRUB_TIMEOUT=30
-declare -i DEFAULT_TIMEOUT=(DEFAULT_GRUB_TIMEOUT*10)
-declare -i TIMEOUT=${DEFAULT_TIMEOUT}
-declare GRUB_TIMEOUT=${DEFAULT_GRUB_TIMEOUT}
+declare FORCE_INSTALL=
 declare PLATFORM_ROOT="opt/platform-backup"
 declare MD5_FILE="container-image.tar.gz.md5"
 declare VOLUME_LABEL="oe_prestaged_iso_boot"
 declare PRESTAGED_KICKSTART="prestaged_installer_ks.cfg"
 declare MENU_NAME="Prestaged Local Installer"
-declare FORCE_INSTALL=false
+
+###############################################################################
+# Get the command line arguments.
+###############################################################################
 
 SHORTOPTS="";    LONGOPTS=""
 SHORTOPTS+="i:"; LONGOPTS+="input:,"
@@ -583,17 +591,26 @@ SHORTOPTS+="I:"; LONGOPTS+="images:,"
 SHORTOPTS+="f";  LONGOPTS+="force-install,"
 SHORTOPTS+="h";  LONGOPTS+="help"
 
+declare -i rc
 OPTS=$(getopt -o "${SHORTOPTS}" --long "${LONGOPTS}" --name "$0" -- "$@")
-
 if [ $? -ne 0 ]; then
     usage
-    exit 1
+    log_fatal "Options to $0 not properly parsed"
 fi
 
 eval set -- "${OPTS}"
 
+if [ $# = 1 ]; then
+    usage
+    log_fatal "No arguments were provided"
+fi
+
 while :; do
     case $1 in
+        -h | --help)
+            usage
+            exit 0
+            ;;
         -i | --input)
             INPUT_ISO=$2
             shift 2
@@ -611,24 +628,27 @@ while :; do
             shift 2
             ;;
         -p | --param)
+            # shellcheck disable=2206
             PARAMS+=(${2//,/ })
             shift 2
             ;;
         -P | --patch)
+            # shellcheck disable=2206
             PATCHES+=(${2//,/ })
             shift 2
             ;;
         -K | --kickstart-patch)
+            # shellcheck disable=2206
             KICKSTART_PATCHES+=(${2//,/ })
             shift 2
             ;;
         -I | --images)
+            # shellcheck disable=2206
             IMAGES+=(${2//,/ })
             shift 2
             ;;
         -d | --default-boot)
             DEFAULT_LABEL=${2}
-
             case ${DEFAULT_LABEL} in
                 0)
                     DEFAULT_SYSLINUX_ENTRY=0
@@ -639,18 +659,16 @@ while :; do
                     DEFAULT_GRUB_ENTRY="graphical"
                     ;;
                 *)
-                    log_error "Invalid default boot menu option: ${DEFAULT_LABEL}"
                     usage
-                    exit 1
+                    log_fatal "Invalid default boot menu option: ${DEFAULT_LABEL}"
                     ;;
             esac
-
             shift 2
             ;;
         -t | --timeout)
-            let -i timeout_arg=${2}
-            if [ ${timeout_arg} -gt 0 ]; then
-                let -i TIMEOUT=${timeout_arg}*10
+            declare -i timeout_arg=${2}
+            if [ "${timeout_arg}" -gt 0 ]; then
+                TIMEOUT=$(( timeout_arg * 10 ))
                 GRUB_TIMEOUT=${timeout_arg}
             elif [ ${timeout_arg} -eq 0 ]; then
                 TIMEOUT=0
@@ -659,7 +677,6 @@ while :; do
                 TIMEOUT=0
                 GRUB_TIMEOUT=${FOREVER_GRUB_TIMEOUT}
             fi
-
             UPDATE_TIMEOUT="yes"
             shift 2
             ;;
@@ -673,111 +690,109 @@ while :; do
             ;;
         *)
             usage
-            exit 1
+            log_fatal "Unexpected argument: $*"
             ;;
     esac
 done
 
-if [ $# -ne 0 ]; then
-    log_error "Error: Unexpected arguments: $@"
-    usage
-    exit 1
-fi
 
+###############################################################################
+# Generate prestage iso.
+#
+###############################################################################
+
+log_info "Checking system requirements"
 check_requirements
 
+## Check for mandatory parameters
 check_required_param "--input" "${INPUT_ISO}"
 check_required_param "--output" "${OUTPUT_ISO}"
 
+# shellcheck disable=2068
 check_files_exist ${INPUT_ISO} ${IMAGES[@]} ${PATCHES[@]} ${KICKSTART_PATCHES[@]} ${KS_SETUP} ${KS_ADDON}
+# shellcheck disable=2068
 check_files_size  ${INPUT_ISO} ${IMAGES[@]} ${PATCHES[@]} ${KICKSTART_PATCHES[@]} ${KS_SETUP} ${KS_ADDON}
 
-if [ -f ${OUTPUT_ISO} ]; then
-    log_error "Output file already exists: ${OUTPUT_ISO}"
-    exit 1
+if [ -e "${OUTPUT_ISO}" ]; then
+    log_fatal "${OUTPUT_ISO} exists. Delete before you execute this script."
 fi
 
-shift $((OPTIND-1))
-
+## Catch Control-C and handle.
 trap cleanup EXIT
 
-BUILDDIR=$(mktemp -d -p $PWD updateiso_build_XXXXXX)
-if [ -z "${BUILDDIR}" -o ! -d ${BUILDDIR} ]; then
-    log_error "Failed to create builddir. Aborting..."
-    exit $rc
+# Create a temporary build directory.
+BUILDDIR=$(mktemp -d -p "${PWD}" updateiso_build_XXXXXX)
+if [ -z "${BUILDDIR}" ] || [ ! -d "${BUILDDIR}" ]; then
+    log_fatal "Failed to create builddir. Aborting..."
 fi
-
-mount_iso ${INPUT_ISO}
+log_info "Using BUILDDIR=${BUILDDIR}"
+mount_iso "${INPUT_ISO}"
 
 #
 # prestaging kickstart
 #
 
-# Verify prestaging kickstart is present, and where.  An original 21.05 iso
-# won't have the kickstart.  It must be provided by a patch.
-# KICKSTART_PATCHES take presedence over a platform PATCHES, which in
-# turn take presedence over any content from the ISO.
-PRESTAGED_KICKSTART_FOUND_IN=""
-PRESTAGED_KICKSTART_PATH=""
+# Verify prestaging kickstart is present, and where.
+# KICKSTART_PATCHES take precedence over a platform PATCHES, which in
+# turn take precedence over any content from the ISO.
+PRESTAGED_KICKSTART_PATCH=
+PRESTAGED_KICKSTART_PATH=
 
-# scan patches last to first.
-for PATCH in $(printf '%s\n' "${KICKSTART_PATCHES[@]}" | tac); do
-    if PRESTAGED_KICKSTART_PATH="$(find_in_patch "${PATCH}" "${PRESTAGED_KICKSTART}")" ; then
-        PRESTAGED_KICKSTART_FOUND_IN="${PATCH}"
+# Scan KICKSTART_PATCHES last to first.
+for patch in $(printf '%s\n' "${KICKSTART_PATCHES[@]}" | tac); do
+    if PRESTAGED_KICKSTART_PATH="$(find_in_patch "${patch}" "${PRESTAGED_KICKSTART}")" ; then
+        PRESTAGED_KICKSTART_PATCH="${patch}"
         break
     fi
 done
 
-# scan patches last to first.   We want to prefer the most recent patch.
+# Scan PATCHES last to first. Prefer the most recent patch.
 # Assumes patches will be listed in order 0001, 0002, .... when given as args.
-if [ -z "${PRESTAGED_KICKSTART_FOUND_IN}" ]; then
-    for PATCH in $(printf '%s\n' "${PATCHES[@]}" | tac); do
-        if PRESTAGED_KICKSTART_PATH="$(find_in_patch "${PATCH}" "${PRESTAGED_KICKSTART}")" ; then
-            PRESTAGED_KICKSTART_FOUND_IN="${PATCH}"
+if [ -z "${PRESTAGED_KICKSTART_PATCH}" ]; then
+    for patch in $(printf '%s\n' "${PATCHES[@]}" | tac); do
+        if PRESTAGED_KICKSTART_PATH="$(find_in_patch "${patch}" "${PRESTAGED_KICKSTART}")" ; then
+            PRESTAGED_KICKSTART_PATCH="${patch}"
             break
         fi
     done
 fi
 
-if [ -z "${PRESTAGED_KICKSTART_FOUND_IN}" ]; then
+if [ -z "${PRESTAGED_KICKSTART_PATCH}" ]; then
     if PRESTAGED_KICKSTART_PATH="$(find_in_mounted_iso "${PRESTAGED_KICKSTART}")" ; then
-        PRESTAGED_KICKSTART_FOUND_IN="iso"
+        log_info "Using ${PRESTAGED_KICKSTART} from original ISO"
+    else
+        log_fatal "Failed to find required file '${PRESTAGED_KICKSTART}' in the supplied iso and patches."
     fi
 fi
 
-if [ -z "${PRESTAGED_KICKSTART_FOUND_IN}" ]; then
-    log_error "Failed to find required file '${PRESTAGED_KICKSTART}' in the supplied iso and patches."
-    exit 1
-fi
-
 #
 # Determine release version from ISO
 #
-if [ ! -f ${MNTDIR}/upgrades/version ]; then
+if [ ! -f "${MNTDIR}"/upgrades/version ]; then
     log_error "Version info not found on ${INPUT_ISO}"
     exit 1
 fi
 
-ISO_VERSION=$(source ${MNTDIR}/upgrades/version && echo ${VERSION})
+ISO_VERSION=$(source "${MNTDIR}/upgrades/version" && echo "${VERSION}")
 if [ -z "${ISO_VERSION}" ]; then
     log_error "Failed to determine version of installation ISO"
     exit 1
 fi
 
+# Copy the contents of the input iso to the build directory.
 
-#
-# copy content
-#
-rsync -a ${MNTDIR}/ ${BUILDDIR}/
+log_info "Copying input ISO"
+rsync -av "${MNTDIR}/" "${BUILDDIR}/"
 rc=$?
-if [ $rc -ne 0 ]; then
-    log_error "Call to rsync ISO content. Aborting..."
-    exit $rc
+if [ "${rc}" -ne 0 ]; then
+    unmount_iso
+    log_fatal "Unable to rsync content from the ISO: Error rc=${rc}"
 fi
 
-# copy kickstart
-if [ "${PRESTAGED_KICKSTART_FOUND_IN}" != "iso" ]; then
-    copy_patch_file_to_iso "${PRESTAGED_KICKSTART_FOUND_IN}" "${PRESTAGED_KICKSTART_PATH%%:*}" "${PRESTAGED_KICKSTART_PATH##*:}" "/" "y"
+# Copy kickstart if it is anywhere outside of the mounted ISO (otherwise it will already have been copied by the above)
+if [ -n "${PRESTAGED_KICKSTART_PATCH}" ]; then
+    log_info "Prestaging kickstart from ${PRESTAGED_KICKSTART_PATCH}"
+    copy_patch_file_to_iso "${PRESTAGED_KICKSTART_PATCH}" "${PRESTAGED_KICKSTART_PATH%%:*}" "${PRESTAGED_KICKSTART_PATH##*:}" "/" "y"
 fi
 
 unmount_iso
@@ -785,31 +800,31 @@ unmount_iso
 #
 # Setup syslinux and grub cfg files
 #
-generate_boot_cfg ${BUILDDIR}
+generate_boot_cfg "${BUILDDIR}"
 
 #
 # Set/update boot parameters
 #
+log_info "Updating boot parameters"
 if [ ${#PARAMS[@]} -gt 0 ]; then
-    for p in ${PARAMS[@]}; do
+    for p in "${PARAMS[@]}"; do
         param=${p%%=*} # Strip from the first '=' on
         value=${p#*=}  # Strip to the first '='
-
-        update_parameter ${BUILDDIR} "${param}" "${value}"
+        update_parameter "${BUILDDIR}" "${param}" "${value}"
     done
 fi
 
 if [ -n "${DEFAULT_LABEL}" ]; then
-    set_default_label ${BUILDDIR}
+    set_default_label "${BUILDDIR}"
 fi
 
 if [ "${UPDATE_TIMEOUT}" = "yes" ]; then
-    set_timeout ${BUILDDIR}
+    set_timeout "${BUILDDIR}"
 fi
 
 if [ -n "${KS_SETUP}" ]; then
-    \rm -f ${BUILDDIR}/ks-setup.cfg
-    \cp ${KS_SETUP} ${BUILDDIR}/ks-setup.cfg
+    \rm -f "${BUILDDIR}"/ks-setup.cfg
+    \cp "${KS_SETUP}" "${BUILDDIR}"/ks-setup.cfg
     if [ $? -ne 0 ]; then
         log_error "Error: Failed to copy ${KS_SETUP}"
         exit 1
@@ -817,8 +832,8 @@ if [ -n "${KS_SETUP}" ]; then
 fi
 
 if [ -n "${KS_ADDON}" ]; then
-    \rm -f ${BUILDDIR}/ks-addon.cfg
-    \cp ${KS_ADDON} ${BUILDDIR}/ks-addon.cfg
+    \rm -f "${BUILDDIR}"/ks-addon.cfg
+    \cp "${KS_ADDON}" "${BUILDDIR}"/ks-addon.cfg
     if [ $? -ne 0 ]; then
         log_error "Error: Failed to copy ${KS_ADDON}"
         exit 1
@@ -836,32 +851,36 @@ unmount_efiboot_img
 PLATFORM_PATH="${PLATFORM_ROOT}/${ISO_VERSION}"
 mkdir_on_iso "${PLATFORM_PATH}"
 
-INPUT_ISO_NAME="$(basename  "${INPUT_ISO}")"
+INPUT_ISO_NAME="$(basename "${INPUT_ISO}")"
 copy_to_iso "${INPUT_ISO}" "${PLATFORM_PATH}/${INPUT_ISO_NAME}"  "${PLATFORM_PATH}/${INPUT_ISO_NAME/%.iso/.md5}"
 
-for PATCH in ${PATCHES[@]}; do
-    copy_to_iso "${PATCH}" "${PLATFORM_PATH}/"
-done
+if [ -n "${PATCHES[*]}" ]; then
+    log_info "Including patches: ${PATCHES[*]}"
+    for patch in "${PATCHES[@]}"; do
+        copy_to_iso "${patch}" "${PLATFORM_PATH}/"
+    done
+fi
 
-for IMAGE in ${IMAGES[@]}; do
-    copy_to_iso "${IMAGE}" "${PLATFORM_PATH}/" "${PLATFORM_PATH}/${MD5_FILE}"
-done
+if [ -n "${IMAGES[*]}" ]; then
+    log_info "Including images: ${IMAGES[*]}"
+    for IMAGE in "${IMAGES[@]}"; do
+        copy_to_iso "${IMAGE}" "${PLATFORM_PATH}/" "${PLATFORM_PATH}/${MD5_FILE}"
+    done
+fi
 
-#
-# Rebuild the ISO
-#
-mkisofs -o ${OUTPUT_ISO} \
-    -R -D -A "${VOLUME_LABEL}" -V "${VOLUME_LABEL}" \
-    -quiet \
-    -b isolinux.bin -c boot.cat -no-emul-boot \
-    -boot-load-size 4 -boot-info-table \
-    -eltorito-alt-boot \
-    -e images/efiboot.img \
-    -no-emul-boot \
-    ${BUILDDIR}
+#  we are ready to create the prestage iso.
+log_info "Creating ${OUTPUT_ISO}"
+mkisofs -o "${OUTPUT_ISO}" \
+        -R -D -A "${VOLUME_LABEL}" -V "${VOLUME_LABEL}" \
+        -quiet \
+        -b isolinux.bin -c boot.cat -no-emul-boot \
+        -boot-load-size 4 -boot-info-table \
+        -eltorito-alt-boot \
+        -e images/efiboot.img \
+        -no-emul-boot \
+        "${BUILDDIR}"
 
-isohybrid --uefi ${OUTPUT_ISO}
-implantisomd5 ${OUTPUT_ISO}
-
-echo "Created ISO: ${OUTPUT_ISO}"
+isohybrid --uefi "${OUTPUT_ISO}"
+implantisomd5 "${OUTPUT_ISO}"
 
+log_info "Prestage ISO created successfully: ${OUTPUT_ISO}"
diff --git a/utilities/platform-util/scripts/gen-prestaged-iso.sh b/utilities/platform-util/scripts/gen-prestaged-iso.sh
index bdd6d082..906afd2c 100755
--- a/utilities/platform-util/scripts/gen-prestaged-iso.sh
+++ b/utilities/platform-util/scripts/gen-prestaged-iso.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 #
-# Copyright (c) 2022 Wind River Systems, Inc.
+# Copyright (c) 2022-2023 Wind River Systems, Inc.
 #
 # SPDX-License-Identifier: Apache-2.0
 #
@@ -14,29 +14,34 @@
 # exceed 4 GB. Multiple archives can be provided.  All archives
 # must have the suffix 'tar.gz'.
 
+# shellcheck disable=1091    # don't warn about following 'source <file>'
+# shellcheck disable=2164    # don't warn about pushd/popd failures
+# shellcheck disable=2181    # don't warn about using rc=$?
 
-# Error log print
 function log_fatal {
-    echo "ERROR: $@" >&2 && exit 1
+    echo "$(date "+%F %H:%M:%S") FATAL: $*" >&2
+    exit 1
 }
 
 function log_error {
-    echo "ERROR: $@" >&2
+    echo "$(date "+%F %H:%M:%S"): ERROR: $*" >&2
 }
 
-# Info log
-function log {
-    echo "INFO: $@" >&2
+function log_warn {
+    echo "$(date "+%F %H:%M:%S"): WARN: $*" >&2
+}
+
+function log_info {
+    echo "$(date "+%F %H:%M:%S"): INFO: $*" >&2
 }
 
 # Usage manual.
 function usage {
     cat <<ENDUSAGE
-Utility to convert a StarlingX installation iso into a Debian prestaged
-subcloud installation iso.
+Utility to convert a StarlingX installation iso into a Debian prestaged subcloud installation iso.
 
 Usage:
-   $(basename $0) --input <input bootimage.iso>
+   $(basename "$0") --input <input bootimage.iso>
                   --output <output bootimage.iso>
                   [ --images <images.tar.gz> ]
                   [ --patch <patch-name.patch> ]
@@ -110,13 +115,14 @@ function normalized_path {
     local path="${1}"
     local default_fn="${2}"
 
-    local path_name="$(basename "${path}")"
-    local path_dir="$(dirname "${path}")"
+    local path_name path_dir
+    path_name="$(basename "${path}")"
+    path_dir="$(dirname "${path}")"
 
     # If 'path' ends in '/' then path was intended to be a directory
-    if [ "${path:(-1):1}" == "/" ]; then
+    if [ "${path: -1:1}" == "/" ]; then   # Note: space is required after : to distinguish from ${path:-...}
         # Drop the trailing '/'
-        path_dir="${path:0:(-1)}"
+        path_dir="${path:0:-1}"
         path_name="${default_fn}"
     fi
 
@@ -147,7 +153,6 @@ function copy_to_iso {
     local final_dest=
     local final_dest_dir=
     local final_md5=
-    local final_md5_dir=
 
     if [ -z "${src}" ] || [ -z "${dest}" ]; then
         log_error "Error: copy_to_iso: missing argument"
@@ -164,8 +169,7 @@ function copy_to_iso {
     final_dest="${BUILDDIR}/${dest}"
     final_dest_dir="$(dirname "${final_dest}")"
 
-    if [ ! -z "${md5}" ]; then
-
+    if [ -n "${md5}" ]; then
         case "${md5}" in
             y | Y | yes | YES )
                 # Use a default name, in same dir as dest
@@ -194,47 +198,47 @@ function copy_to_iso {
         exit 1
     fi
 
-    if [ ! -z "${final_md5}" ]; then
-        pushd ${final_dest_dir} > /dev/null
+    if [ -n "${final_md5}" ]; then
+        pushd "${final_dest_dir}" > /dev/null
             md5sum "$(basename "${final_dest}")" >> "${final_md5}"
         popd > /dev/null
     fi
 }
 
 function generate_boot_cfg {
+    log_info "Generating boot config"
     local isodir=$1
 
     if [ -z "${EFI_MOUNT}" ]; then
-        mount_efiboot_img ${isodir}
+        mount_efiboot_img "${isodir}"
     fi
 
     local PARAM_LIST=
-    log "Generating prestage.iso from params: ${PARAMS[*]}"
     # Set/update boot parameters
     if [ ${#PARAMS[@]} -gt 0 ]; then
+        log_info "Pre-parsing params: ${PARAMS[*]}"
         for p in "${PARAMS[@]}"; do
             param=${p%%=*}
             value=${p#*=}
             # Pull the boot device out of PARAMS and convert to instdev
-            if [[ "${param}" == "boot_device" ]]; then
-                log "Setting instdev=${value} from boot_device param"
+            if [ "${param}" = "boot_device" ]; then
+                log_info "Setting instdev=${value} from boot_device param"
                 instdev=${value}
-            elif [[ "${param}" == "rootfs_device" ]]; then
-                log "Setting instdev=${value} from boot_device param"
+            elif [ "${param}" = "rootfs_device" ]; then
+                log_info "Setting instdev=${value} from boot_device param"
                 instdev=${value}
             fi
 
             PARAM_LIST="${PARAM_LIST} ${param}=${value}"
         done
+        log_info "Using parameters: ${PARAM_LIST}"
     fi
 
-    log "Parameters: ${PARAM_LIST}"
-
     if [[ "${KS_PATCH}" == "true" ]]; then
-        log "Setting Kickstart patch from the kickstart_patches directory"
+        log_info "Setting Kickstart patch from the kickstart_patches directory"
         ks="${KICKSTART_PATCH_DIR}"/kickstart.cfg
     else
-        log "Setting Kickstart patch from the kickstart directory"
+        log_info "Setting Kickstart patch from the kickstart directory"
         ks=kickstart/kickstart.cfg
     fi
 
@@ -248,14 +252,14 @@ function generate_boot_cfg {
     COMMON_ARGS="${COMMON_ARGS} inst_ostree_var=/dev/mapper/cgts--vg-var--lv"
     COMMON_ARGS="${COMMON_ARGS} defaultkernel=vmlinuz*[!t]-amd64"
 
-    if [[ -n "${FORCE_INSTALL}" ]]; then
+    if [ -n "${FORCE_INSTALL}" ]; then
         COMMON_ARGS="${COMMON_ARGS} force_install"
     fi
 
     # Uncomment for LAT debugging:
     #COMMON_ARGS="${COMMON_ARGS} instsh=2"
     COMMON_ARGS="${COMMON_ARGS} ${PARAM_LIST}"
-    log "COMMON_ARGS: $COMMON_ARGS"
+    log_info "COMMON_ARGS: ${COMMON_ARGS}"
 
     for f in ${isodir}/isolinux/isolinux.cfg; do
         cat <<EOF > "${f}"
@@ -283,8 +287,7 @@ LABEL 1
     append ${COMMON_ARGS} traits=controller console=tty0
 
 EOF
-done
-
+    done
     for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg; do
         cat <<EOF > "${f}"
 default=${DEFAULT_GRUB_ENTRY}
@@ -323,18 +326,19 @@ function generate_ostree_checkum {
     fi
     (
         # subshell:
-        log "Calculating new checksum for ostree_repo at ${dest_dir}"
+        log_info "Calculating new checksum for ostree_repo at ${dest_dir}"
         cd "${dest_dir}" || log_fatal "generate_ostree_checkum: cd ${dest_dir} failed"
         find ostree_repo -type f -exec md5sum {} + | LC_ALL=C sort | md5sum | awk '{ print $1; }' \
             > .ostree_repo_checksum
-        log "ostree_repo checksum: $(cat .ostree_repo_checksum)"
+        log_info "ostree_repo checksum: $(cat .ostree_repo_checksum)"
     )
 }
 
 # Constants
 DIR_NAME=$(dirname "$0")
-if [[ ! -e "${DIR_NAME}"/stx-iso-utils.sh ]]; then
-    log_fatal "${DIR_NAME}/stx-iso-utils.sh does not exist"
+if [ ! -e "${DIR_NAME}"/stx-iso-utils.sh ]; then
+    echo "${DIR_NAME}/stx-iso-utils.sh does not exist" >&2
+    exit 1
 else
     source "${DIR_NAME}"/stx-iso-utils.sh
 fi
@@ -343,13 +347,12 @@ fi
 declare INPUT_ISO=
 declare OUTPUT_ISO=
 declare -a IMAGES
-declare ORIG_PWD=$PWD
 declare KS_SETUP=
 declare KS_ADDON=
 declare UPDATE_TIMEOUT="no"
 declare -i FOREVER_GRUB_TIMEOUT=-1
 declare -i DEFAULT_GRUB_TIMEOUT=30
-declare -i DEFAULT_TIMEOUT=(DEFAULT_GRUB_TIMEOUT*10)
+declare -i DEFAULT_TIMEOUT=$(( DEFAULT_GRUB_TIMEOUT*10 ))
 declare -i TIMEOUT=${DEFAULT_TIMEOUT}
 declare -i GRUB_TIMEOUT=${DEFAULT_GRUB_TIMEOUT}
 declare -a PARAMS
@@ -381,97 +384,109 @@ SHORTOPTS+="I:"; LONGOPTS+="images:,"
 SHORTOPTS+="f";  LONGOPTS+="force-install,"
 SHORTOPTS+="h";  LONGOPTS+="help"
 
+declare -i rc
 OPTS=$(getopt -o "${SHORTOPTS}" --long "${LONGOPTS}" --name "$0" -- "$@")
-if [[ "$?" -ne 0 ]]; then
+if [ $? -ne 0 ]; then
     usage
     log_fatal "Options to $0 not properly parsed"
 fi
 
 eval set -- "${OPTS}"
 
-if [[ $# == 1 ]]; then
+if [ $# = 1 ]; then
     usage
     log_fatal "No arguments were provided"
 fi
 
 while :; do
     case $1 in
-    -i | --input)
-        INPUT_ISO="$2"
-        shift 2
-        ;;
-    -o | --output)
-        OUTPUT_ISO=$2
-        shift 2
-        ;;
-    -s | --setup)
-        KS_SETUP=$2
-        shift 2
-        ;;
-    -a | --addon)
-        KS_ADDON=$2
-        shift 2
-        ;;
-    -p | --param)
-        PARAMS+=(${2//,/ })
-        shift 2
-        ;;
-    -P | --patch)
-        PATCHES+=(${2//,/ })
-        shift 2
-        ;;
-    -K | --kickstart-patch)
-        KICKSTART_PATCHES+=(${2//,/ })
-        shift 2
-        ;;
-    -I | --images)
-        IMAGES+=(${2//,/ })
-        shift 2
-        ;;
-    -d | --default-boot)
-        DEFAULT_LABEL=$2
-        case ${DEFAULT_LABEL} in
-            0)
-                DEFAULT_SYSLINUX_ENTRY=0
-                DEFAULT_GRUB_ENTRY="serial"
-                ;;
-            1)
-                DEFAULT_SYSLINUX_ENTRY=1
-                DEFAULT_GRUB_ENTRY="graphical"
-                ;;
-            *)
-                usage
-                log_fatal "Invalid default boot menu option: ${DEFAULT_LABEL}"
-                ;;
-        esac
-        shift 2
-        ;;
-    -t | --timeout)
-        let -i timeout_arg=$2
-        if [[ "${timeout_arg}" -gt 0 ]]; then
-            let -i "TIMEOUT=${timeout_arg}*10"
-            GRUB_TIMEOUT="${timeout_arg}"
-        elif [[ "${timeout_arg}" -eq 0 ]]; then
-            TIMEOUT=0
-            GRUB_TIMEOUT=0.001
-        elif [[ "${timeout_arg}" -lt 0 ]]; then
-            TIMEOUT=0
-            GRUB_TIMEOUT=${FOREVER_GRUB_TIMEOUT}
-        fi
-        UPDATE_TIMEOUT="yes"
-        shift 2
-        ;;
-    -f | --force-install)
-            FORCE_INSTALL="true"
-        shift
-        ;;
-    --)
-        break
-        ;;
-    *)
-        shift
-        break
-        ;;
+        -h | --help)
+            usage
+            exit 0
+            ;;
+        -i | --input)
+            INPUT_ISO=$2
+            shift 2
+            ;;
+        -o | --output)
+            OUTPUT_ISO=$2
+            shift 2
+            ;;
+        -s | --setup)
+            KS_SETUP=$2
+            shift 2
+            ;;
+        -a | --addon)
+            KS_ADDON=$2
+            shift 2
+            ;;
+        -p | --param)
+            # shellcheck disable=2206
+            PARAMS+=(${2//,/ })
+            shift 2
+            ;;
+        # TODO(kmacleod) Does providing patches make sense?
+        -P | --patch)
+            # shellcheck disable=2206
+            PATCHES+=(${2//,/ })
+            shift 2
+            ;;
+        -K | --kickstart-patch)
+            # shellcheck disable=2206
+            KICKSTART_PATCHES+=(${2//,/ })
+            shift 2
+            ;;
+        -I | --images)
+            # shellcheck disable=2206
+            IMAGES+=(${2//,/ })
+            shift 2
+            ;;
+        -d | --default-boot)
+            DEFAULT_LABEL=${2}
+            case ${DEFAULT_LABEL} in
+                0)
+                    DEFAULT_SYSLINUX_ENTRY=0
+                    DEFAULT_GRUB_ENTRY="serial"
+                    ;;
+                1)
+                    DEFAULT_SYSLINUX_ENTRY=1
+                    DEFAULT_GRUB_ENTRY="graphical"
+                    ;;
+                *)
+                    usage
+                    log_fatal "Invalid default boot menu option: ${DEFAULT_LABEL}"
+                    ;;
+            esac
+            shift 2
+            ;;
+        -t | --timeout)
+            declare -i timeout_arg=${2}
+            if [ "${timeout_arg}" -gt 0 ]; then
+                TIMEOUT=$(( timeout_arg * 10 ))
+                GRUB_TIMEOUT=${timeout_arg}
+            elif [ ${timeout_arg} -eq 0 ]; then
+                TIMEOUT=0
+                GRUB_TIMEOUT=0.001
+            elif [ ${timeout_arg} -lt 0 ]; then
+                TIMEOUT=0
+                GRUB_TIMEOUT=${FOREVER_GRUB_TIMEOUT}
+            fi
+            # TODO(kmacleod): UPDATE_TIMEOUT is not used, why is that?
+            UPDATE_TIMEOUT="yes"
+            shift 2
+            ;;
+        -f | --force-install)
+            FORCE_INSTALL=true
+            shift
+            ;;
+        --)
+            shift
+            break
+            ;;
+        *)
+            usage
+            log_fatal "Unexpected argument: $*"
+            ;;
     esac
 done
 
@@ -480,51 +495,78 @@ done
 # Generate prestage iso.
 #
 ###############################################################################
+
+log_info "Checking system requirements"
 check_requirements
 
 ## Check for mandatory parameters
 check_required_param "--input" "${INPUT_ISO}"
 check_required_param "--output" "${OUTPUT_ISO}"
 
+# shellcheck disable=2068
 check_files_exist ${INPUT_ISO} ${PATCHES[@]} ${IMAGES[@]} ${KS_SETUP} ${KS_ADDON} ${KICKSTART_PATCHES[@]}
+# shellcheck disable=2068
 check_files_size               ${PATCHES[@]} ${IMAGES[@]} ${KS_SETUP} ${KS_ADDON} ${KICKSTART_PATCHES[@]}
 
-if [[ -e "${OUTPUT_ISO}" ]]; then
+if [ -e "${OUTPUT_ISO}" ]; then
     log_fatal "${OUTPUT_ISO} exists. Delete before you execute this script."
 fi
+# Check for rootfs_device/boot_device and warn if not present
+found_rootfs_device=
+found_boot_device=
+if [ ${#PARAMS[@]} -gt 0 ]; then
+    for p in "${PARAMS[@]}"; do
+        param=${p%%=*}
+        case "${param}" in
+            rootfs_device)
+                found_rootfs_device=1
+                ;;
+            boot_device)
+                found_boot_device=1
+                ;;
+        esac
+    done
+fi
+if [ -z "${found_rootfs_device}" ]; then
+    log_warn "Missing '--param rootfs_device=...'. A default device will be selected during install, which may not be desired"
+fi
+if [ -z "${found_boot_device}" ]; then
+    log_warn  "Missing '--param boot_device=...'. A default device will be selected during install, which may not be desired"
+fi
 
 ## Catch Control-C and handle.
 trap cleanup EXIT
 
 # Create a temporary build directory.
-BUILDDIR=$(mktemp -d -p $PWD updateiso_build_XXXXXX)
-if [ -z "${BUILDDIR}" -o ! -d ${BUILDDIR} ]; then
+BUILDDIR=$(mktemp -d -p "${PWD}" updateiso_build_XXXXXX)
+if [ -z "${BUILDDIR}" ] || [ ! -d "${BUILDDIR}" ]; then
     log_fatal "Failed to create builddir. Aborting..."
 fi
-echo ${BUILDDIR}
+log_info "Using BUILDDIR=${BUILDDIR}"
 mount_iso "${INPUT_ISO}"
 
 #
 # Determine release version from ISO
 #
-if [ ! -f ${MNTDIR}/upgrades/version ]; then
+if [ ! -f "${MNTDIR}"/upgrades/version ]; then
     log_error "Version info not found on ${INPUT_ISO}"
     exit 1
 fi
 
-ISO_VERSION=$(source ${MNTDIR}/upgrades/version && echo ${VERSION})
+ISO_VERSION=$(source "${MNTDIR}/upgrades/version" && echo "${VERSION}")
 if [ -z "${ISO_VERSION}" ]; then
     log_error "Failed to determine version of installation ISO"
     exit 1
 fi
 
 # Copy the contents of the input iso to the build directory.
-# This ensures that the ostree, kernel and the initramfs are all copied over
+# This ensures that the ostree_repo, kernel and the initramfs are all copied over
 # to the prestage iso.
 
+log_info "Copying input ISO"
 rsync -a --exclude "pxeboot" "${MNTDIR}/" "${BUILDDIR}/"
 rc=$?
-if [[ "${rc}" -ne 0 ]]; then
+if [ "${rc}" -ne 0 ]; then
     unmount_iso
     log_fatal "Unable to rsync content from the ISO: Error rc=${rc}"
 fi
@@ -541,27 +583,32 @@ unmount_iso
 PLATFORM_PATH="${PLATFORM_ROOT}/${ISO_VERSION}"
 mkdir_on_iso "${PLATFORM_PATH}"
 
-for PATCH in ${PATCHES[@]}; do
-    copy_to_iso "${PATCH}" "${PLATFORM_PATH}/"
-done
+if [ -n "${PATCHES[*]}" ]; then
+    log_info "Including patches: ${PATCHES[*]}"
+    for PATCH in "${PATCHES[@]}"; do
+        copy_to_iso "${PATCH}" "${PLATFORM_PATH}/"
+    done
+fi
 
-for IMAGE in ${IMAGES[@]}; do
-    copy_to_iso "${IMAGE}" "${PLATFORM_PATH}/" "${PLATFORM_PATH}/${MD5_FILE}"
-done
+if [ -n "${IMAGES[*]}" ]; then
+    log_info "Including images: ${IMAGES[*]}"
+    for IMAGE in "${IMAGES[@]}"; do
+        copy_to_iso "${IMAGE}" "${PLATFORM_PATH}/" "${PLATFORM_PATH}/${MD5_FILE}"
+    done
+fi
 
 KICKSTART_PATCH_DIR="kickstart_patch"
 mkdir_on_iso "${KICKSTART_PATCH_DIR}"
-for PATCH in ${KICKSTART_PATCHES[@]}; do
-    log "Found kickstart patch"
+for PATCH in "${KICKSTART_PATCHES[@]}"; do
+    log_info "Including kickstart patch: ${PATCH}"
     copy_to_iso "${PATCH}" "${KICKSTART_PATCH_DIR}"
     KS_PATCH="true"
 done
 
 # generate the grub and isolinux cmd line parameters
-
 generate_boot_cfg "${BUILDDIR}"
-# copy the addon and setup files to the BUILDDIR
 
+# copy the addon and setup files to the BUILDDIR
 if [[ -e "${KS_SETUP}" ]]; then
     cp "${KS_SETUP}" "${BUILDDIR}"
 fi
@@ -584,4 +631,4 @@ mkisofs -o "${OUTPUT_ISO}" \
 
 isohybrid --uefi "${OUTPUT_ISO}"
 
-log "Prestage ISO created successfully"
+log_info "Prestage ISO created successfully: ${OUTPUT_ISO}"
diff --git a/utilities/platform-util/scripts/stx-iso-utils-centos.sh b/utilities/platform-util/scripts/stx-iso-utils-centos.sh
index 2e1ff42d..a3fe127b 100644
--- a/utilities/platform-util/scripts/stx-iso-utils-centos.sh
+++ b/utilities/platform-util/scripts/stx-iso-utils-centos.sh
@@ -1,31 +1,49 @@
 #!/bin/bash
 #
-# Copyright (c) 2020 Wind River Systems, Inc.
+# Copyright (c) 2020-2023 Wind River Systems, Inc.
 #
 # SPDX-License-Identifier: Apache-2.0
 #
 # Common bash utility functions for StarlingX ISO tools
 #
+# Global shellcheck ignores:
+# shellcheck disable=2181
 
 declare BUILDDIR=
 declare EFIBOOT_IMG_LOOP=
 declare EFI_MOUNT=
 declare MNTDIR=
 declare WORKDIR=
+declare VERBOSE=
+
+function ilog {
+    echo "$(date "+%F %H:%M:%S"): $*" >&2
+}
+
+function elog {
+    echo "$(date "+%F %H:%M:%S") Error: $*" >&2
+    exit 1
+}
+
+function vlog {
+    [ "${VERBOSE}" = true ] && echo "$(date "+%F %H:%M:%S"): $*" >&2
+}
 
 function common_cleanup {
     unmount_efiboot_img
 
-    if [ -n "$MNTDIR" -a -d "$MNTDIR" ]; then
+    if [ -n "${MNTDIR}" ] && [ -d "${MNTDIR}" ]; then
         unmount_iso
     fi
 
-    if [ -n "$BUILDDIR" -a -d "$BUILDDIR" ]; then
-        \rm -rf $BUILDDIR
+    if [ -n "${BUILDDIR}" ] && [ -d "${BUILDDIR}" ]; then
+        chmod -R 755 "${BUILDDIR}"
+        rm -rf "${BUILDDIR}"
     fi
 
-    if [ -n "$WORKDIR" -a -d "$WORKDIR" ]; then
-        \rm -rf $WORKDIR
+    if [ -n "${WORKDIR}" ] && [ -d "${WORKDIR}" ]; then
+        chmod -R 755 "${WORKDIR}"
+        rm -rf "${WORKDIR}"
     fi
 }
 
@@ -58,27 +76,26 @@ function common_check_requirements {
         )
     fi
 
-    required_utils+=( $@ )
+    required_utils+=( "$@" )
 
     local -i missing=0
 
-    which which >&/dev/null
-    if [ $? -ne 0 ]; then
-        log_error "Unable to find 'which' utility. Aborting..."
-        exit 1
+    if which which >&/dev/null -ne 0 ; then
+        elog "unable to find 'which' utility. Aborting..."
     fi
 
-    for req in ${required_utils[@]}; do
-        which ${req} >&/dev/null
-        if [ $? -ne 0 ]; then
-            log_error "Unable to find required utility: ${req}"
-            let -i missing++
+    for req in "${required_utils[@]}"; do
+        which "${req}" >&/dev/null
+        if [ $? -ne 0 ] ; then
+            ilog "unable to find required utility: ${req}"
+            (( missing++ ))
         fi
     done
 
-    if [ ${missing} -gt 0 ]; then
-        log_error "One or more required utilities are missing. Aborting..."
-        exit 1
+    if [ "${missing}" -gt 0 ]; then
+        elog "one or more required utilities are missing. Aborting..."
+    else
+        ilog "all required iso utilities present"
     fi
 }
 
@@ -87,16 +104,14 @@ function check_required_param {
     local value="${2}"
 
     if [ -z "${value}" ]; then
-        log_error "Required parameter ${param} is not set"
-        exit 1
+        elog "required parameter ${param} is not set"
     fi
 }
 
 function check_files_exist {
     for value in "$@"; do
         if [ ! -f "${value}" ]; then
-            log_error "file path '${value}' is invalid"
-            exit 1
+            elog "file path '${value}' is invalid"
         fi
     done
 }
@@ -108,10 +123,9 @@ function check_files_size {
     local file_size_limit=4000000000
 
     for value in "$@"; do
-        file_size=$(stat --printf="%s" ${value})
-        if [ ${file_size} -gt ${file_size_limit} ]; then
-            log_error "file size of '${value}' exceeds 4 GB limit"
-            exit 1
+        file_size=$(stat --printf="%s" "${value}")
+        if [ "${file_size}" -ge "${file_size_limit}" ]; then
+            elog "file size of '${value}' exceeds 4 GB limit"
         fi
     done
 }
@@ -119,54 +133,61 @@ function check_files_size {
 function mount_iso {
     local input_iso=$1
     local basedir=${2:-$PWD}
+    local guestmount_dev=${3:-"/dev/sda1"}
 
     MNTDIR=$(mktemp -d -p "$basedir" stx-iso-utils_mnt_XXXXXX)
-    if [ -z "${MNTDIR}" -o ! -d ${MNTDIR} ]; then
-        log_error "Failed to create mntdir $MNTDIR. Aborting..."
-        exit 1
+    if [ -z "${MNTDIR}" ] || [ ! -d "${MNTDIR}" ]; then
+        elog "Failed to create mntdir $MNTDIR. Aborting..."
     fi
+    ilog "mount_iso input_iso=${input_iso} basedir=${basedir} MNTDIR=${MNTDIR}"
 
     if [ $UID -eq 0 ]; then
         # Mount the ISO
-        mount -o loop ${input_iso} ${MNTDIR}
+        ilog "mounting ${input_iso} to ${MNTDIR}"
+        mount -o loop "${input_iso}" "${MNTDIR}" >&/dev/null
         if [ $? -ne 0 ]; then
-            echo "Failed to mount ${input_iso}" >&2
-            exit 1
+            elog "Failed to mount ${input_iso}" >&2
         fi
     else
         # As non-root user, mount the ISO using guestmount
-        guestmount -a ${input_iso} -m /dev/sda1 --ro ${MNTDIR}
-        rc=$?
-        if [ $rc -ne 0 ]; then
-            # Add a retry
-            echo "Call to guestmount failed with rc=$rc. Retrying once..."
+        ilog "guestmounting ${input_iso} using ${guestmount_dev} to ${MNTDIR}"
 
-            guestmount -a ${input_iso} -m /dev/sda1 --ro ${MNTDIR}
+        guestmount -a "${input_iso}" -m "${guestmount_dev}" --ro "${MNTDIR}"
+        rc=$?
+        if [ "${rc}" -ne 0 ]; then
+            # Add a retry
+            ilog "guestmount failed with rc=${rc}. Retrying once..."
+
+            guestmount -a "${input_iso}" -m "${guestmount_dev}" --ro "${MNTDIR}" >&/dev/null
             rc=$?
-            if [ $rc -ne 0 ]; then
-                echo "Call to guestmount failed with rc=$rc. Aborting..."
-                exit $rc
+            if [ ${rc} -ne 0 ]; then
+                elog "guestmount retry failed with rc=$rc. Aborting..."
+            else
+                vlog "guestmount retry succeeded"
             fi
+        else
+            vlog "guestmount succeeded."
         fi
     fi
 }
 
 function unmount_iso {
-    if [ $UID -eq 0 ]; then
-        umount ${MNTDIR}
+    if [ "${UID}" -eq 0 ]; then
+        ilog "unmounting ${MNTDIR}"
+        umount "${MNTDIR}" >&/dev/null
     else
-        guestunmount ${MNTDIR}
+        guestunmount "${MNTDIR}" >&/dev/null
     fi
-    rmdir ${MNTDIR}
+    rmdir "${MNTDIR}"
 }
 
 function mount_efiboot_img {
     local isodir=$1
 
-    if [ -e ${isodir}/images/efiboot.img ]; then
-        local efiboot_img=${isodir}/images/efiboot.img
+    if [ -e "${isodir}/images/efiboot.img" ]; then
+        local efiboot_img="${isodir}/images/efiboot.img"
     else
-        local efiboot_img=${isodir}/efi.img
+        local efiboot_img="${isodir}/efi.img"
     fi
 
     local loop_setup_output=
@@ -174,45 +195,42 @@ function mount_efiboot_img {
     if [ $UID -eq 0 ]; then
         # As root, setup a writeable loop device for the
         # efiboot.img file and mount it
-        loop_setup_output=$(losetup --show -f ${efiboot_img})
+        loop_setup_output=$(losetup --show -f "${efiboot_img}")
         if [ $? -ne 0 ]; then
-            echo "Failed losetup" >&2
-            exit 1
+            elog "Failed losetup" >&2
         fi
 
         EFIBOOT_IMG_LOOP=${loop_setup_output}
 
         EFI_MOUNT=$(mktemp -d -p /mnt -t EFI-noudev.XXXXXX)
-        mount ${EFIBOOT_IMG_LOOP} ${EFI_MOUNT}
+        mount "${EFIBOOT_IMG_LOOP}" "${EFI_MOUNT}"
         if [ $? -ne 0 ]; then
-            echo "Failed to mount loop device ${EFIBOOT_IMG_LOOP}" >&2
-            exit 1
+            elog "Failed to mount loop device ${EFIBOOT_IMG_LOOP}" >&2
         fi
     else
         # As non-root user, we can use the udisksctl to setup a loop device
         # and mount the efiboot.img, with read/write access.
-        loop_setup_output=$(udisksctl loop-setup -f ${efiboot_img} --no-user-interaction)
+        loop_setup_output=$(udisksctl loop-setup -f "${efiboot_img}" --no-user-interaction)
         if [ $? -ne 0 ]; then
-            echo "Failed udisksctl loop-setup" >&2
-            exit 1
+            elog "Failed udisksctl loop-setup" >&2
         fi
 
-        EFIBOOT_IMG_LOOP=$(echo ${loop_setup_output} | awk '{print $5;}' | sed -e 's/\.$//g')
+        EFIBOOT_IMG_LOOP=$(echo "${loop_setup_output}" | awk '{print $5;}' | sed -e 's/\.$//g')
         if [ -z "${EFIBOOT_IMG_LOOP}" ]; then
-            echo "Failed to determine loop device from command output:" >&2
+            echo "Error: Failed to determine loop device from command output:" >&2
             echo "${loop_setup_output}" >&2
             exit 1
         fi
 
-        udisksctl mount -b ${EFIBOOT_IMG_LOOP}
+        udisksctl mount -b "${EFIBOOT_IMG_LOOP}" >&/dev/null
         if [ $? -ne 0 ]; then
-            echo "Failed udisksctl mount" >&2
+            echo "Error: Failed udisksctl mount" >&2
             exit 1
         fi
 
-        EFI_MOUNT=$(udisksctl info -b ${EFIBOOT_IMG_LOOP} | grep MountPoints | awk '{print $2;}')
+        EFI_MOUNT=$(udisksctl info -b "${EFIBOOT_IMG_LOOP}" | grep MountPoints | awk '{print $2;}')
         if [ -z "${EFI_MOUNT}" ]; then
-            echo "Failed to determine mount point from udisksctl info command" >&2
+            echo "Error: Failed to determine mount point from udisksctl info command" >&2
             exit 1
         fi
     fi
@@ -221,19 +239,19 @@ function mount_efiboot_img {
 function unmount_efiboot_img {
     if [ $UID -eq 0 ]; then
         if [ -n "${EFI_MOUNT}" ]; then
-            mountpoint -q ${EFI_MOUNT} && umount ${EFI_MOUNT}
-            rmdir ${EFI_MOUNT}
+            mountpoint -q "${EFI_MOUNT}" && umount "${EFI_MOUNT}"
+            rmdir "${EFI_MOUNT}"
             EFI_MOUNT=
         fi
 
         if [ -n "${EFIBOOT_IMG_LOOP}" ]; then
-            losetup -d ${EFIBOOT_IMG_LOOP}
+            losetup -d "${EFIBOOT_IMG_LOOP}"
             EFIBOOT_IMG_LOOP=
         fi
     else
         if [ -n "${EFIBOOT_IMG_LOOP}" ]; then
-            udisksctl unmount -b ${EFIBOOT_IMG_LOOP}
-            udisksctl loop-delete -b ${EFIBOOT_IMG_LOOP}
+            udisksctl unmount -b "${EFIBOOT_IMG_LOOP}" >&/dev/null
+            udisksctl loop-delete -b "${EFIBOOT_IMG_LOOP}"
             EFI_MOUNT=
             EFIBOOT_IMG_LOOP=
         fi
@@ -245,44 +263,41 @@ function update_parameter {
     local param=$2
     local value=$3
 
+    ilog "updating parameter ${param} to ${param}=${value}"
     if [ -z "${EFI_MOUNT}" ]; then
-        mount_efiboot_img ${isodir}
+        mount_efiboot_img "${isodir}"
     fi
 
-    for f in ${isodir}/isolinux.cfg ${isodir}/syslinux.cfg; do
-        grep -q "^[[:space:]]*append\>.*[[:space:]]${param}=" ${f}
+    for f in "${isodir}"/isolinux.cfg "${isodir}"/syslinux.cfg ; do
+        grep -q "^[[:space:]]*append\>.*[[:space:]]${param}=" "${f}"
         if [ $? -eq 0 ]; then
             # Parameter already exists. Update the value
-            sed -i -e "s#^\([[:space:]]*append\>.*${param}\)=[^[:space:]]*#\1=${value}#" ${f}
+            sed -i -e "s#^\([[:space:]]*append\>.*${param}\)=[^[:space:]]*#\1=${value}#" "${f}"
             if [ $? -ne 0 ]; then
-                log_error "Failed to update parameter ($param)"
-                exit 1
+                elog "Failed to update parameter ($param)"
             fi
         else
             # Parameter doesn't exist. Add it to the cmdline
-            sed -i -e "s|^\([[:space:]]*append\>.*\)|\1 ${param}=${value}|" ${f}
+            sed -i -e "s|^\([[:space:]]*append\>.*\)|\1 ${param}=${value}|" "${f}"
             if [ $? -ne 0 ]; then
-                log_error "Failed to add parameter ($param)"
-                exit 1
+                elog "Failed to add parameter ($param)"
             fi
         fi
     done
 
-    for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg; do
-        grep -q "^[[:space:]]*linuxefi\>.*[[:space:]]${param}=" ${f}
+    for f in "${isodir}/EFI/BOOT/grub.cfg" "${EFI_MOUNT}/EFI/BOOT/grub.cfg" ; do
+        grep -q "^[[:space:]]*linuxefi\>.*[[:space:]]${param}=" "${f}"
         if [ $? -eq 0 ]; then
             # Parameter already exists. Update the value
-            sed -i -e "s#^\([[:space:]]*linuxefi\>.*${param}\)=[^[:space:]]*#\1=${value}#" ${f}
+            sed -i -e "s#^\([[:space:]]*linuxefi\>.*${param}\)=[^[:space:]]*#\1=${value}#" "${f}"
             if [ $? -ne 0 ]; then
-                log_error "Failed to update parameter ($param)"
-                exit 1
+                elog "Failed to update parameter ($param)"
             fi
         else
             # Parameter doesn't exist. Add it to the cmdline
-            sed -i -e "s|^\([[:space:]]*linuxefi\>.*\)|\1 ${param}=${value}|" ${f}
+            sed -i -e "s|^\([[:space:]]*linuxefi\>.*\)|\1 ${param}=${value}|" "${f}"
             if [ $? -ne 0 ]; then
-                log_error "Failed to add parameter ($param)"
-                exit 1
+                elog "Failed to add parameter ($param)"
             fi
         fi
     done
diff --git a/utilities/platform-util/scripts/stx-iso-utils.sh b/utilities/platform-util/scripts/stx-iso-utils.sh
index 9e6c3bfa..cafba26f 100644
--- a/utilities/platform-util/scripts/stx-iso-utils.sh
+++ b/utilities/platform-util/scripts/stx-iso-utils.sh
@@ -1,29 +1,32 @@
 #!/bin/bash
 #
-# Copyright (c) 2020,2022 Wind River Systems, Inc.
+# Copyright (c) 2020-2023 Wind River Systems, Inc.
 #
 # SPDX-License-Identifier: Apache-2.0
 #
 # Common bash utility functions for StarlingX ISO tools
 #
+# Global shellcheck ignores:
+# shellcheck disable=2181
 
 declare BUILDDIR=
 declare EFIBOOT_IMG_LOOP=
 declare EFI_MOUNT=
 declare MNTDIR=
 declare WORKDIR=
+declare VERBOSE=
 
 function ilog {
-    echo "$(date "+%F %H-%M-%S"): $*" >&2
+    echo "$(date "+%F %H:%M:%S"): $*" >&2
 }
 
 function elog {
-    echo "$(date "+%F %H-%M-%S") Error: $*" >&2
+    echo "$(date "+%F %H:%M:%S") Error: $*" >&2
     exit 1
 }
 
 function vlog {
-    [ "${VERBOSE}" = true ] && echo "$(date "+%F %H-%M-%S"): $*" >&2
+    [ "${VERBOSE}" = true ] && echo "$(date "+%F %H:%M:%S"): $*" >&2
 }
 
 function common_cleanup {
@@ -130,12 +133,13 @@ function check_files_size {
 function mount_iso {
     local input_iso=$1
     local basedir=${2:-$PWD}
-    local mount=${3:-"/dev/sda1"}
+    local guestmount_dev=${3:-"/dev/sda1"}
 
     MNTDIR=$(mktemp -d -p "$basedir" stx-iso-utils_mnt_XXXXXX)
     if [ -z "${MNTDIR}" ] || [ ! -d "${MNTDIR}" ]; then
         elog "Failed to create mntdir $MNTDIR. Aborting..."
     fi
+    ilog "mount_iso input_iso=${input_iso} basedir=${basedir} MNTDIR=${MNTDIR}"
 
     if [ $UID -eq 0 ]; then
         # Mount the ISO
@@ -146,15 +150,15 @@ function mount_iso {
         fi
     else
         # As non-root user, mount the ISO using guestmount
-        ilog "guestmounting ${input_iso} using ${mount} to ${MNTDIR}"
+        ilog "guestmounting ${input_iso} using ${guestmount_dev} to ${MNTDIR}"
 
-        guestmount -a "${input_iso}" -m "${mount}" --ro "${MNTDIR}"
+        guestmount -a "${input_iso}" -m "${guestmount_dev}" --ro "${MNTDIR}"
         rc=$?
         if [ "${rc}" -ne 0 ]; then
             # Add a retry
             ilog "guestmount failed with rc=${rc}. Retrying once..."
 
-            guestmount -a "${input_iso}" -m "${mount}" --ro "${MNTDIR}" >&/dev/null
+            guestmount -a "${input_iso}" -m "${guestmount_dev}" --ro "${MNTDIR}" >&/dev/null
             rc=$?
             if [ ${rc} -ne 0 ]; then
                 elog "guestmount retry failed with rc=$rc. Aborting..."
@@ -286,7 +290,7 @@ function update_parameter {
         fi
     done
 
-    for f in ${isodir}/EFI/BOOT/grub.cfg ${EFI_MOUNT}/EFI/BOOT/grub.cfg ; do
+    for f in "${isodir}/EFI/BOOT/grub.cfg" "${EFI_MOUNT}/EFI/BOOT/grub.cfg" ; do
         grep -q "^[[:space:]]*linux\>.*[[:space:]]${param}=" "${f}"
         if [ $? -eq 0 ]; then
             # Parameter already exists. Update the value
diff --git a/utilities/platform-util/scripts/test/.gitignore b/utilities/platform-util/scripts/test/.gitignore
new file mode 100644
index 00000000..56cccd49
--- /dev/null
+++ b/utilities/platform-util/scripts/test/.gitignore
@@ -0,0 +1,3 @@
+/input
+/output
+/shunit2
diff --git a/utilities/platform-util/scripts/test/README.md b/utilities/platform-util/scripts/test/README.md
new file mode 100644
index 00000000..3a568d47
--- /dev/null
+++ b/utilities/platform-util/scripts/test/README.md
@@ -0,0 +1,24 @@
+Testing ISO generation scripts
+==============================
+
+This directory contains unit test cases for the `gen-*.sh` scripts:
+
+
+## Running tests
+
+Run all available tests via the `run-tests.sh` script.
+You can also run the individual `gen-*-test.sh` scripts manually.
+
+The shunit2 bash unit test framework is used.  It is automatically retrieved if it doesn't exist.
+
+
+## Directories
+
+Directories used by the tests:
+
+input/
+    - Contents of ISO/image/patch files used for the tests
+    - The contents of this directory are either downloaded or generated by the tests
+
+output/
+    - Temporary test output directory
diff --git a/utilities/platform-util/scripts/test/gen-prestaged-iso-centos-test.sh b/utilities/platform-util/scripts/test/gen-prestaged-iso-centos-test.sh
new file mode 100755
index 00000000..51087252
--- /dev/null
+++ b/utilities/platform-util/scripts/test/gen-prestaged-iso-centos-test.sh
@@ -0,0 +1,300 @@
+#!/bin/bash
+# vim:ft=sh:sts=4:sw=4
+
+# This is a shunit2 test file.
+# See https://github.com/kward/shunit2
+# Run the tests by executing this script.
+
+# shellcheck disable=SC2016
+# shellcheck disable=SC1090
+# shellcheck disable=SC1091
+
+NAME=gen-prestaged-iso-centos-test
+
+# shellcheck disable=SC2155,SC2034
+readonly SCRIPTDIR=$(readlink -m "$(dirname "$0")")
+# shellcheck disable=SC2155,SC2034
+readonly TARGET_SCRIPTDIR=$(readlink -m "${SCRIPTDIR}/..")
+
+INPUT_DIR="${SCRIPTDIR}"/input/centos
+IMAGES_DIR="${INPUT_DIR}/images"
+PATCHES_DIR="${INPUT_DIR}/patches"
+ISOFILE=${ISOFILE:-$INPUT_DIR/bootimage.iso}
+#ISOFILE=/localdisk/designer/kmacleod/dc-libvirt/isofiles/wrcp-22.12-release/starlingx-intel-x86-64-cd.iso
+#ISOFILE=/localdisk/designer/kmacleod/dc-libvirt/isofiles/WRCP-21.12-formal-patch/bootimage.iso
+
+# source the script under test
+. "${TARGET_SCRIPTDIR}"/stx-iso-utils-centos.sh
+. "${SCRIPTDIR}/shunit2_helper.sh"
+
+KEEP_ARTIFACTS=${KEEP_ARTIFACTS:-}
+BUILDDIR=${SCRIPTDIR}/output/${NAME}
+OUTPUT_ISO=${BUILDDIR}/generated.iso
+
+_create_fake_image() {
+    local imagename=$1
+    local targetdir=$2
+    if hash docker 2>/dev/null; then
+        echo "Creating fake image ${imagename} using docker"
+        tar cv --files-from /dev/null | docker import - "${imagename}:latest"
+        docker save -o "${targetdir}/${imagename}.tar.gz" "${imagename}:latest"
+        docker rmi "${imagename}:latest"
+    else
+        echo "Creating fake empty image ${imagename}"
+        touch "${targetdir}/${imagename}.tar.gz"
+    fi
+}
+
+_create_fake_images() {
+    if [ ! -d "${IMAGES_DIR}" ]; then
+        echo "Creating fake images"
+        mkdir -p "${IMAGES_DIR}" || fail "mkdir failed"
+        local image
+        for image in image1 image2 image3; do
+            _create_fake_image "${image}" "${IMAGES_DIR}"
+        done
+    fi
+}
+
+_fetch_iso_and_patches() {
+    # Fetch CentOS ISO and test patches from yow-cgts4-lx
+    [ -d "${INPUT_DIR}" ] || mkdir -p "${INPUT_DIR}" || fail "mkdir failed"
+    if [ ! -f "${INPUT_DIR}/bootimage.iso" ]; then
+        echo "Fetching ISO"
+        scp 'yow-cgts4-lx:/localdisk/loadbuild/jenkins/WRCP_21.12_Build/last_build_with_test_patches/export/bootimage.{iso,sig}' "${INPUT_DIR}/"
+    fi
+    if [ ! -d "${PATCHES_DIR}" ]; then
+        echo "Fetching patches"
+        mkdir -p "${PATCHES_DIR}" || fail "mkdir failed"
+        scp 'yow-cgts4-lx:/localdisk/loadbuild/jenkins/WRCP_21.12_Build/last_build_with_test_patches/test_patches/*{A,B,C}.patch' "${PATCHES_DIR}/"
+    fi
+}
+
+# Executed at start of all tests
+oneTimeSetUp() {
+    th_info oneTimeSetUp
+    if [ -d "${BUILDDIR}" ]; then
+        th_debug "Cleaning ${BUILDDIR}"
+        rm -rf "${BUILDDIR}"
+    fi
+    mkdir -p "${BUILDDIR}" || fail "Failed to create ${BUILDDIR}"
+    _create_fake_images
+    _fetch_iso_and_patches
+}
+
+# Executed at start of each tests
+setUp() {
+    th_info setUp
+}
+
+oneTimeTearDown() {
+    th_info oneTimeTearDown
+    if [ -z "${KEEP_ARTIFACTS}" ]; then
+        if [ -n "${BUILDDIR}" ] && [ -d "${BUILDDIR}" ]; then
+            th_debug "Cleaning ${BUILDDIR}"
+            rm -rf "${BUILDDIR}"
+        fi
+    fi
+}
+
+# Executed at completion of each tests
+tearDown() {
+    th_info tearDown
+    if [ -n "${MNTDIR}" ] && [ -d "${MNTDIR}" ]; then
+        th_info "tearDown: unmounting ${MNTDIR}"
+        sudo umount "${MNTDIR}" 2>/dev/null || th_warn "umount failed for ${MNTDIR}"
+    fi
+    if [ -f "${OUTPUT_ISO}" ]; then
+        sudo rm "${OUTPUT_ISO}"
+    fi
+}
+
+create_ks_addon_file() {
+cat <<EOF > "${BUILDDIR}/ks-addon.cfg"
+ilog "Executing ks-addon.cfg"
+EOF
+}
+
+validate_generated_iso() {
+    local syslinux_boot=1
+    local grub_boot=graphical
+    local param=
+    local syslinux_timeout=300
+    local grub_timeout=30
+    local ks_addon=
+    while [ $# -gt 0 ] ; do
+        case "${1:-""}" in
+            --syslinux-boot)
+                shift
+                syslinux_boot=$1
+                ;;
+            --grub-boot)
+                shift
+                grub_boot=$1
+                ;;
+            --param)
+                shift
+                param=$1
+                ;;
+            --syslinux-timeout)
+                shift
+                syslinux_timeout=$1
+                ;;
+            --grub-timeout)
+                shift
+                grub_timeout=$1
+                ;;
+            --ks-addon)
+                shift
+                ks_addon=ks-addon.cfg
+                ;;
+            *)
+                echo "Invalid expected value '$1'"
+                exit 1
+                ;;
+        esac
+        shift
+    done
+
+    # Mount the ISO
+    [ -f "${OUTPUT_ISO}" ] || fail "${OUTPUT_ISO} does not exist"
+    MNTDIR=${BUILDDIR}/mnt
+    [ -d "${MNTDIR}" ] || mkdir "${MNTDIR}" || fail "mkdir failed"
+    sudo mount "${OUTPUT_ISO}" "${MNTDIR}" || fail "Failed to mount ${OUTPUT_ISO}"
+
+    # Check that images are included:
+    th_info "Image check: validating images are in ISO"
+    for image in "${IMAGES_DIR}"/*.tar.gz; do
+        [ "$(find "${MNTDIR}"/opt/platform-backup -name "$(basename "${image}")" | wc -l)" -eq 1 ]  \
+            || fail "Missing expected image ${image}"
+        th_info "Found expected image: ${image}"
+    done
+    th_info "Image check passed"
+
+    # Check that patches are included:
+    th_info "Patch check: validating patches are in ISO"
+    for patch in "${PATCHES_DIR}"/*.patch; do
+        [ "$(find "${MNTDIR}"/opt/platform-backup -name "$(basename "${patch}")" | wc -l)" -eq 1 ]  \
+            || fail "Missing expected patch ${patch}"
+        th_info "Found expected patch: ${patch}"
+    done
+    th_info "Patch check passed"
+
+    local syslinux_cfg=${MNTDIR}/syslinux.cfg
+    local grub_cfg=${MNTDIR}/EFI/BOOT/grub.cfg
+
+    if [ -n "${syslinux_boot}" ]; then
+        grep -q -i "default ${syslinux_boot}" "${syslinux_cfg}" \
+            || fail "Incorrect syslinux boot: ${syslinux_boot} in ${syslinux_cfg}"
+    fi
+    if [ -n "${syslinux_timeout}" ]; then
+        grep -q "timeout ${syslinux_timeout}" "${syslinux_cfg}" \
+            || fail "Incorrect syslinux timeout: (expected ${syslinux_timeout}) in ${syslinux_cfg}"
+    fi
+    if [ -n "${grub_boot}" ]; then
+        grep 'default=' "${grub_cfg}" | grep -q "${grub_boot}" \
+            || fail "Incorrect grub boot (expected ${grub_boot}) in EFI/BOOT/grub.cfg"
+    fi
+    if [ -n "${grub_timeout}" ]; then
+        grep -q "timeout=${grub_timeout}" "${grub_cfg}" \
+            || fail "Incorrect grub timeout: ${grub_timeout} in EFI/BOOT/grub.cfg"
+    fi
+    if [ -n "${param}" ]; then
+        # There should be two boot entries containing param (graphical + serial)
+        [ "$(grep -c "${param}" "${syslinux_cfg}")" -eq 2 ] \
+            || fail "Incorrect param value (expected ${param}) in ${syslinux_cfg}"
+        [ "$(grep -c "${param}" "${grub_cfg}")" -eq 2 ] \
+            || fail  "Incorrect param value (expected ${param}) in EFI/BOOT/grub.cfg"
+    fi
+    if [ -n "${ks_addon}" ]; then
+        [ -f "${MNTDIR}/${ks_addon}" ] || fail "Expected ks-addon ${ks_addon} not found"
+    fi
+}
+
+test_generate_prestaged_iso_1() {
+    th_info "Running test_generate_prestaged_iso_1"
+    (   # subshell
+        cd "${TARGET_SCRIPTDIR}"
+        local images=""
+        local image
+        for image in "${IMAGES_DIR}"/*.tar.gz; do
+            images="${images} --image ${image}"
+        done
+        local patches=""
+        local patch
+        for patch in "${PATCHES_DIR}"/*.patch; do
+            patches="${patches} --patch ${patch}"
+        done
+        # shellcheck disable=2086
+        sudo ./gen-prestaged-iso-centos.sh --input "${ISOFILE}" \
+            --output "${OUTPUT_ISO}" \
+            ${images} ${patches}
+    ) || fail "gen-prestaged-iso-centos.sh failed"
+
+    th_info "Generated ${OUTPUT_ISO}"
+
+    if [ -n "${KEEP_ARTIFACTS}" ]; then
+        th_info "Preserving ISO in ${SCRIPTDIR}/generated_centos_1.iso"
+        cp "${OUTPUT_ISO}" "${SCRIPTDIR}"/generated_centos_1.iso
+    fi
+
+    validate_generated_iso --syslinux-boot 1 --grub-boot graphical \
+        --syslinux-timeout 300 --grub-timeout 30
+}
+
+test_generate_prestaged_iso_2() {
+    th_info "Running test_generate_prestaged_iso_2"
+    (   # subshell
+        cd "${TARGET_SCRIPTDIR}"
+        local images=""
+        local image
+        for image in "${IMAGES_DIR}"/*.tar.gz; do
+            if [ -z "${images}" ]; then
+                images="--image ${image}"
+            else
+                images="${images},${image}"
+            fi
+        done
+        local patches=""
+        local patch
+        for patch in "${PATCHES_DIR}"/*.patch; do
+            if [ -z "${patches}" ]; then
+                patches="--patch ${patch}"
+            else
+                patches="${patches},${patch}"
+            fi
+        done
+        create_ks_addon_file
+
+        # shellcheck disable=2086
+        sudo ./gen-prestaged-iso-centos.sh --input "${ISOFILE}" \
+            --output "${OUTPUT_ISO}" \
+            --addon "${BUILDDIR}/ks-addon.cfg" \
+            --default-boot 0 \
+            --timeout 90 \
+            --force-install \
+            --param "param1=1,param2=2" \
+            ${images} ${patches}
+    ) || fail "gen-prestaged-iso-centos.sh failed"
+
+    th_info "Generated ${OUTPUT_ISO}"
+
+    if [ -n "${KEEP_ARTIFACTS}" ]; then
+        th_info "Preserving ISO in ${SCRIPTDIR}/generated_centos_2.iso"
+        cp "${OUTPUT_ISO}" "${SCRIPTDIR}"/generated_centos_2.iso
+    fi
+
+    validate_generated_iso --syslinux-boot 0 --grub-boot serial \
+        --syslinux-timeout 900 --grub-timeout 90 \
+        --param "param1=1 param2=2" --ks-addon ks-addon.cfg
+}
+
+# shellcheck disable=SC2154
+trap 'rc=$?; echo "Caught abnormal signal rc=$rc"; exit $rc' 2 3 15
+
+th_info "Running shunit2"
+
+# Load and run shunit2.
+# shellcheck disable=SC2034
+[ -n "${ZSH_VERSION:-}" ] && SHUNIT_PARENT=$0
+. "${TH_SHUNIT}"
diff --git a/utilities/platform-util/scripts/test/gen-prestaged-iso-test.sh b/utilities/platform-util/scripts/test/gen-prestaged-iso-test.sh
new file mode 100755
index 00000000..4e8f7421
--- /dev/null
+++ b/utilities/platform-util/scripts/test/gen-prestaged-iso-test.sh
@@ -0,0 +1,302 @@
+#!/bin/bash
+# vim:ft=sh:sts=4:sw=4
+
+# This is a shunit2 test file.
+# See https://github.com/kward/shunit2
+# Run the tests by executing this script.
+
+# shellcheck disable=SC2016
+# shellcheck disable=SC1090
+# shellcheck disable=SC1091
+
+NAME=gen-prestaged-iso-test
+
+# shellcheck disable=SC2155,SC2034
+readonly SCRIPTDIR=$(readlink -m "$(dirname "$0")")
+# shellcheck disable=SC2155,SC2034
+readonly TARGET_SCRIPTDIR=$(readlink -m "${SCRIPTDIR}/..")
+
+INPUT_DIR="${SCRIPTDIR}"/input/debian
+IMAGES_DIR="${INPUT_DIR}/images"
+PATCHES_DIR="${INPUT_DIR}/patches"
+ISOFILE=${ISOFILE:-$INPUT_DIR/starlingx-intel-x86-64-cd.iso}
+#ISOFILE=/localdisk/designer/kmacleod/dc-libvirt/isofiles/wrcp-22.12-release/starlingx-intel-x86-64-cd.iso
+
+# source the script under test
+. "${TARGET_SCRIPTDIR}"/stx-iso-utils.sh
+. "${SCRIPTDIR}/shunit2_helper.sh"
+
+KEEP_ARTIFACTS=${KEEP_ARTIFACTS:-}
+BUILDDIR=${SCRIPTDIR}/output/${NAME}
+OUTPUT_ISO=${BUILDDIR}/generated.iso
+
+_create_fake_image() {
+    local imagename=$1
+    local targetdir=$2
+    if hash docker 2>/dev/null; then
+        echo "Creating fake image ${imagename} using docker"
+        tar cv --files-from /dev/null | docker import - "${imagename}:latest"
+        docker save -o "${targetdir}/${imagename}.tar.gz" "${imagename}:latest"
+        docker rmi "${imagename}:latest"
+    else
+        echo "Creating fake empty image ${imagename}"
+        touch "${targetdir}/${imagename}.tar.gz"
+    fi
+}
+
+_create_fake_images() {
+    if [ ! -d "${IMAGES_DIR}" ]; then
+        echo "Creating fake images"
+        mkdir -p "${IMAGES_DIR}" || fail "mkdir failed"
+        local image
+        for image in image1 image2 image3; do
+            _create_fake_image "${image}" "${IMAGES_DIR}"
+        done
+    fi
+}
+
+_fetch_iso_and_patches() {
+    # Fetch Debian ISO and test patches from yow-wrcp-lx
+    [ -d "${INPUT_DIR}" ] || mkdir -p "${INPUT_DIR}" || fail "mkdir failed"
+    if [ ! -f "${ISOFILE}" ]; then
+        echo "Fetching ISO"
+        scp 'yow-wrcp-lx:/localdisk/loadbuild/jenkins/wrcp-master-debian/latest_build/export/outputs/iso/starlingx-intel-x86-64-2*-cd.*' "${INPUT_DIR}/" || fail "scp iso failed"
+        ln -s "${INPUT_DIR}"/starlingx-intel-x86-64-2*-cd.iso "${INPUT_DIR}"/starlingx-intel-x86-64-cd.iso
+        ln -s "${INPUT_DIR}"/starlingx-intel-x86-64-2*-cd.sig "${INPUT_DIR}"/starlingx-intel-x86-64-cd.sig
+    fi
+    if [ ! -d "${PATCHES_DIR}" ]; then
+        echo "Fetching patches"
+        mkdir -p "${PATCHES_DIR}" || fail "mkdir failed"
+        scp 'yow-wrcp-lx:/localdisk/loadbuild/jenkins/wrcp-master-debian/latest_build/test_patches/*{NRR_INSVC,RR_ALL_NODES,_RR_ALL_NODES_REQUIRES}.patch' "${INPUT_DIR}/patches/" || fail "scp iso failed"
+    fi
+}
+
+# Executed at start of all tests
+oneTimeSetUp() {
+    th_info oneTimeSetUp
+    if [ -d "${BUILDDIR}" ]; then
+        th_debug "Cleaning ${BUILDDIR}"
+        rm -rf "${BUILDDIR}"
+    fi
+    mkdir -p "${BUILDDIR}" || fail "Failed to create ${BUILDDIR}"
+    _create_fake_images
+    _fetch_iso_and_patches
+}
+
+# Executed at start of each tests
+setUp() {
+    th_info setUp
+}
+
+oneTimeTearDown() {
+    th_info oneTimeTearDown
+    if [ -z "${KEEP_ARTIFACTS}" ]; then
+        if [ -n "${BUILDDIR}" ] && [ -d "${BUILDDIR}" ]; then
+            th_debug "Cleaning ${BUILDDIR}"
+            rm -rf "${BUILDDIR}"
+        fi
+    fi
+}
+
+# Executed at completion of each tests
+tearDown() {
+    th_info tearDown
+    if [ -n "${MNTDIR}" ] && [ -d "${MNTDIR}" ]; then
+        th_info "tearDown: unmounting ${MNTDIR}"
+        sudo umount "${MNTDIR}" 2>/dev/null || th_warn "umount failed for ${MNTDIR}"
+    fi
+    if [ -f "${OUTPUT_ISO}" ]; then
+        sudo rm "${OUTPUT_ISO}"
+    fi
+}
+
+create_ks_addon_file() {
+cat <<EOF > "${BUILDDIR}/ks-addon.cfg"
+ilog "Executing ks-addon.cfg"
+EOF
+}
+
+validate_generated_iso() {
+    local syslinux_boot=1
+    local grub_boot=graphical
+    local param=
+    local syslinux_timeout=300
+    local grub_timeout=30
+    local ks_addon=
+    while [ $# -gt 0 ] ; do
+        case "${1:-""}" in
+            --syslinux-boot)
+                shift
+                syslinux_boot=$1
+                ;;
+            --grub-boot)
+                shift
+                grub_boot=$1
+                ;;
+            --param)
+                shift
+                param=$1
+                ;;
+            --syslinux-timeout)
+                shift
+                syslinux_timeout=$1
+                ;;
+            --grub-timeout)
+                shift
+                grub_timeout=$1
+                ;;
+            --ks-addon)
+                shift
+                ks_addon=ks-addon.cfg
+                ;;
+            *)
+                echo "Invalid expected value '$1'"
+                exit 1
+                ;;
+        esac
+        shift
+    done
+
+    # Mount the ISO
+    [ -f "${OUTPUT_ISO}" ] || fail "${OUTPUT_ISO} does not exist"
+    MNTDIR=${BUILDDIR}/mnt
+    [ -d "${MNTDIR}" ] || mkdir "${MNTDIR}" || fail "mkdir failed"
+    sudo mount "${OUTPUT_ISO}" "${MNTDIR}" || fail "Failed to mount ${OUTPUT_ISO}"
+
+    # Check that images are included:
+    th_info "Image check: validating images are in ISO"
+    for image in "${IMAGES_DIR}"/*.tar.gz; do
+        [ "$(find "${MNTDIR}"/opt/platform-backup -name "$(basename "${image}")" | wc -l)" -eq 1 ]  \
+            || fail "Missing expected image ${image}"
+        th_info "Found expected image: ${image}"
+    done
+    th_info "Image check passed"
+
+    # Check that patches are included:
+    th_info "Patch check: validating patches are in ISO"
+    for patch in "${PATCHES_DIR}"/*.patch; do
+        [ "$(find "${MNTDIR}"/opt/platform-backup -name "$(basename "${patch}")" | wc -l)" -eq 1 ]  \
+            || fail "Missing expected patch ${patch}"
+        th_info "Found expected patch: ${patch}"
+    done
+    th_info "Patch check passed"
+
+    local syslinux_cfg=${MNTDIR}/isolinux/isolinux.cfg
+    local grub_cfg=${MNTDIR}/EFI/BOOT/grub.cfg
+
+    if [ -n "${syslinux_boot}" ]; then
+        grep -q -i "default ${syslinux_boot}" "${syslinux_cfg}" \
+            || fail "Incorrect syslinux boot: ${syslinux_boot} in ${syslinux_cfg}"
+    fi
+    if [ -n "${syslinux_timeout}" ]; then
+        grep -q "timeout ${syslinux_timeout}" "${syslinux_cfg}" \
+            || fail "Incorrect syslinux timeout: (expected ${syslinux_timeout}) in ${syslinux_cfg}"
+    fi
+    if [ -n "${grub_boot}" ]; then
+        grep 'default=' "${grub_cfg}" | grep -q "${grub_boot}" \
+            || fail "Incorrect grub boot (expected ${grub_boot}) in EFI/BOOT/grub.cfg"
+    fi
+    if [ -n "${grub_timeout}" ]; then
+        grep -q "timeout=${grub_timeout}" "${grub_cfg}" \
+            || fail "Incorrect grub timeout: ${grub_timeout} in EFI/BOOT/grub.cfg"
+    fi
+    if [ -n "${param}" ]; then
+        # There should be two boot entries containing param (graphical + serial)
+        [ "$(grep -c "${param}" "${syslinux_cfg}")" -eq 2 ] \
+            || fail "Incorrect param value (expected ${param}) in ${syslinux_cfg}"
+        [ "$(grep -c "${param}" "${grub_cfg}")" -eq 2 ] \
+            || fail  "Incorrect param value (expected ${param}) in EFI/BOOT/grub.cfg"
+    fi
+    if [ -n "${ks_addon}" ]; then
+        [ -f "${MNTDIR}/${ks_addon}" ] || fail "Expected ks-addon ${ks_addon} not found"
+    fi
+}
+
+test_generate_prestaged_iso_1() {
+    th_info "Running test_generate_prestaged_iso_1"
+    (
+        cd "${TARGET_SCRIPTDIR}"
+        local images=""
+        local image
+        for image in "${IMAGES_DIR}"/*.tar.gz; do
+            images="${images} --image ${image}"
+        done
+        local patches=""
+        local patch
+        for patch in "${PATCHES_DIR}"/*.patch; do
+            patches="${patches} --patch ${patch}"
+        done
+        # shellcheck disable=2086
+        sudo ./gen-prestaged-iso.sh --input "${ISOFILE}" \
+            --output "${OUTPUT_ISO}" \
+            ${images} ${patches}
+    ) || fail "gen-prestaged-iso.sh failed"
+
+    th_info "Generated ${OUTPUT_ISO}"
+
+    if [ -n "${KEEP_ARTIFACTS}" ]; then
+        th_info "Preserving ISO in ${SCRIPTDIR}/generated_debian_1.iso"
+        cp "${OUTPUT_ISO}" "${SCRIPTDIR}"/generated_debian_debian_1.iso
+    fi
+
+    validate_generated_iso --syslinux-boot 1 --grub-boot graphical \
+        --syslinux-timeout 300 --grub-timeout 30
+}
+
+test_generate_prestaged_iso_2() {
+    th_info "Running test_generate_prestaged_iso_2"
+    (   # subshell
+        cd "${TARGET_SCRIPTDIR}"
+        local images=""
+        local image
+        for image in "${IMAGES_DIR}"/*.tar.gz; do
+            if [ -z "${images}" ]; then
+                images="--image ${image}"
+            else
+                images="${images},${image}"
+            fi
+        done
+        local patches=""
+        local patch
+        for patch in "${PATCHES_DIR}"/*.patch; do
+            if [ -z "${patches}" ]; then
+                patches="--patch ${patch}"
+            else
+                patches="${patches},${patch}"
+            fi
+        done
+        create_ks_addon_file
+
+        # shellcheck disable=2086
+        sudo ./gen-prestaged-iso.sh --input "${ISOFILE}" \
+            --output "${OUTPUT_ISO}" \
+            --addon "${BUILDDIR}/ks-addon.cfg" \
+            --default-boot 0 \
+            --timeout 90 \
+            --force-install \
+            --param "param1=1,param2=2" \
+            ${images} ${patches} \
+    ) || fail "gen-prestaged-iso.sh failed"
+
+    th_info "Generated ${OUTPUT_ISO}"
+
+    if [ -n "${KEEP_ARTIFACTS}" ]; then
+        th_info "Preserving ISO in ${SCRIPTDIR}/generated_debian_2.iso"
+        cp "${OUTPUT_ISO}" "${SCRIPTDIR}"/generated_debian_2.iso
+    fi
+
+    validate_generated_iso --syslinux-boot 0 --grub-boot serial \
+        --syslinux-timeout 900 --grub-timeout 90 \
+        --param "param1=1 param2=2" --ks-addon ks-addon.cfg
+}
+
+
+# shellcheck disable=SC2154
+trap 'rc=$?; echo "Caught abnormal signal rc=$rc"; exit $rc' 2 3 15
+
+th_info "Running shunit2"
+
+# Load and run shunit2.
+# shellcheck disable=SC2034
+[ -n "${ZSH_VERSION:-}" ] && SHUNIT_PARENT=$0
+. "${TH_SHUNIT}"
diff --git a/utilities/platform-util/scripts/test/run-tests.sh b/utilities/platform-util/scripts/test/run-tests.sh
new file mode 100755
index 00000000..0cdde852
--- /dev/null
+++ b/utilities/platform-util/scripts/test/run-tests.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+echo "Running all unit tests"
+
+# shellcheck disable=SC2155,SC2034
+readonly SCRIPTDIR=$(readlink -m "$(dirname "$0")")
+cd "${SCRIPTDIR}" || { echo "cd failed"; exit 1; }
+
+TESTS="./gen-prestaged-iso-test.sh ./gen-prestaged-iso-test.sh"
+
+log_progress() { echo -e "$(tput setaf 2)$*$(tput sgr0)"; }
+log_error() { echo -e "$(tput setaf 1)ERROR: $*$(tput sgr0)"; }
+
+declare -i rc
+for testscript in ${TESTS}; do
+    log_progress "--------------------------------------------------------------------------------"
+    log_progress "Executing ${testscript}"
+    ${testscript}
+    rc=$?
+    if [ $rc -ne 0 ]; then
+        log_error "failure in ${testscript}, rc=${rc}"
+        exit $rc
+    fi
+done
diff --git a/utilities/platform-util/scripts/test/shunit2_helper.sh b/utilities/platform-util/scripts/test/shunit2_helper.sh
new file mode 100644
index 00000000..17a35383
--- /dev/null
+++ b/utilities/platform-util/scripts/test/shunit2_helper.sh
@@ -0,0 +1,68 @@
+#!/bin/bash
+
+# -----------------------------------------------------------------------------
+# Helper stuff taken from https://github.com/kward/shunit2/blob/master/shunit2_test_helpers
+
+# Note: the last release, 2.1.8 is buggy/not working. Checkout the git repo and use that instead.
+SHUNIT_DIR="${SCRIPTDIR:-.}/shunit2"
+if [ ! -d "${SHUNIT_DIR}" ]; then
+    (   # subprocess
+        cd "$SCRIPTDIR"
+        git clone "https://github.com/kward/shunit2.git" || { echo "ERROR: failed to clone shunit2"; exit 1; }
+    )
+fi
+# Path to shunit2 library. Can be overridden by setting SHUNIT_INC.
+TH_SHUNIT=${SHUNIT_DIR}/shunit2 export TH_SHUNIT
+
+set -e  # Exit immediately if a simple command exits with a non-zero status.
+set -u  # Treat unset variables as an error when performing parameter expansion.
+
+# Set shwordsplit for zsh.
+[ -n "${ZSH_VERSION:-}" ] && setopt shwordsplit
+
+#
+# Constants.
+#
+
+# Configure debugging. Set the DEBUG environment variable to any
+# non-empty value to enable debug output, or TRACE to enable trace
+# output.
+TRACE=${TRACE:+'th_trace '}
+[ -n "${TRACE}" ] && DEBUG=1
+[ -z "${TRACE}" ] && TRACE=':'
+
+DEBUG=${DEBUG:+'th_debug '}
+[ -z "${DEBUG}" ] && DEBUG=':'
+
+#
+# Functions.
+#
+
+# Logging functions.
+th_trace() { echo "test:TRACE $*" >&2; }
+th_debug() { echo "test:DEBUG $*" >&2; }
+th_info()  { echo "test:INFO $*" >&2; }
+th_warn()  { echo "test:WARN $*" >&2; }
+th_error() { echo "test:ERROR $*" >&2; }
+th_fatal() { echo "test:FATAL $*" >&2; }
+
+# Output subtest name.
+th_subtest() { echo " $*" >&2; }
+
+
+#
+# Bind in our own non-exiting functions. These overwrite the functions from stx-iso-utils.sh:
+#
+function elog {
+    echo "$(date "+%F %H-%M-%S") Error: $*" >&2
+    return 1
+}
+
+function check_rc_die {
+    local -i rc=$1; shift
+    if [ $rc -ne 0 ]; then
+        echo "$(date "+%F %H-%M-%S") Error: $*" >&2
+        return $rc
+    fi
+}
+