diff --git a/deploy/Makefile b/deploy/Makefile index e825e0c..cd70934 100644 --- a/deploy/Makefile +++ b/deploy/Makefile @@ -92,6 +92,15 @@ fencing-assisted: keep-instance: @../helpers/keep-instance.sh '$(DAYS)' +baremetal-adopt: + @./openshift-clusters/scripts/baremetal-adopt.sh + +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 get-tnf-logs: @@ -138,6 +147,11 @@ 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 " 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/.gitignore b/deploy/openshift-clusters/.gitignore index 7c14539..96b8f64 100644 --- a/deploy/openshift-clusters/.gitignore +++ b/deploy/openshift-clusters/.gitignore @@ -1,4 +1,6 @@ inventory.ini +inventory_baremetal.ini + 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 0000000..61b2be9 --- /dev/null +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -0,0 +1,64 @@ +# 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 baremetal-adopt +# +# Each node requires: +# 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. +# 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 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 +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 + +# Node CPU architecture +cpu_arch=x86_64 + +[baremetal_network] +# 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) +# 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://10.1.235.49:8080/dev-scripts/ostest/agent.x86_64.iso + +[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/roles/dev-scripts/install-dev/files/.gitignore b/deploy/openshift-clusters/roles/dev-scripts/install-dev/files/.gitignore index 0818832..e6a1d72 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/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 2291e37..2eacf2e 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 new file mode 100755 index 0000000..5180a0f --- /dev/null +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -0,0 +1,510 @@ +#!/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: +# baremetal-adopt.sh [options] +# +# Options: +# --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 +# -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)" + +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_ADDRS=() +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 (from [baremetal_network]) +MACHINE_NETWORK="" +API_VIP="" +INGRESS_VIP="" +ISO_URL="" + +# Group defaults +BMC_PORT="443" +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 + --skip-verify) + SKIP_VERIFY=true + shift + ;; + --verify-only) + VERIFY_ONLY=true + shift + ;; + --inventory) + INVENTORY="$2" + shift 2 + ;; + --config-base) + CONFIG_BASE="$2" + shift 2 + ;; + -h|--help) + head -16 "$0" | tail -11 + exit 0 + ;; + *) + die "Unknown option: $1. Run '$0 --help' for usage." + ;; + esac + done +} + +############################################################################## +# INI parser +############################################################################## + +parse_inventory() { + [[ -f "${INVENTORY}" ]] || die "Inventory file not found: ${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 + 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}" == "[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 + + if ${in_vars}; then + local key val + 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 + continue + fi + + if ${in_network}; then + local key val + key="${line%%=*}" + val="${line#*=}" + case "${key}" in + machine_network) MACHINE_NETWORK="${val}" ;; + api_vip) API_VIP="${val}" ;; + ingress_vip) INGRESS_VIP="${val}" ;; + iso_url) ISO_URL="${val}" ;; + esac + continue + fi + + if ${in_nodes}; then + local name rest + name="${line%% *}" + rest="${line#* }" + + 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%%=*}" + val="${pair#*=}" + case "${key}" in + 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}" ;; + data_mac) data_mac="${val}" ;; + esac + done + + [[ -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" + + NODE_NAMES+=("${name}") + 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_DATA_MACS+=("${data_mac}") + NODE_IPS+=("${node_ip}") + fi + 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" +} + +############################################################################## +# 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" bmc_port="$4" + + local systems_json + systems_json=$(bmc_curl \ + -u "${bmc_user}:${bmc_pass}" \ + "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" 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}:${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}:${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 + + # 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=$(bmc_curl \ + -u "${bmc_user}:${bmc_pass}" \ + "https://${bmc_address}:${bmc_port}${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 [[ "${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 + return 1 +} + +verify_bmc() { + local name="$1" bmc_address="$2" bmc_user="$3" bmc_pass="$4" bmc_port="$5" + local rc=0 + + 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}:${bmc_port}/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_ADDRS[$i]}" \ + "${NODE_BMC_USERS[$i]}" "${NODE_BMC_PASSES[$i]}" "${NODE_BMC_PORTS[$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 incomplete=false + local nodes=() + + for ((i = 0; i < ${#NODE_NAMES[@]}; i++)); do + local name="${NODE_NAMES[$i]}" + 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) + local 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}" "${bmc_port}" 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 + 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}" "${bmc_port}" "${system_id}" 2>/dev/null) || true + if [[ -n "${boot_mac}" ]]; then + info " ${name}: discovered boot MAC ${boot_mac}" + else + echo " ERROR: ${name}: could not discover boot MAC — set boot_mac in inventory" >&2 + incomplete=true + continue + fi + fi + + nodes+=("$(jq -n \ + --arg name "${name}" \ + --arg addr "redfish://${bmc_address}:${bmc_port}/${system_id}" \ + --arg user "${bmc_user}" \ + --arg pass "${bmc_pass}" \ + --arg verify_ca "${BMC_VERIFY_CA}" \ + --arg mac "${boot_mac}" \ + --arg arch "${CPU_ARCH}" \ + '{ + name: $name, + driver: "redfish", + driver_info: { + address: $addr, + username: $user, + password: $pass, + redfish_verify_ca: $verify_ca + }, + ports: [{address: $mac}], + properties: {cpu_arch: $arch} + }')") + done + + if ${incomplete}; then + die "Incomplete artifacts — set missing boot_mac values in inventory" + fi + + printf '%s\n' "${nodes[@]}" | jq -s '{nodes: .}' > "${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 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" + echo "export AGENT_E2E_TEST_SCENARIO=\"TNF_IPV4_DHCP\"" + + # BAREMETAL_IPS is required — dev-scripts crashes under set -u without it + local ip_list="" + for ((i = 0; i < ${#NODE_IPS[@]}; i++)); do + [[ -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 + 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" + [[ -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}\"" + + # 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 + [[ -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]}" + done + echo "export BAREMETAL_MACS=\"${mac_list}\"" + } > "${output_file}" + + info " → ${output_file}" +} + +############################################################################## +# Main +############################################################################## + +main() { + parse_args "$@" + + # 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 + + 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 + + # 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" + 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 " 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" +} + +main "$@" diff --git a/deploy/openshift-clusters/scripts/baremetal-wizard.sh b/deploy/openshift-clusters/scripts/baremetal-wizard.sh new file mode 100755 index 0000000..371b328 --- /dev/null +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -0,0 +1,573 @@ +#!/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: +# baremetal-wizard.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])?$ ]] +} + +valid_bmc_address() { + local addr="$1" + 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 +# +# 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_address() { + local addr + while true; do + read -rp " BMC address (IP or hostname): " addr + if [[ -z "${addr}" ]]; then + echo " Error: BMC address is required" >&2 + continue + fi + if ! valid_bmc_address "${addr}"; then + echo " Error: invalid address (expected IPv4 or FQDN)" >&2 + continue + fi + echo "${addr}" + 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_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 + read -rp " Boot MAC address (Enter to auto-discover): " mac + 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_data_mac() { + local mac + while true; do + read -rp " Data NIC MAC (data network interface): " mac + if [[ -z "${mac}" ]]; then + 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 + continue + fi + echo "${mac}" + return + done +} + +prompt_node_ip() { + local ip + while true; do + read -rp " Node IP address: " ip + if [[ -z "${ip}" ]]; then + echo " Error: node IP is required for baremetal deploy" >&2 + continue + fi + if ! valid_ipv4 "${ip}"; then + echo " Error: invalid IPv4 address" >&2 + continue + fi + echo "${ip}" + return + 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 + 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_api_vip() { + local vip + while true; do + read -rp " API VIP: " vip + if [[ -z "${vip}" ]]; then + echo " Error: API VIP is required for baremetal deploy" >&2 + continue + 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 +} + +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 +############################################################################## + +show_summary() { + echo "" + echo "==================================" + echo " BAREMETAL NODE SUMMARY" + echo "==================================" + 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 %-19s %-17s\n" \ + "$((i + 1))" \ + "${WIZ_NAMES[$i]}" \ + "${display_addr}" \ + "${WIZ_USERS[$i]}" \ + "********" \ + "${display_mac}" \ + "${display_data_mac}" \ + "${display_ip}" + done + + 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}" + echo " ISO URL: ${WIZ_ISO_URL}" + + 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 "==================================" +} + +############################################################################## +# 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=() + WIZ_NODE_IPS=() + + 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_address)") + WIZ_USERS+=("$(prompt_bmc_user)") + WIZ_PASSES+=("$(prompt_bmc_pass)") + WIZ_PORTS+=("$(prompt_bmc_port)") + WIZ_MACS+=("$(prompt_boot_mac)") + WIZ_DATA_MACS+=("$(prompt_data_mac)") + WIZ_NODE_IPS+=("$(prompt_node_ip)") + done + + echo "" + echo "--- Cluster Network ---" + 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) ---" + 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 + 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 baremetal-wizard.sh" + echo "" + echo "[baremetal_nodes]" + } > "${tmp_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]} bmc_port=${WIZ_PORTS[$i]}" + if [[ -n "${WIZ_MACS[$i]}" ]]; then + line+=" boot_mac=${WIZ_MACS[$i]}" + fi + line+=" data_mac=${WIZ_DATA_MACS[$i]}" + line+=" node_ip=${WIZ_NODE_IPS[$i]}" + echo "${line}" >> "${tmp_inventory}" + done + + { + echo "" + echo "[baremetal_nodes:vars]" + echo "bmc_driver=redfish" + echo "bmc_port=443" + echo "bmc_verify_ca=False" + echo "cpu_arch=x86_64" + + echo "" + echo "[baremetal_network]" + echo "api_vip=${WIZ_API_VIP}" + if [[ -n "${WIZ_INGRESS_VIP}" ]]; then + echo "ingress_vip=${WIZ_INGRESS_VIP}" + else + echo "#ingress_vip=" + fi + if [[ -n "${WIZ_MACHINE_NETWORK}" ]]; then + echo "machine_network=${WIZ_MACHINE_NETWORK}" + else + echo "#machine_network=" + fi + echo "iso_url=${WIZ_ISO_URL}" + + 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}" +} + +############################################################################## +# Main +############################################################################## + +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_DATA_MACS=() +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="" +WIZ_WORKING_DIR="" + +parse_args "$@" +run_wizard