patch-iso-debian: refactor script

- Remove need for STX_BUILD_HOME and MY_REPO env variables

- Get base ostree repo from Input ISO, not from /localdisk/deploy.
  This detaches the script from the build env
  and guarantees the ostree repo matches the input ISO.

- Fixed shellcheck errors and warnings

- No longer uses mount/guestmount to get Input ISO contents,
  as these require sudo privileges [1, 2]. Now uses 7z instead.

- To produce output ISO's ostree repo, use 'ostree prune'
  instead of 'ostree pull'. This generates "tombstone" commits,
  which signal that the missing commits were intentionally removed.
  This prevents some ostree errors, such as running
  "ostree pull --depth=-1" to pull from the resulting repo.

- If the Input ISO already has a "patches" folder, delete it before
  trying to add patches metadata. This allows pre-patched ISOs
  to be used as input, which is useful for testing.

With these changes, the only requirement left is that
the script be placed in the root repo so that it can find
the .pem file needed for signing.

IMPORTANT: Only works if ISO has ostree version 2022.2 or later.
But this requirement was introduced in an earlier commit,
when "ostree pull --depth=-1" was changed to "--depth=0",
as ostree had a blocking bug before that version:
https://review.opendev.org/c/starlingx/root/+/903888

Refs:
[1] https://bugs.launchpad.net/ubuntu/+source/libguestfs/+bug/1673431
[2] https://bugs.launchpad.net/fuel/+bug/1467579

Test Plan:
pass - create pre-patched ISO
pass - install pre-patched ISO
pass - apply and remove a patch on the installed system
pass - system shows patch metadata on "sw-patch" cmd

Story: 2011098
Task: 50534

Depends-On: https://review.opendev.org/c/starlingx/tools/+/926443

Change-Id: I7e5cb6865b715ab789f489dff44a7d39327a01c1
Signed-off-by: Leonardo Fagundes Luz Serrano <Leonardo.FagundesLuzSerrano@windriver.com>
This commit is contained in:
Leonardo Fagundes Luz Serrano
2024-07-09 13:01:08 -03:00
parent c188f6211b
commit 615bf2f250

View File

