root/build-tools/patch-iso-debian
Leonardo Fagundes Luz Serrano d8a917020f Updated prepatched ISO generation command
patch-iso-debian script uses the mkisofs command
to generate an ISO. Some flags are used there to define
the El Torito configs, but one of these flags needs to be
updated in order to work in the LAT container.

Changes:
- Replaced the '-e' flag for '-eltorito-boot'
- Added '-eltorito-platform 0xEF' flag, following [1]
- Removed "-quiet" flag
- Added support for mkisofs backup option: xorrisofs

Note:
The mkisofs command available in the LAT container has
been customized in order to properly support El Torito
configs [2]. In order to execute patch-iso-debian outside
the LAT container, the developer can use cmds from the
xorriso package: either "xorrisofs" or "xorriso -as mkisofs" [3]

Ref:
[1] https://github.com/yoctoproject/poky/blob/master/scripts/lib/wic/plugins/source/isoimage-isohybrid.py#L424
[2] 19c6b9a324
[3] https://wiki.debian.org/RepackBootableISO

Test Plan: Generate and install pre-patched ISO produced
pass - Script ran inside LAT
pass - Script ran outside LAT

Story: 2011098
Task: 50217

Depends-On: https://review.opendev.org/c/starlingx/root/+/923771

Change-Id: I9c6b15b16385d3e2b4d385378211113d02438c23
Signed-off-by: Leonardo Fagundes Luz Serrano <Leonardo.FagundesLuzSerrano@windriver.com>
2024-08-21 17:59:22 -03:00

316 lines
9.2 KiB
Bash
Executable File

#!/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 <input filename.iso> -o <output filename.iso> [ -p ] <patch> ..."
echo " -i <file>: Specify input ISO file"
echo " -o <file>: Specify output ISO file"
echo " -p <file>: 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}")"