From ac4377165b00baae5bcd3f7e7e3b3f9526fd0950 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Tue, 16 Jun 2026 10:29:41 +0200 Subject: [PATCH 1/3] helpers: add arm64 Metal3 image builder Upstream metal3-io only publishes amd64 container images for ironic, vbmc, and sushy-tools. This blocks running dev-scripts on aarch64 hypervisors (e.g., AWS Graviton). Add a helper script that builds arm64 variants from the same ironic-image source and pushes them to a registry you control. Supports native builds on aarch64 hosts and cross-builds via QEMU from x86_64. Assisted-by: Claude (Anthropic) --- helpers/build-metal3-arm64.sh | 275 ++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100755 helpers/build-metal3-arm64.sh diff --git a/helpers/build-metal3-arm64.sh b/helpers/build-metal3-arm64.sh new file mode 100755 index 0000000..a845324 --- /dev/null +++ b/helpers/build-metal3-arm64.sh @@ -0,0 +1,275 @@ +#!/usr/bin/bash +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_NAME +readonly DEFAULT_REGISTRY="quay.io" +readonly DEFAULT_NAMESPACE="rh-edge-enablement" +readonly DEFAULT_REF="main" +readonly DEFAULT_TAG="latest" +readonly IRONIC_IMAGE_REPO="https://github.com/metal3-io/ironic-image.git" +readonly ALL_IMAGES="ironic,vbmc,sushy-tools" + +readonly COLOR_RED='\033[0;31m' +readonly COLOR_YELLOW='\033[0;33m' +readonly COLOR_GREEN='\033[0;32m' +readonly COLOR_BLUE='\033[0;34m' +readonly COLOR_CLEAR='\033[0m' + +msg_err() { echo -e "${COLOR_RED}ERROR: ${1}${COLOR_CLEAR}" >&2; } +msg_warn() { echo -e "${COLOR_YELLOW}WARN: ${1}${COLOR_CLEAR}" >&2; } +msg_ok() { echo -e "${COLOR_GREEN}OK: ${1}${COLOR_CLEAR}"; } +msg_info() { echo -e "${COLOR_BLUE}INFO: ${1}${COLOR_CLEAR}"; } + +valreq() { [[ -n "${2-}" && "$2" != -* ]]; } + +usage() { + cat < Registry namespace (default: ${DEFAULT_NAMESPACE}) + --registry Container registry (default: ${DEFAULT_REGISTRY}) + --ref ironic-image git ref to build from (default: ${DEFAULT_REF}) + --tag Image tag to apply (default: ${DEFAULT_TAG}) + --images Comma-separated images to build (default: ${ALL_IMAGES}) + --no-push Build only, do not push to registry + --keep-source Do not remove cloned source after build + --source-dir Use existing ironic-image checkout instead of cloning + -h, --help Show this help + +Examples: + # Build all images and push to quay.io/rh-edge-enablement + ${SCRIPT_NAME} + + # Build from a release tag, push with date-based tag + ${SCRIPT_NAME} --ref v28.0.0 --tag 2026-06 + + # Build only sushy-tools, don't push + ${SCRIPT_NAME} --images sushy-tools --no-push + + # Use a different registry namespace + ${SCRIPT_NAME} --namespace pfontani + +Prerequisites: + - podman (with qemu-user-static if cross-building from x86_64) + - Authenticated to the target registry (podman login ${DEFAULT_REGISTRY}) + - Git +EOF + exit "${1:-0}" +} + +REGISTRY="${DEFAULT_REGISTRY}" +NAMESPACE="${DEFAULT_NAMESPACE}" +REF="${DEFAULT_REF}" +TAG="${DEFAULT_TAG}" +IMAGES="${ALL_IMAGES}" +PUSH="true" +KEEP_SOURCE="false" +SOURCE_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) + valreq "$1" "${2-}" || { msg_err "--namespace requires a value"; exit 1; } + NAMESPACE="$2"; shift 2 ;; + --registry) + valreq "$1" "${2-}" || { msg_err "--registry requires a value"; exit 1; } + REGISTRY="$2"; shift 2 ;; + --ref) + valreq "$1" "${2-}" || { msg_err "--ref requires a value"; exit 1; } + REF="$2"; shift 2 ;; + --tag) + valreq "$1" "${2-}" || { msg_err "--tag requires a value"; exit 1; } + TAG="$2"; shift 2 ;; + --images) + valreq "$1" "${2-}" || { msg_err "--images requires a value"; exit 1; } + IMAGES="$2"; shift 2 ;; + --no-push) + PUSH="false"; shift ;; + --keep-source) + KEEP_SOURCE="true"; shift ;; + --source-dir) + valreq "$1" "${2-}" || { msg_err "--source-dir requires a value"; exit 1; } + SOURCE_DIR="$2"; shift 2 ;; + -h|--help) + usage 0 ;; + *) + msg_err "Unknown option: $1"; usage 1 ;; + esac +done + +check_prerequisites() { + if ! command -v podman &>/dev/null; then + msg_err "podman is required but not found" + exit 1 + fi + if ! command -v git &>/dev/null; then + msg_err "git is required but not found" + exit 1 + fi + + local host_arch + host_arch="$(uname -m)" + + if [[ "${host_arch}" != "aarch64" ]]; then + msg_info "Host is ${host_arch} — cross-building for arm64 via QEMU" + if ! ls /proc/sys/fs/binfmt_misc/qemu-aarch64 &>/dev/null; then + msg_warn "QEMU binfmt handler for aarch64 not found" + msg_info "Install with: sudo dnf install -y qemu-user-static" + msg_info "Then restart binfmt: sudo systemctl restart systemd-binfmt" + exit 1 + fi + else + msg_info "Host is aarch64 — building natively" + fi + + if [[ "${PUSH}" == "true" ]]; then + if ! podman login --get-login "${REGISTRY}" &>/dev/null; then + msg_err "Not authenticated to ${REGISTRY}. Run: podman login ${REGISTRY}" + exit 1 + fi + fi +} + +prepare_source() { + if [[ -n "${SOURCE_DIR}" ]]; then + if [[ ! -d "${SOURCE_DIR}" ]]; then + msg_err "Source directory does not exist: ${SOURCE_DIR}" + exit 1 + fi + WORK_DIR="${SOURCE_DIR}" + KEEP_SOURCE="true" + msg_info "Using existing source: ${WORK_DIR}" + else + WORK_DIR="$(mktemp -d)" + msg_info "Cloning ironic-image at ref '${REF}' into ${WORK_DIR}" + git clone --depth 1 --branch "${REF}" "${IRONIC_IMAGE_REPO}" "${WORK_DIR}" 2>&1 \ + || git clone "${IRONIC_IMAGE_REPO}" "${WORK_DIR}" 2>&1 + if [[ "$(git -C "${WORK_DIR}" rev-parse HEAD 2>/dev/null)" != *"${REF}"* ]]; then + git -C "${WORK_DIR}" fetch origin "${REF}" 2>&1 + git -C "${WORK_DIR}" checkout "${REF}" 2>&1 + fi + fi + + local commit + commit="$(git -C "${WORK_DIR}" rev-parse --short HEAD 2>/dev/null || echo 'unknown')" + msg_info "Source commit: ${commit}" +} + +cleanup_source() { + if [[ "${KEEP_SOURCE}" == "false" && -n "${WORK_DIR:-}" && -d "${WORK_DIR}" ]]; then + rm -rf "${WORK_DIR}" + msg_info "Cleaned up source directory" + fi +} + +build_image() { + local image_name="$1" + local full_tag="${REGISTRY}/${NAMESPACE}/${image_name}:${TAG}" + local dockerfile_dir="." + local build_args=() + + case "${image_name}" in + ironic) + dockerfile_dir="." + ;; + vbmc) + dockerfile_dir="resources/vbmc" + ;; + sushy-tools) + dockerfile_dir="resources/sushy-tools" + ;; + *) + msg_err "Unknown image: ${image_name}" + return 1 + ;; + esac + + msg_info "Building ${full_tag} from ${dockerfile_dir}/Dockerfile" + + local platform_flag=() + if [[ "$(uname -m)" != "aarch64" ]]; then + platform_flag=(--platform linux/arm64) + fi + + if ! podman build \ + "${platform_flag[@]}" \ + -t "${full_tag}" \ + -f "${WORK_DIR}/${dockerfile_dir}/Dockerfile" \ + "${build_args[@]}" \ + "${WORK_DIR}/${dockerfile_dir}" 2>&1; then + msg_err "Failed to build ${full_tag}" + return 1 + fi + + msg_ok "Built ${full_tag}" +} + +push_image() { + local image_name="$1" + local full_tag="${REGISTRY}/${NAMESPACE}/${image_name}:${TAG}" + + msg_info "Pushing ${full_tag}" + if ! podman push "${full_tag}" 2>&1; then + msg_err "Failed to push ${full_tag}" + return 1 + fi + msg_ok "Pushed ${full_tag}" +} + +main() { + msg_info "Metal3 arm64 image builder" + msg_info "Registry: ${REGISTRY}/${NAMESPACE}" + msg_info "Git ref: ${REF} | Tag: ${TAG}" + msg_info "Images: ${IMAGES}" + msg_info "Push: ${PUSH}" + echo "" + + check_prerequisites + prepare_source + + trap cleanup_source EXIT + + IFS=',' read -ra IMAGE_LIST <<< "${IMAGES}" + + local failed=() + for image in "${IMAGE_LIST[@]}"; do + image="$(echo "${image}" | xargs)" + if build_image "${image}"; then + if [[ "${PUSH}" == "true" ]]; then + push_image "${image}" || failed+=("${image}") + fi + else + failed+=("${image}") + fi + done + + echo "" + if [[ ${#failed[@]} -gt 0 ]]; then + msg_err "Failed images: ${failed[*]}" + exit 1 + fi + + msg_ok "All images built successfully" + echo "" + msg_info "To use these images with dev-scripts, add to your config:" + for image in "${IMAGE_LIST[@]}"; do + image="$(echo "${image}" | xargs)" + local var_name + case "${image}" in + ironic) var_name="IRONIC_IMAGE" ;; + vbmc) var_name="VBMC_IMAGE" ;; + sushy-tools) var_name="SUSHY_TOOLS_IMAGE" ;; + esac + echo " export ${var_name}=${REGISTRY}/${NAMESPACE}/${image}:${TAG}" + done +} + +main From ae3d08ced5931b1de1563928cf75d0f57c3c8bbe Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Tue, 16 Jun 2026 10:29:49 +0200 Subject: [PATCH 2/3] fix: address review feedback on ref resolution and trap ordering - Fix broken ref check: replace SHA-vs-name substring match with rev-parse --verify to only fetch/checkout when the ref isn't already available locally - Validate --source-dir against --ref: verify the checkout is a git repo and HEAD matches the requested ref - Move EXIT trap before prepare_source so temp dirs are cleaned up on failure Assisted-by: Claude (Anthropic) --- helpers/build-metal3-arm64.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/helpers/build-metal3-arm64.sh b/helpers/build-metal3-arm64.sh index a845324..c4f423c 100755 --- a/helpers/build-metal3-arm64.sh +++ b/helpers/build-metal3-arm64.sh @@ -146,16 +146,31 @@ prepare_source() { fi WORK_DIR="${SOURCE_DIR}" KEEP_SOURCE="true" + if ! git -C "${WORK_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + msg_err "--source-dir is not a git checkout: ${WORK_DIR}" + exit 1 + fi + if ! git -C "${WORK_DIR}" rev-parse --verify "${REF}^{commit}" >/dev/null 2>&1; then + msg_err "Ref '${REF}' not found in --source-dir. Fetch it or pass a valid --ref." + exit 1 + fi + local head_commit ref_commit + head_commit="$(git -C "${WORK_DIR}" rev-parse HEAD)" + ref_commit="$(git -C "${WORK_DIR}" rev-parse "${REF}^{commit}")" + if [[ "${head_commit}" != "${ref_commit}" ]]; then + msg_err "--source-dir HEAD does not match --ref '${REF}'. Checkout the ref first." + exit 1 + fi msg_info "Using existing source: ${WORK_DIR}" else WORK_DIR="$(mktemp -d)" msg_info "Cloning ironic-image at ref '${REF}' into ${WORK_DIR}" git clone --depth 1 --branch "${REF}" "${IRONIC_IMAGE_REPO}" "${WORK_DIR}" 2>&1 \ || git clone "${IRONIC_IMAGE_REPO}" "${WORK_DIR}" 2>&1 - if [[ "$(git -C "${WORK_DIR}" rev-parse HEAD 2>/dev/null)" != *"${REF}"* ]]; then + if ! git -C "${WORK_DIR}" rev-parse --verify "${REF}^{commit}" >/dev/null 2>&1; then git -C "${WORK_DIR}" fetch origin "${REF}" 2>&1 - git -C "${WORK_DIR}" checkout "${REF}" 2>&1 fi + git -C "${WORK_DIR}" checkout "${REF}" 2>&1 fi local commit @@ -232,11 +247,10 @@ main() { msg_info "Push: ${PUSH}" echo "" + trap cleanup_source EXIT check_prerequisites prepare_source - trap cleanup_source EXIT - IFS=',' read -ra IMAGE_LIST <<< "${IMAGES}" local failed=() From 14507bee6b76c66c34792f97c36fa5ec75ca3cbb Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Tue, 16 Jun 2026 10:44:16 +0200 Subject: [PATCH 3/3] Extract proxy instructions into shared print_proxy_instructions() All lifecycle scripts (deploy, redeploy, startup) now call a common function for the post-operation "source proxy.env" message. Also adds the missing proxy instructions to startup-cluster.sh. Co-Authored-By: Claude Opus 4.6 --- deploy/aws-hypervisor/scripts/common.sh | 10 ++++++++++ deploy/openshift-clusters/scripts/deploy-cluster.sh | 8 +------- deploy/openshift-clusters/scripts/redeploy-cluster.sh | 8 +------- deploy/openshift-clusters/scripts/startup-cluster.sh | 4 +++- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/deploy/aws-hypervisor/scripts/common.sh b/deploy/aws-hypervisor/scripts/common.sh index 160d1d3..ec96107 100755 --- a/deploy/aws-hypervisor/scripts/common.sh +++ b/deploy/aws-hypervisor/scripts/common.sh @@ -30,6 +30,16 @@ get_node_dir() { echo "$node_dir" } +function print_proxy_instructions() { + echo "" + echo "Next steps:" + echo "1. Source the proxy environment from anywhere:" + echo " source ${DEPLOY_DIR}/openshift-clusters/proxy.env" + echo " (or from openshift-clusters directory: source proxy.env)" + echo "2. Verify cluster access: oc get nodes" + echo "3. Access the cluster console if needed" +} + readonly COLOR_RED='\033[0;31m' readonly COLOR_YELLOW='\033[0;33m' readonly COLOR_BLUE='\033[0;34m' diff --git a/deploy/openshift-clusters/scripts/deploy-cluster.sh b/deploy/openshift-clusters/scripts/deploy-cluster.sh index 7360362..746a425 100755 --- a/deploy/openshift-clusters/scripts/deploy-cluster.sh +++ b/deploy/openshift-clusters/scripts/deploy-cluster.sh @@ -114,13 +114,7 @@ echo "Running Ansible ${PLAYBOOK} playbook with ${TOPOLOGY} topology in non-inte if ansible-playbook "${PLAYBOOK}" "${EXTRA_VARS[@]}" -i inventory.ini; then echo "" echo "OpenShift ${TOPOLOGY} cluster deployment (${METHOD_DISPLAY}) completed successfully!" - echo "" - echo "Next steps:" - echo "1. Source the proxy environment from anywhere:" - echo " source ${DEPLOY_DIR}/openshift-clusters/proxy.env" - echo " (or from openshift-clusters directory: source proxy.env)" - echo "2. Verify cluster access: oc get nodes" - echo "3. Access the cluster console if needed" + print_proxy_instructions else echo "Error: OpenShift cluster deployment failed!" echo "Check the Ansible logs for more details." diff --git a/deploy/openshift-clusters/scripts/redeploy-cluster.sh b/deploy/openshift-clusters/scripts/redeploy-cluster.sh index 0ba0220..a14efdf 100755 --- a/deploy/openshift-clusters/scripts/redeploy-cluster.sh +++ b/deploy/openshift-clusters/scripts/redeploy-cluster.sh @@ -226,10 +226,4 @@ ansible-playbook redeploy.yml -i inventory.ini \ echo "==================================" echo "✓ OpenShift cluster redeploy completed successfully!" echo "==================================" -echo "" -echo "Next steps:" -echo "1. Source the proxy environment from anywhere:" -echo " source ${DEPLOY_DIR}/openshift-clusters/proxy.env" -echo " (or from openshift-clusters directory: source proxy.env)" -echo "2. Verify cluster access: oc get nodes" -echo "3. Access the cluster console if needed" +print_proxy_instructions diff --git a/deploy/openshift-clusters/scripts/startup-cluster.sh b/deploy/openshift-clusters/scripts/startup-cluster.sh index e532cc9..89d0e08 100755 --- a/deploy/openshift-clusters/scripts/startup-cluster.sh +++ b/deploy/openshift-clusters/scripts/startup-cluster.sh @@ -215,5 +215,7 @@ EOF fi echo "" +echo "==================================" echo "OpenShift cluster startup completed successfully!" -echo "If you need to redeploy the cluster, use: make redeploy-cluster" \ No newline at end of file +echo "==================================" +print_proxy_instructions \ No newline at end of file