v0.8.32: nc_provision_jumps — capstone inter-server jump-thread provisioner
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>
This commit is contained in:
parent
7a715c802a
commit
39f0e00c01
78
CHANGELOG.md
78
CHANGELOG.md
@ -4,6 +4,84 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here.
|
|||||||
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
||||||
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
||||||
|
|
||||||
|
## v0.8.32 — 2026-05-28
|
||||||
|
|
||||||
|
**★ CAPSTONE TOOL: `nc_provision_jumps` — point at a SITE and build the
|
||||||
|
cross-environment `server_jump` thread set for ALL inbound (root) threads,
|
||||||
|
routing the existing-env inbound feed (the ADT feed) into a NEW environment.**
|
||||||
|
Bryan's stated ultimate goal. PURE COMPOSITION of tools all validated in the
|
||||||
|
read-pass (v0.8.29) and write/mutate pass (v0.8.30) — it reimplements nothing:
|
||||||
|
`nc-inbound.sh` enumerates the inbound roots → `nc-parse.sh` reads each root's
|
||||||
|
`PROTOCOL.PORT` + `ENCODING` → `nc-make-jump.sh` generates the per-inbound
|
||||||
|
3-thread set + splice route → `nc-insert-protocol.sh` persists them → `journal.sh`
|
||||||
|
wraps the WHOLE batch in ONE session.
|
||||||
|
|
||||||
|
- **Invocation:** `nc-provision-jumps --site <SITE> --new-host <H> --new-port-base <BASE>`.
|
||||||
|
Flags: `--dry-run` (preview the FULL plan — every inbound + every block/route
|
||||||
|
that WOULD be created — write NOTHING), `--confirm yes` (required to write;
|
||||||
|
absent = plan-only refusal), `--filter REGEX` (limit which inbound roots),
|
||||||
|
`--scope tcp-listen|all`, `--netconfig` / `--new-netconfig` (explicit OLD /
|
||||||
|
server_jump NetConfigs; new defaults to the site NetConfig), `--hciroot`,
|
||||||
|
`--inbound-host`, `--process-jump`, `--format text|tsv`.
|
||||||
|
- **Per-inbound loop is name-driven** (tag = the inbound name, so it loops
|
||||||
|
cleanly): `jump_port = new_port_base + index`, collision-checked. For each root
|
||||||
|
it generates `linux_<name>_out` (OLD/site NetConfig), `windows_<name>_in` +
|
||||||
|
`windows_<name>_out` (NEW-env server_jump NetConfig), and splices a route onto
|
||||||
|
the OLD inbound's DATAXLATE → `linux_<name>_out` (existing routes preserved).
|
||||||
|
- **★ SINGLE-SESSION JOURNALING — the key safety property.** All 3N inserts +
|
||||||
|
N splice routes for the whole batch share ONE `LARRY_SESSION_ID`, so
|
||||||
|
`larry-rollback.sh --session <id>` undoes the ENTIRE provisioning run
|
||||||
|
byte-identical in one shot (not per-thread).
|
||||||
|
- **★ FAIL-SAFE — the batch is ALL-OR-NOTHING (robustness hardening).** Each
|
||||||
|
inbound's later steps are gated on its earlier steps: if ① (`linux_<x>_out`
|
||||||
|
insert) fails, ②/③/④ are skipped — so the ④ splice can never route to a
|
||||||
|
thread that was never created (no dangling DEST). On the FIRST hard failure
|
||||||
|
ANYWHERE in the batch the loop STOPS and auto-rolls-back the whole session via
|
||||||
|
the proven byte-identical `larry-rollback.sh --session <id> --yes`, then exits
|
||||||
|
6 with `provisioning failed at <inbound>; auto-rolled-back the batch`. If the
|
||||||
|
rollback tool can't run, it fails loud and prints the manual rollback command.
|
||||||
|
- **★ PRE-FLIGHT COLLISION CHECK against the EXISTING NetConfig (robustness
|
||||||
|
hardening).** Before ANY write (and surfaced in `--dry-run`) it scans the
|
||||||
|
target NetConfig — and the `--new-netconfig` when different — via `nc-parse`
|
||||||
|
(`protocol-summary`; never a hand-grep) for (a) any planned `jump_port`
|
||||||
|
already in use by an existing `PROTOCOL.PORT` (would create a duplicate
|
||||||
|
listener) and (b) any planned thread NAME already present. ANY collision
|
||||||
|
ABORTS before writing anything (exit 5), listing every conflict (inbound,
|
||||||
|
port/name, what it collides with). This complements the existing intra-batch
|
||||||
|
port/name guards.
|
||||||
|
- Splice route artifact path is now a proper `PLAN_ROUTE[]` array built at
|
||||||
|
plan time (was a fragile `${oldout%old_out.tcl}route_add.tcl` suffix-strip).
|
||||||
|
- **Regression handoff (capstone's SEPARATE, already-shipped half):** output ends
|
||||||
|
with a `roots: <csv>` line consumable directly as the `nc_regression` scope
|
||||||
|
(server / threads:<csv>, `route_test_cmd` via the exposed `nc_engine route-test`).
|
||||||
|
This tool does NOT run regression.
|
||||||
|
- **Wired into all 4 larry.sh surfaces** (manual-tools registry, `tool_nc_provision_jumps`
|
||||||
|
wrapper, `execute_tool` case, TOOLS_JSON schema).
|
||||||
|
- Verified on a COPY of the real 24-site integrator (`/tmp/clvf_jumps_test`, site
|
||||||
|
`epic` = 21 inbound roots; read fixture sha-confirmed untouched at
|
||||||
|
`fa129cfc…`): (a) full-site `--dry-run` listed all 21 roots → 63 new threads +
|
||||||
|
21 splice routes, wrote NOTHING (sha unchanged); (b) a real full-site provision
|
||||||
|
inserted 63 balanced-brace blocks + 21 splice routes (topology spot-checked:
|
||||||
|
`windows_<x>_in` listens on the jump port and routes internally to
|
||||||
|
`windows_<x>_out` → 127.0.0.1:<orig_port>; `linux_<x>_out` → new-host:jump_port;
|
||||||
|
OLD inbound keeps its existing dests + the new jump-out), all under ONE journal
|
||||||
|
session (84 entries, 1 session id); (c) `larry-rollback.sh --session <id>`
|
||||||
|
removed the ENTIRE batch BYTE-IDENTICAL (sha back to `fa129cfc…`); (d)
|
||||||
|
`--filter '^MFNfr'` correctly limited to 2 roots, and that batch also rolled
|
||||||
|
back byte-identical.
|
||||||
|
- Robustness re-test on a COPY (`/tmp/clvf_jumps_h`, site `epic` = 21 roots; read
|
||||||
|
fixture `/tmp/clvf_realtest` sha-confirmed untouched): (a) a CLEAN run still
|
||||||
|
provisions 21 roots → 63 blocks + 21 splice routes under ONE session (84
|
||||||
|
entries) and `--session` rollback is still byte-identical; (b) both `--dry-run`
|
||||||
|
AND a real run ABORT cleanly (exit 5, sha unchanged, no journal minted) when a
|
||||||
|
`jump_port`/NAME collides with the existing NetConfig (crafted via
|
||||||
|
`--new-port-base` = an existing listener port); (c) a simulated mid-batch
|
||||||
|
insert failure fail-fasts (exit 6), auto-rolls-back the whole session
|
||||||
|
byte-identical, and leaves NO dangling `linux_<x>_out` / splice.
|
||||||
|
- VERSION + `larry.sh` `LARRY_VERSION` → 0.8.32; MANIFEST regenerated (`--check`
|
||||||
|
clean); `bash -n` clean. (Version held at 0.8.32 — still unpushed; the
|
||||||
|
fail-safe + collision pre-flight harden the same release.)
|
||||||
|
|
||||||
## v0.8.31 — 2026-05-28
|
## v0.8.31 — 2026-05-28
|
||||||
|
|
||||||
**★ NEW WRITE TOOL: `nc_set_field` — change a settable field (PORT, HOST/IP,
|
**★ NEW WRITE TOOL: `nc_set_field` — change a settable field (PORT, HOST/IP,
|
||||||
|
|||||||
7
MANIFEST
7
MANIFEST
@ -23,16 +23,16 @@
|
|||||||
# scripts/make-manifest.sh and bump VERSION.
|
# scripts/make-manifest.sh and bump VERSION.
|
||||||
|
|
||||||
# Top-level scripts
|
# Top-level scripts
|
||||||
larry.sh 940d8fad8bffc42f6b5b7e7b295f8218ee43c03f327ca250e71bbb7dbce1a002
|
larry.sh e37681171c8d3b7de10b1ebaf4c6c6db4198b2d9c20b106c6b7d43ba48f5a90d
|
||||||
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
||||||
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
||||||
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
||||||
install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423
|
install-larry.sh fa36e23a39eacbd0d7ecedd3b42131902f816ee7e98241dfc6e28c6e4ba80423
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
VERSION bae94dce70052efa657cca9bf24209ef8ae9cb277deb79f38e7fdbdfdc5bd254
|
VERSION 4bd9f5643b9ceafed59c1be74ee8fec03786be48797dfa118157e894ddbc3ed9
|
||||||
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
|
MANUAL.md c64bd0251a51ad150508b4e1185355bc4826a64071d4de339f92ed550dbfacde
|
||||||
CHANGELOG.md 8696119944e16e8b7ab798d0641fb9f6beda48f871208132b7929401fc611d75
|
CHANGELOG.md d07489204cea869763e8f9a8091a5f5a2931b9aa893eaf92ca5113dc3981288c
|
||||||
|
|
||||||
# Agent personas (system-prompt overlays)
|
# Agent personas (system-prompt overlays)
|
||||||
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
|
agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1
|
||||||
@ -102,6 +102,7 @@ lib/nc-parse.sh 52fef42d7a4b361534ab0d921deef74586dfeb6c199c941cebb55abcc2c39d4f
|
|||||||
lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
|
lib/nc-paths.sh 388d2f4560736587a01218cadc1de612cd59e392819d16db2f56f19174c1111b
|
||||||
lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b
|
lib/nc-inbound.sh 52d28c5f8d97bdf96f0fc7b5300d35b106b8e1226578f4cda430deb2a8b4a91b
|
||||||
lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79
|
lib/nc-make-jump.sh 08a0bc58a299c95c60a59a5202792daf0ada3a8a0be7dc1b4cccc5724f5c9c79
|
||||||
|
lib/nc-provision-jumps.sh cf80abe572a4eb241b351363ffa85406829f0c458882dc8d14f1628c458432f8
|
||||||
lib/nc-msgs.sh 20517922d1153ec7827c833987497fb305d087b579911d1b9067d65ae156a19f
|
lib/nc-msgs.sh 20517922d1153ec7827c833987497fb305d087b579911d1b9067d65ae156a19f
|
||||||
lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880
|
lib/nc-document.sh 47211e99089c0446d25a1e84545a734894720a1c9ad8f59b920332035e4ea880
|
||||||
lib/nc-revisions.sh c27856f7decfc4c2e2c990f59eb20136fdff9cf0a52b9d9fbd9370613666a802
|
lib/nc-revisions.sh c27856f7decfc4c2e2c990f59eb20136fdff9cf0a52b9d9fbd9370613666a802
|
||||||
|
|||||||
49
larry.sh
49
larry.sh
@ -78,7 +78,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.8.31"
|
LARRY_VERSION="0.8.32"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -353,6 +353,7 @@ nc-create-thread.sh|High-level: create a new thread in a NetConfig (and optional
|
|||||||
nc-set-field.sh|Change ONE settable field (PORT, HOST/IP, PROCESSNAME, ENCODING) on an existing thread — anchored to the right block, journaled, rollback-reversible
|
nc-set-field.sh|Change ONE settable field (PORT, HOST/IP, PROCESSNAME, ENCODING) on an existing thread — anchored to the right block, journaled, rollback-reversible
|
||||||
nc-insert-protocol.sh|Low-level write side: insert/replace a protocol block in a NetConfig
|
nc-insert-protocol.sh|Low-level write side: insert/replace a protocol block in a NetConfig
|
||||||
nc-make-jump.sh|Generate the 3-thread "jump" pattern for cross-environment data replay
|
nc-make-jump.sh|Generate the 3-thread "jump" pattern for cross-environment data replay
|
||||||
|
nc-provision-jumps.sh|CAPSTONE: point at a SITE and build server_jump thread sets for ALL inbound roots (routes the existing-env inbound feed to a new env). Composes nc-inbound + nc-make-jump + nc-insert-protocol; whole batch under ONE journal session → single --session rollback. --dry-run previews the full plan; --confirm yes writes
|
||||||
nc-tclgen.sh|Generate annotated TCL UPOC scaffolding (skeletons for common Cloverleaf proc patterns)
|
nc-tclgen.sh|Generate annotated TCL UPOC scaffolding (skeletons for common Cloverleaf proc patterns)
|
||||||
nc-document.sh|Generate a markdown knowledge entry documenting a Cloverleaf subsystem/interface
|
nc-document.sh|Generate a markdown knowledge entry documenting a Cloverleaf subsystem/interface
|
||||||
#Diff & regression
|
#Diff & regression
|
||||||
@ -1861,6 +1862,44 @@ tool_nc_regression() {
|
|||||||
LARRY_LIB_DIR="$LARRY_LIB_DIR" "$LARRY_LIB_DIR/nc-regression.sh" "${args[@]}" 2>&1
|
LARRY_LIB_DIR="$LARRY_LIB_DIR" "$LARRY_LIB_DIR/nc-regression.sh" "${args[@]}" 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# nc_provision_jumps — CAPSTONE. Point at a SITE and build the cross-environment
|
||||||
|
# server_jump thread set for EVERY inbound (root) thread, routing the existing-env
|
||||||
|
# inbound feed into a new environment. PURE COMPOSITION of validated tools
|
||||||
|
# (nc-inbound enumerate, nc-parse read port/encoding, nc-make-jump generate the
|
||||||
|
# 3-thread set + splice route, nc-insert-protocol persist) — reimplements nothing.
|
||||||
|
# THE WHOLE BATCH writes under ONE journal session, so larry-rollback.sh
|
||||||
|
# --session <id> undoes the entire provisioning run byte-identical in one shot.
|
||||||
|
# dry_run previews the FULL plan (N inbounds → 3N threads + N splice routes)
|
||||||
|
# without writing; confirm must be 'yes' to persist. The provisioned inbound-root
|
||||||
|
# list is emitted as a regression-ready scope (handoff to nc_regression, already
|
||||||
|
# shipped). Inherits LARRY_SESSION_ID so journal entries group with the session.
|
||||||
|
tool_nc_provision_jumps() {
|
||||||
|
local site="$1" new_host="$2" new_port_base="$3" hciroot="${4:-${HCIROOT:-}}"
|
||||||
|
local netconfig="${5:-}" new_netconfig="${6:-}" scope="${7:-tcp-listen}" filter="${8:-}"
|
||||||
|
local inbound_host="${9:-127.0.0.1}" process_jump="${10:-server_jump}"
|
||||||
|
local dry_run="${11:-0}" confirm="${12:-}" format="${13:-text}"
|
||||||
|
_lib_err_if_missing || return
|
||||||
|
[ -n "$new_host" ] && [ -n "$new_port_base" ] \
|
||||||
|
|| { echo "ERROR: nc_provision_jumps needs new_host and new_port_base"; return 1; }
|
||||||
|
[ -n "$site" ] || [ -n "$netconfig" ] \
|
||||||
|
|| { echo "ERROR: nc_provision_jumps needs site (or an explicit netconfig)"; return 1; }
|
||||||
|
local args=(--new-host "$new_host" --new-port-base "$new_port_base" \
|
||||||
|
--scope "$scope" --inbound-host "$inbound_host" \
|
||||||
|
--process-jump "$process_jump" --format "$format")
|
||||||
|
[ -n "$site" ] && args+=(--site "$site")
|
||||||
|
[ -n "$hciroot" ] && args+=(--hciroot "$hciroot")
|
||||||
|
[ -n "$netconfig" ] && args+=(--netconfig "$netconfig")
|
||||||
|
[ -n "$new_netconfig" ] && args+=(--new-netconfig "$new_netconfig")
|
||||||
|
[ -n "$filter" ] && args+=(--filter "$filter")
|
||||||
|
if [ "$dry_run" = "1" ]; then
|
||||||
|
args+=(--dry-run)
|
||||||
|
elif [ "$confirm" = "yes" ]; then
|
||||||
|
args+=(--confirm yes)
|
||||||
|
fi
|
||||||
|
LARRY_SESSION_ID="${LARRY_SESSION_ID:-$SESSION_ID}" \
|
||||||
|
"$LARRY_LIB_DIR/nc-provision-jumps.sh" "${args[@]}" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
tool_hl7_diff() {
|
tool_hl7_diff() {
|
||||||
local left_path="$1" right_path="$2" ignore="${3:-MSH.7}" include="${4:-}" format="${5:-text}"
|
local left_path="$1" right_path="$2" ignore="${3:-MSH.7}" include="${4:-}" format="${5:-text}"
|
||||||
_lib_err_if_missing || return
|
_lib_err_if_missing || return
|
||||||
@ -4423,6 +4462,12 @@ execute_tool() {
|
|||||||
"$(J '.route_test_cmd // ""')" "$(J '.ignore // "MSH.7"')" \
|
"$(J '.route_test_cmd // ""')" "$(J '.ignore // "MSH.7"')" \
|
||||||
"$(J '.phase // "all"')" "$(J '.dry_run // 0' | sed "s/false/0/;s/true/1/")" \
|
"$(J '.phase // "all"')" "$(J '.dry_run // 0' | sed "s/false/0/;s/true/1/")" \
|
||||||
"$(J '.source_ssh_alias // ""')" "$(J '.target_ssh_alias // ""')" ;;
|
"$(J '.source_ssh_alias // ""')" "$(J '.target_ssh_alias // ""')" ;;
|
||||||
|
nc_provision_jumps) tool_nc_provision_jumps "$(J '.site // ""')" "$(J '.new_host')" "$(J '.new_port_base')" \
|
||||||
|
"$(J '.hciroot // ""')" "$(J '.netconfig // ""')" "$(J '.new_netconfig // ""')" \
|
||||||
|
"$(J '.scope // "tcp-listen"')" "$(J '.filter // ""')" \
|
||||||
|
"$(J '.inbound_host // "127.0.0.1"')" "$(J '.process_jump // "server_jump"')" \
|
||||||
|
"$(J '.dry_run // 0' | sed "s/false/0/;s/true/1/")" \
|
||||||
|
"$(J '.confirm // ""')" "$(J '.format // "text"')" ;;
|
||||||
lesson_record) tool_lesson_record "$(J '.text')" "$(J '.topic // ""')" "$(J '.site // ""')" "$(J '.severity // "info"')" ;;
|
lesson_record) tool_lesson_record "$(J '.text')" "$(J '.topic // ""')" "$(J '.site // ""')" "$(J '.severity // "info"')" ;;
|
||||||
hl7_sanitize) tool_hl7_sanitize "$(J '.input_path')" "$(J '.strict // 0' | sed "s/false/0/;s/true/1/")" ;;
|
hl7_sanitize) tool_hl7_sanitize "$(J '.input_path')" "$(J '.strict // 0' | sed "s/false/0/;s/true/1/")" ;;
|
||||||
ssh_exec) tool_ssh_exec "$(J '.alias')" "$(J '.command')" "$(J '.max_lines // 500')" ;;
|
ssh_exec) tool_ssh_exec "$(J '.alias')" "$(J '.command')" "$(J '.max_lines // 500')" ;;
|
||||||
@ -4491,6 +4536,8 @@ TOOLS_JSON=$(cat <<'TOOLS_END'
|
|||||||
|
|
||||||
{"name":"nc_regression","description":"End-to-end regression testing between two Cloverleaf environments. 6 phases: discover inbounds in scope, sample N messages per inbound from env-A smatdbs, run route_test on env-A, run route_test on env-B with same inputs, hl7_diff every paired output file, compile summary report. Phases 3/4 require the Cloverleaf route_test command; pass it via route_test_cmd with placeholders {THREAD} {INPUT} {OUTPUT_DIR} {HCIROOT} {HCISITE}. If route_test_cmd is empty, phases 3/4 are skipped and you can run them manually using the generated input files. For cross-env regression testing across SSH-aliased hosts, set source_ssh_alias and target_ssh_alias to existing SSH aliases (run ssh_status to list them first). When set, phases 1–4 run remotely via ssh_exec + ssh_pull/ssh_push; phases 5–6 stay local. env_a / env_b remain the HCIROOT paths AS SEEN ON THE REMOTE for that alias.","input_schema":{"type":"object","properties":{"scope":{"type":"string","description":"thread:NAME | threads:N1,N2 | site (needs site_a) | server (all sites)"},"count":{"type":"integer","description":"Messages to sample per inbound. Default 10."},"env_a":{"type":"string","description":"HCIROOT of env-A (the test/source env). If source_ssh_alias is set, this is the remote-side path."},"site_a":{"type":"string","description":"Site name on env-A. Required if scope=site."},"env_b":{"type":"string","description":"HCIROOT of env-B (the prod/target env). If target_ssh_alias is set, this is the remote-side path."},"site_b":{"type":"string","description":"Site name on env-B."},"out":{"type":"string","description":"LOCAL output root directory for inputs, outputs, diffs, and summary."},"route_test_cmd":{"type":"string","description":"Command template for invoking route_test. Use {THREAD} {INPUT} {OUTPUT_DIR} {HCIROOT} {HCISITE} as placeholders."},"ignore":{"type":"string","description":"hl7_diff ignore list. Default MSH.7."},"phase":{"type":"string","enum":["1","2","3","4","5","6","all"],"description":"Run a specific phase or all. Default all."},"dry_run":{"type":"integer","description":"1 = print what would happen, do not execute. Default 0."},"source_ssh_alias":{"type":"string","description":"SSH alias for the env-A (source) host. When set, phases 1–3 run remotely. Master must be open (ssh_status). Default empty = local."},"target_ssh_alias":{"type":"string","description":"SSH alias for the env-B (target) host. When set, phase 4 runs remotely. Master must be open. Default empty = local."}},"required":["scope","env_a","env_b","out"]}},
|
{"name":"nc_regression","description":"End-to-end regression testing between two Cloverleaf environments. 6 phases: discover inbounds in scope, sample N messages per inbound from env-A smatdbs, run route_test on env-A, run route_test on env-B with same inputs, hl7_diff every paired output file, compile summary report. Phases 3/4 require the Cloverleaf route_test command; pass it via route_test_cmd with placeholders {THREAD} {INPUT} {OUTPUT_DIR} {HCIROOT} {HCISITE}. If route_test_cmd is empty, phases 3/4 are skipped and you can run them manually using the generated input files. For cross-env regression testing across SSH-aliased hosts, set source_ssh_alias and target_ssh_alias to existing SSH aliases (run ssh_status to list them first). When set, phases 1–4 run remotely via ssh_exec + ssh_pull/ssh_push; phases 5–6 stay local. env_a / env_b remain the HCIROOT paths AS SEEN ON THE REMOTE for that alias.","input_schema":{"type":"object","properties":{"scope":{"type":"string","description":"thread:NAME | threads:N1,N2 | site (needs site_a) | server (all sites)"},"count":{"type":"integer","description":"Messages to sample per inbound. Default 10."},"env_a":{"type":"string","description":"HCIROOT of env-A (the test/source env). If source_ssh_alias is set, this is the remote-side path."},"site_a":{"type":"string","description":"Site name on env-A. Required if scope=site."},"env_b":{"type":"string","description":"HCIROOT of env-B (the prod/target env). If target_ssh_alias is set, this is the remote-side path."},"site_b":{"type":"string","description":"Site name on env-B."},"out":{"type":"string","description":"LOCAL output root directory for inputs, outputs, diffs, and summary."},"route_test_cmd":{"type":"string","description":"Command template for invoking route_test. Use {THREAD} {INPUT} {OUTPUT_DIR} {HCIROOT} {HCISITE} as placeholders."},"ignore":{"type":"string","description":"hl7_diff ignore list. Default MSH.7."},"phase":{"type":"string","enum":["1","2","3","4","5","6","all"],"description":"Run a specific phase or all. Default all."},"dry_run":{"type":"integer","description":"1 = print what would happen, do not execute. Default 0."},"source_ssh_alias":{"type":"string","description":"SSH alias for the env-A (source) host. When set, phases 1–3 run remotely. Master must be open (ssh_status). Default empty = local."},"target_ssh_alias":{"type":"string","description":"SSH alias for the env-B (target) host. When set, phase 4 runs remotely. Master must be open. Default empty = local."}},"required":["scope","env_a","env_b","out"]}},
|
||||||
|
|
||||||
|
{"name":"nc_provision_jumps","description":"CAPSTONE provisioner — Bryan's ultimate goal. Point at a SITE and build the cross-environment 'server_jump' thread set for EVERY inbound (root) thread there, to route the existing-env inbound feed (e.g. the ADT feed) into a NEW environment. PURE COMPOSITION of the validated read+write tools — it reimplements nothing: nc_find_inbound enumerates the inbound roots; nc_protocol_nested/nc_protocol_field read each root's PROTOCOL.PORT + ENCODING; nc_make_jump generates the per-inbound 3-thread set (linux_<name>_out on the OLD/site NetConfig + windows_<name>_in & windows_<name>_out on the NEW-env server_jump NetConfig) plus the splice route onto the OLD inbound's DATAXLATE; nc_insert_protocol / nc_add_route persist them. PER-INBOUND LOOP is name-driven: tag = the inbound thread name, jump_port = new_port_base + index. THE KEY SAFETY PROPERTY — the ENTIRE batch (all 3N inserts + N splice routes) writes under ONE journal session, so larry-rollback.sh --session <id> undoes the WHOLE provisioning run byte-identical in a single shot (not per-thread). dry_run=true previews the COMPLETE plan (N inbounds → 3N new threads + N splice routes) and writes NOTHING; otherwise you MUST pass confirm=yes to actually write (no confirm = a plan-only refusal, still no writes). filter is a regex limiting which inbound roots are provisioned. Output is pipe-friendly and ends with a 'roots: <csv>' line — that provisioned-root list is directly consumable as the nc_regression scope (server / threads:<csv>, route_test_cmd via nc_engine route-test) for the capstone's SEPARATE regression half (already shipped). This tool does NOT run regression.","input_schema":{"type":"object","properties":{"site":{"type":"string","description":"The target site whose inbound roots get jump sets (resolved as $HCIROOT/<site>/NetConfig). Give this OR netconfig."},"new_host":{"type":"string","description":"Hostname/IP of the NEW (linux) env that each OLD-side linux_<name>_out TCPs to."},"new_port_base":{"type":"string","description":"Base TCP port for the OLD→NEW jump hops. Per-inbound jump_port = new_port_base + index (stable, collision-checked)."},"hciroot":{"type":"string","description":"Override $HCIROOT for site resolution."},"netconfig":{"type":"string","description":"Explicit OLD/site NetConfig path (overrides site-based resolution)."},"new_netconfig":{"type":"string","description":"NEW-env server_jump NetConfig (gets windows_<name>_in/_out). Default = the site NetConfig (self-contained single-file run)."},"scope":{"type":"string","enum":["tcp-listen","all"],"description":"Which inbound class to provision. tcp-listen (default) = upstream-fed TCP-listener roots (those with a PROTOCOL.PORT, which the jump pattern needs). all = include ICL/file inbounds too (most will be skipped — no listen port)."},"filter":{"type":"string","description":"Regex limiting which inbound root NAMES are provisioned."},"inbound_host":{"type":"string","description":"Host that each windows_<name>_out connects to on the NEW env (the cloned existing inbound). Default 127.0.0.1 (loopback)."},"process_jump":{"type":"string","description":"Process for the NEW-side server_jump threads. Default server_jump."},"dry_run":{"type":"boolean","description":"true = print the FULL plan (every inbound + every block/route that WOULD be created) and write NOTHING. Default false."},"confirm":{"type":"string","description":"Must be 'yes' to actually write. Anything else (or unset) = plan-only, no writes. The whole batch stays reversible via larry-rollback.sh --session."},"format":{"type":"string","enum":["text","tsv"],"description":"Summary format. text (default) = per-inbound plan blocks + rollback command; tsv = one row per inbound (inbound, orig_port, jump_port, encoding, process, the three generated thread names, route_splice)."}},"required":["new_host","new_port_base"]}},
|
||||||
|
|
||||||
{"name":"ssh_pull","description":"Pull a file from a remote SSH-aliased host to a local path via the existing ControlMaster (no second auth, no second TCP handshake). Use this BEFORE calling any local tool (read_file, nc_diff_interface, grep_files, hl7_diff, etc.) when the source file lives on a remote host. The local path returned by this tool is stable for re-use within and across turns — pulling the same remote_path again returns the same local_path. If local_path is omitted, a deterministic temp path /tmp/larry-pulls/<alias>.<basename>.<hash> is used. Verifies the master is open first; if not, fails with a clear message ('open the master with /ssh-setup <alias> first'). Validates the transferred size matches the remote stat.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"SSH alias (see ssh_status). Master must be open."},"remote_path":{"type":"string","description":"Absolute path on the remote host."},"local_path":{"type":"string","description":"Optional explicit local destination. If omitted, a deterministic /tmp/larry-pulls/<alias>.<basename>.<hash> path is used and printed in the tool result."}},"required":["alias","remote_path"]}},
|
{"name":"ssh_pull","description":"Pull a file from a remote SSH-aliased host to a local path via the existing ControlMaster (no second auth, no second TCP handshake). Use this BEFORE calling any local tool (read_file, nc_diff_interface, grep_files, hl7_diff, etc.) when the source file lives on a remote host. The local path returned by this tool is stable for re-use within and across turns — pulling the same remote_path again returns the same local_path. If local_path is omitted, a deterministic temp path /tmp/larry-pulls/<alias>.<basename>.<hash> is used. Verifies the master is open first; if not, fails with a clear message ('open the master with /ssh-setup <alias> first'). Validates the transferred size matches the remote stat.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"SSH alias (see ssh_status). Master must be open."},"remote_path":{"type":"string","description":"Absolute path on the remote host."},"local_path":{"type":"string","description":"Optional explicit local destination. If omitted, a deterministic /tmp/larry-pulls/<alias>.<basename>.<hash> path is used and printed in the tool result."}},"required":["alias","remote_path"]}},
|
||||||
|
|
||||||
{"name":"ssh_push","description":"Push a local file to a remote SSH-aliased host via the existing ControlMaster. Use for sending small input bundles to a remote env (e.g. regression-test input messages, a sanitized HL7 file to feed into route_test). Same multiplexing + error handling as ssh_pull. Validates remote-side size matches local size post-transfer.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"SSH alias (see ssh_status). Master must be open."},"local_path":{"type":"string","description":"Absolute local path to the file to send."},"remote_path":{"type":"string","description":"Absolute remote destination path."}},"required":["alias","local_path","remote_path"]}},
|
{"name":"ssh_push","description":"Push a local file to a remote SSH-aliased host via the existing ControlMaster. Use for sending small input bundles to a remote env (e.g. regression-test input messages, a sanitized HL7 file to feed into route_test). Same multiplexing + error handling as ssh_pull. Validates remote-side size matches local size post-transfer.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"SSH alias (see ssh_status). Master must be open."},"local_path":{"type":"string","description":"Absolute local path to the file to send."},"remote_path":{"type":"string","description":"Absolute remote destination path."}},"required":["alias","local_path","remote_path"]}},
|
||||||
|
|||||||
465
lib/nc-provision-jumps.sh
Executable file
465
lib/nc-provision-jumps.sh
Executable file
@ -0,0 +1,465 @@
|
|||||||
|
#!/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
|
||||||
Loading…
Reference in New Issue
Block a user