@@ -1,37 +1,37 @@
#!/bin/bash
#
# Copyright (c) 2023 Wind River Systems, Inc.
# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# Utility for adding patch metadata to the iso
# Debian patches are sequential and uses ostree
# so any patch will produce an updated iso, this
# utility injects the patch metadata into the iso.
# During install the kickstart will copy the metadata
# into the right location and the sw-patch query
# command will output the correct patch level
# 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.
#
source "$(dirname $0)/image-utils.sh"
BUILD_TOOLS_DIR="$(dirname "$0")"
if [ -z "${STX_BUILD_HOME}" ]; then
echo "Required environment variable STX_BUILD_HOME is not set"
exit 1
fi
# 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
echo "Required environment variable MY_REPO is not set"
exit 1
MY_REPO="$(dirname "${BUILD_TOOLS_DIR}")"
fi
DEPLOY_DIR="${STX_BUILD_HOME}/localdisk/deploy"
OSTREE_REPO="${DEPLOY_DIR}/ostree_repo"
function usage() {
echo ""
echo "Usage: "
echo " $(basename $0) -i <input filename.iso> -o <output filename.iso> [ -p ] <patch> ..."
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."
@@ -44,28 +44,33 @@ function extract_ostree_commit_from_metadata_xml() {
# Check if xmllint is available. Otherwise, use python's xml standard lib
if (which xmllint &>/dev/null); then
xmllint --xpath "string(${XPATH})" ${XML_PATH}
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=${BUILDDIR}/patches
local patchfile=$1
local patchid=$(basename $patchfile .patch)
local ostree_log=$(ostree --repo=${OSTREE_REPO} log starlingx)
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
tar xf ${patchfile} -O metadata.tar | tar x -O > ${patchesdir}/${patchid}-metadata.xml
if [ $? -ne 0 ]; then
echo "Failed to extract metadata from ${patchfile}"
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=$(xmllint --xpath "string(//contents/ostree/commit1/commit)" ${patchesdir}/${patchid}-metadata.xml)
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}"
@@ -74,8 +79,6 @@ function extract_metadata() {
declare INPUT_ISO=
declare OUTPUT_ISO=
declare ORIG_PWD=$PWD
declare DO_UPGRADES=1
while getopts "i:o:p:" opt; do
case $opt in
@@ -86,7 +89,7 @@ while getopts "i:o:p:" opt; do
OUTPUT_ISO=$OPTARG
;;
p)
PATCH_FILES+=($OPTARG)
PATCH_FILES+=("$OPTARG")
;;
*)
usage
@@ -95,183 +98,162 @@ while getopts "i:o:p:" opt; do
esac
done
if [ -z "$INPUT_ISO" -o -z "$OUTPUT_ISO" ]; then
if [ -z "$INPUT_ISO" ] || [ -z "$OUTPUT_ISO" ]; then
usage
exit 1
fi
if [ ! -f ${INPUT_ISO} ]; then
echo "Input file does not exist: ${INPUT_ISO}"
if [ ! -f "${INPUT_ISO}" ]; then
echo "ERROR: Input file does not exist: ${INPUT_ISO}"
exit 1
fi
if [ -f ${OUTPUT_ISO} ]; then
echo "Output file already exists: ${OUTPUT_ISO}"
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 "Patch file dos not exists: ${PATCH}"
if [ ! -f "${PATCH}" ]; then
echo "ERROR: Patch file dos not exists: ${PATCH}"
exit 1
fi
if [[ ! ${PATCH} =~ \.patch$ ]]; then
echo "Specified file ${PATCH} does not have .patch extension"
if [[ ! "${PATCH}" =~ \.patch$ ]]; then
echo "ERROR: Specified file ${PATCH} does not have .patch extension"
exit 1
fi
done
shift $((OPTIND-1))
declare MNTDIR=
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=(
rsync
mkisofs
isohybrid
implantisomd5
ostree
xmllint__python3
7z # p7zip-full
mkisofs # genisoimage
isohybrid # syslinux-utils
implantisomd5 # isomd5sum
ostree # ostree
xmllint__python3 # libxml2-utils
)
if [ $UID -ne 0 ]; then
# If running as non-root user, additional utils are required
required_utils+=(
guestmount
guestunmount
)
fi
local -i missing=0
local reqA
local reqB
for req in ${required_utils[@]}; do
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
if ! (which "${reqA}" &>/dev/null) && ! (which "${reqB}" &>/dev/null); then
echo "Unable to find required utility: either ${reqA} or ${reqB}" >&2
let missing++
missing=$(( missing+1 ))
fi
else
if ! (which ${req} &>/dev/null); then
if ! (which "${req}" &>/dev/null); then
echo "Unable to find required utility: ${req}" >&2
let missing++
missing=$(( missing+1 ))
fi
fi
done
if [ ${missing} -gt 0 ]; then
echo "One or more required utilities are missing. Aborting..." >&2
if [ "${missing}" -gt 0 ]; then
echo "ERROR: One or more required utilities are missing" >&2
exit 1
fi
}
function mount_iso {
if [ $UID -eq 0 ]; then
# Mount the ISO
mount -o loop ${INPUT_ISO} ${MNTDIR}
if [ $? -ne 0 ]; then
echo "Failed to mount ${INPUT_ISO}" >&2
exit 1
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..."
guestmount -a ${INPUT_ISO} -m /dev/sda1 --ro ${MNTDIR}
rc=$?
if [ $rc -ne 0 ]; then
echo "Call to guestmount failed with rc=$rc. Aborting..."
exit $rc
fi
fi
fi
}
function unmount_iso {
if [ $UID -eq 0 ]; then
umount ${MNTDIR}
else
guestunmount ${MNTDIR}
fi
rmdir ${MNTDIR}
}
function cleanup() {
if [ -n "$MNTDIR" -a -d "$MNTDIR" ]; then
unmount_iso
# Delete temporary build directory
if [ -n "$BUILDDIR" ] && [ -d "$BUILDDIR" ]; then
chmod -R +w "$BUILDDIR"
\rm -rf "$BUILDDIR"
fi
if [ -n "$BUILDDIR" -a -d "$BUILDDIR" ]; then
chmod -R +w $BUILDDIR
\rm -rf $BUILDDIR
fi
}
check_requirements
# Run cleanup() when finishing/interrupting execution
trap cleanup EXIT
MNTDIR=$(mktemp -d -p $PWD patchiso_mnt_XXXXXX)
if [ -z "${MNTDIR}" -o ! -d ${MNTDIR} ]; then
echo "Failed to create mntdir. Aborting..."
exit $rc
# 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
BUILDDIR=$(mktemp -d -p $PWD patchiso_build_XXXXXX)
if [ -z "${BUILDDIR}" -o ! -d ${BUILDDIR} ]; then
echo "Failed to create builddir. Aborting..."
exit $rc
fi
# Mount the ISO
mount_iso
rsync -a --exclude 'ostree_repo' ${MNTDIR}/ ${BUILDDIR}/
rc=$?
if [ $rc -ne 0 ]; then
echo "Call to rsync ISO content. Aborting..."
exit $rc
fi
unmount_iso
# Fix for permission denied if not running as root
chmod +w ${BUILDDIR}
chmod -R +w ${BUILDDIR}/isolinux
chmod +w "${BUILDDIR}"
if [ -d "${BUILDDIR}/isolinux" ]; then
chmod -R +w "${BUILDDIR}/isolinux"
fi
# Create the directory where metadata will be stored
mkdir -p ${BUILDDIR}/patches
chmod -R +w ${BUILDDIR}/patches
# 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
echo "Copying only the latest commit from ostree_repo..."
ostree --repo=${BUILDDIR}/ostree_repo init --mode=archive-z2
ostree --repo=${BUILDDIR}/ostree_repo pull-local --depth=0 ${OSTREE_REPO} starlingx
ostree --repo=${BUILDDIR}/ostree_repo summary --update
echo "Updated iso ostree commit:"
ostree --repo=${BUILDDIR}/ostree_repo log starlingx
# Create the directory where patch metadata will be stored
mkdir -p "${BUILDDIR}/patches"
chmod -R +w "${BUILDDIR}/patches"
echo "Extracting patch metadata"
echo "Extracting Input ISO contents..."
7z x "${INPUT_ISO}" -o"${BUILDDIR}" 1>/dev/null
# 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
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
@@ -279,30 +261,39 @@ if [ -z "${ISO_LABEL}" ] ; then
fi
echo "ISO Label: ${ISO_LABEL}"
# Repack the ISO
mkisofs -o "${OUTPUT_ISO}" \
-A "${ISO_LABEL}" -V "${ISO_LABEL}" \
-quiet -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 \
-e efi.img \
-no-emul-boot \
"${BUILDDIR}"
function pack_iso(){
# Repack the ISO
mkisofs -o "${OUTPUT_ISO}" \
-A "${ISO_LABEL}" -V "${ISO_LABEL}" \
-quiet -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 \
-e efi.img \
-no-emul-boot \
"${BUILDDIR}"
isohybrid --uefi ${OUTPUT_ISO}
implantisomd5 ${OUTPUT_ISO}
# Sign the .iso with the developer private key
openssl dgst -sha256 \
-sign ${MY_REPO}/build-tools/signing/dev-private-key.pem \
-binary \
-out ${OUTPUT_ISO/%.iso/.sig} \
${OUTPUT_ISO}
rc=$?
if [ $rc -ne 0 ]; then
echo "Call to $(basename ${SETUP_PATCH_REPO}) failed with rc=$rc. Aborting..."
exit $rc
isohybrid --uefi "${OUTPUT_ISO}"
implantisomd5 "${OUTPUT_ISO}"
}
if ! pack_iso; then
echo "ERROR: Failed to build output ISO!"
exit 1
fi
echo "Patched ISO: ${OUTPUT_ISO}"
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}")"