From e1edbf33c5293ce1574ca7d1b6937063543cf7f4 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 12:34:58 +0200 Subject: [PATCH 01/17] Add baremetal node adoption automation for TNF Introduce adopt-baremetal.sh to onboard existing baremetal nodes into the dev-scripts deployment workflow. Parses inventory_baremetal.ini (template or wizard-generated), validates BMC credentials via Redfish, and generates ironic_nodes.json + config_baremetal_fencing.sh artifacts for NODES_PLATFORM=baremetal deployments. OCPEDGE-2774 Co-Authored-By: Claude Opus 4.6 --- deploy/Makefile | 10 + deploy/openshift-clusters/.gitignore | 4 + .../inventory_baremetal.ini.sample | 32 ++ .../scripts/adopt-baremetal.sh | 419 ++++++++++++++++++ 4 files changed, 465 insertions(+) create mode 100644 deploy/openshift-clusters/inventory_baremetal.ini.sample create mode 100755 deploy/openshift-clusters/scripts/adopt-baremetal.sh diff --git a/deploy/Makefile b/deploy/Makefile index e825e0ca..f7d20512 100644 --- a/deploy/Makefile +++ b/deploy/Makefile @@ -92,6 +92,12 @@ fencing-assisted: keep-instance: @../helpers/keep-instance.sh '$(DAYS)' +adopt-baremetal: + @./openshift-clusters/scripts/adopt-baremetal.sh + +verify-baremetal: + @./openshift-clusters/scripts/adopt-baremetal.sh --verify-only + patch-nodes: @./openshift-clusters/scripts/patch-nodes.sh get-tnf-logs: @@ -138,6 +144,10 @@ help: @echo " clean-spoke - Clean spoke cluster resources (VMs, network, auth) from assisted installer" @echo " patch-nodes - Build resource-agents RPM and patch cluster nodes (default version: 4.11)" @echo "" + @echo "Baremetal Adoption:" + @echo " adopt-baremetal - Adopt baremetal nodes: validate BMC + generate dev-scripts artifacts" + @echo " verify-baremetal - Verify BMC credentials for adopted baremetal nodes (no artifacts)" + @echo "" @echo "Cluster Utilities:" @echo " get-tnf-logs - Collect pacemaker and etcd logs from cluster nodes" diff --git a/deploy/openshift-clusters/.gitignore b/deploy/openshift-clusters/.gitignore index 7c145393..a77bcddb 100644 --- a/deploy/openshift-clusters/.gitignore +++ b/deploy/openshift-clusters/.gitignore @@ -1,4 +1,8 @@ inventory.ini +inventory_baremetal.ini + +# Generated adoption artifacts (contain BMC credentials) +clusters/ proxy.env kubeconfig kubeadmin-password diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample new file mode 100644 index 00000000..88019edc --- /dev/null +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -0,0 +1,32 @@ +# Baremetal node inventory for TNF adoption +# +# NOTE: This is separate from inventory.ini, which targets the hypervisor host. +# This file describes the physical baremetal nodes to be adopted as OpenShift nodes. +# inventory.ini → hypervisor (where dev-scripts runs) +# inventory_baremetal.ini → baremetal nodes (BMC endpoints for adoption) +# +# Copy this file to inventory_baremetal.ini and fill in your node details. +# Then run: make adopt-baremetal +# +# Each node requires: +# bmc_ip - BMC/iDRAC/iLO management IP address +# bmc_user - BMC login username +# bmc_pass - BMC login password +# boot_mac - MAC address of the NIC used for PXE boot +# +# The hostname (first field) becomes the node name in ironic_nodes.json. +# For TNF, you need exactly 2 nodes (master-0 and master-1). + +[baremetal_nodes] +master-0 bmc_ip=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 +master-1 bmc_ip=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 + +[baremetal_nodes:vars] +# BMC driver — only redfish is supported for TNF fencing +bmc_driver=redfish + +# Skip TLS verification for BMC endpoints (common with self-signed certs) +bmc_verify_ca=False + +# Node CPU architecture +cpu_arch=x86_64 diff --git a/deploy/openshift-clusters/scripts/adopt-baremetal.sh b/deploy/openshift-clusters/scripts/adopt-baremetal.sh new file mode 100755 index 00000000..b3473c12 --- /dev/null +++ b/deploy/openshift-clusters/scripts/adopt-baremetal.sh @@ -0,0 +1,419 @@ +#!/usr/bin/bash +# +# Adopt existing baremetal nodes for TNF deployment. +# +# Parses inventory_baremetal.ini, validates BMC credentials via Redfish, +# and generates ironic_nodes.json + config_baremetal_fencing.sh for dev-scripts. +# +# Usage: +# adopt-baremetal.sh [options] +# +# Options: +# --cluster-name NAME Cluster name for output directory (default: ostest) +# --skip-verify Skip BMC credential verification +# --verify-only Only verify BMC credentials, don't generate artifacts +# --config-base FILE Base config to derive baremetal config from +# -h, --help Show this help message + +set -o nounset +set -o errexit +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OC_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +CLUSTER_NAME="${CLUSTER_NAME:-ostest}" +SKIP_VERIFY=false +VERIFY_ONLY=false +CONFIG_BASE="" +INVENTORY="${OC_DIR}/inventory_baremetal.ini" + +# Node data arrays — populated by parse_inventory +declare -a NODE_NAMES=() +declare -a NODE_BMC_IPS=() +declare -a NODE_BMC_USERS=() +declare -a NODE_BMC_PASSES=() +declare -a NODE_BOOT_MACS=() + +# Group defaults +BMC_VERIFY_CA="False" +CPU_ARCH="x86_64" + +############################################################################## +# Helpers +############################################################################## + +die() { echo "Error: $*" >&2; exit 1; } + +info() { echo "==> $*"; } + +############################################################################## +# Argument parsing +############################################################################## + +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --cluster-name) + CLUSTER_NAME="$2" + shift 2 + ;; + --skip-verify) + SKIP_VERIFY=true + shift + ;; + --verify-only) + VERIFY_ONLY=true + shift + ;; + --config-base) + CONFIG_BASE="$2" + shift 2 + ;; + -h|--help) + head -17 "$0" | tail -12 + exit 0 + ;; + *) + die "Unknown option: $1. Run '$0 --help' for usage." + ;; + esac + done +} + +############################################################################## +# Wizard — interactive inventory creation +############################################################################## + +run_wizard() { + info "No inventory_baremetal.ini found — starting interactive wizard" + echo "" + + local node_count + read -rp "Number of baremetal nodes [2]: " node_count + node_count="${node_count:-2}" + + if [[ "${node_count}" -lt 2 ]]; then + die "TNF requires at least 2 nodes" + fi + + local tmp_inventory + tmp_inventory=$(mktemp) + { + echo "# Generated by adopt-baremetal.sh wizard" + echo "" + echo "[baremetal_nodes]" + } > "${tmp_inventory}" + + for ((i = 0; i < node_count; i++)); do + local default_name="master-${i}" + echo "" + echo "--- Node $((i + 1)) of ${node_count} ---" + + local name bmc_ip bmc_user bmc_pass boot_mac + read -rp " Hostname [${default_name}]: " name + name="${name:-${default_name}}" + + read -rp " BMC IP address: " bmc_ip + [[ -z "${bmc_ip}" ]] && die "BMC IP is required" + + read -rp " BMC username [admin]: " bmc_user + bmc_user="${bmc_user:-admin}" + + read -rsp " BMC password: " bmc_pass + echo "" + [[ -z "${bmc_pass}" ]] && die "BMC password is required" + + read -rp " Boot MAC address: " boot_mac + [[ -z "${boot_mac}" ]] && die "Boot MAC is required" + + echo "${name} bmc_ip=${bmc_ip} bmc_user=${bmc_user} bmc_pass=${bmc_pass} boot_mac=${boot_mac}" >> "${tmp_inventory}" + done + + { + echo "" + echo "[baremetal_nodes:vars]" + echo "bmc_driver=redfish" + echo "bmc_verify_ca=False" + echo "cpu_arch=x86_64" + } >> "${tmp_inventory}" + + mv "${tmp_inventory}" "${INVENTORY}" + echo "" + info "Inventory written to inventory_baremetal.ini" +} + +############################################################################## +# INI parser +############################################################################## + +parse_inventory() { + [[ -f "${INVENTORY}" ]] || die "Inventory file not found: ${INVENTORY}" + + local in_nodes=false + local in_vars=false + + while IFS= read -r line || [[ -n "${line}" ]]; do + # Strip comments and leading/trailing whitespace + line="${line%%#*}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + [[ -z "${line}" ]] && continue + + if [[ "${line}" == "[baremetal_nodes]" ]]; then + in_nodes=true + in_vars=false + continue + elif [[ "${line}" == "[baremetal_nodes:vars]" ]]; then + in_nodes=false + in_vars=true + continue + elif [[ "${line}" =~ ^\[.*\] ]]; then + in_nodes=false + in_vars=false + continue + fi + + if ${in_vars}; then + local key val + key="${line%%=*}" + val="${line#*=}" + case "${key}" in + bmc_verify_ca) BMC_VERIFY_CA="${val}" ;; + cpu_arch) CPU_ARCH="${val}" ;; + esac + continue + fi + + if ${in_nodes}; then + local name rest + name="${line%% *}" + rest="${line#* }" + + local bmc_ip="" bmc_user="" bmc_pass="" boot_mac="" + for pair in ${rest}; do + local key val + key="${pair%%=*}" + val="${pair#*=}" + case "${key}" in + bmc_ip) bmc_ip="${val}" ;; + bmc_user) bmc_user="${val}" ;; + bmc_pass) bmc_pass="${val}" ;; + boot_mac) boot_mac="${val}" ;; + esac + done + + [[ -z "${bmc_ip}" ]] && die "Node '${name}': missing bmc_ip" + [[ -z "${bmc_user}" ]] && die "Node '${name}': missing bmc_user" + [[ -z "${bmc_pass}" ]] && die "Node '${name}': missing bmc_pass" + [[ -z "${boot_mac}" ]] && die "Node '${name}': missing boot_mac" + + NODE_NAMES+=("${name}") + NODE_BMC_IPS+=("${bmc_ip}") + NODE_BMC_USERS+=("${bmc_user}") + NODE_BMC_PASSES+=("${bmc_pass}") + NODE_BOOT_MACS+=("${boot_mac}") + fi + done < "${INVENTORY}" + + [[ ${#NODE_NAMES[@]} -eq 0 ]] && die "No nodes found in inventory" + info "Parsed ${#NODE_NAMES[@]} node(s) from inventory" +} + +############################################################################## +# BMC verification via Redfish +############################################################################## + +discover_redfish_system_id() { + local bmc_ip="$1" bmc_user="$2" bmc_pass="$3" + + local systems_json + systems_json=$(curl -sk --connect-timeout 5 --max-time 10 \ + -u "${bmc_user}:${bmc_pass}" \ + "https://${bmc_ip}/redfish/v1/Systems/" 2>/dev/null) || return 1 + + echo "${systems_json}" | jq -r '.Members[0]."@odata.id"' 2>/dev/null +} + +verify_bmc() { + local name="$1" bmc_ip="$2" bmc_user="$3" bmc_pass="$4" + local rc=0 + + printf " %-12s %-20s " "${name}" "${bmc_ip}" + + # Verify Redfish root is reachable and credentials work + local http_code + http_code=$(curl -sk --connect-timeout 5 --max-time 10 \ + -o /dev/null -w '%{http_code}' \ + -u "${bmc_user}:${bmc_pass}" \ + "https://${bmc_ip}/redfish/v1/" 2>/dev/null) || http_code="000" + + if [[ "${http_code}" == "200" ]]; then + echo "OK (HTTP ${http_code})" + elif [[ "${http_code}" == "401" ]]; then + echo "FAIL — bad credentials (HTTP 401)" + rc=1 + elif [[ "${http_code}" == "000" ]]; then + echo "FAIL — unreachable" + rc=1 + else + echo "FAIL (HTTP ${http_code})" + rc=1 + fi + + return ${rc} +} + +verify_all_bmcs() { + info "Verifying BMC credentials via Redfish" + echo "" + + local failed=0 + for ((i = 0; i < ${#NODE_NAMES[@]}; i++)); do + if ! verify_bmc "${NODE_NAMES[$i]}" "${NODE_BMC_IPS[$i]}" \ + "${NODE_BMC_USERS[$i]}" "${NODE_BMC_PASSES[$i]}"; then + failed=$((failed + 1)) + fi + done + echo "" + + if [[ ${failed} -gt 0 ]]; then + die "${failed} node(s) failed BMC verification" + fi + info "All BMC endpoints verified" +} + +############################################################################## +# Artifact generation +############################################################################## + +generate_ironic_nodes_json() { + local output_file="$1" + + info "Generating ironic_nodes.json" + + local nodes_json='{"nodes":[' + local first=true + + for ((i = 0; i < ${#NODE_NAMES[@]}; i++)); do + local name="${NODE_NAMES[$i]}" + local bmc_ip="${NODE_BMC_IPS[$i]}" + local bmc_user="${NODE_BMC_USERS[$i]}" + local bmc_pass="${NODE_BMC_PASSES[$i]}" + local boot_mac="${NODE_BOOT_MACS[$i]}" + + # Discover the Redfish system path from the BMC, fall back to standard + local system_id + system_id=$(discover_redfish_system_id "${bmc_ip}" "${bmc_user}" "${bmc_pass}" 2>/dev/null) || true + system_id="${system_id:-/redfish/v1/Systems/1}" + # Strip leading slash for URL construction + system_id="${system_id#/}" + + ${first} || nodes_json+="," + first=false + + nodes_json+=$(cat < "${output_file}" + info " → ${output_file}" +} + +generate_baremetal_config() { + local output_file="$1" + local nodes_file_path="$2" + + info "Generating config_baremetal_fencing.sh" + + # Find the base config to derive from + local base_config="${CONFIG_BASE}" + if [[ -z "${base_config}" ]]; then + local files_dir="${OC_DIR}/roles/dev-scripts/install-dev/files" + if [[ -f "${files_dir}/config_fencing.sh" ]]; then + base_config="${files_dir}/config_fencing.sh" + elif [[ -f "${files_dir}/config_fencing_example.sh" ]]; then + base_config="${files_dir}/config_fencing_example.sh" + else + die "No base config found. Provide one with --config-base." + fi + fi + + [[ -f "${base_config}" ]] || die "Base config not found: ${base_config}" + info " Base config: ${base_config}" + + { + cat "${base_config}" + echo "" + echo "# Baremetal adoption overrides (generated by adopt-baremetal.sh)" + echo "export NODES_PLATFORM=baremetal" + echo "export NODES_FILE=\"${nodes_file_path}\"" + } > "${output_file}" + + info " → ${output_file}" +} + +############################################################################## +# Main +############################################################################## + +main() { + parse_args "$@" + + # Wizard if no inventory exists + if [[ ! -f "${INVENTORY}" ]]; then + run_wizard + fi + + parse_inventory + + # BMC verification + if ! ${SKIP_VERIFY}; then + verify_all_bmcs + fi + + if ${VERIFY_ONLY}; then + info "Verification complete (--verify-only). No artifacts generated." + exit 0 + fi + + # Create output directory + local output_dir="${OC_DIR}/clusters/${CLUSTER_NAME}" + mkdir -p "${output_dir}" + + # Generate artifacts + local nodes_file="${output_dir}/ironic_nodes.json" + generate_ironic_nodes_json "${nodes_file}" + + # NODES_FILE path on the hypervisor — resolves when dev-scripts sources the config + local remote_nodes_path="\${PWD}/ironic_nodes.json" + generate_baremetal_config "${output_dir}/config_baremetal_fencing.sh" "${remote_nodes_path}" + + echo "" + info "Adoption complete. Generated artifacts:" + echo " ${nodes_file}" + echo " ${output_dir}/config_baremetal_fencing.sh" + echo "" + echo " Next: deploy to hypervisor with the baremetal install workflow (OCPEDGE-2775)" +} + +main "$@" From 251d1277ba5d7637ea71562b254c13cf4de202e0 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 12:59:15 +0200 Subject: [PATCH 02/17] Extract wizard into standalone script with input validation Split the interactive wizard out of adopt-baremetal.sh into its own baremetal-wizard.sh script. Add input validation (IPv4, MAC format, hostname), re-prompt on invalid input instead of dying, a summary table with masked passwords before confirmation, and Y/n/q flow. Rename all baremetal scripts and Make targets to use a baremetal-* prefix for consistent grouping (baremetal-adopt, baremetal-verify, baremetal-wizard). OCPEDGE-2774 Co-Authored-By: Claude Opus 4.6 --- deploy/Makefile | 16 +- ...{adopt-baremetal.sh => baremetal-adopt.sh} | 66 +--- .../scripts/baremetal-wizard.sh | 304 ++++++++++++++++++ 3 files changed, 316 insertions(+), 70 deletions(-) rename deploy/openshift-clusters/scripts/{adopt-baremetal.sh => baremetal-adopt.sh} (84%) create mode 100755 deploy/openshift-clusters/scripts/baremetal-wizard.sh diff --git a/deploy/Makefile b/deploy/Makefile index f7d20512..cd709341 100644 --- a/deploy/Makefile +++ b/deploy/Makefile @@ -92,11 +92,14 @@ fencing-assisted: keep-instance: @../helpers/keep-instance.sh '$(DAYS)' -adopt-baremetal: - @./openshift-clusters/scripts/adopt-baremetal.sh +baremetal-adopt: + @./openshift-clusters/scripts/baremetal-adopt.sh -verify-baremetal: - @./openshift-clusters/scripts/adopt-baremetal.sh --verify-only +baremetal-verify: + @./openshift-clusters/scripts/baremetal-adopt.sh --verify-only + +baremetal-wizard: + @./openshift-clusters/scripts/baremetal-wizard.sh patch-nodes: @./openshift-clusters/scripts/patch-nodes.sh @@ -145,8 +148,9 @@ help: @echo " patch-nodes - Build resource-agents RPM and patch cluster nodes (default version: 4.11)" @echo "" @echo "Baremetal Adoption:" - @echo " adopt-baremetal - Adopt baremetal nodes: validate BMC + generate dev-scripts artifacts" - @echo " verify-baremetal - Verify BMC credentials for adopted baremetal nodes (no artifacts)" + @echo " baremetal-adopt - Adopt baremetal nodes: validate BMC + generate dev-scripts artifacts" + @echo " baremetal-verify - Verify BMC credentials for adopted baremetal nodes (no artifacts)" + @echo " baremetal-wizard - Interactive wizard to create baremetal node inventory" @echo "" @echo "Cluster Utilities:" @echo " get-tnf-logs - Collect pacemaker and etcd logs from cluster nodes" diff --git a/deploy/openshift-clusters/scripts/adopt-baremetal.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh similarity index 84% rename from deploy/openshift-clusters/scripts/adopt-baremetal.sh rename to deploy/openshift-clusters/scripts/baremetal-adopt.sh index b3473c12..3657ab78 100755 --- a/deploy/openshift-clusters/scripts/adopt-baremetal.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -81,68 +81,6 @@ parse_args() { done } -############################################################################## -# Wizard — interactive inventory creation -############################################################################## - -run_wizard() { - info "No inventory_baremetal.ini found — starting interactive wizard" - echo "" - - local node_count - read -rp "Number of baremetal nodes [2]: " node_count - node_count="${node_count:-2}" - - if [[ "${node_count}" -lt 2 ]]; then - die "TNF requires at least 2 nodes" - fi - - local tmp_inventory - tmp_inventory=$(mktemp) - { - echo "# Generated by adopt-baremetal.sh wizard" - echo "" - echo "[baremetal_nodes]" - } > "${tmp_inventory}" - - for ((i = 0; i < node_count; i++)); do - local default_name="master-${i}" - echo "" - echo "--- Node $((i + 1)) of ${node_count} ---" - - local name bmc_ip bmc_user bmc_pass boot_mac - read -rp " Hostname [${default_name}]: " name - name="${name:-${default_name}}" - - read -rp " BMC IP address: " bmc_ip - [[ -z "${bmc_ip}" ]] && die "BMC IP is required" - - read -rp " BMC username [admin]: " bmc_user - bmc_user="${bmc_user:-admin}" - - read -rsp " BMC password: " bmc_pass - echo "" - [[ -z "${bmc_pass}" ]] && die "BMC password is required" - - read -rp " Boot MAC address: " boot_mac - [[ -z "${boot_mac}" ]] && die "Boot MAC is required" - - echo "${name} bmc_ip=${bmc_ip} bmc_user=${bmc_user} bmc_pass=${bmc_pass} boot_mac=${boot_mac}" >> "${tmp_inventory}" - done - - { - echo "" - echo "[baremetal_nodes:vars]" - echo "bmc_driver=redfish" - echo "bmc_verify_ca=False" - echo "cpu_arch=x86_64" - } >> "${tmp_inventory}" - - mv "${tmp_inventory}" "${INVENTORY}" - echo "" - info "Inventory written to inventory_baremetal.ini" -} - ############################################################################## # INI parser ############################################################################## @@ -379,9 +317,9 @@ generate_baremetal_config() { main() { parse_args "$@" - # Wizard if no inventory exists + # Launch interactive wizard if no inventory exists if [[ ! -f "${INVENTORY}" ]]; then - run_wizard + "${SCRIPT_DIR}/baremetal-wizard.sh" --output "${INVENTORY}" fi parse_inventory diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh new file mode 100755 index 00000000..de6de400 --- /dev/null +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -0,0 +1,304 @@ +#!/usr/bin/bash +# +# Interactive wizard for creating a baremetal node inventory. +# +# Collects BMC credentials and network info for each node, validates input, +# displays a summary for confirmation, and writes inventory_baremetal.ini. +# +# Usage: +# wizard-baremetal.sh [options] +# +# Options: +# --output FILE Inventory output path (default: inventory_baremetal.ini) +# -h, --help Show this help message + +set -o nounset +set -o errexit +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OC_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +OUTPUT="${OC_DIR}/inventory_baremetal.ini" + +############################################################################## +# Helpers +############################################################################## + +die() { echo "Error: $*" >&2; exit 1; } + +info() { echo "==> $*"; } + +############################################################################## +# Argument parsing +############################################################################## + +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --output) + OUTPUT="$2" + shift 2 + ;; + -h|--help) + head -14 "$0" | tail -9 + exit 0 + ;; + *) + die "Unknown option: $1. Run '$0 --help' for usage." + ;; + esac + done +} + +############################################################################## +# Validators +############################################################################## + +valid_ipv4() { + local ip="$1" + local IFS='.' + local -a octets + read -ra octets <<< "${ip}" + [[ ${#octets[@]} -ne 4 ]] && return 1 + local octet + for octet in "${octets[@]}"; do + [[ "${octet}" =~ ^[0-9]+$ ]] || return 1 + (( octet > 255 )) && return 1 + done + return 0 +} + +valid_mac() { + local mac="$1" + [[ "${mac}" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]] +} + +valid_hostname() { + local name="$1" + [[ -n "${name}" ]] && [[ "${name}" =~ ^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$ ]] +} + +############################################################################## +# Prompt functions +# +# Each loops until valid input is received. Values go to stdout (for capture +# with val=$(...)), prompts and errors go to stderr (displayed on terminal). +############################################################################## + +prompt_node_count() { + local count + while true; do + read -rp "Number of baremetal nodes [2]: " count + count="${count:-2}" + if ! [[ "${count}" =~ ^[0-9]+$ ]]; then + echo " Error: must be a number" >&2 + continue + fi + if (( count < 2 )); then + echo " Error: TNF requires at least 2 nodes" >&2 + continue + fi + echo "${count}" + return + done +} + +prompt_hostname() { + local default_name="$1" + local name + while true; do + read -rp " Hostname [${default_name}]: " name + name="${name:-${default_name}}" + if ! valid_hostname "${name}"; then + echo " Error: invalid hostname (use alphanumeric, hyphens, dots)" >&2 + continue + fi + echo "${name}" + return + done +} + +prompt_bmc_ip() { + local ip + while true; do + read -rp " BMC IP address: " ip + if [[ -z "${ip}" ]]; then + echo " Error: BMC IP is required" >&2 + continue + fi + if ! valid_ipv4 "${ip}"; then + echo " Error: invalid IPv4 address (expected N.N.N.N)" >&2 + continue + fi + echo "${ip}" + return + done +} + +prompt_bmc_user() { + local user + read -rp " BMC username [admin]: " user + user="${user:-admin}" + echo "${user}" +} + +prompt_bmc_pass() { + local pass + while true; do + read -rsp " BMC password: " pass + echo "" >&2 + if [[ -z "${pass}" ]]; then + echo " Error: BMC password is required" >&2 + continue + fi + echo "${pass}" + return + done +} + +prompt_boot_mac() { + local mac + while true; do + read -rp " Boot MAC address: " mac + if [[ -z "${mac}" ]]; then + echo " Error: boot MAC is required" >&2 + continue + fi + if ! valid_mac "${mac}"; then + echo " Error: invalid MAC (expected XX:XX:XX:XX:XX:XX)" >&2 + continue + fi + echo "${mac}" + return + done +} + +############################################################################## +# Summary display +############################################################################## + +show_summary() { + echo "" + echo "==================================" + echo " BAREMETAL NODE SUMMARY" + echo "==================================" + printf " %-4s %-14s %-17s %-10s %-10s %-19s\n" \ + "#" "HOSTNAME" "BMC IP" "BMC USER" "PASSWORD" "BOOT MAC" + printf " %-4s %-14s %-17s %-10s %-10s %-19s\n" \ + "---" "------------" "---------------" "--------" "--------" "-----------------" + + local i + for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do + printf " %-4s %-14s %-17s %-10s %-10s %-19s\n" \ + "$((i + 1))" \ + "${WIZ_NAMES[$i]}" \ + "${WIZ_IPS[$i]}" \ + "${WIZ_USERS[$i]}" \ + "********" \ + "${WIZ_MACS[$i]}" + done + + echo "==================================" +} + +############################################################################## +# Wizard flow +############################################################################## + +run_wizard() { + info "Baremetal node inventory wizard" + echo "" + + while true; do + local node_count + node_count=$(prompt_node_count) + + WIZ_NAMES=() + WIZ_IPS=() + WIZ_USERS=() + WIZ_PASSES=() + WIZ_MACS=() + + local i + for ((i = 0; i < node_count; i++)); do + local default_name="master-${i}" + echo "" + echo "--- Node $((i + 1)) of ${node_count} ---" + + WIZ_NAMES+=("$(prompt_hostname "${default_name}")") + WIZ_IPS+=("$(prompt_bmc_ip)") + WIZ_USERS+=("$(prompt_bmc_user)") + WIZ_PASSES+=("$(prompt_bmc_pass)") + WIZ_MACS+=("$(prompt_boot_mac)") + done + + show_summary + + local confirm + read -rp "Proceed with this configuration? [Y/n/q]: " confirm + confirm="${confirm:-Y}" + + case "${confirm}" in + [Yy]|[Yy]es) + break + ;; + [Qq]|[Qq]uit) + die "Wizard cancelled by user" + ;; + *) + echo "" + info "Starting over — re-enter node information" + echo "" + continue + ;; + esac + done + + write_inventory +} + +############################################################################## +# Inventory writer +############################################################################## + +write_inventory() { + local tmp_inventory + tmp_inventory=$(mktemp) + + { + echo "# Generated by wizard-baremetal.sh" + echo "" + echo "[baremetal_nodes]" + } > "${tmp_inventory}" + + local i + for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do + echo "${WIZ_NAMES[$i]} bmc_ip=${WIZ_IPS[$i]} bmc_user=${WIZ_USERS[$i]} bmc_pass=${WIZ_PASSES[$i]} boot_mac=${WIZ_MACS[$i]}" >> "${tmp_inventory}" + done + + { + echo "" + echo "[baremetal_nodes:vars]" + echo "bmc_driver=redfish" + echo "bmc_verify_ca=False" + echo "cpu_arch=x86_64" + } >> "${tmp_inventory}" + + mv "${tmp_inventory}" "${OUTPUT}" + echo "" + info "Inventory written to ${OUTPUT}" +} + +############################################################################## +# Main +############################################################################## + +declare -a WIZ_NAMES=() +declare -a WIZ_IPS=() +declare -a WIZ_USERS=() +declare -a WIZ_PASSES=() +declare -a WIZ_MACS=() + +parse_args "$@" +run_wizard From 314088847b94af97634015aa67a5ec74e0930865 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 15:06:18 +0200 Subject: [PATCH 03/17] Accept hostnames in BMC address wizard input The wizard only accepted IPv4 addresses for BMC endpoints, but real environments (e.g., HPE iLO) commonly use FQDNs. Accept both IPv4 and hostnames, and update prompts/labels accordingly. Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 4 +-- .../scripts/baremetal-wizard.sh | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index 88019edc..e4e35e27 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -6,10 +6,10 @@ # inventory_baremetal.ini → baremetal nodes (BMC endpoints for adoption) # # Copy this file to inventory_baremetal.ini and fill in your node details. -# Then run: make adopt-baremetal +# Then run: make baremetal-adopt # # Each node requires: -# bmc_ip - BMC/iDRAC/iLO management IP address +# bmc_ip - BMC/iDRAC/iLO management address (IP or hostname) # bmc_user - BMC login username # bmc_pass - BMC login password # boot_mac - MAC address of the NIC used for PXE boot diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index de6de400..7bd567c6 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -79,6 +79,11 @@ valid_hostname() { [[ -n "${name}" ]] && [[ "${name}" =~ ^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$ ]] } +valid_bmc_address() { + local addr="$1" + valid_ipv4 "${addr}" || valid_hostname "${addr}" +} + ############################################################################## # Prompt functions # @@ -119,19 +124,19 @@ prompt_hostname() { done } -prompt_bmc_ip() { - local ip +prompt_bmc_address() { + local addr while true; do - read -rp " BMC IP address: " ip - if [[ -z "${ip}" ]]; then - echo " Error: BMC IP is required" >&2 + read -rp " BMC address (IP or hostname): " addr + if [[ -z "${addr}" ]]; then + echo " Error: BMC address is required" >&2 continue fi - if ! valid_ipv4 "${ip}"; then - echo " Error: invalid IPv4 address (expected N.N.N.N)" >&2 + if ! valid_bmc_address "${addr}"; then + echo " Error: invalid address (expected IPv4 or FQDN)" >&2 continue fi - echo "${ip}" + echo "${addr}" return done } @@ -183,14 +188,14 @@ show_summary() { echo "==================================" echo " BAREMETAL NODE SUMMARY" echo "==================================" - printf " %-4s %-14s %-17s %-10s %-10s %-19s\n" \ - "#" "HOSTNAME" "BMC IP" "BMC USER" "PASSWORD" "BOOT MAC" - printf " %-4s %-14s %-17s %-10s %-10s %-19s\n" \ - "---" "------------" "---------------" "--------" "--------" "-----------------" + printf " %-4s %-14s %-40s %-10s %-10s %-19s\n" \ + "#" "HOSTNAME" "BMC ADDRESS" "BMC USER" "PASSWORD" "BOOT MAC" + printf " %-4s %-14s %-40s %-10s %-10s %-19s\n" \ + "---" "------------" "--------------------------------------" "--------" "--------" "-----------------" local i for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do - printf " %-4s %-14s %-17s %-10s %-10s %-19s\n" \ + printf " %-4s %-14s %-40s %-10s %-10s %-19s\n" \ "$((i + 1))" \ "${WIZ_NAMES[$i]}" \ "${WIZ_IPS[$i]}" \ @@ -227,7 +232,7 @@ run_wizard() { echo "--- Node $((i + 1)) of ${node_count} ---" WIZ_NAMES+=("$(prompt_hostname "${default_name}")") - WIZ_IPS+=("$(prompt_bmc_ip)") + WIZ_IPS+=("$(prompt_bmc_address)") WIZ_USERS+=("$(prompt_bmc_user)") WIZ_PASSES+=("$(prompt_bmc_pass)") WIZ_MACS+=("$(prompt_boot_mac)") From 1a1c5e35cce2733d0a2df1c7159cd26d7dac9f0c Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 15:10:20 +0200 Subject: [PATCH 04/17] Rename bmc_ip to bmc_address, make boot_mac optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bmc_address accepts both IPs and hostnames (matching wizard change). boot_mac is now optional — when omitted, the adopt script queries Redfish EthernetInterfaces for an enabled NIC's MAC. Falls back to a clear warning if discovery fails (e.g., firmware doesn't expose MACs). Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 13 ++-- .../scripts/baremetal-adopt.sh | 66 ++++++++++++++----- .../scripts/baremetal-wizard.sh | 15 +++-- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index e4e35e27..3300d057 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -9,17 +9,18 @@ # Then run: make baremetal-adopt # # Each node requires: -# bmc_ip - BMC/iDRAC/iLO management address (IP or hostname) -# bmc_user - BMC login username -# bmc_pass - BMC login password -# boot_mac - MAC address of the NIC used for PXE boot +# bmc_address - BMC/iDRAC/iLO management address (IP or hostname) +# bmc_user - BMC login username +# bmc_pass - BMC login password +# boot_mac - (optional) MAC address of the NIC used for PXE boot +# If omitted, the adopt script attempts Redfish discovery. # # The hostname (first field) becomes the node name in ironic_nodes.json. # For TNF, you need exactly 2 nodes (master-0 and master-1). [baremetal_nodes] -master-0 bmc_ip=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 -master-1 bmc_ip=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 +master-0 bmc_address=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 +master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 [baremetal_nodes:vars] # BMC driver — only redfish is supported for TNF fencing diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 3657ab78..fc1f9f64 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -30,7 +30,7 @@ INVENTORY="${OC_DIR}/inventory_baremetal.ini" # Node data arrays — populated by parse_inventory declare -a NODE_NAMES=() -declare -a NODE_BMC_IPS=() +declare -a NODE_BMC_ADDRS=() declare -a NODE_BMC_USERS=() declare -a NODE_BMC_PASSES=() declare -a NODE_BOOT_MACS=() @@ -128,26 +128,25 @@ parse_inventory() { name="${line%% *}" rest="${line#* }" - local bmc_ip="" bmc_user="" bmc_pass="" boot_mac="" + local bmc_address="" bmc_user="" bmc_pass="" boot_mac="" for pair in ${rest}; do local key val key="${pair%%=*}" val="${pair#*=}" case "${key}" in - bmc_ip) bmc_ip="${val}" ;; + bmc_address) bmc_address="${val}" ;; bmc_user) bmc_user="${val}" ;; bmc_pass) bmc_pass="${val}" ;; boot_mac) boot_mac="${val}" ;; esac done - [[ -z "${bmc_ip}" ]] && die "Node '${name}': missing bmc_ip" + [[ -z "${bmc_address}" ]] && die "Node '${name}': missing bmc_address" [[ -z "${bmc_user}" ]] && die "Node '${name}': missing bmc_user" [[ -z "${bmc_pass}" ]] && die "Node '${name}': missing bmc_pass" - [[ -z "${boot_mac}" ]] && die "Node '${name}': missing boot_mac" NODE_NAMES+=("${name}") - NODE_BMC_IPS+=("${bmc_ip}") + NODE_BMC_ADDRS+=("${bmc_address}") NODE_BMC_USERS+=("${bmc_user}") NODE_BMC_PASSES+=("${bmc_pass}") NODE_BOOT_MACS+=("${boot_mac}") @@ -163,28 +162,53 @@ parse_inventory() { ############################################################################## discover_redfish_system_id() { - local bmc_ip="$1" bmc_user="$2" bmc_pass="$3" + local bmc_address="$1" bmc_user="$2" bmc_pass="$3" local systems_json systems_json=$(curl -sk --connect-timeout 5 --max-time 10 \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_ip}/redfish/v1/Systems/" 2>/dev/null) || return 1 + "https://${bmc_address}/redfish/v1/Systems/" 2>/dev/null) || return 1 echo "${systems_json}" | jq -r '.Members[0]."@odata.id"' 2>/dev/null } +discover_boot_mac() { + local bmc_address="$1" bmc_user="$2" bmc_pass="$3" system_id="$4" + + local ifaces_json mac + ifaces_json=$(curl -sk --connect-timeout 5 --max-time 15 \ + -u "${bmc_user}:${bmc_pass}" \ + "https://${bmc_address}/${system_id}EthernetInterfaces/" 2>/dev/null) || return 1 + + local iface_paths + iface_paths=$(echo "${ifaces_json}" | jq -r '.Members[]."@odata.id"' 2>/dev/null) || return 1 + + for iface_path in ${iface_paths}; do + mac=$(curl -sk --connect-timeout 5 --max-time 10 \ + -u "${bmc_user}:${bmc_pass}" \ + "https://${bmc_address}${iface_path}" 2>/dev/null \ + | jq -r 'select(.Status.State == "Enabled") | .MACAddress // empty' 2>/dev/null) + + if [[ -n "${mac}" && "${mac}" != "00:00:00:00:00:00" ]]; then + echo "${mac}" + return 0 + fi + done + return 1 +} + verify_bmc() { - local name="$1" bmc_ip="$2" bmc_user="$3" bmc_pass="$4" + local name="$1" bmc_address="$2" bmc_user="$3" bmc_pass="$4" local rc=0 - printf " %-12s %-20s " "${name}" "${bmc_ip}" + printf " %-12s %-20s " "${name}" "${bmc_address}" # Verify Redfish root is reachable and credentials work local http_code http_code=$(curl -sk --connect-timeout 5 --max-time 10 \ -o /dev/null -w '%{http_code}' \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_ip}/redfish/v1/" 2>/dev/null) || http_code="000" + "https://${bmc_address}/redfish/v1/" 2>/dev/null) || http_code="000" if [[ "${http_code}" == "200" ]]; then echo "OK (HTTP ${http_code})" @@ -208,7 +232,7 @@ verify_all_bmcs() { local failed=0 for ((i = 0; i < ${#NODE_NAMES[@]}; i++)); do - if ! verify_bmc "${NODE_NAMES[$i]}" "${NODE_BMC_IPS[$i]}" \ + if ! verify_bmc "${NODE_NAMES[$i]}" "${NODE_BMC_ADDRS[$i]}" \ "${NODE_BMC_USERS[$i]}" "${NODE_BMC_PASSES[$i]}"; then failed=$((failed + 1)) fi @@ -235,18 +259,30 @@ generate_ironic_nodes_json() { for ((i = 0; i < ${#NODE_NAMES[@]}; i++)); do local name="${NODE_NAMES[$i]}" - local bmc_ip="${NODE_BMC_IPS[$i]}" + local bmc_address="${NODE_BMC_ADDRS[$i]}" local bmc_user="${NODE_BMC_USERS[$i]}" local bmc_pass="${NODE_BMC_PASSES[$i]}" local boot_mac="${NODE_BOOT_MACS[$i]}" # Discover the Redfish system path from the BMC, fall back to standard local system_id - system_id=$(discover_redfish_system_id "${bmc_ip}" "${bmc_user}" "${bmc_pass}" 2>/dev/null) || true + system_id=$(discover_redfish_system_id "${bmc_address}" "${bmc_user}" "${bmc_pass}" 2>/dev/null) || true system_id="${system_id:-/redfish/v1/Systems/1}" # Strip leading slash for URL construction system_id="${system_id#/}" + # Auto-discover boot MAC via Redfish if not provided + if [[ -z "${boot_mac}" ]]; then + info " ${name}: boot_mac not set, attempting Redfish discovery..." + boot_mac=$(discover_boot_mac "${bmc_address}" "${bmc_user}" "${bmc_pass}" "${system_id}" 2>/dev/null) || true + if [[ -n "${boot_mac}" ]]; then + info " ${name}: discovered boot MAC ${boot_mac}" + else + echo " WARNING: ${name}: could not discover boot MAC — set boot_mac in inventory" >&2 + boot_mac="DISCOVERY_FAILED" + fi + fi + ${first} || nodes_json+="," first=false @@ -255,7 +291,7 @@ generate_ironic_nodes_json() { "name": "${name}", "driver": "redfish", "driver_info": { - "address": "redfish://${bmc_ip}/${system_id}", + "address": "redfish://${bmc_address}/${system_id}", "username": "${bmc_user}", "password": "${bmc_pass}", "redfish_verify_ca": "${BMC_VERIFY_CA}" diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index 7bd567c6..b33d4e1c 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -165,10 +165,10 @@ prompt_bmc_pass() { prompt_boot_mac() { local mac while true; do - read -rp " Boot MAC address: " mac + read -rp " Boot MAC address (Enter to auto-discover): " mac if [[ -z "${mac}" ]]; then - echo " Error: boot MAC is required" >&2 - continue + echo "${mac}" + return fi if ! valid_mac "${mac}"; then echo " Error: invalid MAC (expected XX:XX:XX:XX:XX:XX)" >&2 @@ -195,13 +195,14 @@ show_summary() { local i for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do + local display_mac="${WIZ_MACS[$i]:-auto-discover}" printf " %-4s %-14s %-40s %-10s %-10s %-19s\n" \ "$((i + 1))" \ "${WIZ_NAMES[$i]}" \ "${WIZ_IPS[$i]}" \ "${WIZ_USERS[$i]}" \ "********" \ - "${WIZ_MACS[$i]}" + "${display_mac}" done echo "==================================" @@ -279,7 +280,11 @@ write_inventory() { local i for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do - echo "${WIZ_NAMES[$i]} bmc_ip=${WIZ_IPS[$i]} bmc_user=${WIZ_USERS[$i]} bmc_pass=${WIZ_PASSES[$i]} boot_mac=${WIZ_MACS[$i]}" >> "${tmp_inventory}" + local line="${WIZ_NAMES[$i]} bmc_address=${WIZ_IPS[$i]} bmc_user=${WIZ_USERS[$i]} bmc_pass=${WIZ_PASSES[$i]}" + if [[ -n "${WIZ_MACS[$i]}" ]]; then + line+=" boot_mac=${WIZ_MACS[$i]}" + fi + echo "${line}" >> "${tmp_inventory}" done { From 10747b27804cb9a4d778f207044b06e00588d93c Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 15:24:32 +0200 Subject: [PATCH 05/17] Use Redfish BootOptions for boot MAC discovery Replace the EthernetInterfaces-based discovery (which returned the wrong NIC) with BootOptions-based discovery. Walks the BIOS boot order, finds the first PXE IPv4 entry, and extracts the MAC from the UEFI device path. Tested against HPE iLO 5 (Edgeline e920t). Co-Authored-By: Claude Opus 4.6 --- .../scripts/baremetal-adopt.sh | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index fc1f9f64..911deec9 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -175,22 +175,46 @@ discover_redfish_system_id() { discover_boot_mac() { local bmc_address="$1" bmc_user="$2" bmc_pass="$3" system_id="$4" - local ifaces_json mac - ifaces_json=$(curl -sk --connect-timeout 5 --max-time 15 \ + # Get boot order from the system resource + local boot_order + boot_order=$(curl -sk --connect-timeout 5 --max-time 10 \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}/${system_id}EthernetInterfaces/" 2>/dev/null) || return 1 + "https://${bmc_address}/${system_id}" 2>/dev/null \ + | jq -r '.Boot.BootOrder[]' 2>/dev/null) || return 1 - local iface_paths - iface_paths=$(echo "${ifaces_json}" | jq -r '.Members[]."@odata.id"' 2>/dev/null) || return 1 + # Fetch all boot options and index by BootOptionReference + local options_json + options_json=$(curl -sk --connect-timeout 5 --max-time 10 \ + -u "${bmc_user}:${bmc_pass}" \ + "https://${bmc_address}/${system_id}BootOptions/" 2>/dev/null) || return 1 + + local option_paths + option_paths=$(echo "${options_json}" | jq -r '.Members[]."@odata.id"' 2>/dev/null) || return 1 - for iface_path in ${iface_paths}; do - mac=$(curl -sk --connect-timeout 5 --max-time 10 \ + # Build associative arrays: ref → display_name, ref → uefi_path + declare -A opt_display opt_path + for option_url in ${option_paths}; do + local option + option=$(curl -sk --connect-timeout 5 --max-time 10 \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}${iface_path}" 2>/dev/null \ - | jq -r 'select(.Status.State == "Enabled") | .MACAddress // empty' 2>/dev/null) + "https://${bmc_address}${option_url}" 2>/dev/null) || continue + + local ref + ref=$(echo "${option}" | jq -r '.BootOptionReference // empty' 2>/dev/null) + [[ -z "${ref}" ]] && continue + opt_display["${ref}"]=$(echo "${option}" | jq -r '.DisplayName // empty' 2>/dev/null) + opt_path["${ref}"]=$(echo "${option}" | jq -r '.UefiDevicePath // empty' 2>/dev/null) + done + + # Walk boot order, find the first PXE IPv4 entry + for boot_ref in ${boot_order}; do + local display_name="${opt_display[${boot_ref}]:-}" + local uefi_path="${opt_path[${boot_ref}]:-}" - if [[ -n "${mac}" && "${mac}" != "00:00:00:00:00:00" ]]; then - echo "${mac}" + if [[ "${display_name}" == *"PXE IPv4"* ]] && [[ "${uefi_path}" == *MAC* ]]; then + local raw_mac + raw_mac=$(echo "${uefi_path}" | grep -oP 'MAC\(\K[0-9A-Fa-f]+' 2>/dev/null) || continue + echo "${raw_mac}" | sed 's/\(..\)/\1:/g; s/:$//' | tr '[:lower:]' '[:upper:]' return 0 fi done From 84ebb546837d0c6a04eb3e4cc73e36ed77ef8e97 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 16:27:46 +0200 Subject: [PATCH 06/17] Address CodeRabbit review findings on PR #88 - Fix jq null handling in Redfish system ID discovery (// empty) - Fix script names in usage text and generated comments - Fix missing / separator in BootOptions URL construction - Add --inventory flag and info messages to wizard trigger - Add warning when inventory has != 2 nodes - Honor bmc_verify_ca in curl calls via bmc_curl wrapper - Restrict permissions on generated credential artifacts (umask 077) Co-Authored-By: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- .../scripts/baremetal-adopt.sh | 37 ++++++++++++++----- .../scripts/baremetal-wizard.sh | 4 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 911deec9..8f4c5a46 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -6,12 +6,13 @@ # and generates ironic_nodes.json + config_baremetal_fencing.sh for dev-scripts. # # Usage: -# adopt-baremetal.sh [options] +# baremetal-adopt.sh [options] # # Options: # --cluster-name NAME Cluster name for output directory (default: ostest) # --skip-verify Skip BMC credential verification # --verify-only Only verify BMC credentials, don't generate artifacts +# --inventory FILE Path to baremetal inventory (default: inventory_baremetal.ini) # --config-base FILE Base config to derive baremetal config from # -h, --help Show this help message @@ -66,6 +67,10 @@ parse_args() { VERIFY_ONLY=true shift ;; + --inventory) + INVENTORY="$2" + shift 2 + ;; --config-base) CONFIG_BASE="$2" shift 2 @@ -154,6 +159,9 @@ parse_inventory() { done < "${INVENTORY}" [[ ${#NODE_NAMES[@]} -eq 0 ]] && die "No nodes found in inventory" + if [[ ${#NODE_NAMES[@]} -ne 2 ]]; then + echo " WARNING: TNF requires exactly 2 nodes, found ${#NODE_NAMES[@]}" >&2 + fi info "Parsed ${#NODE_NAMES[@]} node(s) from inventory" } @@ -161,15 +169,21 @@ parse_inventory() { # BMC verification via Redfish ############################################################################## +bmc_curl() { + local opts=(-s --connect-timeout 5 --max-time 10) + [[ "${BMC_VERIFY_CA}" == "False" ]] && opts+=(-k) + curl "${opts[@]}" "$@" +} + discover_redfish_system_id() { local bmc_address="$1" bmc_user="$2" bmc_pass="$3" local systems_json - systems_json=$(curl -sk --connect-timeout 5 --max-time 10 \ + systems_json=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ "https://${bmc_address}/redfish/v1/Systems/" 2>/dev/null) || return 1 - echo "${systems_json}" | jq -r '.Members[0]."@odata.id"' 2>/dev/null + echo "${systems_json}" | jq -r '.Members[0]."@odata.id" // empty' 2>/dev/null } discover_boot_mac() { @@ -177,16 +191,16 @@ discover_boot_mac() { # Get boot order from the system resource local boot_order - boot_order=$(curl -sk --connect-timeout 5 --max-time 10 \ + boot_order=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ "https://${bmc_address}/${system_id}" 2>/dev/null \ | jq -r '.Boot.BootOrder[]' 2>/dev/null) || return 1 # Fetch all boot options and index by BootOptionReference local options_json - options_json=$(curl -sk --connect-timeout 5 --max-time 10 \ + options_json=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}/${system_id}BootOptions/" 2>/dev/null) || return 1 + "https://${bmc_address}/${system_id}/BootOptions/" 2>/dev/null) || return 1 local option_paths option_paths=$(echo "${options_json}" | jq -r '.Members[]."@odata.id"' 2>/dev/null) || return 1 @@ -195,7 +209,7 @@ discover_boot_mac() { declare -A opt_display opt_path for option_url in ${option_paths}; do local option - option=$(curl -sk --connect-timeout 5 --max-time 10 \ + option=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ "https://${bmc_address}${option_url}" 2>/dev/null) || continue @@ -229,7 +243,7 @@ verify_bmc() { # Verify Redfish root is reachable and credentials work local http_code - http_code=$(curl -sk --connect-timeout 5 --max-time 10 \ + http_code=$(bmc_curl \ -o /dev/null -w '%{http_code}' \ -u "${bmc_user}:${bmc_pass}" \ "https://${bmc_address}/redfish/v1/" 2>/dev/null) || http_code="000" @@ -362,7 +376,7 @@ generate_baremetal_config() { { cat "${base_config}" echo "" - echo "# Baremetal adoption overrides (generated by adopt-baremetal.sh)" + echo "# Baremetal adoption overrides (generated by baremetal-adopt.sh)" echo "export NODES_PLATFORM=baremetal" echo "export NODES_FILE=\"${nodes_file_path}\"" } > "${output_file}" @@ -379,6 +393,8 @@ main() { # Launch interactive wizard if no inventory exists if [[ ! -f "${INVENTORY}" ]]; then + info "No inventory found at ${INVENTORY}" + info "Launching interactive wizard (or provide --inventory PATH)" "${SCRIPT_DIR}/baremetal-wizard.sh" --output "${INVENTORY}" fi @@ -396,6 +412,7 @@ main() { # Create output directory local output_dir="${OC_DIR}/clusters/${CLUSTER_NAME}" + umask 077 mkdir -p "${output_dir}" # Generate artifacts @@ -411,7 +428,7 @@ main() { echo " ${nodes_file}" echo " ${output_dir}/config_baremetal_fencing.sh" echo "" - echo " Next: deploy to hypervisor with the baremetal install workflow (OCPEDGE-2775)" + echo " Next: deploy to the nodes using one of the baremetal-deploy* options" } main "$@" diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index b33d4e1c..fd727493 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -6,7 +6,7 @@ # displays a summary for confirmation, and writes inventory_baremetal.ini. # # Usage: -# wizard-baremetal.sh [options] +# baremetal-wizard.sh [options] # # Options: # --output FILE Inventory output path (default: inventory_baremetal.ini) @@ -273,7 +273,7 @@ write_inventory() { tmp_inventory=$(mktemp) { - echo "# Generated by wizard-baremetal.sh" + echo "# Generated by baremetal-wizard.sh" echo "" echo "[baremetal_nodes]" } > "${tmp_inventory}" From 388f2fe29a8b9980b49845a7e9040aefeba273c9 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 17:01:20 +0200 Subject: [PATCH 07/17] Address Doug's poka-yoke review findings on PR #88 - Replace heredoc JSON interpolation with jq -n --arg to prevent malformed output from passwords containing quotes or backslashes - Fail hard on missing boot MAC instead of writing DISCOVERY_FAILED placeholder that silently breaks dev-scripts downstream - Gate all BMC contact (discovery + verification) behind --skip-verify so offline users don't get silent failures baked into artifacts Co-Authored-By: Claude Opus 4.6 --- .../scripts/baremetal-adopt.sh | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 8f4c5a46..53ae602b 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -10,7 +10,7 @@ # # Options: # --cluster-name NAME Cluster name for output directory (default: ostest) -# --skip-verify Skip BMC credential verification +# --skip-verify Skip all BMC access (verify + discovery); requires boot_mac in inventory # --verify-only Only verify BMC credentials, don't generate artifacts # --inventory FILE Path to baremetal inventory (default: inventory_baremetal.ini) # --config-base FILE Base config to derive baremetal config from @@ -292,8 +292,8 @@ generate_ironic_nodes_json() { info "Generating ironic_nodes.json" - local nodes_json='{"nodes":[' - local first=true + local incomplete=false + local nodes=() for ((i = 0; i < ${#NODE_NAMES[@]}; i++)); do local name="${NODE_NAMES[$i]}" @@ -302,52 +302,61 @@ generate_ironic_nodes_json() { local bmc_pass="${NODE_BMC_PASSES[$i]}" local boot_mac="${NODE_BOOT_MACS[$i]}" - # Discover the Redfish system path from the BMC, fall back to standard + # Discover Redfish system path (requires BMC access) local system_id - system_id=$(discover_redfish_system_id "${bmc_address}" "${bmc_user}" "${bmc_pass}" 2>/dev/null) || true - system_id="${system_id:-/redfish/v1/Systems/1}" - # Strip leading slash for URL construction - system_id="${system_id#/}" + if ${SKIP_VERIFY}; then + system_id="redfish/v1/Systems/1" + else + system_id=$(discover_redfish_system_id "${bmc_address}" "${bmc_user}" "${bmc_pass}" 2>/dev/null) || true + system_id="${system_id:-/redfish/v1/Systems/1}" + system_id="${system_id#/}" + fi # Auto-discover boot MAC via Redfish if not provided if [[ -z "${boot_mac}" ]]; then + if ${SKIP_VERIFY}; then + echo " ERROR: ${name}: boot_mac required when using --skip-verify" >&2 + incomplete=true + continue + fi info " ${name}: boot_mac not set, attempting Redfish discovery..." boot_mac=$(discover_boot_mac "${bmc_address}" "${bmc_user}" "${bmc_pass}" "${system_id}" 2>/dev/null) || true if [[ -n "${boot_mac}" ]]; then info " ${name}: discovered boot MAC ${boot_mac}" else - echo " WARNING: ${name}: could not discover boot MAC — set boot_mac in inventory" >&2 - boot_mac="DISCOVERY_FAILED" + echo " ERROR: ${name}: could not discover boot MAC — set boot_mac in inventory" >&2 + incomplete=true + continue fi fi - ${first} || nodes_json+="," - first=false - - nodes_json+=$(cat < "${output_file}" + printf '%s\n' "${nodes[@]}" | jq -s '{nodes: .}' > "${output_file}" info " → ${output_file}" } From 4f112dee85eae67923bdc594434103804b06b0b6 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Mon, 29 Jun 2026 17:07:54 +0200 Subject: [PATCH 08/17] Fix boot MAC discovery failing on HPE iLO (trailing slash in system_id) Redfish @odata.id values include a trailing slash (/redfish/v1/Systems/1/), which produced a double-slash URL (.../Systems/1//BootOptions/) that iLO returns null content for. Strip the trailing slash after the leading one. Co-Authored-By: Claude Opus 4.6 --- deploy/openshift-clusters/scripts/baremetal-adopt.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 53ae602b..9ffa2e9c 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -310,6 +310,7 @@ generate_ironic_nodes_json() { system_id=$(discover_redfish_system_id "${bmc_address}" "${bmc_user}" "${bmc_pass}" 2>/dev/null) || true system_id="${system_id:-/redfish/v1/Systems/1}" system_id="${system_id#/}" + system_id="${system_id%/}" fi # Auto-discover boot MAC via Redfish if not provided From 9b6cefb839a750cb722e3312bfffe7a70da77146 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Tue, 30 Jun 2026 10:44:30 +0200 Subject: [PATCH 09/17] Add OS-level network config to baremetal adoption workflow Add declarative [baremetal_network] INI section for cluster-wide network vars (machine_network, gateway, api_vip, ingress_vip) and per-node node_ip field. The adoption script translates these into dev-scripts exports (EXTERNAL_SUBNET_V4, BAREMETAL_GATEWAY, BAREMETAL_API_VIP, BAREMETAL_INGRESS_VIP, BAREMETAL_IPS) and always emits bridge overrides (MANAGE_BR_BRIDGE=n, MANAGE_PRO_BRIDGE=n, MANAGE_INT_BRIDGE=n). All new fields are optional for backward compatibility. The wizard emits skipped fields as commented placeholders so users can fill them later without referencing the sample file. BAREMETAL_IPS is only emitted when ALL nodes have node_ip set. Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 17 +- .../files/config_fencing_example.sh | 3 + .../scripts/baremetal-adopt.sh | 66 ++++++- .../scripts/baremetal-wizard.sh | 163 +++++++++++++++++- 4 files changed, 236 insertions(+), 13 deletions(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index 3300d057..47b73da5 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -14,13 +14,15 @@ # bmc_pass - BMC login password # boot_mac - (optional) MAC address of the NIC used for PXE boot # If omitted, the adopt script attempts Redfish discovery. +# node_ip - (optional) Static IP address for this node on the machine network +# Required for baremetal ABI deployments with static IPs. # # The hostname (first field) becomes the node name in ironic_nodes.json. # For TNF, you need exactly 2 nodes (master-0 and master-1). [baremetal_nodes] -master-0 bmc_address=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 -master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 +master-0 bmc_address=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 node_ip=192.168.1.10 +master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 node_ip=192.168.1.11 [baremetal_nodes:vars] # BMC driver — only redfish is supported for TNF fencing @@ -31,3 +33,14 @@ bmc_verify_ca=False # Node CPU architecture cpu_arch=x86_64 + +[baremetal_network] +# Cluster-wide network config for baremetal ABI deployments (all optional). +# machine_network - Machine network CIDR (e.g. 192.168.1.0/24) +# gateway - Default gateway IP +# api_vip - API virtual IP +# ingress_vip - Ingress virtual IP +#machine_network=192.168.1.0/24 +#gateway=192.168.1.1 +#api_vip=192.168.1.100 +#ingress_vip=192.168.1.101 diff --git a/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/config_fencing_example.sh b/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/config_fencing_example.sh index 2291e37c..2eacf2ee 100644 --- a/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/config_fencing_example.sh +++ b/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/config_fencing_example.sh @@ -36,3 +36,6 @@ export OPENSHIFT_INSTALL_EXPERIMENTAL_DISABLE_IMAGE_POLICY=true # export VBMC_IMAGE=quay.io/rh-edge-enablement/vbmc:2026-06 # export SUSHY_TOOLS_IMAGE=quay.io/rh-edge-enablement/sushy-tools:2026-06 # fi + +# Baremetal network config (node IPs, VIPs, bridge overrides) is auto-generated +# by 'make baremetal-adopt' into config_baremetal_fencing.sh — do not add here. diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 9ffa2e9c..4521407d 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -35,6 +35,13 @@ declare -a NODE_BMC_ADDRS=() declare -a NODE_BMC_USERS=() declare -a NODE_BMC_PASSES=() declare -a NODE_BOOT_MACS=() +declare -a NODE_IPS=() + +# Cluster-wide network config (optional, from [baremetal_network]) +MACHINE_NETWORK="" +GATEWAY="" +API_VIP="" +INGRESS_VIP="" # Group defaults BMC_VERIFY_CA="False" @@ -95,6 +102,7 @@ parse_inventory() { local in_nodes=false local in_vars=false + local in_network=false while IFS= read -r line || [[ -n "${line}" ]]; do # Strip comments and leading/trailing whitespace @@ -111,9 +119,15 @@ parse_inventory() { in_nodes=false in_vars=true continue + elif [[ "${line}" == "[baremetal_network]" ]]; then + in_nodes=false + in_vars=false + in_network=true + continue elif [[ "${line}" =~ ^\[.*\] ]]; then in_nodes=false in_vars=false + in_network=false continue fi @@ -128,21 +142,35 @@ parse_inventory() { continue fi + if ${in_network}; then + local key val + key="${line%%=*}" + val="${line#*=}" + case "${key}" in + machine_network) MACHINE_NETWORK="${val}" ;; + gateway) GATEWAY="${val}" ;; + api_vip) API_VIP="${val}" ;; + ingress_vip) INGRESS_VIP="${val}" ;; + esac + continue + fi + if ${in_nodes}; then local name rest name="${line%% *}" rest="${line#* }" - local bmc_address="" bmc_user="" bmc_pass="" boot_mac="" + local bmc_address="" bmc_user="" bmc_pass="" boot_mac="" node_ip="" for pair in ${rest}; do local key val key="${pair%%=*}" val="${pair#*=}" case "${key}" in - bmc_address) bmc_address="${val}" ;; - bmc_user) bmc_user="${val}" ;; - bmc_pass) bmc_pass="${val}" ;; - boot_mac) boot_mac="${val}" ;; + bmc_address) bmc_address="${val}" ;; + bmc_user) bmc_user="${val}" ;; + bmc_pass) bmc_pass="${val}" ;; + boot_mac) boot_mac="${val}" ;; + node_ip) node_ip="${val}" ;; esac done @@ -155,6 +183,7 @@ parse_inventory() { NODE_BMC_USERS+=("${bmc_user}") NODE_BMC_PASSES+=("${bmc_pass}") NODE_BOOT_MACS+=("${boot_mac}") + NODE_IPS+=("${node_ip}") fi done < "${INVENTORY}" @@ -389,6 +418,33 @@ generate_baremetal_config() { echo "# Baremetal adoption overrides (generated by baremetal-adopt.sh)" echo "export NODES_PLATFORM=baremetal" echo "export NODES_FILE=\"${nodes_file_path}\"" + echo "export MANAGE_BR_BRIDGE=n" + echo "export MANAGE_PRO_BRIDGE=n" + echo "export MANAGE_INT_BRIDGE=n" + + if [[ -n "${MACHINE_NETWORK}" || -n "${GATEWAY}" || -n "${API_VIP}" || -n "${INGRESS_VIP}" ]]; then + echo "" + echo "# Baremetal network config" + [[ -n "${MACHINE_NETWORK}" ]] && echo "export EXTERNAL_SUBNET_V4=\"${MACHINE_NETWORK}\"" + [[ -n "${GATEWAY}" ]] && echo "export BAREMETAL_GATEWAY=\"${GATEWAY}\"" + [[ -n "${API_VIP}" ]] && echo "export BAREMETAL_API_VIP=\"${API_VIP}\"" + [[ -n "${INGRESS_VIP}" ]] && echo "export BAREMETAL_INGRESS_VIP=\"${INGRESS_VIP}\"" + fi + + # Emit BAREMETAL_IPS only if ALL nodes have node_ip set + local all_have_ips=true + local ip_list="" + for ((i = 0; i < ${#NODE_IPS[@]}; i++)); do + if [[ -z "${NODE_IPS[$i]}" ]]; then + all_have_ips=false + break + fi + [[ -n "${ip_list}" ]] && ip_list+="," + ip_list+="${NODE_IPS[$i]}" + done + if ${all_have_ips} && [[ -n "${ip_list}" ]]; then + echo "export BAREMETAL_IPS=\"${ip_list}\"" + fi } > "${output_file}" info " → ${output_file}" diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index fd727493..b21bf172 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -84,6 +84,17 @@ valid_bmc_address() { valid_ipv4 "${addr}" || valid_hostname "${addr}" } +valid_cidr() { + local cidr="$1" + local ip="${cidr%%/*}" + local prefix="${cidr##*/}" + [[ "${cidr}" == *"/"* ]] || return 1 + valid_ipv4 "${ip}" || return 1 + [[ "${prefix}" =~ ^[0-9]+$ ]] || return 1 + (( prefix <= 32 )) || return 1 + return 0 +} + ############################################################################## # Prompt functions # @@ -179,6 +190,91 @@ prompt_boot_mac() { done } +prompt_node_ip() { + local ip + while true; do + read -rp " Node IP address (Enter to skip): " ip + if [[ -z "${ip}" ]]; then + echo "${ip}" + return + fi + if ! valid_ipv4 "${ip}"; then + echo " Error: invalid IPv4 address" >&2 + continue + fi + echo "${ip}" + return + done +} + +prompt_machine_network() { + local cidr + while true; do + read -rp " Machine network CIDR (e.g. 192.168.1.0/24, Enter to skip): " cidr + if [[ -z "${cidr}" ]]; then + echo "${cidr}" + return + fi + if ! valid_cidr "${cidr}"; then + echo " Error: invalid CIDR (expected x.x.x.x/prefix)" >&2 + continue + fi + echo "${cidr}" + return + done +} + +prompt_gateway() { + local gw + while true; do + read -rp " Gateway IP (Enter to skip): " gw + if [[ -z "${gw}" ]]; then + echo "${gw}" + return + fi + if ! valid_ipv4 "${gw}"; then + echo " Error: invalid IPv4 address" >&2 + continue + fi + echo "${gw}" + return + done +} + +prompt_api_vip() { + local vip + while true; do + read -rp " API VIP (Enter to skip): " vip + if [[ -z "${vip}" ]]; then + echo "${vip}" + return + fi + if ! valid_ipv4 "${vip}"; then + echo " Error: invalid IPv4 address" >&2 + continue + fi + echo "${vip}" + return + done +} + +prompt_ingress_vip() { + local vip + while true; do + read -rp " Ingress VIP (Enter to skip): " vip + if [[ -z "${vip}" ]]; then + echo "${vip}" + return + fi + if ! valid_ipv4 "${vip}"; then + echo " Error: invalid IPv4 address" >&2 + continue + fi + echo "${vip}" + return + done +} + ############################################################################## # Summary display ############################################################################## @@ -188,23 +284,34 @@ show_summary() { echo "==================================" echo " BAREMETAL NODE SUMMARY" echo "==================================" - printf " %-4s %-14s %-40s %-10s %-10s %-19s\n" \ - "#" "HOSTNAME" "BMC ADDRESS" "BMC USER" "PASSWORD" "BOOT MAC" - printf " %-4s %-14s %-40s %-10s %-10s %-19s\n" \ - "---" "------------" "--------------------------------------" "--------" "--------" "-----------------" + printf " %-4s %-14s %-40s %-10s %-10s %-19s %-17s\n" \ + "#" "HOSTNAME" "BMC ADDRESS" "BMC USER" "PASSWORD" "BOOT MAC" "NODE IP" + printf " %-4s %-14s %-40s %-10s %-10s %-19s %-17s\n" \ + "---" "------------" "--------------------------------------" "--------" "--------" "-----------------" "---------------" local i for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do local display_mac="${WIZ_MACS[$i]:-auto-discover}" - printf " %-4s %-14s %-40s %-10s %-10s %-19s\n" \ + local display_ip="${WIZ_NODE_IPS[$i]:---}" + printf " %-4s %-14s %-40s %-10s %-10s %-19s %-17s\n" \ "$((i + 1))" \ "${WIZ_NAMES[$i]}" \ "${WIZ_IPS[$i]}" \ "${WIZ_USERS[$i]}" \ "********" \ - "${display_mac}" + "${display_mac}" \ + "${display_ip}" done + if [[ -n "${WIZ_MACHINE_NETWORK}" || -n "${WIZ_GATEWAY}" || -n "${WIZ_API_VIP}" || -n "${WIZ_INGRESS_VIP}" ]]; then + echo "" + echo " Cluster Network:" + [[ -n "${WIZ_MACHINE_NETWORK}" ]] && echo " Machine network: ${WIZ_MACHINE_NETWORK}" + [[ -n "${WIZ_GATEWAY}" ]] && echo " Gateway: ${WIZ_GATEWAY}" + [[ -n "${WIZ_API_VIP}" ]] && echo " API VIP: ${WIZ_API_VIP}" + [[ -n "${WIZ_INGRESS_VIP}" ]] && echo " Ingress VIP: ${WIZ_INGRESS_VIP}" + fi + echo "==================================" } @@ -225,6 +332,7 @@ run_wizard() { WIZ_USERS=() WIZ_PASSES=() WIZ_MACS=() + WIZ_NODE_IPS=() local i for ((i = 0; i < node_count; i++)); do @@ -237,8 +345,16 @@ run_wizard() { WIZ_USERS+=("$(prompt_bmc_user)") WIZ_PASSES+=("$(prompt_bmc_pass)") WIZ_MACS+=("$(prompt_boot_mac)") + WIZ_NODE_IPS+=("$(prompt_node_ip)") done + echo "" + echo "--- Cluster Network (all optional) ---" + WIZ_MACHINE_NETWORK="$(prompt_machine_network)" + WIZ_GATEWAY="$(prompt_gateway)" + WIZ_API_VIP="$(prompt_api_vip)" + WIZ_INGRESS_VIP="$(prompt_ingress_vip)" + show_summary local confirm @@ -284,6 +400,11 @@ write_inventory() { if [[ -n "${WIZ_MACS[$i]}" ]]; then line+=" boot_mac=${WIZ_MACS[$i]}" fi + if [[ -n "${WIZ_NODE_IPS[$i]}" ]]; then + line+=" node_ip=${WIZ_NODE_IPS[$i]}" + else + line+=" #node_ip=" + fi echo "${line}" >> "${tmp_inventory}" done @@ -295,6 +416,31 @@ write_inventory() { echo "cpu_arch=x86_64" } >> "${tmp_inventory}" + { + echo "" + echo "[baremetal_network]" + if [[ -n "${WIZ_MACHINE_NETWORK}" ]]; then + echo "machine_network=${WIZ_MACHINE_NETWORK}" + else + echo "#machine_network=" + fi + if [[ -n "${WIZ_GATEWAY}" ]]; then + echo "gateway=${WIZ_GATEWAY}" + else + echo "#gateway=" + fi + if [[ -n "${WIZ_API_VIP}" ]]; then + echo "api_vip=${WIZ_API_VIP}" + else + echo "#api_vip=" + fi + if [[ -n "${WIZ_INGRESS_VIP}" ]]; then + echo "ingress_vip=${WIZ_INGRESS_VIP}" + else + echo "#ingress_vip=" + fi + } >> "${tmp_inventory}" + mv "${tmp_inventory}" "${OUTPUT}" echo "" info "Inventory written to ${OUTPUT}" @@ -309,6 +455,11 @@ declare -a WIZ_IPS=() declare -a WIZ_USERS=() declare -a WIZ_PASSES=() declare -a WIZ_MACS=() +declare -a WIZ_NODE_IPS=() +WIZ_MACHINE_NETWORK="" +WIZ_GATEWAY="" +WIZ_API_VIP="" +WIZ_INGRESS_VIP="" parse_args "$@" run_wizard From b895d7decafcb63f331f0b60bdbdbd023f7ffcd1 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Tue, 30 Jun 2026 16:14:25 +0200 Subject: [PATCH 10/17] Add [provisioning_host] prompts to baremetal wizard The wizard now collects optional provisioning host config (ssh_target, ssh_key, dev_scripts_path, working_dir) and writes a [provisioning_host] section to inventory_baremetal.ini. Skipped fields are commented out. Co-Authored-By: Claude Opus 4.6 --- .../scripts/baremetal-wizard.sh | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index b21bf172..6d636876 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -275,6 +275,41 @@ prompt_ingress_vip() { done } +prompt_ssh_target() { + local target + while true; do + read -rp " SSH target for remote deployment (user@host, Enter to skip): " target + if [[ -z "${target}" ]]; then + echo "${target}" + return + fi + if ! [[ "${target}" == *@* ]]; then + echo " Error: expected user@host format" >&2 + continue + fi + echo "${target}" + return + done +} + +prompt_ssh_key() { + local key + read -rp " SSH key path (Enter for ssh-agent/default): " key + echo "${key}" +} + +prompt_dev_scripts_path() { + local path + read -rp " dev-scripts path on remote [~/openshift-metal3/dev-scripts]: " path + echo "${path}" +} + +prompt_working_dir() { + local dir + read -rp " Remote working directory [~/tnt-baremetal]: " dir + echo "${dir}" +} + ############################################################################## # Summary display ############################################################################## @@ -312,6 +347,15 @@ show_summary() { [[ -n "${WIZ_INGRESS_VIP}" ]] && echo " Ingress VIP: ${WIZ_INGRESS_VIP}" fi + if [[ -n "${WIZ_SSH_TARGET}" ]]; then + echo "" + echo " Provisioning Host:" + echo " SSH target: ${WIZ_SSH_TARGET}" + echo " SSH key: ${WIZ_SSH_KEY:---}" + echo " Dev-scripts: ${WIZ_DEV_SCRIPTS_PATH:-~/openshift-metal3/dev-scripts}" + echo " Working dir: ${WIZ_WORKING_DIR:-~/tnt-baremetal}" + fi + echo "==================================" } @@ -355,6 +399,19 @@ run_wizard() { WIZ_API_VIP="$(prompt_api_vip)" WIZ_INGRESS_VIP="$(prompt_ingress_vip)" + echo "" + echo "--- Provisioning Host (optional) ---" + WIZ_SSH_TARGET="$(prompt_ssh_target)" + if [[ -n "${WIZ_SSH_TARGET}" ]]; then + WIZ_SSH_KEY="$(prompt_ssh_key)" + WIZ_DEV_SCRIPTS_PATH="$(prompt_dev_scripts_path)" + WIZ_WORKING_DIR="$(prompt_working_dir)" + else + WIZ_SSH_KEY="" + WIZ_DEV_SCRIPTS_PATH="" + WIZ_WORKING_DIR="" + fi + show_summary local confirm @@ -441,6 +498,31 @@ write_inventory() { fi } >> "${tmp_inventory}" + { + echo "" + echo "[provisioning_host]" + if [[ -n "${WIZ_SSH_TARGET}" ]]; then + echo "ssh_target=${WIZ_SSH_TARGET}" + else + echo "#ssh_target=" + fi + if [[ -n "${WIZ_SSH_KEY}" ]]; then + echo "ssh_key=${WIZ_SSH_KEY}" + else + echo "#ssh_key=" + fi + if [[ -n "${WIZ_DEV_SCRIPTS_PATH}" ]]; then + echo "dev_scripts_path=${WIZ_DEV_SCRIPTS_PATH}" + else + echo "#dev_scripts_path=" + fi + if [[ -n "${WIZ_WORKING_DIR}" ]]; then + echo "working_dir=${WIZ_WORKING_DIR}" + else + echo "#working_dir=" + fi + } >> "${tmp_inventory}" + mv "${tmp_inventory}" "${OUTPUT}" echo "" info "Inventory written to ${OUTPUT}" @@ -460,6 +542,10 @@ WIZ_MACHINE_NETWORK="" WIZ_GATEWAY="" WIZ_API_VIP="" WIZ_INGRESS_VIP="" +WIZ_SSH_TARGET="" +WIZ_SSH_KEY="" +WIZ_DEV_SCRIPTS_PATH="" +WIZ_WORKING_DIR="" parse_args "$@" run_wizard From 101f5335f50290cc62f9f00c3527b3c5704d7a52 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Thu, 2 Jul 2026 10:30:54 +0200 Subject: [PATCH 11/17] Add BMC port to Redfish addresses for CEO fencing compatibility Without an explicit port in the Redfish URL, CEO's fence_redfish agent fails to connect to the BMC during TNF auth jobs. The port (default 443) now flows through verification, discovery, and ironic_nodes.json generation as redfish://host:port/redfish/v1/Systems/1. Adds bmc_port as a per-node field with a group default in [baremetal_nodes:vars], matching the existing inheritance pattern. Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 4 +++ .../scripts/baremetal-adopt.sh | 34 +++++++++++-------- .../scripts/baremetal-wizard.sh | 30 ++++++++++++---- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index 47b73da5..f27a1d90 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -12,6 +12,7 @@ # bmc_address - BMC/iDRAC/iLO management address (IP or hostname) # bmc_user - BMC login username # bmc_pass - BMC login password +# bmc_port - (optional) BMC Redfish port (default: 443) # boot_mac - (optional) MAC address of the NIC used for PXE boot # If omitted, the adopt script attempts Redfish discovery. # node_ip - (optional) Static IP address for this node on the machine network @@ -28,6 +29,9 @@ master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52: # BMC driver — only redfish is supported for TNF fencing bmc_driver=redfish +# BMC Redfish port (per-node bmc_port overrides this) +bmc_port=443 + # Skip TLS verification for BMC endpoints (common with self-signed certs) bmc_verify_ca=False diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 4521407d..d5f701c2 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -34,6 +34,7 @@ declare -a NODE_NAMES=() declare -a NODE_BMC_ADDRS=() declare -a NODE_BMC_USERS=() declare -a NODE_BMC_PASSES=() +declare -a NODE_BMC_PORTS=() declare -a NODE_BOOT_MACS=() declare -a NODE_IPS=() @@ -44,6 +45,7 @@ API_VIP="" INGRESS_VIP="" # Group defaults +BMC_PORT="443" BMC_VERIFY_CA="False" CPU_ARCH="x86_64" @@ -136,6 +138,7 @@ parse_inventory() { key="${line%%=*}" val="${line#*=}" case "${key}" in + bmc_port) BMC_PORT="${val}" ;; bmc_verify_ca) BMC_VERIFY_CA="${val}" ;; cpu_arch) CPU_ARCH="${val}" ;; esac @@ -160,7 +163,7 @@ parse_inventory() { name="${line%% *}" rest="${line#* }" - local bmc_address="" bmc_user="" bmc_pass="" boot_mac="" node_ip="" + local bmc_address="" bmc_user="" bmc_pass="" bmc_port="" boot_mac="" node_ip="" for pair in ${rest}; do local key val key="${pair%%=*}" @@ -169,6 +172,7 @@ parse_inventory() { bmc_address) bmc_address="${val}" ;; bmc_user) bmc_user="${val}" ;; bmc_pass) bmc_pass="${val}" ;; + bmc_port) bmc_port="${val}" ;; boot_mac) boot_mac="${val}" ;; node_ip) node_ip="${val}" ;; esac @@ -182,6 +186,7 @@ parse_inventory() { NODE_BMC_ADDRS+=("${bmc_address}") NODE_BMC_USERS+=("${bmc_user}") NODE_BMC_PASSES+=("${bmc_pass}") + NODE_BMC_PORTS+=("${bmc_port:-${BMC_PORT}}") NODE_BOOT_MACS+=("${boot_mac}") NODE_IPS+=("${node_ip}") fi @@ -205,31 +210,31 @@ bmc_curl() { } discover_redfish_system_id() { - local bmc_address="$1" bmc_user="$2" bmc_pass="$3" + local bmc_address="$1" bmc_user="$2" bmc_pass="$3" bmc_port="$4" local systems_json systems_json=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}/redfish/v1/Systems/" 2>/dev/null) || return 1 + "https://${bmc_address}:${bmc_port}/redfish/v1/Systems/" 2>/dev/null) || return 1 echo "${systems_json}" | jq -r '.Members[0]."@odata.id" // empty' 2>/dev/null } discover_boot_mac() { - local bmc_address="$1" bmc_user="$2" bmc_pass="$3" system_id="$4" + local bmc_address="$1" bmc_user="$2" bmc_pass="$3" bmc_port="$4" system_id="$5" # Get boot order from the system resource local boot_order boot_order=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}/${system_id}" 2>/dev/null \ + "https://${bmc_address}:${bmc_port}/${system_id}" 2>/dev/null \ | jq -r '.Boot.BootOrder[]' 2>/dev/null) || return 1 # Fetch all boot options and index by BootOptionReference local options_json options_json=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}/${system_id}/BootOptions/" 2>/dev/null) || return 1 + "https://${bmc_address}:${bmc_port}/${system_id}/BootOptions/" 2>/dev/null) || return 1 local option_paths option_paths=$(echo "${options_json}" | jq -r '.Members[]."@odata.id"' 2>/dev/null) || return 1 @@ -240,7 +245,7 @@ discover_boot_mac() { local option option=$(bmc_curl \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}${option_url}" 2>/dev/null) || continue + "https://${bmc_address}:${bmc_port}${option_url}" 2>/dev/null) || continue local ref ref=$(echo "${option}" | jq -r '.BootOptionReference // empty' 2>/dev/null) @@ -265,17 +270,17 @@ discover_boot_mac() { } verify_bmc() { - local name="$1" bmc_address="$2" bmc_user="$3" bmc_pass="$4" + local name="$1" bmc_address="$2" bmc_user="$3" bmc_pass="$4" bmc_port="$5" local rc=0 - printf " %-12s %-20s " "${name}" "${bmc_address}" + printf " %-12s %-20s " "${name}" "${bmc_address}:${bmc_port}" # Verify Redfish root is reachable and credentials work local http_code http_code=$(bmc_curl \ -o /dev/null -w '%{http_code}' \ -u "${bmc_user}:${bmc_pass}" \ - "https://${bmc_address}/redfish/v1/" 2>/dev/null) || http_code="000" + "https://${bmc_address}:${bmc_port}/redfish/v1/" 2>/dev/null) || http_code="000" if [[ "${http_code}" == "200" ]]; then echo "OK (HTTP ${http_code})" @@ -300,7 +305,7 @@ verify_all_bmcs() { local failed=0 for ((i = 0; i < ${#NODE_NAMES[@]}; i++)); do if ! verify_bmc "${NODE_NAMES[$i]}" "${NODE_BMC_ADDRS[$i]}" \ - "${NODE_BMC_USERS[$i]}" "${NODE_BMC_PASSES[$i]}"; then + "${NODE_BMC_USERS[$i]}" "${NODE_BMC_PASSES[$i]}" "${NODE_BMC_PORTS[$i]}"; then failed=$((failed + 1)) fi done @@ -329,6 +334,7 @@ generate_ironic_nodes_json() { local bmc_address="${NODE_BMC_ADDRS[$i]}" local bmc_user="${NODE_BMC_USERS[$i]}" local bmc_pass="${NODE_BMC_PASSES[$i]}" + local bmc_port="${NODE_BMC_PORTS[$i]}" local boot_mac="${NODE_BOOT_MACS[$i]}" # Discover Redfish system path (requires BMC access) @@ -336,7 +342,7 @@ generate_ironic_nodes_json() { if ${SKIP_VERIFY}; then system_id="redfish/v1/Systems/1" else - system_id=$(discover_redfish_system_id "${bmc_address}" "${bmc_user}" "${bmc_pass}" 2>/dev/null) || true + system_id=$(discover_redfish_system_id "${bmc_address}" "${bmc_user}" "${bmc_pass}" "${bmc_port}" 2>/dev/null) || true system_id="${system_id:-/redfish/v1/Systems/1}" system_id="${system_id#/}" system_id="${system_id%/}" @@ -350,7 +356,7 @@ generate_ironic_nodes_json() { continue fi info " ${name}: boot_mac not set, attempting Redfish discovery..." - boot_mac=$(discover_boot_mac "${bmc_address}" "${bmc_user}" "${bmc_pass}" "${system_id}" 2>/dev/null) || true + boot_mac=$(discover_boot_mac "${bmc_address}" "${bmc_user}" "${bmc_pass}" "${bmc_port}" "${system_id}" 2>/dev/null) || true if [[ -n "${boot_mac}" ]]; then info " ${name}: discovered boot MAC ${boot_mac}" else @@ -362,7 +368,7 @@ generate_ironic_nodes_json() { nodes+=("$(jq -n \ --arg name "${name}" \ - --arg addr "redfish://${bmc_address}/${system_id}" \ + --arg addr "redfish://${bmc_address}:${bmc_port}/${system_id}" \ --arg user "${bmc_user}" \ --arg pass "${bmc_pass}" \ --arg verify_ca "${BMC_VERIFY_CA}" \ diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index 6d636876..fe13efa8 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -173,6 +173,20 @@ prompt_bmc_pass() { done } +prompt_bmc_port() { + local port + while true; do + read -rp " BMC port [443]: " port + port="${port:-443}" + if ! [[ "${port}" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then + echo " Error: invalid port (expected 1-65535)" >&2 + continue + fi + echo "${port}" + return + done +} + prompt_boot_mac() { local mac while true; do @@ -319,19 +333,20 @@ show_summary() { echo "==================================" echo " BAREMETAL NODE SUMMARY" echo "==================================" - printf " %-4s %-14s %-40s %-10s %-10s %-19s %-17s\n" \ + printf " %-4s %-14s %-46s %-10s %-10s %-19s %-17s\n" \ "#" "HOSTNAME" "BMC ADDRESS" "BMC USER" "PASSWORD" "BOOT MAC" "NODE IP" - printf " %-4s %-14s %-40s %-10s %-10s %-19s %-17s\n" \ - "---" "------------" "--------------------------------------" "--------" "--------" "-----------------" "---------------" + printf " %-4s %-14s %-46s %-10s %-10s %-19s %-17s\n" \ + "---" "------------" "--------------------------------------------" "--------" "--------" "-----------------" "---------------" local i for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do local display_mac="${WIZ_MACS[$i]:-auto-discover}" local display_ip="${WIZ_NODE_IPS[$i]:---}" - printf " %-4s %-14s %-40s %-10s %-10s %-19s %-17s\n" \ + local display_addr="${WIZ_IPS[$i]}:${WIZ_PORTS[$i]}" + printf " %-4s %-14s %-46s %-10s %-10s %-19s %-17s\n" \ "$((i + 1))" \ "${WIZ_NAMES[$i]}" \ - "${WIZ_IPS[$i]}" \ + "${display_addr}" \ "${WIZ_USERS[$i]}" \ "********" \ "${display_mac}" \ @@ -388,6 +403,7 @@ run_wizard() { WIZ_IPS+=("$(prompt_bmc_address)") WIZ_USERS+=("$(prompt_bmc_user)") WIZ_PASSES+=("$(prompt_bmc_pass)") + WIZ_PORTS+=("$(prompt_bmc_port)") WIZ_MACS+=("$(prompt_boot_mac)") WIZ_NODE_IPS+=("$(prompt_node_ip)") done @@ -453,7 +469,7 @@ write_inventory() { local i for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do - local line="${WIZ_NAMES[$i]} bmc_address=${WIZ_IPS[$i]} bmc_user=${WIZ_USERS[$i]} bmc_pass=${WIZ_PASSES[$i]}" + local line="${WIZ_NAMES[$i]} bmc_address=${WIZ_IPS[$i]} bmc_user=${WIZ_USERS[$i]} bmc_pass=${WIZ_PASSES[$i]} bmc_port=${WIZ_PORTS[$i]}" if [[ -n "${WIZ_MACS[$i]}" ]]; then line+=" boot_mac=${WIZ_MACS[$i]}" fi @@ -469,6 +485,7 @@ write_inventory() { echo "" echo "[baremetal_nodes:vars]" echo "bmc_driver=redfish" + echo "bmc_port=443" echo "bmc_verify_ca=False" echo "cpu_arch=x86_64" } >> "${tmp_inventory}" @@ -536,6 +553,7 @@ declare -a WIZ_NAMES=() declare -a WIZ_IPS=() declare -a WIZ_USERS=() declare -a WIZ_PASSES=() +declare -a WIZ_PORTS=() declare -a WIZ_MACS=() declare -a WIZ_NODE_IPS=() WIZ_MACHINE_NETWORK="" From 8d9c936acb04d1df13f4f84bdeb27343fef6e7cb Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Thu, 2 Jul 2026 11:23:46 +0200 Subject: [PATCH 12/17] Move generated adoption artifacts to role files/ directory Place ironic_nodes.json and config_baremetal_fencing.sh in roles/dev-scripts/install-dev/files/ instead of clusters// so existing Ansible config.yml tasks can reference them by basename. Remove --cluster-name flag (only used for the clusters/ subdirectory). Co-Authored-By: Claude Opus 4.6 --- deploy/openshift-clusters/.gitignore | 2 -- .../roles/dev-scripts/install-dev/files/.gitignore | 4 +++- .../openshift-clusters/scripts/baremetal-adopt.sh | 14 +++----------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/deploy/openshift-clusters/.gitignore b/deploy/openshift-clusters/.gitignore index a77bcddb..96b8f64c 100644 --- a/deploy/openshift-clusters/.gitignore +++ b/deploy/openshift-clusters/.gitignore @@ -1,8 +1,6 @@ inventory.ini inventory_baremetal.ini -# Generated adoption artifacts (contain BMC credentials) -clusters/ proxy.env kubeconfig kubeadmin-password diff --git a/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/.gitignore b/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/.gitignore index 0818832e..e6a1d72e 100644 --- a/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/.gitignore +++ b/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/.gitignore @@ -3,4 +3,6 @@ ci_token clusterbot-ci_token config_arbiter.sh config_fencing.sh -config_sno.sh \ No newline at end of file +config_sno.sh +config_baremetal_fencing.sh +ironic_nodes.json diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index d5f701c2..0f4bb301 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -9,7 +9,6 @@ # baremetal-adopt.sh [options] # # Options: -# --cluster-name NAME Cluster name for output directory (default: ostest) # --skip-verify Skip all BMC access (verify + discovery); requires boot_mac in inventory # --verify-only Only verify BMC credentials, don't generate artifacts # --inventory FILE Path to baremetal inventory (default: inventory_baremetal.ini) @@ -23,7 +22,6 @@ set -o pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OC_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -CLUSTER_NAME="${CLUSTER_NAME:-ostest}" SKIP_VERIFY=false VERIFY_ONLY=false CONFIG_BASE="" @@ -64,10 +62,6 @@ info() { echo "==> $*"; } parse_args() { while [[ $# -gt 0 ]]; do case $1 in - --cluster-name) - CLUSTER_NAME="$2" - shift 2 - ;; --skip-verify) SKIP_VERIFY=true shift @@ -85,7 +79,7 @@ parse_args() { shift 2 ;; -h|--help) - head -17 "$0" | tail -12 + head -16 "$0" | tail -11 exit 0 ;; *) @@ -482,10 +476,8 @@ main() { exit 0 fi - # Create output directory - local output_dir="${OC_DIR}/clusters/${CLUSTER_NAME}" - umask 077 - mkdir -p "${output_dir}" + # Output alongside existing dev-scripts config files + local output_dir="${OC_DIR}/roles/dev-scripts/install-dev/files" # Generate artifacts local nodes_file="${output_dir}/ironic_nodes.json" From 58c41f5b2bddf8cdf04ca939fe2111ac6930271d Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Thu, 2 Jul 2026 12:55:53 +0200 Subject: [PATCH 13/17] Add data_mac support for multi-NIC baremetal servers On multi-NIC servers the PXE boot NIC may differ from the data NIC. Add a per-node data_mac inventory field that emits BAREMETAL_MACS in config_baremetal_fencing.sh so dev-scripts uses the correct MAC for agent-config hostname mapping. Also add the missing [provisioning_host] section to the inventory sample. Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 18 ++++++++- .../scripts/baremetal-adopt.sh | 19 ++++++++- .../scripts/baremetal-wizard.sh | 39 ++++++++++++++++--- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index f27a1d90..f9f2f05c 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -15,6 +15,9 @@ # bmc_port - (optional) BMC Redfish port (default: 443) # boot_mac - (optional) MAC address of the NIC used for PXE boot # If omitted, the adopt script attempts Redfish discovery. +# data_mac - (optional) MAC address of the data NIC (for agent-config hostname mapping) +# Use when the data NIC differs from the PXE boot NIC (multi-NIC servers). +# Emitted as BAREMETAL_MACS in config. If omitted, boot MAC is used. # node_ip - (optional) Static IP address for this node on the machine network # Required for baremetal ABI deployments with static IPs. # @@ -22,8 +25,8 @@ # For TNF, you need exactly 2 nodes (master-0 and master-1). [baremetal_nodes] -master-0 bmc_address=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 node_ip=192.168.1.10 -master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 node_ip=192.168.1.11 +master-0 bmc_address=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 node_ip=192.168.1.10 #data_mac= +master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 node_ip=192.168.1.11 #data_mac= [baremetal_nodes:vars] # BMC driver — only redfish is supported for TNF fencing @@ -48,3 +51,14 @@ cpu_arch=x86_64 #gateway=192.168.1.1 #api_vip=192.168.1.100 #ingress_vip=192.168.1.101 + +[provisioning_host] +# Remote host where dev-scripts runs (all optional). +# ssh_target - user@host for SSH access to the provisioning machine +# ssh_key - Path to SSH private key (omit to use ssh-agent/default) +# dev_scripts_path - dev-scripts checkout on the remote host +# working_dir - Working directory on the remote host for adoption artifacts +#ssh_target=root@hypervisor.example.com +#ssh_key=~/.ssh/id_ed25519 +#dev_scripts_path=~/openshift-metal3/dev-scripts +#working_dir=~/tnt-baremetal diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 0f4bb301..ae395952 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -34,6 +34,7 @@ declare -a NODE_BMC_USERS=() declare -a NODE_BMC_PASSES=() declare -a NODE_BMC_PORTS=() declare -a NODE_BOOT_MACS=() +declare -a NODE_DATA_MACS=() declare -a NODE_IPS=() # Cluster-wide network config (optional, from [baremetal_network]) @@ -157,7 +158,7 @@ parse_inventory() { name="${line%% *}" rest="${line#* }" - local bmc_address="" bmc_user="" bmc_pass="" bmc_port="" boot_mac="" node_ip="" + local bmc_address="" bmc_user="" bmc_pass="" bmc_port="" boot_mac="" node_ip="" data_mac="" for pair in ${rest}; do local key val key="${pair%%=*}" @@ -169,6 +170,7 @@ parse_inventory() { bmc_port) bmc_port="${val}" ;; boot_mac) boot_mac="${val}" ;; node_ip) node_ip="${val}" ;; + data_mac) data_mac="${val}" ;; esac done @@ -182,6 +184,7 @@ parse_inventory() { NODE_BMC_PASSES+=("${bmc_pass}") NODE_BMC_PORTS+=("${bmc_port:-${BMC_PORT}}") NODE_BOOT_MACS+=("${boot_mac}") + NODE_DATA_MACS+=("${data_mac}") NODE_IPS+=("${node_ip}") fi done < "${INVENTORY}" @@ -445,6 +448,20 @@ generate_baremetal_config() { if ${all_have_ips} && [[ -n "${ip_list}" ]]; then echo "export BAREMETAL_IPS=\"${ip_list}\"" fi + + # Emit BAREMETAL_MACS only if ANY node has data_mac set + local has_data_macs=false + local mac_list="" + for ((i = 0; i < ${#NODE_DATA_MACS[@]}; i++)); do + if [[ -n "${NODE_DATA_MACS[$i]}" ]]; then + has_data_macs=true + fi + [[ -n "${mac_list}" ]] && mac_list+="," + mac_list+="${NODE_DATA_MACS[$i]:-${NODE_BOOT_MACS[$i]}}" + done + if ${has_data_macs} && [[ -n "${mac_list}" ]]; then + echo "export BAREMETAL_MACS=\"${mac_list}\"" + fi } > "${output_file}" info " → ${output_file}" diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index fe13efa8..45426fd8 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -204,6 +204,28 @@ prompt_boot_mac() { done } +prompt_data_mac() { + local boot_mac="$1" + local mac + while true; do + if [[ -n "${boot_mac}" ]]; then + read -rp " Data NIC MAC (Enter to use boot MAC ${boot_mac}): " mac + else + read -rp " Data NIC MAC (Enter to skip): " mac + fi + if [[ -z "${mac}" ]]; then + echo "${mac}" + return + fi + if ! valid_mac "${mac}"; then + echo " Error: invalid MAC (expected XX:XX:XX:XX:XX:XX)" >&2 + continue + fi + echo "${mac}" + return + done +} + prompt_node_ip() { local ip while true; do @@ -333,23 +355,25 @@ show_summary() { echo "==================================" echo " BAREMETAL NODE SUMMARY" echo "==================================" - printf " %-4s %-14s %-46s %-10s %-10s %-19s %-17s\n" \ - "#" "HOSTNAME" "BMC ADDRESS" "BMC USER" "PASSWORD" "BOOT MAC" "NODE IP" - printf " %-4s %-14s %-46s %-10s %-10s %-19s %-17s\n" \ - "---" "------------" "--------------------------------------------" "--------" "--------" "-----------------" "---------------" + printf " %-4s %-14s %-46s %-10s %-10s %-19s %-19s %-17s\n" \ + "#" "HOSTNAME" "BMC ADDRESS" "BMC USER" "PASSWORD" "BOOT MAC" "DATA MAC" "NODE IP" + printf " %-4s %-14s %-46s %-10s %-10s %-19s %-19s %-17s\n" \ + "---" "------------" "--------------------------------------------" "--------" "--------" "-----------------" "-----------------" "---------------" local i for ((i = 0; i < ${#WIZ_NAMES[@]}; i++)); do local display_mac="${WIZ_MACS[$i]:-auto-discover}" + local display_data_mac="${WIZ_DATA_MACS[$i]:---}" local display_ip="${WIZ_NODE_IPS[$i]:---}" local display_addr="${WIZ_IPS[$i]}:${WIZ_PORTS[$i]}" - printf " %-4s %-14s %-46s %-10s %-10s %-19s %-17s\n" \ + printf " %-4s %-14s %-46s %-10s %-10s %-19s %-19s %-17s\n" \ "$((i + 1))" \ "${WIZ_NAMES[$i]}" \ "${display_addr}" \ "${WIZ_USERS[$i]}" \ "********" \ "${display_mac}" \ + "${display_data_mac}" \ "${display_ip}" done @@ -405,6 +429,7 @@ run_wizard() { WIZ_PASSES+=("$(prompt_bmc_pass)") WIZ_PORTS+=("$(prompt_bmc_port)") WIZ_MACS+=("$(prompt_boot_mac)") + WIZ_DATA_MACS+=("$(prompt_data_mac "${WIZ_MACS[$i]}")") WIZ_NODE_IPS+=("$(prompt_node_ip)") done @@ -473,6 +498,9 @@ write_inventory() { if [[ -n "${WIZ_MACS[$i]}" ]]; then line+=" boot_mac=${WIZ_MACS[$i]}" fi + if [[ -n "${WIZ_DATA_MACS[$i]}" ]]; then + line+=" data_mac=${WIZ_DATA_MACS[$i]}" + fi if [[ -n "${WIZ_NODE_IPS[$i]}" ]]; then line+=" node_ip=${WIZ_NODE_IPS[$i]}" else @@ -555,6 +583,7 @@ declare -a WIZ_USERS=() declare -a WIZ_PASSES=() declare -a WIZ_PORTS=() declare -a WIZ_MACS=() +declare -a WIZ_DATA_MACS=() declare -a WIZ_NODE_IPS=() WIZ_MACHINE_NETWORK="" WIZ_GATEWAY="" From 935502980456c33050fdd5ffe3c146dbe5994b83 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Thu, 2 Jul 2026 13:40:48 +0200 Subject: [PATCH 14/17] Make node_ip and api_vip mandatory, add AGENT_E2E_TEST_SCENARIO, remove GATEWAY Three vars are required for the baremetal DHCP deploy path but were silently optional: BAREMETAL_IPS, BAREMETAL_API_VIP, and AGENT_E2E_TEST_SCENARIO. Fail early at adoption time instead of crashing deep in dev-scripts. Remove GATEWAY (nothing reads it) and add a post-generation reminder to verify CI_TOKEN and release image. Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 17 ++-- .../scripts/baremetal-adopt.sh | 36 ++++----- .../scripts/baremetal-wizard.sh | 79 +++++-------------- 3 files changed, 43 insertions(+), 89 deletions(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index f9f2f05c..249efe3b 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -18,8 +18,7 @@ # data_mac - (optional) MAC address of the data NIC (for agent-config hostname mapping) # Use when the data NIC differs from the PXE boot NIC (multi-NIC servers). # Emitted as BAREMETAL_MACS in config. If omitted, boot MAC is used. -# node_ip - (optional) Static IP address for this node on the machine network -# Required for baremetal ABI deployments with static IPs. +# node_ip - Static IP address for this node on the machine network # # The hostname (first field) becomes the node name in ironic_nodes.json. # For TNF, you need exactly 2 nodes (master-0 and master-1). @@ -42,15 +41,13 @@ bmc_verify_ca=False cpu_arch=x86_64 [baremetal_network] -# Cluster-wide network config for baremetal ABI deployments (all optional). -# machine_network - Machine network CIDR (e.g. 192.168.1.0/24) -# gateway - Default gateway IP -# api_vip - API virtual IP -# ingress_vip - Ingress virtual IP -#machine_network=192.168.1.0/24 -#gateway=192.168.1.1 -#api_vip=192.168.1.100 +# Cluster-wide network config for baremetal deploy. +# api_vip - API virtual IP (required) +# ingress_vip - (optional) Ingress virtual IP — defaults to api_vip if omitted +# machine_network - (optional) Machine network CIDR (e.g. 192.168.1.0/24) +api_vip=192.168.1.100 #ingress_vip=192.168.1.101 +#machine_network=192.168.1.0/24 [provisioning_host] # Remote host where dev-scripts runs (all optional). diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index ae395952..fcfa140f 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -39,7 +39,6 @@ declare -a NODE_IPS=() # Cluster-wide network config (optional, from [baremetal_network]) MACHINE_NETWORK="" -GATEWAY="" API_VIP="" INGRESS_VIP="" @@ -146,7 +145,6 @@ parse_inventory() { val="${line#*=}" case "${key}" in machine_network) MACHINE_NETWORK="${val}" ;; - gateway) GATEWAY="${val}" ;; api_vip) API_VIP="${val}" ;; ingress_vip) INGRESS_VIP="${val}" ;; esac @@ -424,30 +422,24 @@ generate_baremetal_config() { echo "export MANAGE_BR_BRIDGE=n" echo "export MANAGE_PRO_BRIDGE=n" echo "export MANAGE_INT_BRIDGE=n" + echo "export AGENT_E2E_TEST_SCENARIO=\"TNF_IPV4_DHCP\"" - if [[ -n "${MACHINE_NETWORK}" || -n "${GATEWAY}" || -n "${API_VIP}" || -n "${INGRESS_VIP}" ]]; then - echo "" - echo "# Baremetal network config" - [[ -n "${MACHINE_NETWORK}" ]] && echo "export EXTERNAL_SUBNET_V4=\"${MACHINE_NETWORK}\"" - [[ -n "${GATEWAY}" ]] && echo "export BAREMETAL_GATEWAY=\"${GATEWAY}\"" - [[ -n "${API_VIP}" ]] && echo "export BAREMETAL_API_VIP=\"${API_VIP}\"" - [[ -n "${INGRESS_VIP}" ]] && echo "export BAREMETAL_INGRESS_VIP=\"${INGRESS_VIP}\"" - fi - - # Emit BAREMETAL_IPS only if ALL nodes have node_ip set - local all_have_ips=true + # BAREMETAL_IPS is required — dev-scripts crashes under set -u without it local ip_list="" for ((i = 0; i < ${#NODE_IPS[@]}; i++)); do - if [[ -z "${NODE_IPS[$i]}" ]]; then - all_have_ips=false - break - fi + [[ -z "${NODE_IPS[$i]}" ]] && die "Node '${NODE_NAMES[$i]}': node_ip is required for baremetal deploy" [[ -n "${ip_list}" ]] && ip_list+="," ip_list+="${NODE_IPS[$i]}" done - if ${all_have_ips} && [[ -n "${ip_list}" ]]; then - echo "export BAREMETAL_IPS=\"${ip_list}\"" - fi + echo "export BAREMETAL_IPS=\"${ip_list}\"" + + # BAREMETAL_API_VIP is required — set_api_and_ingress_vip() needs it + [[ -z "${API_VIP}" ]] && die "api_vip is required in [baremetal_network] for baremetal deploy" + echo "" + echo "# Baremetal network config" + echo "export BAREMETAL_API_VIP=\"${API_VIP}\"" + [[ -n "${MACHINE_NETWORK}" ]] && echo "export EXTERNAL_SUBNET_V4=\"${MACHINE_NETWORK}\"" + [[ -n "${INGRESS_VIP}" ]] && echo "export BAREMETAL_INGRESS_VIP=\"${INGRESS_VIP}\"" # Emit BAREMETAL_MACS only if ANY node has data_mac set local has_data_macs=false @@ -509,6 +501,10 @@ main() { echo " ${nodes_file}" echo " ${output_dir}/config_baremetal_fencing.sh" echo "" + echo " Before deploying, verify these values in ${output_dir}/config_baremetal_fencing.sh:" + echo " - CI_TOKEN (get from console-openshift-console.apps.ci.l2s4.p1.openshiftapps.com)" + echo " - OPENSHIFT_RELEASE_IMAGE (find tags at quay.io/openshift-release-dev/ocp-release)" + echo "" echo " Next: deploy to the nodes using one of the baremetal-deploy* options" } diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index 45426fd8..c3f1ec7b 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -229,10 +229,10 @@ prompt_data_mac() { prompt_node_ip() { local ip while true; do - read -rp " Node IP address (Enter to skip): " ip + read -rp " Node IP address: " ip if [[ -z "${ip}" ]]; then - echo "${ip}" - return + echo " Error: node IP is required for baremetal deploy" >&2 + continue fi if ! valid_ipv4 "${ip}"; then echo " Error: invalid IPv4 address" >&2 @@ -260,30 +260,13 @@ prompt_machine_network() { done } -prompt_gateway() { - local gw - while true; do - read -rp " Gateway IP (Enter to skip): " gw - if [[ -z "${gw}" ]]; then - echo "${gw}" - return - fi - if ! valid_ipv4 "${gw}"; then - echo " Error: invalid IPv4 address" >&2 - continue - fi - echo "${gw}" - return - done -} - prompt_api_vip() { local vip while true; do - read -rp " API VIP (Enter to skip): " vip + read -rp " API VIP: " vip if [[ -z "${vip}" ]]; then - echo "${vip}" - return + echo " Error: API VIP is required for baremetal deploy" >&2 + continue fi if ! valid_ipv4 "${vip}"; then echo " Error: invalid IPv4 address" >&2 @@ -377,14 +360,11 @@ show_summary() { "${display_ip}" done - if [[ -n "${WIZ_MACHINE_NETWORK}" || -n "${WIZ_GATEWAY}" || -n "${WIZ_API_VIP}" || -n "${WIZ_INGRESS_VIP}" ]]; then - echo "" - echo " Cluster Network:" - [[ -n "${WIZ_MACHINE_NETWORK}" ]] && echo " Machine network: ${WIZ_MACHINE_NETWORK}" - [[ -n "${WIZ_GATEWAY}" ]] && echo " Gateway: ${WIZ_GATEWAY}" - [[ -n "${WIZ_API_VIP}" ]] && echo " API VIP: ${WIZ_API_VIP}" - [[ -n "${WIZ_INGRESS_VIP}" ]] && echo " Ingress VIP: ${WIZ_INGRESS_VIP}" - fi + echo "" + echo " Cluster Network:" + echo " API VIP: ${WIZ_API_VIP}" + [[ -n "${WIZ_INGRESS_VIP}" ]] && echo " Ingress VIP: ${WIZ_INGRESS_VIP}" + [[ -n "${WIZ_MACHINE_NETWORK}" ]] && echo " Machine network: ${WIZ_MACHINE_NETWORK}" if [[ -n "${WIZ_SSH_TARGET}" ]]; then echo "" @@ -434,11 +414,10 @@ run_wizard() { done echo "" - echo "--- Cluster Network (all optional) ---" - WIZ_MACHINE_NETWORK="$(prompt_machine_network)" - WIZ_GATEWAY="$(prompt_gateway)" + echo "--- Cluster Network ---" WIZ_API_VIP="$(prompt_api_vip)" WIZ_INGRESS_VIP="$(prompt_ingress_vip)" + WIZ_MACHINE_NETWORK="$(prompt_machine_network)" echo "" echo "--- Provisioning Host (optional) ---" @@ -501,11 +480,7 @@ write_inventory() { if [[ -n "${WIZ_DATA_MACS[$i]}" ]]; then line+=" data_mac=${WIZ_DATA_MACS[$i]}" fi - if [[ -n "${WIZ_NODE_IPS[$i]}" ]]; then - line+=" node_ip=${WIZ_NODE_IPS[$i]}" - else - line+=" #node_ip=" - fi + line+=" node_ip=${WIZ_NODE_IPS[$i]}" echo "${line}" >> "${tmp_inventory}" done @@ -516,34 +491,21 @@ write_inventory() { echo "bmc_port=443" echo "bmc_verify_ca=False" echo "cpu_arch=x86_64" - } >> "${tmp_inventory}" - { echo "" echo "[baremetal_network]" - if [[ -n "${WIZ_MACHINE_NETWORK}" ]]; then - echo "machine_network=${WIZ_MACHINE_NETWORK}" - else - echo "#machine_network=" - fi - if [[ -n "${WIZ_GATEWAY}" ]]; then - echo "gateway=${WIZ_GATEWAY}" - else - echo "#gateway=" - fi - if [[ -n "${WIZ_API_VIP}" ]]; then - echo "api_vip=${WIZ_API_VIP}" - else - echo "#api_vip=" - fi + echo "api_vip=${WIZ_API_VIP}" if [[ -n "${WIZ_INGRESS_VIP}" ]]; then echo "ingress_vip=${WIZ_INGRESS_VIP}" else echo "#ingress_vip=" fi - } >> "${tmp_inventory}" + if [[ -n "${WIZ_MACHINE_NETWORK}" ]]; then + echo "machine_network=${WIZ_MACHINE_NETWORK}" + else + echo "#machine_network=" + fi - { echo "" echo "[provisioning_host]" if [[ -n "${WIZ_SSH_TARGET}" ]]; then @@ -586,7 +548,6 @@ declare -a WIZ_MACS=() declare -a WIZ_DATA_MACS=() declare -a WIZ_NODE_IPS=() WIZ_MACHINE_NETWORK="" -WIZ_GATEWAY="" WIZ_API_VIP="" WIZ_INGRESS_VIP="" WIZ_SSH_TARGET="" From d08ac3122b8cb60f80967cb04a4aa8cb3d20576b Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Thu, 2 Jul 2026 13:47:29 +0200 Subject: [PATCH 15/17] Make data_mac mandatory for baremetal deploy DHCP is the only supported network mode and agent-config always needs the data NIC MAC for hostname mapping. Error early if data_mac is missing rather than silently falling back to the boot MAC. Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 9 ++++----- .../scripts/baremetal-adopt.sh | 13 ++++--------- .../scripts/baremetal-wizard.sh | 17 +++++------------ 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index 249efe3b..fc3058d1 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -15,17 +15,16 @@ # bmc_port - (optional) BMC Redfish port (default: 443) # boot_mac - (optional) MAC address of the NIC used for PXE boot # If omitted, the adopt script attempts Redfish discovery. -# data_mac - (optional) MAC address of the data NIC (for agent-config hostname mapping) -# Use when the data NIC differs from the PXE boot NIC (multi-NIC servers). -# Emitted as BAREMETAL_MACS in config. If omitted, boot MAC is used. +# data_mac - MAC address of the data NIC (for agent-config hostname mapping) +# May differ from boot_mac on multi-NIC servers. Emitted as BAREMETAL_MACS. # node_ip - Static IP address for this node on the machine network # # The hostname (first field) becomes the node name in ironic_nodes.json. # For TNF, you need exactly 2 nodes (master-0 and master-1). [baremetal_nodes] -master-0 bmc_address=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 node_ip=192.168.1.10 #data_mac= -master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 node_ip=192.168.1.11 #data_mac= +master-0 bmc_address=192.168.1.100 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:01 data_mac=52:54:00:00:00:01 node_ip=192.168.1.10 +master-1 bmc_address=192.168.1.101 bmc_user=admin bmc_pass=changeme boot_mac=52:54:00:00:00:02 data_mac=52:54:00:00:00:02 node_ip=192.168.1.11 [baremetal_nodes:vars] # BMC driver — only redfish is supported for TNF fencing diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index fcfa140f..5e56392e 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -441,19 +441,14 @@ generate_baremetal_config() { [[ -n "${MACHINE_NETWORK}" ]] && echo "export EXTERNAL_SUBNET_V4=\"${MACHINE_NETWORK}\"" [[ -n "${INGRESS_VIP}" ]] && echo "export BAREMETAL_INGRESS_VIP=\"${INGRESS_VIP}\"" - # Emit BAREMETAL_MACS only if ANY node has data_mac set - local has_data_macs=false + # BAREMETAL_MACS is required — agent-config needs data NIC MACs for hostname mapping local mac_list="" for ((i = 0; i < ${#NODE_DATA_MACS[@]}; i++)); do - if [[ -n "${NODE_DATA_MACS[$i]}" ]]; then - has_data_macs=true - fi + [[ -z "${NODE_DATA_MACS[$i]}" ]] && die "Node '${NODE_NAMES[$i]}': data_mac is required for baremetal deploy" [[ -n "${mac_list}" ]] && mac_list+="," - mac_list+="${NODE_DATA_MACS[$i]:-${NODE_BOOT_MACS[$i]}}" + mac_list+="${NODE_DATA_MACS[$i]}" done - if ${has_data_macs} && [[ -n "${mac_list}" ]]; then - echo "export BAREMETAL_MACS=\"${mac_list}\"" - fi + echo "export BAREMETAL_MACS=\"${mac_list}\"" } > "${output_file}" info " → ${output_file}" diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index c3f1ec7b..b022974f 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -205,17 +205,12 @@ prompt_boot_mac() { } prompt_data_mac() { - local boot_mac="$1" local mac while true; do - if [[ -n "${boot_mac}" ]]; then - read -rp " Data NIC MAC (Enter to use boot MAC ${boot_mac}): " mac - else - read -rp " Data NIC MAC (Enter to skip): " mac - fi + read -rp " Data NIC MAC (data network interface): " mac if [[ -z "${mac}" ]]; then - echo "${mac}" - return + echo " Error: data NIC MAC is required for baremetal deploy" >&2 + continue fi if ! valid_mac "${mac}"; then echo " Error: invalid MAC (expected XX:XX:XX:XX:XX:XX)" >&2 @@ -409,7 +404,7 @@ run_wizard() { WIZ_PASSES+=("$(prompt_bmc_pass)") WIZ_PORTS+=("$(prompt_bmc_port)") WIZ_MACS+=("$(prompt_boot_mac)") - WIZ_DATA_MACS+=("$(prompt_data_mac "${WIZ_MACS[$i]}")") + WIZ_DATA_MACS+=("$(prompt_data_mac)") WIZ_NODE_IPS+=("$(prompt_node_ip)") done @@ -477,9 +472,7 @@ write_inventory() { if [[ -n "${WIZ_MACS[$i]}" ]]; then line+=" boot_mac=${WIZ_MACS[$i]}" fi - if [[ -n "${WIZ_DATA_MACS[$i]}" ]]; then - line+=" data_mac=${WIZ_DATA_MACS[$i]}" - fi + line+=" data_mac=${WIZ_DATA_MACS[$i]}" line+=" node_ip=${WIZ_NODE_IPS[$i]}" echo "${line}" >> "${tmp_inventory}" done From 34ec8fe1ecb3726921411d73542ee9001ad9c52a Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Thu, 2 Jul 2026 15:03:44 +0200 Subject: [PATCH 16/17] Add iso_url for Redfish VirtualMedia boot and make data_mac mandatory BMCs need the full agent ISO URL to mount via Redfish VirtualMedia. Add iso_url as a required field in [baremetal_network], parsed and emitted as BAREMETAL_ISO_SERVER in config_baremetal_fencing.sh. Also make data_mac mandatory since DHCP is the only supported mode. Co-Authored-By: Claude Opus 4.6 --- .../inventory_baremetal.ini.sample | 4 ++++ .../scripts/baremetal-adopt.sh | 6 +++++- .../scripts/baremetal-wizard.sh | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index fc3058d1..7ddc3983 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -44,9 +44,13 @@ cpu_arch=x86_64 # api_vip - API virtual IP (required) # ingress_vip - (optional) Ingress virtual IP — defaults to api_vip if omitted # machine_network - (optional) Machine network CIDR (e.g. 192.168.1.0/24) +# iso_url - Full HTTP URL for agent ISO VirtualMedia boot (required) +# Must be reachable from BMCs. dev-scripts stages the ISO at +# ${WORKING_DIR}/${CLUSTER_NAME}/agent.x86_64.iso on the provisioning host. api_vip=192.168.1.100 #ingress_vip=192.168.1.101 #machine_network=192.168.1.0/24 +iso_url=http://provisioner:8080/agent.x86_64.iso [provisioning_host] # Remote host where dev-scripts runs (all optional). diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh index 5e56392e..5180a0f0 100755 --- a/deploy/openshift-clusters/scripts/baremetal-adopt.sh +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -37,10 +37,11 @@ declare -a NODE_BOOT_MACS=() declare -a NODE_DATA_MACS=() declare -a NODE_IPS=() -# Cluster-wide network config (optional, from [baremetal_network]) +# Cluster-wide network config (from [baremetal_network]) MACHINE_NETWORK="" API_VIP="" INGRESS_VIP="" +ISO_URL="" # Group defaults BMC_PORT="443" @@ -147,6 +148,7 @@ parse_inventory() { machine_network) MACHINE_NETWORK="${val}" ;; api_vip) API_VIP="${val}" ;; ingress_vip) INGRESS_VIP="${val}" ;; + iso_url) ISO_URL="${val}" ;; esac continue fi @@ -435,9 +437,11 @@ generate_baremetal_config() { # BAREMETAL_API_VIP is required — set_api_and_ingress_vip() needs it [[ -z "${API_VIP}" ]] && die "api_vip is required in [baremetal_network] for baremetal deploy" + [[ -z "${ISO_URL}" ]] && die "iso_url is required in [baremetal_network] for baremetal deploy" echo "" echo "# Baremetal network config" echo "export BAREMETAL_API_VIP=\"${API_VIP}\"" + echo "export BAREMETAL_ISO_SERVER=\"${ISO_URL}\"" [[ -n "${MACHINE_NETWORK}" ]] && echo "export EXTERNAL_SUBNET_V4=\"${MACHINE_NETWORK}\"" [[ -n "${INGRESS_VIP}" ]] && echo "export BAREMETAL_INGRESS_VIP=\"${INGRESS_VIP}\"" diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh index b022974f..371b328f 100755 --- a/deploy/openshift-clusters/scripts/baremetal-wizard.sh +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -238,6 +238,23 @@ prompt_node_ip() { done } +prompt_iso_url() { + local url + while true; do + read -rp " ISO URL for VirtualMedia boot (e.g. http://host:8080/path/agent.x86_64.iso): " url + if [[ -z "${url}" ]]; then + echo " Error: ISO URL is required — BMCs mount the agent ISO via Redfish VirtualMedia" >&2 + continue + fi + if ! [[ "${url}" =~ ^https?:// ]]; then + echo " Error: expected http:// or https:// URL" >&2 + continue + fi + echo "${url}" + return + done +} + prompt_machine_network() { local cidr while true; do @@ -360,6 +377,7 @@ show_summary() { echo " API VIP: ${WIZ_API_VIP}" [[ -n "${WIZ_INGRESS_VIP}" ]] && echo " Ingress VIP: ${WIZ_INGRESS_VIP}" [[ -n "${WIZ_MACHINE_NETWORK}" ]] && echo " Machine network: ${WIZ_MACHINE_NETWORK}" + echo " ISO URL: ${WIZ_ISO_URL}" if [[ -n "${WIZ_SSH_TARGET}" ]]; then echo "" @@ -413,6 +431,7 @@ run_wizard() { WIZ_API_VIP="$(prompt_api_vip)" WIZ_INGRESS_VIP="$(prompt_ingress_vip)" WIZ_MACHINE_NETWORK="$(prompt_machine_network)" + WIZ_ISO_URL="$(prompt_iso_url)" echo "" echo "--- Provisioning Host (optional) ---" @@ -498,6 +517,7 @@ write_inventory() { else echo "#machine_network=" fi + echo "iso_url=${WIZ_ISO_URL}" echo "" echo "[provisioning_host]" @@ -543,6 +563,7 @@ declare -a WIZ_NODE_IPS=() WIZ_MACHINE_NETWORK="" WIZ_API_VIP="" WIZ_INGRESS_VIP="" +WIZ_ISO_URL="" WIZ_SSH_TARGET="" WIZ_SSH_KEY="" WIZ_DEV_SCRIPTS_PATH="" From 13c5ee068826d53452cd5f440c1bc43953c4dc90 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Thu, 2 Jul 2026 16:27:49 +0200 Subject: [PATCH 17/17] Use RDU2 provisioner ISO URL in inventory sample Co-Authored-By: Claude Opus 4.6 --- deploy/openshift-clusters/inventory_baremetal.ini.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample index 7ddc3983..61b2be9b 100644 --- a/deploy/openshift-clusters/inventory_baremetal.ini.sample +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -50,7 +50,7 @@ cpu_arch=x86_64 api_vip=192.168.1.100 #ingress_vip=192.168.1.101 #machine_network=192.168.1.0/24 -iso_url=http://provisioner:8080/agent.x86_64.iso +iso_url=http://10.1.235.49:8080/dev-scripts/ostest/agent.x86_64.iso [provisioning_host] # Remote host where dev-scripts runs (all optional).