d24207427f
CI / Lint + build + test (push) Failing after 5m17s
Two bugs compounded on Proxmox hosts: primary_iface walked `ip link show` and picked the physical NIC (e.g. enp1s0), which has no IPv4 on Proxmox because the address lives on vmbr0. Even if vmbr0 had been picked, the kernel reports its broadcast as 0.0.0.0, so the script fell all the way back to 255.255.255.255. Now we prefer the default-route interface (vmbr0 on Proxmox, eno1 on bare metal) and, when `ip` doesn't surface a usable `brd`, compute the broadcast from the inet CIDR instead of giving up.
180 lines
6.1 KiB
Cheetah
180 lines
6.1 KiB
Cheetah
#!/usr/bin/env bash
|
|
# Vetting quick-register.
|
|
#
|
|
# Run on the target host (any Linux with root) before the host is wiped:
|
|
# curl -fsSL {{.OrchestratorURL}}/register/quick.sh | sudo bash
|
|
#
|
|
# Detects the primary NIC's MAC, probes hardware (CPU / RAM / disks /
|
|
# NICs / GPUs) into an expected-spec YAML, and POSTs everything to
|
|
# {{.OrchestratorURL}}/api/v1/hosts. After registration, go to the
|
|
# orchestrator's dashboard and click "Start vetting" for the new host.
|
|
#
|
|
# Env overrides (all optional):
|
|
# NAME Host display name (default: `hostname -s`)
|
|
# MAC Force a specific MAC (default: autodetect)
|
|
# WOL_BROADCAST WoL broadcast IP (default: primary iface broadcast)
|
|
# WOL_PORT WoL UDP port (default: 9)
|
|
# NOTES Free-text notes
|
|
# ORCH_URL Override orchestrator base URL
|
|
set -euo pipefail
|
|
|
|
ORCH_URL="${ORCH_URL:-{{.OrchestratorURL}}}"
|
|
|
|
if [[ -z "${ORCH_URL}" ]]; then
|
|
echo "ERROR: ORCH_URL is empty; pass it via env." >&2
|
|
exit 1
|
|
fi
|
|
|
|
primary_iface() {
|
|
# Prefer the interface carrying the default route — that's the
|
|
# canonical "primary" iface (e.g. vmbr0 on Proxmox, eno1 on bare
|
|
# metal). `ip link show` order picks the physical NIC on Proxmox,
|
|
# but that NIC has no IPv4, so we'd miss the broadcast address.
|
|
local iface
|
|
iface="$(ip -o -4 route show default 2>/dev/null \
|
|
| awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')"
|
|
if [[ -n "${iface}" ]]; then
|
|
echo "${iface}"
|
|
return
|
|
fi
|
|
# Fallback: first non-virtual interface that has an IPv4 address.
|
|
ip -o -4 addr show 2>/dev/null \
|
|
| awk '{
|
|
name=$2
|
|
if (name ~ /^(lo|docker|br-|veth|virbr|bond|tun|tap|fwbr|fwpr|fwln|wlan|wlp)/) next
|
|
print name; exit
|
|
}'
|
|
}
|
|
|
|
# compute_broadcast "192.168.1.250/24" → "192.168.1.255"
|
|
compute_broadcast() {
|
|
local cidr="$1" ip prefix a b c d host mask inv bc
|
|
ip="${cidr%/*}"
|
|
prefix="${cidr#*/}"
|
|
[[ "${ip}" == *.*.*.* && "${prefix}" =~ ^[0-9]+$ ]] || return 1
|
|
IFS=. read -r a b c d <<<"${ip}"
|
|
host=$(( (a<<24) | (b<<16) | (c<<8) | d ))
|
|
mask=$(( (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF ))
|
|
inv=$(( (~mask) & 0xFFFFFFFF ))
|
|
bc=$(( host | inv ))
|
|
printf '%d.%d.%d.%d' $(( (bc>>24)&0xFF )) $(( (bc>>16)&0xFF )) $(( (bc>>8)&0xFF )) $(( bc&0xFF ))
|
|
}
|
|
|
|
IFACE="$(primary_iface || true)"
|
|
if [[ -z "${IFACE}" ]]; then
|
|
echo "ERROR: could not pick a primary network interface." >&2
|
|
exit 1
|
|
fi
|
|
|
|
NAME="${NAME:-$(hostname -s 2>/dev/null || hostname)}"
|
|
MAC="${MAC:-$(cat /sys/class/net/${IFACE}/address 2>/dev/null || true)}"
|
|
if [[ -z "${MAC}" ]]; then
|
|
echo "ERROR: could not read MAC for ${IFACE}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${WOL_BROADCAST:-}" ]]; then
|
|
ipinfo="$(ip -o -4 addr show dev "${IFACE}" 2>/dev/null || true)"
|
|
WOL_BROADCAST="$(awk '{for(i=1;i<=NF;i++) if($i=="brd") {print $(i+1); exit}}' <<<"${ipinfo}")"
|
|
# Bridges (vmbr0 on Proxmox, br0 on Linux bridges) often report
|
|
# brd 0.0.0.0 or omit it entirely. Compute from the inet CIDR
|
|
# before giving up on 255.255.255.255.
|
|
if [[ -z "${WOL_BROADCAST}" || "${WOL_BROADCAST}" == "0.0.0.0" ]]; then
|
|
cidr="$(awk '{for(i=1;i<=NF;i++) if($i=="inet") {print $(i+1); exit}}' <<<"${ipinfo}")"
|
|
if [[ "${cidr}" == */* ]]; then
|
|
WOL_BROADCAST="$(compute_broadcast "${cidr}" || true)"
|
|
fi
|
|
fi
|
|
fi
|
|
WOL_BROADCAST="${WOL_BROADCAST:-255.255.255.255}"
|
|
WOL_PORT="${WOL_PORT:-9}"
|
|
|
|
# --- Hardware probes ---
|
|
cores="$(nproc 2>/dev/null || echo 0)"
|
|
cpu_model="$(awk -F: '/^model name/ {sub(/^ */, "", $2); print $2; exit}' /proc/cpuinfo 2>/dev/null || true)"
|
|
mem_gib="$(awk '/^MemTotal:/ {printf "%d", ($2/1024/1024) + 0.5; exit}' /proc/meminfo 2>/dev/null || echo 0)"
|
|
|
|
disk_yaml=""
|
|
while read -r name size serial; do
|
|
[[ -z "${name}" ]] && continue
|
|
[[ "${name}" =~ ^(sd|nvme|vd|hd) ]] || continue
|
|
[[ -z "${serial}" ]] && continue
|
|
size_gb=$(( size / 1000 / 1000 / 1000 ))
|
|
disk_yaml+=" - serial: \"${serial}\"
|
|
size_gb: ${size_gb}
|
|
"
|
|
done < <(lsblk -dn -b -o NAME,SIZE,SERIAL 2>/dev/null || true)
|
|
|
|
nic_yaml=""
|
|
for iface_dir in /sys/class/net/*; do
|
|
iface_name="$(basename "${iface_dir}")"
|
|
[[ "${iface_name}" =~ ^(lo|docker|br-|veth|virbr|bond|tun|tap)$ ]] && continue
|
|
nic_mac="$(cat "${iface_dir}/address" 2>/dev/null || true)"
|
|
[[ -z "${nic_mac}" || "${nic_mac}" == "00:00:00:00:00:00" ]] && continue
|
|
speed_mbps="$(cat "${iface_dir}/speed" 2>/dev/null || echo 0)"
|
|
if [[ "${speed_mbps}" =~ ^[0-9]+$ ]] && (( speed_mbps > 0 )); then
|
|
speed_gbps=$(( (speed_mbps + 999) / 1000 ))
|
|
else
|
|
speed_gbps=0
|
|
fi
|
|
nic_yaml+=" - mac: \"${nic_mac}\"
|
|
speed_gbps: ${speed_gbps}
|
|
"
|
|
done
|
|
|
|
gpu_yaml=""
|
|
if command -v lspci >/dev/null 2>&1; then
|
|
while IFS= read -r gpu; do
|
|
[[ -z "${gpu}" ]] && continue
|
|
gpu_yaml+=" - model: \"${gpu}\"
|
|
"
|
|
done < <(lspci -mm 2>/dev/null | awk -F\" '/"(VGA|3D|Display)/ {print $6}')
|
|
fi
|
|
|
|
# Assemble the YAML spec. Empty sections are omitted so the diff engine
|
|
# skips them rather than demanding empty arrays on the actual side.
|
|
spec="cpu:
|
|
model: \"${cpu_model}\"
|
|
logical_cores: ${cores}
|
|
memory:
|
|
total_gib: ${mem_gib}
|
|
"
|
|
[[ -n "${disk_yaml}" ]] && spec+="disks:
|
|
${disk_yaml}"
|
|
[[ -n "${nic_yaml}" ]] && spec+="nics:
|
|
${nic_yaml}"
|
|
[[ -n "${gpu_yaml}" ]] && spec+="gpus:
|
|
${gpu_yaml}"
|
|
|
|
# --- JSON escape (backslash, double-quote, newline, tab, CR) ---
|
|
json_escape() {
|
|
local s="$1"
|
|
s="${s//\\/\\\\}"
|
|
s="${s//\"/\\\"}"
|
|
s="${s//$'\t'/\\t}"
|
|
s="${s//$'\r'/\\r}"
|
|
s="${s//$'\n'/\\n}"
|
|
printf '%s' "${s}"
|
|
}
|
|
|
|
payload=$(cat <<EOF
|
|
{
|
|
"name": "$(json_escape "${NAME}")",
|
|
"mac": "$(json_escape "${MAC}")",
|
|
"wol_broadcast_ip": "$(json_escape "${WOL_BROADCAST}")",
|
|
"wol_port": ${WOL_PORT},
|
|
"expected_spec_yaml": "$(json_escape "${spec}")",
|
|
"notes": "$(json_escape "${NOTES:-registered via quick-register on $(date -Is)}")"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
echo "Registering ${NAME} (${MAC}) with ${ORCH_URL}..."
|
|
resp="$(curl -fsS -X POST \
|
|
-H 'Content-Type: application/json' \
|
|
-d "${payload}" \
|
|
"${ORCH_URL}/api/v1/hosts")"
|
|
echo "OK: ${resp}"
|
|
echo
|
|
echo "Open ${ORCH_URL}/ and click 'Start vetting' on ${NAME}."
|