Point at a site and provision server_jump thread sets for ALL inbound root threads (route the existing-env inbound feed to a new env). Pure composition of validated tools (nc_find_inbound, nc-parse, nc_make_jump, nc_insert_protocol, nc_add_route) under ONE journal session — whole batch rolls back in one command. ALL-OR-NOTHING: steps gated on prior success, first failure auto-rolls-back the session (exit 6); pre-flight collision check aborts (exit 5) before any write if a jump-port or thread-name already exists. --dry-run previews the full plan. Output hands `roots: <csv>` to nc_regression for bulk env-A-vs-B testing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
466 lines
22 KiB
Bash
Executable File
466 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# nc-provision-jumps.sh — CAPSTONE orchestrator (v0.8.32).
|
|
#
|
|
# Point at a SITE and build the cross-environment "server_jump" thread sets for
|
|
# EVERY inbound (root) thread at that site, to route the existing-env inbound
|
|
# ADT feed into a NEW environment. This is the pure composition of the validated
|
|
# read + write tools — it REIMPLEMENTS NOTHING:
|
|
#
|
|
# nc-inbound.sh enumerate inbound root threads at the site
|
|
# nc-parse.sh read each inbound's PROTOCOL.PORT + ENCODING
|
|
# nc-make-jump.sh generate the 3-thread jump set + splice route per inbound
|
|
# nc-insert-protocol.sh persist each block (insert) and the splice (add-route)
|
|
# journal.sh ONE session id wraps the WHOLE batch (via LARRY_SESSION_ID)
|
|
#
|
|
# THE KEY SAFETY PROPERTY — SINGLE-SESSION JOURNALING:
|
|
# Every insert + route-add for the whole batch shares ONE LARRY_SESSION_ID, so
|
|
# larry-rollback.sh --session <id>
|
|
# undoes the ENTIRE provisioning run in one shot (not per-thread), restoring
|
|
# the NetConfig(s) byte-identical to baseline.
|
|
#
|
|
# PER-INBOUND LOOP (for each inbound root NAME found at the site):
|
|
# 1. read NAME's PROTOCOL.PORT (the orig listen port) + ENCODING (via nc-parse)
|
|
# 2. derive jump_port = port_base + index (stable, collision-checked)
|
|
# 3. nc-make-jump --inbound NAME --new-host H --jump-port P → 4 artifacts:
|
|
# linux_<NAME>_out (OLD env / site NetConfig, same process as NAME)
|
|
# windows_<NAME>_in (NEW env server_jump NetConfig)
|
|
# windows_<NAME>_out (NEW env server_jump NetConfig)
|
|
# route_add snippet (spliced into NAME's DATAXLATE on the OLD/site NetConfig)
|
|
# 4. persist: insert the 3 blocks + add the splice route, ALL under one session
|
|
#
|
|
# tag = the inbound thread name itself (nc-make-jump's auto-derived scheme) so
|
|
# the loop is name-driven and idempotent.
|
|
#
|
|
# TOPOLOGY / WHICH FILE GETS WHAT:
|
|
# linux_<tag>_out + the splice route → the SITE NetConfig (--netconfig).
|
|
# windows_<tag>_in + windows_<tag>_out → the NEW-env server_jump NetConfig
|
|
# (--new-netconfig; defaults to the SITE
|
|
# NetConfig so a single-file run is
|
|
# self-contained and testable).
|
|
#
|
|
# Usage:
|
|
# nc-provision-jumps.sh --site <SITE> --new-host <HOST> --new-port-base <BASE>
|
|
# [--hciroot DIR] # site discovery root (or $HCIROOT)
|
|
# [--netconfig FILE] # explicit OLD/site NetConfig (overrides --site)
|
|
# [--new-netconfig FILE] # NEW-env server_jump NetConfig (default = site NetConfig)
|
|
# [--scope tcp-listen|all] # which inbound class (default tcp-listen = upstream-fed roots)
|
|
# [--filter REGEX] # limit which inbound roots (matched on name)
|
|
# [--inbound-host HOST] # NEW-side dest host (default 127.0.0.1)
|
|
# [--process-jump PROC] # process for NEW-side threads (default server_jump)
|
|
# [--dry-run] # preview the FULL plan, write NOTHING
|
|
# [--confirm yes] # required to actually write (else dry-run-like refusal)
|
|
# [--format text|tsv] # summary format (default text)
|
|
#
|
|
# DOWNSTREAM HANDOFF (regression — the capstone's OTHER half, already shipped):
|
|
# The provisioned inbound roots are emitted as a `roots:` line (and, with
|
|
# --format tsv, one root per row) so the list is directly consumable as the
|
|
# nc_regression scope (scope=server / threads:<csv>) with route_test_cmd driven
|
|
# by the now-exposed nc_engine route-test. This tool does NOT run regression.
|
|
#
|
|
# Exit: 0 OK (or clean dry-run), 2 usage, 3 target not found, 4 nothing to do,
|
|
# 5 pre-flight collision with the existing NetConfig (nothing written),
|
|
# 6 mid-batch provisioning failure — the batch was AUTO-ROLLED-BACK.
|
|
set -u
|
|
set -o pipefail
|
|
|
|
NC_SELF="$0"
|
|
LIB_DIR="$(cd "$(dirname "$NC_SELF")" && pwd)"
|
|
NCP="$LIB_DIR/nc-parse.sh"
|
|
NC_INBOUND="$LIB_DIR/nc-inbound.sh"
|
|
NC_MAKE_JUMP="$LIB_DIR/nc-make-jump.sh"
|
|
NC_INSERT="$LIB_DIR/nc-insert-protocol.sh"
|
|
# larry-rollback.sh ships at the bundle root (sibling of lib/). Used by the
|
|
# fail-fast auto-rollback (MAJOR 1). Fall back to one on PATH if not found there.
|
|
NC_ROLLBACK="$LIB_DIR/../larry-rollback.sh"
|
|
[ -f "$NC_ROLLBACK" ] || NC_ROLLBACK="$(command -v larry-rollback.sh 2>/dev/null || printf '%s' "$NC_ROLLBACK")"
|
|
|
|
die() { printf 'nc-provision-jumps: %s\n' "$*" >&2; exit "${2:-1}"; }
|
|
|
|
SITE=""
|
|
NEW_HOST=""
|
|
PORT_BASE=""
|
|
HCIROOT_OPT="${HCIROOT:-}"
|
|
NETCONFIG=""
|
|
NEW_NETCONFIG=""
|
|
SCOPE="tcp-listen"
|
|
FILTER=""
|
|
INBOUND_HOST="127.0.0.1"
|
|
PROC_JUMP="server_jump"
|
|
DRY_RUN=0
|
|
CONFIRM=""
|
|
FMT="text"
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--site) shift; SITE="${1:-}" ;;
|
|
--new-host) shift; NEW_HOST="${1:-}" ;;
|
|
--new-port-base) shift; PORT_BASE="${1:-}" ;;
|
|
--hciroot) shift; HCIROOT_OPT="${1:-}" ;;
|
|
--netconfig) shift; NETCONFIG="${1:-}" ;;
|
|
--new-netconfig) shift; NEW_NETCONFIG="${1:-}" ;;
|
|
--scope) shift; SCOPE="${1:-}" ;;
|
|
--filter) shift; FILTER="${1:-}" ;;
|
|
--inbound-host) shift; INBOUND_HOST="${1:-}" ;;
|
|
--process-jump) shift; PROC_JUMP="${1:-}" ;;
|
|
--dry-run) DRY_RUN=1 ;;
|
|
--confirm) shift; CONFIRM="${1:-}" ;;
|
|
--format) shift; FMT="${1:-}" ;;
|
|
-h|--help) sed -n '2,80p' "$NC_SELF"; exit 0 ;;
|
|
-*) die "unknown flag: $1" 2 ;;
|
|
*) die "unexpected arg: $1 (use flags; see --help)" 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# ── Validate + resolve the OLD/site NetConfig ────────────────────────────────
|
|
[ -n "$NEW_HOST" ] || die "missing --new-host" 2
|
|
[ -n "$PORT_BASE" ] || die "missing --new-port-base" 2
|
|
case "$PORT_BASE" in (*[!0-9]*|'') die "--new-port-base must be numeric: $PORT_BASE" 2 ;; esac
|
|
case "$SCOPE" in tcp-listen|all) ;; *) die "bad --scope: $SCOPE (tcp-listen|all)" 2 ;; esac
|
|
case "$FMT" in text|tsv) ;; *) die "bad --format: $FMT (text|tsv)" 2 ;; esac
|
|
|
|
if [ -z "$NETCONFIG" ]; then
|
|
[ -n "$SITE" ] || die "need --site or --netconfig" 2
|
|
[ -n "$HCIROOT_OPT" ] || die "no \$HCIROOT and no --hciroot; cannot resolve --site $SITE" 2
|
|
NETCONFIG="$HCIROOT_OPT/$SITE/NetConfig"
|
|
fi
|
|
[ -f "$NETCONFIG" ] || die "site NetConfig not found: $NETCONFIG" 3
|
|
|
|
# server_jump (NEW-env) NetConfig defaults to the site NetConfig (self-contained).
|
|
[ -n "$NEW_NETCONFIG" ] || NEW_NETCONFIG="$NETCONFIG"
|
|
[ -f "$NEW_NETCONFIG" ] || die "new-env NetConfig not found: $NEW_NETCONFIG" 3
|
|
|
|
[ -n "$SITE" ] || SITE="$(basename "$(dirname "$NETCONFIG")")"
|
|
|
|
# ── Enumerate inbound roots (reuse nc-inbound.sh, do not reimplement) ────────
|
|
# Column 1 of the TSV is the thread name; skip the header.
|
|
mapfile -t ALL_ROOTS < <("$NC_INBOUND" "$NETCONFIG" --mode "$SCOPE" --format tsv 2>/dev/null \
|
|
| tail -n +2 | awk -F'\t' 'NF>0 && $1!="" {print $1}')
|
|
|
|
# Apply --filter (regex on the name)
|
|
ROOTS=()
|
|
for r in "${ALL_ROOTS[@]:-}"; do
|
|
[ -n "$r" ] || continue
|
|
if [ -n "$FILTER" ]; then
|
|
printf '%s' "$r" | grep -qE "$FILTER" || continue
|
|
fi
|
|
ROOTS+=("$r")
|
|
done
|
|
|
|
if [ "${#ROOTS[@]}" -eq 0 ]; then
|
|
printf 'No inbound roots matched at site %s (scope=%s%s). Nothing to provision.\n' \
|
|
"$SITE" "$SCOPE" "${FILTER:+, filter=/$FILTER/}" >&2
|
|
exit 4
|
|
fi
|
|
|
|
# ── Build the per-inbound plan (read each root's port/encoding + jump set) ───
|
|
# We DRY-RUN nc-make-jump (stdout, no writes) to materialise each block + route
|
|
# into temp files, regardless of whether we ultimately persist — same generator,
|
|
# so the dry-run plan and the real run are byte-identical artifacts.
|
|
PLAN_DIR="$(mktemp -d "${TMPDIR:-/tmp}/nc-provjumps.XXXXXX")" || die "mktemp -d failed"
|
|
trap 'rm -rf "$PLAN_DIR"' EXIT
|
|
|
|
# Track names we'd create to catch intra-batch collisions early (port + thread).
|
|
declare -A SEEN_PORT=()
|
|
declare -A SEEN_NAME=()
|
|
|
|
PLAN_COUNT=0
|
|
PLAN_ROOTS=() # the inbound roots actually planned (regression scope)
|
|
PLAN_OLDOUT=()
|
|
PLAN_NEWIN=()
|
|
PLAN_NEWOUT=()
|
|
PLAN_ROUTE=() # the route_add splice snippet for each inbound (proper array, not suffix-strip)
|
|
PLAN_PORT=()
|
|
PLAN_JUMP=()
|
|
PLAN_ENC=()
|
|
PLAN_PROC=()
|
|
# Planned NAMEs per inbound (for the pre-flight collision pass below). One
|
|
# space-joined list per planned inbound, parallel to PLAN_ROOTS.
|
|
PLAN_NAMES=()
|
|
|
|
idx=0
|
|
for inb in "${ROOTS[@]}"; do
|
|
# Read the orig listen port + encoding via the native parser (reuse nc-parse).
|
|
orig_port="$("$NCP" protocol-nested "$NETCONFIG" "$inb" PROTOCOL.PORT 2>/dev/null | head -1)"
|
|
enc="$("$NCP" protocol-field "$NETCONFIG" "$inb" ENCODING 2>/dev/null | head -1)"
|
|
[ -z "$enc" ] && enc="ASCII"
|
|
|
|
if [ -z "$orig_port" ]; then
|
|
printf 'SKIP %s — no PROTOCOL.PORT (not a TCP listener; jump pattern needs a listen port)\n' "$inb" >&2
|
|
continue
|
|
fi
|
|
|
|
jump_port=$(( PORT_BASE + idx ))
|
|
idx=$(( idx + 1 ))
|
|
|
|
# Intra-batch collision guards.
|
|
if [ -n "${SEEN_PORT[$jump_port]:-}" ]; then
|
|
die "internal: jump_port $jump_port collides (inbound $inb vs ${SEEN_PORT[$jump_port]})"
|
|
fi
|
|
SEEN_PORT[$jump_port]="$inb"
|
|
|
|
prefix="$PLAN_DIR/$inb"
|
|
# nc-make-jump with --out-prefix writes the 4 artifact files and prints paths.
|
|
# This is its generation path — NO NetConfig write happens here.
|
|
if ! "$NC_MAKE_JUMP" "$NETCONFIG" \
|
|
--inbound "$inb" --new-host "$NEW_HOST" --jump-port "$jump_port" \
|
|
--inbound-host "$INBOUND_HOST" --process-jump "$PROC_JUMP" \
|
|
--encoding "$enc" --out-prefix "$prefix" >/dev/null 2>"$prefix.err"; then
|
|
printf 'SKIP %s — nc-make-jump failed: %s\n' "$inb" "$(cat "$prefix.err" 2>/dev/null)" >&2
|
|
continue
|
|
fi
|
|
|
|
oldout="$prefix.old_out.tcl"
|
|
newin="$prefix.new_in.tcl"
|
|
newout="$prefix.new_out.tcl"
|
|
route="$prefix.route_add.tcl" # MINOR fix: proper array, not ${oldout%old_out.tcl}route_add.tcl
|
|
|
|
# Names that would be created (for collision reporting against the NetConfig).
|
|
planned_names=""
|
|
for blk in "$oldout" "$newin" "$newout"; do
|
|
nm=$(awk '/^protocol [A-Za-z0-9_]+/ {print $2; exit}' "$blk")
|
|
if [ -n "${SEEN_NAME[$nm]:-}" ]; then
|
|
die "internal: jump thread name $nm generated twice"
|
|
fi
|
|
SEEN_NAME[$nm]="$inb"
|
|
planned_names="${planned_names:+$planned_names }$nm"
|
|
done
|
|
|
|
PLAN_ROOTS+=("$inb")
|
|
PLAN_OLDOUT+=("$oldout")
|
|
PLAN_NEWIN+=("$newin")
|
|
PLAN_NEWOUT+=("$newout")
|
|
PLAN_ROUTE+=("$route")
|
|
PLAN_NAMES+=("$planned_names")
|
|
PLAN_PORT+=("$orig_port")
|
|
PLAN_JUMP+=("$jump_port")
|
|
PLAN_ENC+=("$enc")
|
|
PLAN_PROC+=("$("$NCP" protocol-field "$NETCONFIG" "$inb" PROCESSNAME 2>/dev/null | head -1)")
|
|
PLAN_COUNT=$(( PLAN_COUNT + 1 ))
|
|
done
|
|
|
|
if [ "$PLAN_COUNT" -eq 0 ]; then
|
|
printf 'No provisionable inbound roots (all skipped — see messages above).\n' >&2
|
|
exit 4
|
|
fi
|
|
|
|
NEW_THREADS=$(( PLAN_COUNT * 3 ))
|
|
|
|
# ── PRE-FLIGHT COLLISION CHECK against the EXISTING NetConfig(s) ──────────────
|
|
# (MAJOR 2) Intra-batch collisions (port/name) are already caught above. But a
|
|
# planned jump_port or generated thread NAME can ALSO collide with something
|
|
# ALREADY in the target NetConfig — a duplicate listener (port) or a name
|
|
# nc-insert-protocol would refuse late. Catch BOTH up front, before any write,
|
|
# and surface in --dry-run. We read existing ports/names via nc-parse
|
|
# (protocol-summary: col1=name, col4=port) — never a brittle hand-grep.
|
|
#
|
|
# Topology of where each planned object lands:
|
|
# jump_port → listened on by windows_<name>_in in the NEW-env NetConfig
|
|
# linux_<name>_out → SITE NetConfig
|
|
# windows_<name>_in/out→ NEW-env NetConfig
|
|
# To be conservative we check every planned NAME against BOTH files' existing
|
|
# name sets, and each jump_port against the NEW-env file's existing ports (where
|
|
# the listener is created). If site==new-env (the common self-contained run)
|
|
# the two sets are the same file read once.
|
|
declare -A EXIST_PORT_OLD=() # port -> existing protocol name (site NetConfig)
|
|
declare -A EXIST_NAME_OLD=() # name -> 1 (site NetConfig)
|
|
declare -A EXIST_PORT_NEW=() # port -> existing protocol name (new-env NetConfig)
|
|
declare -A EXIST_NAME_NEW=() # name -> 1 (new-env NetConfig)
|
|
|
|
# _scan_existing FILE PORTVAR NAMEVAR — populate the named assoc arrays from FILE.
|
|
_scan_existing() {
|
|
local file="$1" __pv="$2" __nv="$3" nm pt
|
|
while IFS=$'\t' read -r nm pt; do
|
|
[ -n "$nm" ] || continue
|
|
eval "$__nv[\"\$nm\"]=1"
|
|
[ -n "$pt" ] && eval "$__pv[\"\$pt\"]=\"\$nm\""
|
|
done < <("$NCP" protocol-summary "$file" 2>/dev/null | tail -n +2 | awk -F'\t' 'NF>0 && $1!="" {print $1"\t"$4}')
|
|
}
|
|
|
|
_scan_existing "$NETCONFIG" EXIST_PORT_OLD EXIST_NAME_OLD
|
|
if [ "$NEW_NETCONFIG" = "$NETCONFIG" ]; then
|
|
# Same file — alias the new-env sets to the old-env scan (avoid a second read).
|
|
for k in "${!EXIST_PORT_OLD[@]}"; do EXIST_PORT_NEW["$k"]="${EXIST_PORT_OLD[$k]}"; done
|
|
for k in "${!EXIST_NAME_OLD[@]}"; do EXIST_NAME_NEW["$k"]=1; done
|
|
else
|
|
_scan_existing "$NEW_NETCONFIG" EXIST_PORT_NEW EXIST_NAME_NEW
|
|
fi
|
|
|
|
PREFLIGHT_CONFLICTS=()
|
|
for i in "${!PLAN_ROOTS[@]}"; do
|
|
inb="${PLAN_ROOTS[$i]}"
|
|
jp="${PLAN_JUMP[$i]}"
|
|
# (a) jump_port already listened on in the NEW-env NetConfig?
|
|
if [ -n "${EXIST_PORT_NEW[$jp]:-}" ]; then
|
|
PREFLIGHT_CONFLICTS+=("inbound $inb: jump_port $jp already in use by existing protocol '${EXIST_PORT_NEW[$jp]}' in $NEW_NETCONFIG")
|
|
fi
|
|
# (b) any planned thread NAME already present in either NetConfig?
|
|
for nm in ${PLAN_NAMES[$i]}; do
|
|
if [ -n "${EXIST_NAME_OLD[$nm]:-}" ]; then
|
|
PREFLIGHT_CONFLICTS+=("inbound $inb: thread name '$nm' already exists in $NETCONFIG")
|
|
fi
|
|
if [ "$NEW_NETCONFIG" != "$NETCONFIG" ] && [ -n "${EXIST_NAME_NEW[$nm]:-}" ]; then
|
|
PREFLIGHT_CONFLICTS+=("inbound $inb: thread name '$nm' already exists in $NEW_NETCONFIG")
|
|
fi
|
|
done
|
|
done
|
|
|
|
if [ "${#PREFLIGHT_CONFLICTS[@]}" -gt 0 ]; then
|
|
printf 'PRE-FLIGHT COLLISION CHECK FAILED — %d conflict(s) with the existing NetConfig:\n' \
|
|
"${#PREFLIGHT_CONFLICTS[@]}" >&2
|
|
for c in "${PREFLIGHT_CONFLICTS[@]}"; do printf ' ✗ %s\n' "$c" >&2; done
|
|
printf 'ABORTED before writing anything. Resolve the collisions (e.g. choose a different\n' >&2
|
|
printf ' --new-port-base, rename the inbound roots, or --filter them out) and re-run.\n' >&2
|
|
exit 5
|
|
fi
|
|
|
|
# ── Plan / summary renderer ──────────────────────────────────────────────────
|
|
print_plan_header() {
|
|
local mode="$1"
|
|
if [ "$FMT" = "tsv" ]; then return 0; fi
|
|
printf '═══════════════════════════════════════════════════════════════════════\n'
|
|
printf '%s — site=%s\n' "$mode" "$SITE"
|
|
printf ' site NetConfig (OLD) : %s\n' "$NETCONFIG"
|
|
printf ' server_jump NetConfig : %s%s\n' "$NEW_NETCONFIG" \
|
|
"$( [ "$NEW_NETCONFIG" = "$NETCONFIG" ] && printf ' (same file)' )"
|
|
printf ' new-env host : %s port-base : %s inbound-host : %s\n' "$NEW_HOST" "$PORT_BASE" "$INBOUND_HOST"
|
|
printf ' scope=%s filter=%s process-jump=%s\n' "$SCOPE" "${FILTER:-(none)}" "$PROC_JUMP"
|
|
printf ' PLAN: %d inbound root(s) → %d new threads + %d splice route(s)\n' \
|
|
"$PLAN_COUNT" "$NEW_THREADS" "$PLAN_COUNT"
|
|
printf '═══════════════════════════════════════════════════════════════════════\n'
|
|
}
|
|
|
|
print_plan_body() {
|
|
local i
|
|
if [ "$FMT" = "tsv" ]; then
|
|
printf 'inbound\torig_port\tjump_port\tencoding\tprocess\tlinux_out\twindows_in\twindows_out\troute_splice\n'
|
|
for i in "${!PLAN_ROOTS[@]}"; do
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"${PLAN_ROOTS[$i]}" "${PLAN_PORT[$i]}" "${PLAN_JUMP[$i]}" "${PLAN_ENC[$i]}" "${PLAN_PROC[$i]}" \
|
|
"linux_${PLAN_ROOTS[$i]}_out" "windows_${PLAN_ROOTS[$i]}_in" "windows_${PLAN_ROOTS[$i]}_out" \
|
|
"${PLAN_ROOTS[$i]}"
|
|
done
|
|
return 0
|
|
fi
|
|
for i in "${!PLAN_ROOTS[@]}"; do
|
|
local inb="${PLAN_ROOTS[$i]}"
|
|
printf '\n• inbound %s (port %s → jump %s, enc %s, proc %s)\n' \
|
|
"$inb" "${PLAN_PORT[$i]}" "${PLAN_JUMP[$i]}" "${PLAN_ENC[$i]}" "${PLAN_PROC[$i]}"
|
|
printf ' [OLD %s] + protocol linux_%s_out (tcpip-client → %s:%s)\n' \
|
|
"$(basename "$(dirname "$NETCONFIG")")" "$inb" "$NEW_HOST" "${PLAN_JUMP[$i]}"
|
|
printf ' [NEW server_jump] + protocol windows_%s_in (tcpip-server, listen %s)\n' "$inb" "${PLAN_JUMP[$i]}"
|
|
printf ' [NEW server_jump] + protocol windows_%s_out (tcpip-client → %s:%s)\n' "$inb" "$INBOUND_HOST" "${PLAN_PORT[$i]}"
|
|
printf ' [OLD %s] ~ splice route on %s DATAXLATE → linux_%s_out\n' \
|
|
"$(basename "$(dirname "$NETCONFIG")")" "$inb" "$inb"
|
|
done
|
|
}
|
|
|
|
# ── DRY-RUN: show the full plan, write NOTHING ───────────────────────────────
|
|
if [ "$DRY_RUN" = "1" ] || [ "$CONFIRM" != "yes" ]; then
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
print_plan_header "DRY-RUN PLAN (no writes)"
|
|
else
|
|
print_plan_header "PLAN (refused to write — pass --confirm yes)"
|
|
fi
|
|
print_plan_body
|
|
if [ "$FMT" != "tsv" ]; then
|
|
printf '\nroots: %s\n' "$(printf '%s,' "${PLAN_ROOTS[@]}" | sed 's/,$//')"
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
printf '(dry-run — nothing written. Re-run with --confirm yes to provision.)\n'
|
|
else
|
|
printf '(no --confirm yes — nothing written. Add --confirm yes to provision, or --dry-run to preview.)\n'
|
|
fi
|
|
printf 'regression handoff: feed the roots above as nc_regression scope (server / threads:<csv>),\n'
|
|
printf ' route_test_cmd via nc_engine route-test.\n'
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# ── REAL RUN: persist the whole batch under ONE journal session ──────────────
|
|
# Establish ONE session id for the ENTIRE batch. Honour an inherited
|
|
# LARRY_SESSION_ID (so larry.sh's running session groups it), else mint one.
|
|
SESSION="${LARRY_SESSION_ID:-provjumps-$(date +%Y-%m-%d-%H%M%S)-$$}"
|
|
export LARRY_SESSION_ID="$SESSION"
|
|
export LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
|
|
|
print_plan_header "PROVISIONING (writing under one journal session)"
|
|
|
|
# ── Fail-fast auto-rollback (MAJOR 1) ────────────────────────────────────────
|
|
# This batch is ALL-OR-NOTHING. On the FIRST hard failure of any step we STOP
|
|
# provisioning the rest of the batch and auto-roll-back the WHOLE session via the
|
|
# proven byte-identical `larry-rollback.sh --session <id>`, so the NetConfig is
|
|
# never left half-provisioned (and a route is never spliced to a thread that was
|
|
# never created — see the per-inbound step gating below). Then exit 6.
|
|
abort_rollback() {
|
|
local where="$1"
|
|
printf '\n✗ PROVISIONING FAILED at %s — stopping the batch.\n' "$where" >&2
|
|
printf 'Auto-rolling-back the WHOLE session %s (byte-identical) so nothing is left half-provisioned…\n' \
|
|
"$SESSION" >&2
|
|
if [ -f "$NC_ROLLBACK" ] && "$NC_ROLLBACK" --session "$SESSION" --yes >&2 2>&1; then
|
|
printf '✓ Auto-rolled-back the batch. NetConfig(s) restored to baseline.\n' >&2
|
|
printf 'provisioning failed at %s; auto-rolled-back the batch.\n' "$where" >&2
|
|
else
|
|
# Auto-rollback not feasible (rollback tool missing / errored): fail loud and
|
|
# print the manual command so the operator can recover deterministically.
|
|
printf '✗ AUTO-ROLLBACK COULD NOT RUN (rollback tool: %s).\n' "$NC_ROLLBACK" >&2
|
|
printf '!! The NetConfig MAY be partially provisioned. ROLL BACK MANUALLY NOW:\n' >&2
|
|
printf ' larry-rollback.sh --session %s\n' "$SESSION" >&2
|
|
fi
|
|
exit 6
|
|
}
|
|
|
|
inserts=0
|
|
routes=0
|
|
for i in "${!PLAN_ROOTS[@]}"; do
|
|
inb="${PLAN_ROOTS[$i]}"
|
|
|
|
# ① OLD env: insert linux_<tag>_out into the SITE NetConfig.
|
|
if "$NC_INSERT" insert "$NETCONFIG" "${PLAN_OLDOUT[$i]}" --mode end >/dev/null 2>&1; then
|
|
inserts=$(( inserts + 1 ))
|
|
else
|
|
printf 'FAIL insert linux_%s_out into %s\n' "$inb" "$NETCONFIG" >&2
|
|
# ① failed → do NOT run ②/③/④ for this inbound. In particular the splice ④
|
|
# MUST NOT run, or we'd splice a route to a linux_<tag>_out that was never
|
|
# created (dangling DEST). Fail-fast + rollback the whole batch.
|
|
abort_rollback "insert linux_${inb}_out"
|
|
fi
|
|
|
|
# ② NEW env: insert windows_<tag>_in into the server_jump NetConfig.
|
|
if "$NC_INSERT" insert "$NEW_NETCONFIG" "${PLAN_NEWIN[$i]}" --mode end >/dev/null 2>&1; then
|
|
inserts=$(( inserts + 1 ))
|
|
else
|
|
printf 'FAIL insert windows_%s_in into %s\n' "$inb" "$NEW_NETCONFIG" >&2
|
|
abort_rollback "insert windows_${inb}_in"
|
|
fi
|
|
|
|
# ③ NEW env: insert windows_<tag>_out into the server_jump NetConfig.
|
|
if "$NC_INSERT" insert "$NEW_NETCONFIG" "${PLAN_NEWOUT[$i]}" --mode end >/dev/null 2>&1; then
|
|
inserts=$(( inserts + 1 ))
|
|
else
|
|
printf 'FAIL insert windows_%s_out into %s\n' "$inb" "$NEW_NETCONFIG" >&2
|
|
abort_rollback "insert windows_${inb}_out"
|
|
fi
|
|
|
|
# ④ splice the route onto the OLD inbound's DATAXLATE (route_add artifact).
|
|
# Only reached because ① succeeded above → linux_<tag>_out exists → no dangling
|
|
# DEST. Uses the proper PLAN_ROUTE[] array (MINOR fix), not a suffix-strip.
|
|
if "$NC_INSERT" add-route "$NETCONFIG" "$inb" "${PLAN_ROUTE[$i]}" >/dev/null 2>&1; then
|
|
routes=$(( routes + 1 ))
|
|
else
|
|
printf 'FAIL splice route on %s in %s\n' "$inb" "$NETCONFIG" >&2
|
|
abort_rollback "splice route on ${inb}"
|
|
fi
|
|
done
|
|
|
|
print_plan_body
|
|
printf '\n───────────────────────────────────────────────────────────────────────\n'
|
|
printf 'PROVISIONED: %d inbound root(s) → %d block(s) inserted + %d splice route(s).\n' \
|
|
"$PLAN_COUNT" "$inserts" "$routes"
|
|
printf 'journal session: %s\n' "$SESSION"
|
|
printf 'WHOLE-BATCH ROLLBACK: larry-rollback.sh --session %s\n' "$SESSION"
|
|
printf 'roots: %s\n' "$(printf '%s,' "${PLAN_ROOTS[@]}" | sed 's/,$//')"
|
|
printf 'regression handoff: feed the roots above as nc_regression scope (server / threads:<csv>),\n'
|
|
printf ' route_test_cmd via nc_engine route-test.\n'
|
|
|
|
exit 0
|