diff --git a/deploy/Makefile b/deploy/Makefile index e825e0ca..98872591 100644 --- a/deploy/Makefile +++ b/deploy/Makefile @@ -92,6 +92,18 @@ 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-fencing-agent: + @./openshift-clusters/scripts/deploy-baremetal.sh + +baremetal-wizard: + @./openshift-clusters/scripts/baremetal-wizard.sh + patch-nodes: @./openshift-clusters/scripts/patch-nodes.sh get-tnf-logs: @@ -138,6 +150,12 @@ 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-fencing-agent - Deploy TNF cluster on adopted baremetal nodes via provisioning host" + @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 7c145393..c5ba337d 100644 --- a/deploy/openshift-clusters/.gitignore +++ b/deploy/openshift-clusters/.gitignore @@ -1,7 +1,10 @@ inventory.ini +inventory_baremetal.ini + proxy.env kubeconfig kubeadmin-password +clusters/ *.pyc *.pyo diff --git a/deploy/openshift-clusters/deploy-baremetal.yml b/deploy/openshift-clusters/deploy-baremetal.yml new file mode 100644 index 00000000..e6dc5a3d --- /dev/null +++ b/deploy/openshift-clusters/deploy-baremetal.yml @@ -0,0 +1,168 @@ +--- +# Deploy a TNF fencing cluster on adopted baremetal nodes via dev-scripts ABI. +# +# Targets the [provisioning_host] group from inventory_baremetal.ini. +# Expects adoption artifacts from 'make baremetal-adopt' in +# roles/dev-scripts/install-dev/files/. +# +# Usage: +# ansible-playbook deploy-baremetal.yml -i inventory_baremetal.ini +# ansible-playbook deploy-baremetal.yml -i inventory_baremetal.ini -e dev_scripts_branch=my-branch +# +- hosts: provisioning_host + gather_facts: no + force_handlers: yes + + vars: + method: agent + topology: fencing + test_cluster_name: ostest + + pre_tasks: + - name: Check adoption artifacts exist on controller + ansible.builtin.stat: + path: "{{ playbook_dir }}/roles/dev-scripts/install-dev/files/{{ item }}" + delegate_to: localhost + become: false + register: artifact_check + loop: + - config_baremetal_fencing.sh + - ironic_nodes.json + + - name: Fail if adoption artifacts are missing + ansible.builtin.fail: + msg: >- + Adoption artifact not found: {{ item.item }}. + Run 'make baremetal-adopt' first. + when: not item.stat.exists + loop: "{{ artifact_check.results }}" + loop_control: + label: "{{ item.item }}" + + tasks: + # --- Validation, config deploy, pull-secret (reused from install-dev role) --- + - name: Validate and deploy config + pull-secret + ansible.builtin.include_role: + name: dev-scripts/install-dev + tasks_from: config + vars: + method: agent + config_file: + agent: config_baremetal_fencing.sh + install_host_deps: false + + # --- Git checkout --- + - name: Checkout dev-scripts + ansible.builtin.git: + dest: "{{ dev_scripts_path }}" + repo: "{{ dev_scripts_src_repo }}" + version: "{{ dev_scripts_branch }}" + + # --- Baremetal-specific setup --- + - name: Copy ironic_nodes.json to dev-scripts + ansible.builtin.copy: + src: "{{ playbook_dir }}/roles/dev-scripts/install-dev/files/ironic_nodes.json" + dest: "{{ dev_scripts_path }}/ironic_nodes.json" + mode: "0600" + + - name: Create working directory + ansible.builtin.shell: mkdir -p "${HOME}/dev-scripts-workdir" + changed_when: false + + - name: Append WORKING_DIR to deployed config + ansible.builtin.lineinfile: + path: "{{ dev_scripts_path }}/config_{{ whoami.stdout }}.sh" + regexp: '^export WORKING_DIR=' + line: 'export WORKING_DIR="${HOME}/dev-scripts-workdir"' + + # --- Ensure SSH key exists for node access --- + - name: Check for existing SSH keypair + ansible.builtin.stat: + path: ~/.ssh/id_ed25519 + register: ssh_key_check + + - name: Generate SSH keypair if missing + ansible.builtin.command: + cmd: ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N '' + when: not ssh_key_check.stat.exists + + - name: Read SSH public key + ansible.builtin.slurp: + src: ~/.ssh/id_ed25519.pub + register: ssh_pub_key + + - name: Append SSH_PUB_KEY to deployed config + ansible.builtin.lineinfile: + path: "{{ dev_scripts_path }}/config_{{ whoami.stdout }}.sh" + regexp: '^export SSH_PUB_KEY=' + line: 'export SSH_PUB_KEY="{{ ssh_pub_key.content | b64decode | trim }}"' + + - name: Create empty mirror registry credentials + ansible.builtin.shell: | + f="${HOME}/private-mirror-{{ test_cluster_name }}.json" + [ -f "$f" ] || echo '{"auths":{}}' > "$f" + changed_when: false + + # --- Clean prior deployment state --- + - name: Remove prior cluster state (required by dev-scripts verifyClean) + ansible.builtin.file: + path: "{{ dev_scripts_path }}/ocp/{{ test_cluster_name }}" + state: absent + + # --- Deploy --- + - name: Run dev-scripts ABI pipeline + block: + - name: Run dev-scripts make target + make: + chdir: "{{ dev_scripts_path }}" + target: "{{ item }}" + loop: + - requirements + - agent_requirements + - agent_build_installer + - agent_prepare_release + - agent_configure + - agent_create_cluster + loop_control: + label: "{{ item }}" + rescue: + - name: Display recovery instructions + ansible.builtin.debug: + msg: | + DEPLOYMENT FAILED. To recover: + 1. Power off baremetal nodes via BMC + 2. Clean dev-scripts state: + make -C {{ dev_scripts_path }} clean + 3. Fix the issue and re-run: + make baremetal-fencing-agent + - name: Fail after displaying recovery steps + ansible.builtin.fail: + msg: "dev-scripts ABI pipeline failed" + + # --- Post-deploy: proxy + credentials --- + - name: Setup proxy and fetch credentials + ansible.builtin.include_role: + name: proxy-setup + vars: + kubeconfig_path: "{{ dev_scripts_path }}/ocp/{{ test_cluster_name }}/auth/kubeconfig" + kubeadmin_password_path: "{{ dev_scripts_path }}/ocp/{{ test_cluster_name }}/auth/kubeadmin-password" + + - name: Fetch SSH private key to controller + ansible.builtin.fetch: + src: ~/.ssh/id_ed25519 + dest: "clusters/{{ test_cluster_name }}/auth/id_ed25519" + flat: true + mode: "0600" + + - name: Display access information + ansible.builtin.debug: + msg: |- + Baremetal TNF cluster deployed successfully! + + Next steps: + 1. Source the proxy environment from anywhere: + source {{ playbook_dir }}/proxy.env + (or from openshift-clusters directory: source proxy.env) + 2. Verify cluster access: oc get nodes + 3. SSH to nodes: + ssh -i clusters/{{ test_cluster_name }}/auth/id_ed25519 core@ diff --git a/deploy/openshift-clusters/inventory_baremetal.ini.sample b/deploy/openshift-clusters/inventory_baremetal.ini.sample new file mode 100644 index 00000000..a145e459 --- /dev/null +++ b/deploy/openshift-clusters/inventory_baremetal.ini.sample @@ -0,0 +1,74 @@ +# 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. +# 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 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 +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 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 +# dns_servers - Comma-separated DNS server IPs reachable from the nodes. +# Used as the node DNS resolver during install (resolves quay.io, etc.). +# If unset, falls back to gateway — which may not run DNS. +#machine_network=192.168.1.0/24 +#gateway=192.168.1.1 +#api_vip=192.168.1.100 +#ingress_vip=192.168.1.101 +#dns_servers=10.11.5.160,10.2.32.85 + +[provisioning_host] +# Provisioning host for baremetal ABI deployment. Must be on the same L2 network +# as the baremetal nodes (serves agent ISO via HTTP, runs dnsmasq, acts as gateway). +# +# Standard Ansible inventory format — one host entry with connection variables. +# For local deployment, use: localhost ansible_connection=local +# +#10.1.155.50 ansible_user=root ansible_ssh_private_key_file=~/.ssh/lab_key + +[provisioning_host:vars] +# Override dev-scripts checkout on the provisioning host (optional). +# Defaults come from roles/dev-scripts/install-dev/defaults/main.yml: +# dev_scripts_path=openshift-metal3/dev-scripts +# dev_scripts_src_repo=https://github.com/openshift-metal3/dev-scripts +# dev_scripts_branch=master +# +#dev_scripts_path=~/openshift-metal3/dev-scripts +#dev_scripts_src_repo=https://github.com/myuser/dev-scripts +#dev_scripts_branch=my-feature-branch 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/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/roles/dev-scripts/install-dev/tasks/config.yml b/deploy/openshift-clusters/roles/dev-scripts/install-dev/tasks/config.yml index c973118b..d320495a 100644 --- a/deploy/openshift-clusters/roles/dev-scripts/install-dev/tasks/config.yml +++ b/deploy/openshift-clusters/roles/dev-scripts/install-dev/tasks/config.yml @@ -125,3 +125,4 @@ - containernetworking-plugins state: present become: true + when: install_host_deps | default(true) diff --git a/deploy/openshift-clusters/roles/proxy-setup/tasks/environment.yml b/deploy/openshift-clusters/roles/proxy-setup/tasks/environment.yml index 693b2032..1f1f9a18 100644 --- a/deploy/openshift-clusters/roles/proxy-setup/tasks/environment.yml +++ b/deploy/openshift-clusters/roles/proxy-setup/tasks/environment.yml @@ -7,7 +7,7 @@ # Determine the directory where this proxy.env file is located PROXY_ENV_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" - export EC2_PUBLIC_IP={{ hostvars[inventory_hostname]['inventory_hostname'].split('@')[1] }} + export EC2_PUBLIC_IP={{ hostvars[inventory_hostname]['ansible_host'] | default(inventory_hostname.split('@')[1] if '@' in inventory_hostname else inventory_hostname) }} export PROXYPORT={{ proxy_port }} export HTTP_PROXY=http://${EC2_PUBLIC_IP}:${PROXYPORT}/ export HTTPS_PROXY=http://${EC2_PUBLIC_IP}:${PROXYPORT}/ diff --git a/deploy/openshift-clusters/scripts/baremetal-adopt.sh b/deploy/openshift-clusters/scripts/baremetal-adopt.sh new file mode 100755 index 00000000..b6a27e30 --- /dev/null +++ b/deploy/openshift-clusters/scripts/baremetal-adopt.sh @@ -0,0 +1,499 @@ +#!/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_IPS=() + +# Cluster-wide network config (optional, from [baremetal_network]) +MACHINE_NETWORK="" +GATEWAY="" +API_VIP="" +INGRESS_VIP="" + +# 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}" ;; + 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="" bmc_port="" 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}" ;; + bmc_port) bmc_port="${val}" ;; + boot_mac) boot_mac="${val}" ;; + node_ip) node_ip="${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_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\"" + + 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}" +} + +############################################################################## +# 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 " 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 00000000..fe13efa8 --- /dev/null +++ b/deploy/openshift-clusters/scripts/baremetal-wizard.sh @@ -0,0 +1,569 @@ +#!/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_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 +} + +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 %-17s\n" \ + "#" "HOSTNAME" "BMC ADDRESS" "BMC USER" "PASSWORD" "BOOT MAC" "NODE IP" + 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]:---}" + local display_addr="${WIZ_IPS[$i]}:${WIZ_PORTS[$i]}" + printf " %-4s %-14s %-46s %-10s %-10s %-19s %-17s\n" \ + "$((i + 1))" \ + "${WIZ_NAMES[$i]}" \ + "${display_addr}" \ + "${WIZ_USERS[$i]}" \ + "********" \ + "${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 + + 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_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)" + + 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 + if [[ -n "${WIZ_NODE_IPS[$i]}" ]]; then + line+=" node_ip=${WIZ_NODE_IPS[$i]}" + else + line+=" #node_ip=" + fi + 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" + } >> "${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}" + + { + 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_NODE_IPS=() +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 diff --git a/deploy/openshift-clusters/scripts/deploy-baremetal.sh b/deploy/openshift-clusters/scripts/deploy-baremetal.sh new file mode 100755 index 00000000..a5e8c161 --- /dev/null +++ b/deploy/openshift-clusters/scripts/deploy-baremetal.sh @@ -0,0 +1,46 @@ +#!/usr/bin/bash +# +# Deploy a TNF fencing cluster on adopted baremetal nodes. +# +# Thin wrapper that calls the deploy-baremetal.yml Ansible playbook +# targeting the [provisioning_host] group from inventory_baremetal.ini. +# +# Expects adoption artifacts from 'make baremetal-adopt'. +# +# Usage: +# deploy-baremetal.sh [-- ] + +set -o nounset +set -o errexit +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OC_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +INVENTORY="${OC_DIR}/inventory_baremetal.ini" +PLAYBOOK="${OC_DIR}/deploy-baremetal.yml" + +if [[ ! -f "${INVENTORY}" ]]; then + echo "Error: inventory_baremetal.ini not found in ${OC_DIR}/" + echo "Copy inventory_baremetal.ini.sample, fill in your node details," + echo "and configure the [provisioning_host] section." + exit 1 +fi + +if ! grep -qE '^\[provisioning_host\]' "${INVENTORY}"; then + echo "Error: [provisioning_host] section not found in ${INVENTORY}" + echo "Add a [provisioning_host] section with the target host." + exit 1 +fi + +if ! grep -qvE '^\s*(#|$|\[)' <(sed -n '/\[provisioning_host\]/,/^\[/p' "${INVENTORY}" | tail -n +2); then + echo "Error: [provisioning_host] section has no hosts configured." + echo "Uncomment and configure a host entry in ${INVENTORY}." + echo "For local deployment, add: localhost ansible_connection=local" + exit 1 +fi + +echo "Deploying baremetal TNF cluster via provisioning host..." + +cd "${OC_DIR}" +ansible-playbook "${PLAYBOOK}" -i "${INVENTORY}" "$@"