#!/bin/bash -e
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# This script takes as input an ISO and one or more patches
# and generates as output an ISO with the following changes:
#
# - Contains only the latest ostree commit from the input ISO
# - ISO has a "patches" folder with the patches' metadata files.
# This folder is processed by kickstart during install, so that
# 'sw-patch query' has access to this info.
#
# The intent is for the system to have record of the patches that are
# already pre-installed in the system.
#
BUILD_TOOLS_DIR="$(dirname "$0")"
# shellcheck source="./build-tools/image-utils.sh"
source "${BUILD_TOOLS_DIR}/image-utils.sh"
# Define MY_REPO, which is the path to the 'root' repo. Eg.: $REPO_ROOT/cgcs_root
# Value is used to locate the following file for ISO signing:
# ${MY_REPO}/build-tools/signing/dev-private-key.pem
if [ -z "${MY_REPO}" ]; then
MY_REPO="$(dirname "${BUILD_TOOLS_DIR}")"
fi
function usage() {
echo ""
echo "Usage: "
echo " $(basename "$0") -i -o [ -p ] ..."
echo " -i : Specify input ISO file"
echo " -o : Specify output ISO file"
echo " -p : Patch files. You can call it multiple times."
echo ""
}
function extract_ostree_commit_from_metadata_xml() {
local XML_PATH=$1
local XPATH="//contents/ostree/commit1/commit"
# Check if xmllint is available. Otherwise, use python's xml standard lib
if (which xmllint &>/dev/null); then
xmllint --xpath "string(${XPATH})" "${XML_PATH}"
else
python3 -c "import xml.etree.ElementTree as ET ; print(ET.parse('${XML_PATH}').find('.${XPATH}').text, end='')"
fi
}
function extract_metadata() {
local patchesdir
local patchfile
local patchid
local ostree_log
local ostree_commit
patchesdir="${BUILDDIR}/patches"
patchfile="$1"
patchid="$(basename "$patchfile" .patch)"
ostree_log="$(ostree --repo="${2}" log starlingx)"
echo "Extracting ${patchfile}"
# Extract it
if ! tar xf "${patchfile}" -O metadata.tar | tar x -O > "${patchesdir}/${patchid}-metadata.xml"; then
echo "ERROR: Failed to extract metadata from ${patchfile}"
exit 1
fi
# Verify if top commit from metadata exist in ostree log
patch_ostree_commit1="$(extract_ostree_commit_from_metadata_xml "${patchesdir}/${patchid}-metadata.xml")"
if [[ "$ostree_log" != *"$patch_ostree_commit1"* ]]; then
echo "WARNING: Patch ostree commit 1 not found in input ISO."
echo "Patch ostree commit 1: ${patch_ostree_commit1}"
fi
}
declare INPUT_ISO=
declare OUTPUT_ISO=
while getopts "i:o:p:" opt; do
case $opt in
i)
INPUT_ISO=$OPTARG
;;
o)
OUTPUT_ISO=$OPTARG
;;
p)
PATCH_FILES+=("$OPTARG")
;;
*)
usage
exit 1
;;
esac
done
if [ -z "$INPUT_ISO" ] || [ -z "$OUTPUT_ISO" ]; then
usage
exit 1
fi
if [ ! -f "${INPUT_ISO}" ]; then
echo "ERROR: Input file does not exist: ${INPUT_ISO}"
exit 1
fi
if [ -f "${OUTPUT_ISO}" ]; then
echo "ERROR: Output file already exists: ${OUTPUT_ISO}"
exit 1
fi
for PATCH in "${PATCH_FILES[@]}";
do
if [ ! -f "${PATCH}" ]; then
echo "ERROR: Patch file dos not exists: ${PATCH}"
exit 1
fi
if [[ ! "${PATCH}" =~ \.patch$ ]]; then
echo "ERROR: Specified file ${PATCH} does not have .patch extension"
exit 1
fi
done
shift $((OPTIND-1))
declare BUILDDIR=
function check_requirements {
# Next to each requirement is the deb package which provides the command listed.
# Run "apt install ..."
# Declare "require reqA or reqB" as "reqA__reqB"
local -a required_utils=(
7z # p7zip-full
mkisofs__xorrisofs # genisoimage / xorriso
isohybrid # syslinux-utils
implantisomd5 # isomd5sum
ostree # ostree
xmllint__python3 # libxml2-utils / python3
)
local -i missing=0
local reqA
local reqB
for req in "${required_utils[@]}"; do
if [[ "$req" = *"__"* ]]; then
reqA="${req%__*}" # select everything before "__"
reqB="${req#*__}" # select everything after "__"
if ! (which "${reqA}" &>/dev/null) && ! (which "${reqB}" &>/dev/null); then
echo "Unable to find required utility: either ${reqA} or ${reqB}" >&2
missing=$(( missing+1 ))
fi
else
if ! (which "${req}" &>/dev/null); then
echo "Unable to find required utility: ${req}" >&2
missing=$(( missing+1 ))
fi
fi
done
if [ "${missing}" -gt 0 ]; then
echo "ERROR: One or more required utilities are missing" >&2
exit 1
fi
}
function cleanup() {
# Delete temporary build directory
if [ -n "$BUILDDIR" ] && [ -d "$BUILDDIR" ]; then
chmod -R +w "$BUILDDIR"
\rm -rf "$BUILDDIR"
fi
}
check_requirements
# Run cleanup() when finishing/interrupting execution
trap cleanup EXIT
# Create temporary build directory
BUILDDIR=$(mktemp -d -p "$PWD" patchiso_build_XXXXXX)
if [ -z "${BUILDDIR}" ] || [ ! -d "${BUILDDIR}" ]; then
echo "ERROR: Failed to create temporary build directory"
exit 1
fi
# Fix for permission denied if not running as root
chmod +w "${BUILDDIR}"
if [ -d "${BUILDDIR}/isolinux" ]; then
chmod -R +w "${BUILDDIR}/isolinux"
fi
# Erase current patch metadata from ISO if it exists
# This way, this script can be used on pre-patched ISOs
if [ -d "${BUILDDIR}/patches" ]; then
rm -rf "${BUILDDIR}/patches"
fi
# Create the directory where patch metadata will be stored
mkdir -p "${BUILDDIR}/patches"
chmod -R +w "${BUILDDIR}/patches"
echo "Extracting Input ISO contents..."
if ! 7z x "${INPUT_ISO}" -o"${BUILDDIR}" 1>/dev/null ; then
echo "ERROR: Extract ISO contents"
exit 1
fi
# Delete boot directory. It will be re-created when packing the output ISO
if [ -d "${BUILDDIR}/[BOOT]" ]; then
rm -rf "${BUILDDIR}/[BOOT]"
fi
echo "Extracting patch metadata..."
for PATCH in "${PATCH_FILES[@]}";
do
extract_metadata "$PATCH" "${BUILDDIR}/ostree_repo"
done
echo "Original ostree repo history:"
echo "----------------------------------------------------------------------------------"
ostree --repo="${BUILDDIR}/ostree_repo" log starlingx
echo "----------------------------------------------------------------------------------"
echo "Clean up all commits from ostree repo except the latest one..."
function clean_ostree(){
# Create array of ostree commit IDs
mapfile -t ostree_commits < <(ostree --repo="${BUILDDIR}/ostree_repo" log starlingx | grep '^commit' | cut -d ' ' -f 2)
# Delete each commit except the latest one
for ostree_commit in "${ostree_commits[@]:1}"; do
echo "Removing commit: ${ostree_commit}"
ostree --repo="${BUILDDIR}/ostree_repo" prune --delete-commit="${ostree_commit}"
done
ostree --repo="${BUILDDIR}/ostree_repo" summary --update
}
if ! clean_ostree; then
echo "ERROR: Failed to clean ostree repo!"
exit 1
fi
echo "Output ISO ostree history:"
echo "----------------------------------------------------------------------------------"
ostree --repo="${BUILDDIR}/ostree_repo" log starlingx
echo "----------------------------------------------------------------------------------"
echo "Packing iso..."
# get the install label
ISO_LABEL=$(grep -ri instiso "${BUILDDIR}"/isolinux/isolinux.cfg | head -1 | xargs -n1 | awk -F= /instiso/'{print $2}')
if [ -z "${ISO_LABEL}" ] ; then
echo "Error: Failed to get iso install label"
fi
echo "ISO Label: ${ISO_LABEL}"
function pack_iso(){
if (which xorrisofs &>/dev/null); then
PACK_ISO_CMD="xorrisofs"
else
PACK_ISO_CMD="mkisofs"
fi
echo "ISO packaging command: ${PACK_ISO_CMD}"
# Command Reference:
# https://github.com/yoctoproject/poky/blob/master/scripts/lib/wic/plugins/source/isoimage-isohybrid.py#L419
${PACK_ISO_CMD} \
-V "${ISO_LABEL}" \
-o "${OUTPUT_ISO}" -U \
-J -joliet-long -r -iso-level 2 -b "isolinux/isolinux.bin" \
-c "isolinux/boot.cat" -no-emul-boot -boot-load-size 4 \
-boot-info-table -eltorito-alt-boot \
-eltorito-platform "0xEF" -eltorito-boot "efi.img" \
-no-emul-boot "${BUILDDIR}"
isohybrid --uefi "${OUTPUT_ISO}"
implantisomd5 "${OUTPUT_ISO}"
}
if ! pack_iso; then
if [ "${PACK_ISO_CMD}" = "mkisofs" ]; then
echo "NOTE: mkisofs has a customization in the LAT container to provide the '-eltorito-boot' flag."
echo " To execute this script outside the LAT container, install the 'xorriso' package and run again."
fi
echo "ERROR: Failed to build output ISO!"
exit 1
fi
echo "Signing the .iso with the developer private key..."
function sign_iso(){
openssl dgst -sha256 \
-sign "${MY_REPO}/build-tools/signing/dev-private-key.pem" \
-binary \
-out "${OUTPUT_ISO/%.iso/.sig}" \
"${OUTPUT_ISO}"
}
if ! sign_iso; then
echo "ERROR: Failed to sign ISO!"
exit 1
fi
echo ""
echo "Output ISO: $(realpath "${OUTPUT_ISO}")"