R-50 Phase A: host-install v1.19.0 island default + hub version sync
- felhom-host-install v1.19.0: portless vmbr9 island bridge, appliance binds local_api on 169.254.253.1:8443, writes island_bridge/island_guest_addr, pins lan_resolver.host_ip to the LAN IP (Finding-1). --no-island opt-out. - hub hostInstallVersion 1.16.0 -> 1.19.0 (F-1 sync). hostinstall_gates PASS. - Pairs with agent v0.96.0 (attaches guest net1). byo unchanged. Coupling: island install requires agent >= 0.96.0 (vouch first).
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
# Felhom scripts — Changelog
|
||||
|
||||
## felhom-host-install v1.19.0 — R-50 island control plane is the appliance default (2026-07-25)
|
||||
|
||||
Implements Phase A of the R-50 island bridge (spike GO, `documentation/audits/SPIKE-island-bridge-2026-07-25.md`).
|
||||
A fresh **appliance** install is now born immune to F1 (a LAN/DHCP/site move can no longer take the
|
||||
control plane down). **byo is unchanged**; an explicit `--bridge-ip` or `--no-island` keeps the LAN bind.
|
||||
|
||||
- New `ensure_island_bridge()` creates a portless host-internal bridge `vmbr9` (`169.254.253.1/30`),
|
||||
idempotently, before the agent binds it. Portless = no physical port, so the /30 cannot collide with
|
||||
any customer LAN and survives any renumber. vmbr0 untouched.
|
||||
- `step_agent_config` (appliance default): `local_api.listen_addr` → `169.254.253.1:8443`; writes
|
||||
`local_api.island_bridge`/`island_guest_addr` (the agent ≥ 0.96.0 attaches the guest's `net1` from
|
||||
these); and pins `lan_resolver.host_ip` to the **LAN** IP explicitly — **Finding-1**: without it the
|
||||
agent derives the DNS listen-addr from `listen_addr` and silently moves LAN DNS onto the island.
|
||||
- `--no-island` opt-out; `--bridge-ip` still overrides. `configs/felhom-localapi-firewall.example`
|
||||
(agent repo) rewritten — the portless island bind IS the LAN close.
|
||||
- **Coupling:** this install REQUIRES agent ≥ 0.96.0 (older agents ignore the island fields → no net1 →
|
||||
broken control plane). Vouch 0.96.0 before island installs go live. Hub `hostInstallVersion` synced to
|
||||
1.19.0 (F-1). Existing boxes migrate via `documentation/runbooks/RUNBOOK-island-migration.md`.
|
||||
|
||||
## ISO train v1.25.0 — the belt goes fleet-wide, the repo goes free, the screen learns ő, fresh boxes boot at the floor (2026-07-23)
|
||||
|
||||
Four rulings from 2026-07-23. host-install **1.17.0 → 1.18.0**, build-felhom-iso **1.24.0 → 1.25.0**,
|
||||
|
||||
@@ -111,7 +111,9 @@
|
||||
# --force-gitea-golden ignore any local golden; fetch+verify the golden from
|
||||
# Gitea (proves the fetch path; used by the live test)
|
||||
# --node NAME PVE node name (default: pvesh /nodes, else hostname)
|
||||
# --bridge-ip IP[:PORT] local-api listen addr (default: vmbr0 IP : 8443)
|
||||
# --bridge-ip IP[:PORT] local-api listen addr; an explicit value overrides the island default
|
||||
# (default: appliance → island 169.254.253.1:8443; byo → vmbr0 IP:8443)
|
||||
# --no-island appliance only: keep the historical LAN bind instead of the R-50 island
|
||||
# --rootfs-grow N grow OS rootfs by N GiB (default: auto-compute)
|
||||
# --datavol-grow N grow Docker-data vol by N GiB (default: auto-compute)
|
||||
# --sysdata-grow N grow user-data vol by N GiB (default: auto-compute)
|
||||
@@ -182,7 +184,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_VERSION="1.18.0" # the SINGLE version source (F-1): -h, the run banners, and the hub
|
||||
SCRIPT_VERSION="1.19.0" # the SINGLE version source (F-1): -h, the run banners, and the hub
|
||||
# Setup-tab copy (hub internal/web/configs.go hostInstallVersion —
|
||||
# scripts/hostinstall_gates.py asserts the two stay equal) all follow it.
|
||||
# 1.16.0: the FELHOM_ESCROW sudoers alias (controller-driven escrow
|
||||
@@ -229,6 +231,19 @@ ARCHIVE_STORAGE="local"
|
||||
NODE=""
|
||||
NODE_EXPLICIT=false # set true when --node is given; gates the multi-node wrong-node guard
|
||||
BRIDGE_ADDR=""
|
||||
# R-50 island control plane (SPIKE-island-bridge-2026-07-25, GO). The appliance default binds the
|
||||
# local API on a host-internal, portless bridge with a fixed private /30 so the controller→agent
|
||||
# channel survives any LAN/DHCP/site move (the F1 fix). These are the spike's VALIDATED parameters —
|
||||
# do not change them. Set ISLAND_ON=1 only in appliance mode with no explicit --bridge-ip override
|
||||
# (byo and an explicit --bridge-ip keep the historical LAN bind). --no-island opts an appliance out.
|
||||
ISLAND_BRIDGE="vmbr9"
|
||||
ISLAND_HOST_CIDR="169.254.253.1/30"
|
||||
ISLAND_HOST_IP="169.254.253.1"
|
||||
ISLAND_GUEST_CIDR="169.254.253.2/30"
|
||||
ISLAND_PORT="8443"
|
||||
ISLAND_ON=0
|
||||
WANT_ISLAND=true # appliance default; --no-island sets false
|
||||
LAN_IP="" # vmbr0 IPv4 (computed in step_agent_config; the LAN anchor for lan_resolver)
|
||||
ROOTFS_GROW=""
|
||||
DATAVOL_GROW=""
|
||||
SYSDATA_GROW=""
|
||||
@@ -1061,6 +1076,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--archive-storage) ARCHIVE_STORAGE="$2"; shift 2 ;;
|
||||
--node) NODE="$2"; NODE_EXPLICIT=true; shift 2 ;;
|
||||
--bridge-ip) BRIDGE_ADDR="$2"; shift 2 ;;
|
||||
--no-island) WANT_ISLAND=false; shift ;; # R-50: keep the historical LAN bind on an appliance
|
||||
--rootfs-grow) ROOTFS_GROW="$2"; shift 2 ;;
|
||||
--datavol-grow) DATAVOL_GROW="$2"; shift 2 ;;
|
||||
--sysdata-grow) SYSDATA_GROW="$2"; shift 2 ;;
|
||||
@@ -2148,6 +2164,47 @@ install_oob() {
|
||||
log_success " installed OOB felhom-sshd instance + static belt (agent renders config + fills sets once oob.enabled)"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# ensure_island_bridge — create the R-50 host-internal island bridge (vmbr9, portless, fixed /30) if
|
||||
# absent, idempotently: append an ifupdown2 stanza to /etc/network/interfaces + `ifreload -a`.
|
||||
# Portless (bridge-ports none) = no physical NIC, so the /30 cannot collide with any customer LAN and
|
||||
# survives any LAN renumber; vmbr0 is a separate stanza and is never touched. Safe to re-run (no-ops
|
||||
# when present). Spike-proven non-disruptive (SPIKE-island-bridge-2026-07-25, probe P2).
|
||||
#-------------------------------------------------------------------------------
|
||||
ensure_island_bridge() {
|
||||
local ifaces=/etc/network/interfaces
|
||||
if grep -qE "^[[:space:]]*iface[[:space:]]+${ISLAND_BRIDGE}[[:space:]]" "$ifaces" 2>/dev/null; then
|
||||
log_info " island bridge ${ISLAND_BRIDGE} already present — leaving it"
|
||||
return 0
|
||||
fi
|
||||
if $DRY_RUN; then
|
||||
log_dry "append ${ISLAND_BRIDGE} stanza (portless, ${ISLAND_HOST_CIDR}) to $ifaces + ifreload -a"
|
||||
return 0
|
||||
fi
|
||||
log_info " creating island bridge ${ISLAND_BRIDGE} (portless, ${ISLAND_HOST_CIDR})"
|
||||
cp -a "$ifaces" "${ifaces}.pre-island.bak" 2>/dev/null || true
|
||||
cat >> "$ifaces" <<EOF
|
||||
|
||||
# R-50 island control plane (felhom-host-install) — host-internal, no physical port. The agent binds
|
||||
# ${ISLAND_HOST_IP}:${ISLAND_PORT} here; the guest gets ${ISLAND_GUEST_CIDR} on net1. Never add a
|
||||
# bridge-port — the portlessness is what makes the /30 uncollidable with any customer LAN.
|
||||
auto ${ISLAND_BRIDGE}
|
||||
iface ${ISLAND_BRIDGE} inet static
|
||||
address ${ISLAND_HOST_CIDR}
|
||||
bridge-ports none
|
||||
bridge-stp off
|
||||
bridge-fd 0
|
||||
EOF
|
||||
if command -v ifreload >/dev/null 2>&1; then
|
||||
run ifreload -a || log_warn " ifreload -a returned non-zero — verify ${ISLAND_BRIDGE} is up"
|
||||
else
|
||||
run ifup "${ISLAND_BRIDGE}" || log_warn " ifup ${ISLAND_BRIDGE} returned non-zero"
|
||||
fi
|
||||
ip -4 -o addr show "${ISLAND_BRIDGE}" 2>/dev/null | grep -q "${ISLAND_HOST_IP}" \
|
||||
&& log_success " ${ISLAND_BRIDGE} up: ${ISLAND_HOST_CIDR}" \
|
||||
|| log_warn " ${ISLAND_BRIDGE} did not come up with ${ISLAND_HOST_IP} — check $ifaces"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# STEP 6 — write agent config + ensure service healthy
|
||||
#-------------------------------------------------------------------------------
|
||||
@@ -2157,10 +2214,20 @@ step_agent_config() {
|
||||
local fp
|
||||
fp=$(echo | openssl s_client -connect 127.0.0.1:8006 2>/dev/null | openssl x509 -noout -fingerprint -sha256 2>/dev/null | sed 's/.*=//')
|
||||
[[ -n "$fp" ]] || log_warn " could not compute TLS fingerprint (leaving empty — agent will use system trust)"
|
||||
# bridge / local-api addr
|
||||
if [[ -z "$BRIDGE_ADDR" ]]; then
|
||||
local ip; ip=$(ip -4 -o addr show vmbr0 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
||||
BRIDGE_ADDR="${ip:-127.0.0.1}:8443"
|
||||
# bridge / local-api addr. LAN_IP (vmbr0) is always resolved — it is the lan_resolver anchor even
|
||||
# on an island install (Finding-1), and the historical bind on a non-island install.
|
||||
LAN_IP=$(ip -4 -o addr show vmbr0 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
||||
if [[ "$MODE" == "appliance" && "$WANT_ISLAND" == true && -z "$BRIDGE_ADDR" ]]; then
|
||||
# R-50 appliance default: bind the local API on the host-internal island bridge (LAN-move
|
||||
# immune). Create the bridge first so the agent can bind it; point listen_addr at it; the guest
|
||||
# gets net1 from the agent (island_bridge/island_guest_addr, written below). lan_resolver.host_ip
|
||||
# is pinned to the LAN IP in the config write — never let it default off the island listen_addr.
|
||||
ensure_island_bridge
|
||||
BRIDGE_ADDR="${ISLAND_HOST_IP}:${ISLAND_PORT}"
|
||||
ISLAND_ON=1
|
||||
log_info " R-50 island ON: local_api=${BRIDGE_ADDR} (${ISLAND_BRIDGE}); guest net1=${ISLAND_GUEST_CIDR}; lan_resolver.host_ip=${LAN_IP:-<none>}"
|
||||
elif [[ -z "$BRIDGE_ADDR" ]]; then
|
||||
BRIDGE_ADDR="${LAN_IP:-127.0.0.1}:8443" # byo / --no-island / explicit-LAN: historical bind
|
||||
elif [[ "$BRIDGE_ADDR" != *:* ]]; then
|
||||
BRIDGE_ADDR="${BRIDGE_ADDR}:8443"
|
||||
fi
|
||||
@@ -2187,6 +2254,7 @@ step_agent_config() {
|
||||
# Secrets passed via env (NOT argv) to avoid ps exposure.
|
||||
PVE_TOKEN="$PVE_TOKEN" HOST_API_KEY="$HOST_API_KEY" \
|
||||
NODE="$NODE" FP="$fp" HUB_URL="$HUB_URL" HOST_ID="$HOST_ID" BRIDGE_ADDR="$BRIDGE_ADDR" \
|
||||
ISLAND_ON="$ISLAND_ON" ISLAND_BRIDGE="$ISLAND_BRIDGE" ISLAND_GUEST_CIDR="$ISLAND_GUEST_CIDR" LAN_IP="$LAN_IP" \
|
||||
OP_KEY_ID="$RESOLVED_OP_ID" OP_KEY_LINE="$RESOLVED_OP_LINE" \
|
||||
REC_KEY_ID="$RESOLVED_REC_ID" REC_KEY_LINE="$RESOLVED_REC_LINE" \
|
||||
PRESERVE_FROM="$PRESERVE_FROM" INSTALL_MODE="$MODE" ENABLE_OOB="$ENABLE_OOB" OUT="$AGENT_CONFIG" python3 <<'PY'
|
||||
@@ -2224,6 +2292,12 @@ base.setdefault('backup', {"local_backup_target":"local","local_backup_retention
|
||||
base.setdefault('local_api', {})
|
||||
base['local_api'].setdefault('enable', True)
|
||||
base['local_api']['listen_addr'] = os.environ['BRIDGE_ADDR']
|
||||
# R-50: on an island install, record the island bridge + the guest net1 CIDR so the provisioner
|
||||
# (bringup) attaches the guest's static island NIC. All-or-nothing — the agent's config.Validate
|
||||
# rejects a half-set island. On a non-island install these keys are ABSENT (pre-R-50, LAN-only).
|
||||
if os.environ.get('ISLAND_ON') == '1':
|
||||
base['local_api']['island_bridge'] = os.environ['ISLAND_BRIDGE']
|
||||
base['local_api']['island_guest_addr'] = os.environ['ISLAND_GUEST_CIDR']
|
||||
base['local_api'].setdefault('cert_file','/var/lib/felhom-agent/local-api.crt')
|
||||
base['local_api'].setdefault('key_file','/var/lib/felhom-agent/local-api.key')
|
||||
base['local_api'].setdefault('token_store','/var/lib/felhom-agent/local-tokens.log')
|
||||
@@ -2234,6 +2308,13 @@ if os.environ.get('INSTALL_MODE') == 'byo':
|
||||
base.setdefault('lan_resolver', {"enable": False})
|
||||
else:
|
||||
base.setdefault('lan_resolver', {"enable": True})
|
||||
# R-50 Finding-1 (the dnsmasq trap): pin the split-horizon DNS to the LAN IP EXPLICITLY. Without
|
||||
# this, config.go's LANResolverConfig.WithDefaults derives host_ip from listen_addr — which on an
|
||||
# island install is the island IP (169.254.253.1) — silently moving LAN DNS onto the island bridge
|
||||
# and killing it (spike-confirmed live). Set whenever the LAN IP is known (island or LAN bind).
|
||||
_lan = os.environ.get('LAN_IP','').strip()
|
||||
if _lan:
|
||||
base['lan_resolver']['host_ip'] = _lan
|
||||
# WG tunnel (drill F-9; operator decision 2026-07-12 #5): WG is BASE infrastructure like
|
||||
# cloudflared — always-on for every install, both modes (it is also the future OOB-management
|
||||
# transport; the PBS-DR tier merely RIDES it). Registration is hands-free (the drill proved it:
|
||||
|
||||
Reference in New Issue
Block a user