utilities/utilities/platform-util/scripts/gen-bootloader-iso.sh

789 lines
21 KiB
Bash
Executable File

#!/bin/bash
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# Utility for setting up a mini ISO and boot structure to support a
# hybrid boot that combines an ISO and network boot, where:
# - mini ISO contains kernel and initrd, with boot parameters
# configured to access everything else from network
# - setup rootfs (squashfs.img), kickstart, and software repositories
# under an http/https served directory
#
#
# Source shared utility functions
source $(dirname $0)/stx-iso-utils.sh
declare LOG_TAG=$(basename $0)
function log_error {
logger -i -s -t ${LOG_TAG} -- "$@"
}
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 usage {
cat <<ENDUSAGE
Description: Sets up a mini bootimage.iso that includes the minimum required to
retrieve the rootfs and software packages needed for installation via http or
https, generated for a specific node.
Mandatory parameters for setup:
--input <file>: Specify input ISO file
--www-root <dir>: Specify www-serviced directory
--baseurl <url>: Specify URL for www-root dir
--id <node id>: Specify ID for target node
--boot-interface <intf>: Specify target node boot interface
--boot-ip <ip address>: Specify address for boot interface
--default-boot <0-5>: Specify install type:
0 - Standard Controller, Serial Console
1 - Standard Controller, Graphical Console
2 - AIO, Serial Console
3 - AIO, Graphical Console
4 - AIO Low-latency, Serial Console
5 - AIO Low-latency, Graphical Console
Optional parameters for setup:
--addon <file>: Specify custom kickstart %post addon, for
post-install interface config
--boot-hostname <host>: Specify temporary hostname for target node
--boot-netmask <mask>: Specify netmask for boot interface
--boot-gateway <addr>: Specify gateway for boot interface
--timeout <seconds>: Specify boot menu timeout, in seconds
--lock-timeout <secs>: Specify time to wait for mutex lock before aborting
--patches-from-iso: Use patches from the ISO, if any, rather than host
--param <p=v>: Specify boot parameter customization
Examples:
--param rootfs_device=nvme0n1 --param boot_device=nvme0n1
--param rootfs_device=/dev/disk/by-path/pci-0000:00:0d.0-ata-1.0
--param boot_device=/dev/disk/by-path/pci-0000:00:0d.0-ata-1.0
Generated ISO will be: <www-root>/nodes/<node-id>/bootimage.iso
Mandatory parameters for cleanup:
--www-root <dir>: Specify www-serviced directory
--id <node id>: Specify ID for target node
--delete: Request file deletion
Example kickstart addon, to define a VLAN on initial OAM interface setup:
#### start custom kickstart
OAM_DEV=enp0s3
OAM_VLAN=1234
cat << EOF > /etc/sysconfig/network-scripts/ifcfg-\$OAM_DEV
DEVICE=\$OAM_DEV
BOOTPROTO=none
ONBOOT=yes
LINKDELAY=20
EOF
cat << EOF > /etc/sysconfig/network-scripts/ifcfg-\$OAM_DEV.\$OAM_VLAN
DEVICE=\$OAM_DEV.\$OAM_VLAN
BOOTPROTO=dhcp
ONBOOT=yes
VLAN=yes
LINKDELAY=20
EOF
#### end custom kickstart
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 generate_boot_cfg {
local isodir=$1
if [ -z "${EFI_MOUNT}" ]; then
mount_efiboot_img ${isodir}
fi
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}"
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}
menu begin
menu title ${NODE_ID}
# Serial Console submenu
label 0
menu label Serial Console
kernel vmlinuz
initrd initrd.img
append rootwait ${COMMON_ARGS} console=ttyS0,115200 serial
# Graphical Console submenu
label 1
menu label Graphical Console
kernel vmlinuz
initrd initrd.img
append rootwait ${COMMON_ARGS} console=tty0
menu end
EOF
done
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}" {
echo " "
}
menuentry 'Serial Console' --id=serial {
linuxefi /vmlinuz ${COMMON_ARGS} console=ttyS0,115200 serial
initrdefi /initrd.img
}
menuentry 'Graphical Console' --id=graphical {
linuxefi /vmlinuz ${COMMON_ARGS} console=tty0
initrdefi /initrd.img
}
EOF
done
}
function cleanup {
if [ $? -ne 0 ]; then
# Clean up from failure
handle_delete
fi
common_cleanup
}
function check_requirements {
common_check_requirements
}
function handle_delete {
# Remove node-specific files
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}
fi
if [ -d ${SHARED_DIR} ]; then
rm -rf ${SHARED_DIR}
fi
fi
# Mark the DNF cache expired
dnf clean expire-cache
}
function get_patches_from_host {
local host_patch_repo=/www/pages/updates/rel-${ISO_VERSION}
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
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
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
# 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
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
local metadata_to_copy=
for state in applied committed; do
if [ ! -d /opt/patching/metadata/${state} ]; then
continue
fi
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
fi
done
}
function query_patched_pkg {
local pkg=$1
local pkg_location=
local shared_patch_repo=${SHARED_DIR}/patches
pkg_location=$(dnf repoquery --disablerepo=* --repofrompath local,file:///${shared_patch_repo} --latest-limit=1 --location -q ${pkg})
if [ $? -eq 0 -a -n "${pkg_location}" ]; then
echo ${pkg_location/file:\/\/\//}
fi
}
function extract_pkg_to_workdir {
local pkg=$1
local pkgfile=
pkgfile=$(query_patched_pkg ${pkg})
if [ -z "${pkgfile}" ]; then
# Nothing to do
return
fi
if [ ! -f "${pkgfile}" ]; then
log_error "File doesn't exist, unable to extract: ${pkgfile}"
exit 1
fi
pushd ${WORKDIR} >/dev/null
echo "Extracting files from ${pkgfile}"
rpm2cpio ${pkgfile} | cpio -idmv
if [ $? -ne 0 ]; then
log_error "Failed to extract files from ${pkgfile}"
exit 1
fi
popd >/dev/null
}
function extract_shared_files {
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
# Check ISO content
if [ ! -f ${MNTDIR}/LiveOS/squashfs.img ]; then
log_error "squashfs.img not found on ${INPUT_ISO}"
exit 1
fi
# Setup shared patch data
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
fi
fi
# Mark the DNF cache expired, in case there was previous ad-hoc repo data
dnf clean expire-cache
local squashfs_img_file=${MNTDIR}/LiveOS/squashfs.img
if [ ${PATCHES_FROM_HOST} = "yes" ]; then
extract_pkg_to_workdir 'pxe-network-installer'
local patched_squashfs_img_file=${WORKDIR}/www/pages/feed/rel-${ISO_VERSION}/LiveOS/squashfs.img
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
fi
local pxeboot_files_dir=${MNTDIR}/pxeboot
if [ ${PATCHES_FROM_HOST} = "yes" ]; then
extract_pkg_to_workdir 'platform-kickstarts-pxeboot'
local patched_pxeboot_files_dir=${WORKDIR}/pxeboot
if [ -f ${patched_pxeboot_files_dir}/pxeboot_controller.cfg ]; then
# Use the patched pxeboot files
pxeboot_files_dir=${patched_pxeboot_files_dir}
fi
fi
mkdir ${SHARED_DIR}/pxeboot/
rsync -a ${pxeboot_files_dir}/pxeboot_*.cfg ${SHARED_DIR}/pxeboot/
if [ $? -ne 0 ]; then
log_error "Failed to copy pxeboot files from ${pxeboot_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
}
function extract_node_files {
# Copy files for mini ISO build
rsync -a \
--exclude LiveOS/ \
--exclude Packages/ \
--exclude repodata/ \
--exclude patches/ \
--exclude pxeboot/ \
--exclude pxeboot_setup.sh \
--exclude upgrades/ \
--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
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
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
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
fi
fi
# Setup syslinux and grub cfg files
generate_boot_cfg ${BUILDDIR}
# Set/update boot parameters
if [ ${#PARAMS[@]} -gt 0 ]; then
for p in ${PARAMS[@]}; do
param=${p%%=*} # Strip from the first '=' on
value=${p#*=} # Strip to the first '='
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
# Setup symlinks to the shared content, which lighttpd can serve
pushd ${NODE_DIR} >/dev/null
ln -s ../../shared/* .
popd >/dev/null
# Rebuild the ISO
OUTPUT_ISO=${NODE_DIR}/bootimage.iso
mkisofs -o ${OUTPUT_ISO} \
-R -D -A 'oe_iso_boot' -V 'oe_iso_boot' \
-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}
# Setup the kickstart
local ksfile=${SHARED_DIR}/pxeboot/pxeboot_${KS_NODETYPE}.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
# Number of dirs in the NODE_URL: Count the / characters, subtracting 2 for http:// or https://
DIRS=$(($(grep -o "/" <<< "$NODE_URL" | wc -l) - 2))
# Escape the / chars for use in sed
NODE_URL_SED="${NODE_URL//\//\\/}"
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
# Append the custom addon
if [ -n "${ADDON}" ]; then
cat <<EOF >>${NODE_DIR}/miniboot_${KS_NODETYPE}.cfg
%post --erroronfail
# Source common functions
. /tmp/ks-functions.sh
$(cat ${ADDON})
%end
EOF
fi
}
#
# 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
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}"
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}
# 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