Deliver master.conf to a node that has no Varaverk on it yet, which is the node that needs it

Phase 1 exists to hand the partner its identity before the install, but the push refused any
host without varaverk.cfg — so the conf could only ever reach a node that no longer needed it
to be told who it was. It now resolves the remote's conf directory across both install layouts
and creates the internal one when neither exists.
This commit is contained in:
Gmer4Lfe
2026-08-16 16:14:23 -04:00
parent f627658567
commit 4f95cc6d13
5 changed files with 118 additions and 34 deletions
+13 -8
View File
@@ -535,17 +535,22 @@ if [[ "$PHASE1_ONLY" == true ]]; then
echo " Duration: $(format_duration $(( END - START )))" echo " Duration: $(format_duration $(( END - START )))"
echo "" echo ""
echo " HOST1 is fully set up. HOST2 ($MIRROR) can now install the Varaverk plugin." echo " HOST1 is fully set up. HOST2 ($MIRROR) can now install the Varaverk plugin."
# The push needs a Varaverk install to land in — varaverk.cfg and Configurations/ have to # The push no longer waits for a Varaverk install on the far side. It resolves the partner's
# exist on the far side. Before the plugin is installed there is nowhere to put the file, so # conf directory across both layouts and creates the internal one if neither exists, so the
# a skip here is the expected result of running Phase 1 first, not a fault to chase. # conf arrives BEFORE the plugin — which is the order that makes it useful. The .plg only
# seeds master.conf from the template when none is present, so what lands here survives the
# install and the wizard reads its identity straight out of it.
if [[ "$CONF_PUSH_OK" == true ]]; then if [[ "$CONF_PUSH_OK" == true ]]; then
echo " master.conf is on $MIRROR — the wizard will find it and take the partner path." echo " master.conf is on $MIRROR — the wizard will find it and take the partner path,"
echo " already knowing $MY_ID and $MIRROR_ID. If the operator picks flash storage there,"
echo " storage_migrate.sh moves the conf to appdata with the rest of the install."
else else
echo " master.conf was NOT delivered$MIRROR has no Varaverk install to receive it yet." echo " master.conf was NOT delivered. Phase 1 seeds it into a bare host, so this is a"
echo " That is expected at this stage. Once the plugin is installed there, either:" echo " real failure, not the pre-install state — check SSH and that /boot is writable"
echo " • re-run: bash Partnership/partnership_onboard.sh --phase1-only --skip-ssh" echo " on $MIRROR. Retry with:"
echo " • bash Partnership/partnership_onboard.sh --phase1-only --skip-ssh"
echo " • or push from Scheduler → master.conf → Save Conf" echo " • or push from Scheduler → master.conf → Save Conf"
echo " • or, from HOST2's Setup tab, use 'Pull from HOST1' on the master.conf row" echo " • or, once the plugin is installed, 'Pull from HOST1' on HOST2's Setup tab"
fi fi
echo " When HOST2 completes its onboard, it will automatically trigger Phase 2 here." echo " When HOST2 completes its onboard, it will automatically trigger Phase 2 here."
echo "━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━"
+6 -1
View File
@@ -412,7 +412,12 @@ require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
if (empty(\$results)) { echo 'no remote hosts'; exit(0); } if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
\$failed = 0; \$failed = 0;
foreach (\$results as \$r) { foreach (\$results as \$r) {
echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL; // 'seed' means the partner had no master.conf anywhere — say so, because that is the case
// where the push carried the mesh's identity to a node that could not have known it.
\$what = (\$r['mode'] ?? '') === 'seed'
? 'seeded → ' . (\$r['path'] ?? '')
: 'pushed → ' . (\$r['path'] ?? '');
echo \$r['host'] . ': ' . (\$r['ok'] ? \$what : 'FAILED — ' . \$r['error']) . PHP_EOL;
if (!\$r['ok']) \$failed++; if (!\$r['ok']) \$failed++;
} }
exit(\$failed > 0 ? 1 : 0); exit(\$failed > 0 ? 1 : 0);
+30 -7
View File
@@ -176,18 +176,41 @@ if (!empty($jfContainer)) {
]; ];
} }
// ── master.conf pull (partner servers only) ─────────────────────────────────────────────────── // ── master.conf delivery (partner servers only) ───────────────────────────────────────────────
//
// The question is whether this host HAS the owner's master.conf, not whether it went and
// fetched one. There are two ways it arrives and only one of them used to count:
//
// pull — this host ran 'Pull from HOST1', which sets master_conf_pulled in the setup state.
// push — HOST1 sent it during Phase 1 of onboarding, before this host even had Varaverk
// installed. Nothing on this side runs, so no flag is written here.
//
// Keying solely off the pull flag meant a node whose conf had been seeded by Phase 1 —
// populated, correct, naming both hosts — was told "Not yet pulled from HOST1" and offered a
// button to fetch what it already had.
//
// The push leaves its own evidence: Phase 1 ends by writing <THIS_HOST>_PHASE1_DONE into the
// owner's setup state and pushing that file here, so the flag can only be present on this
// machine because the owner completed a push AT this machine. That is the discriminator.
//
// A populated HOST1 line is deliberately NOT the test. The wizard's manual partner path writes
// HOST1 and HOST2 into the LOCAL template conf from what the operator typed, which would make
// a template look delivered and hide the one button that fixes it.
if ($hostId !== 'host1' && $hostId !== 'unknown') { if ($hostId !== 'host1' && $hostId !== 'unknown') {
$state = vv_setup_state_read(); $state = vv_setup_state_read();
$pulled = !empty($state['master_conf_pulled']); $pulled = !empty($state['master_conf_pulled']);
// Both spellings, for the same reason the partnership item below accepts both.
$pushed = !empty($state[$hostIdUp . '_PHASE1_DONE']) || !empty($state[$hostId . '_phase1_done']);
$items[] = [ $items[] = [
'id' => 'master_conf', 'id' => 'master_conf',
'label' => 'master.conf', 'label' => 'master.conf',
'ok' => $pulled, 'ok' => $pulled || $pushed,
'detail' => $pulled 'detail' => $pulled
? 'Synced from HOST1' ? "Pulled from $host1"
: ($host1 ? "Not yet pulled from $host1" : 'HOST1 hostname not set in master.conf'), : ($pushed
'action' => (!$pulled && $host1) ? 'pull_master' : null, ? "Seeded by $host1 — identity read from it"
: ($host1 ? "Not yet pulled from $host1" : 'HOST1 hostname not set in master.conf')),
'action' => (!$pulled && !$pushed && $host1) ? 'pull_master' : null,
]; ];
} }
+59 -18
View File
@@ -162,6 +162,20 @@ unset($_vv_cfg);
define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db'); define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db');
// ── The two install layouts ───────────────────────────────────────────────────
// The same pair storage_migrate.sh moves an installation between, and the same pair
// HOST*_STORAGE_MODE_INTERNAL selects: true → internal, false → flash. They are stated here
// because vv_push_master_conf() has to reason about a *remote* host's layout, where SCRIPTS_DIR
// describes this host and says nothing about the partner's — and about a partner that has no
// layout yet because Varaverk is not installed on it.
//
// Naming follows the conf, which names the boot device rather than the destination: "internal"
// is a host that boots from internal NVMe/SSD and can therefore keep everything on /boot;
// "flash" is a host booting from a USB stick, which must not take the writes, so its data lives
// in appdata instead.
define('VV_DIR_INTERNAL', '/boot/config/plugins/varaverk');
define('VV_DIR_FLASH', '/mnt/user/appdata/Varaverk');
// ── Cache roots ─────────────────────────────────────────────────────────────── // ── Cache roots ───────────────────────────────────────────────────────────────
// Read from master.conf so this layer and load_config.sh resolve the same paths from the same // Read from master.conf so this layer and load_config.sh resolve the same paths from the same
// line. They used to be literals in three PHP files, restating what load_config.sh already said, // line. They used to be literals in three PHP files, restating what load_config.sh already said,
@@ -296,18 +310,32 @@ function vv_push_master_conf(): array {
continue; continue;
} }
// Single SSH call: get remote SCRIPTS_DIR and verify the plugin is installed and // Single SSH call decides which directory on the remote receives the conf. Two layouts
// Configurations/ exists. Any missing piece means the remote isn't ready — skip rather // exist and a third state — not installed yet — is the one this has to serve first.
// than push blind.
// //
// It deliberately does NOT require master.conf to already be there. Requiring it meant // 1. varaverk.cfg names SCRIPTS_DIR and that install has a Configurations/ — the
// this could only ever *update* a conf, never deliver one — and delivering is what Step 10 // remote has told us where it lives, and it outranks anything found by looking.
// of partnership_onboard.sh exists to do. Every conf is gitignored, so a freshly installed // A node migrated to appdata can still have a stale master.conf on /boot; picking
// node has varaverk.cfg and Configurations/ (README.md is tracked, so the clone creates the // the file over the declaration would write to the copy nothing reads.
// dir) but no master.conf, and the push skipped exactly the node it was meant to seed. // 2. no varaverk.cfg — look for an existing master.conf in the internal layout, then
// the flash layout (the same two roots storage_migrate.sh moves between). A
// half-installed or part-migrated node is found this way.
// 3. neither — SEED. mkdir the internal path and deliver there.
// //
// The two surviving checks still refuse a bare or half-installed host, so nothing is // Case 3 is the point of the whole function during Phase 1 of onboarding. The partner
// pushed into a directory that is not a Varaverk install. // has no Varaverk at all yet, and the conf is what tells its installer who HOST1 and
// HOST2 are, so the wizard can skip the identity questions it would otherwise ask about
// a mesh it is already a member of. Refusing to push until the plugin was installed
// made that impossible: the information had to arrive first to be useful.
//
// Internal is the seed target because /boot is mounted before the array is and the .plg
// installs there unconditionally — appdata may not exist yet on a node whose array has
// never started. If the operator then chooses flash in the wizard, storage_migrate.sh
// carries the conf across with the rest of the install.
//
// The .plg only seeds master.conf from the template when none is present, so a conf
// delivered ahead of the install survives it, as does `git reset --hard` — every conf
// is gitignored and therefore untracked.
// //
// NOTE: this sends the owner's master.conf as-is, credentials included — TAILSCALE_API_KEY // NOTE: this sends the owner's master.conf as-is, credentials included — TAILSCALE_API_KEY
// and WEBHOOK_SECRET among them. That is the accepted trade for one shared config across // and WEBHOOK_SECRET among them. That is the accepted trade for one shared config across
@@ -318,27 +346,40 @@ function vv_push_master_conf(): array {
// of on the remote host. escapeshellarg() keeps it opaque until the remote shell runs it. // of on the remote host. escapeshellarg() keeps it opaque until the remote shell runs it.
$sshBase = 'ssh -i ' . escapeshellarg($sshKey) $sshBase = 'ssh -i ' . escapeshellarg($sshKey)
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip; . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
$remoteCmd = 'cfg=$(grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null)' $remoteCmd = 'sd=$(grep -oP \'(?<=SCRIPTS_DIR=")[^"]+\' /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null); '
. ' && sd=$(echo "$cfg" | grep -oP \'(?<=SCRIPTS_DIR=")[^"]+\')' . 'd=""; '
. ' && test -d "${sd}/Configurations"' . '[ -n "$sd" ] && [ -d "$sd/Configurations" ] && d="$sd/Configurations"; '
. ' && echo "$sd"'; . '[ -z "$d" ] && [ -f "' . VV_DIR_INTERNAL . '/Configurations/master.conf" ] && d="' . VV_DIR_INTERNAL . '/Configurations"; '
$probe = trim(shell_exec($sshBase . ' ' . escapeshellarg($remoteCmd)) ?: ''); . '[ -z "$d" ] && [ -f "' . VV_DIR_FLASH . '/Configurations/master.conf" ] && d="' . VV_DIR_FLASH . '/Configurations"; '
. '[ -z "$d" ] && { mkdir -p "' . VV_DIR_INTERNAL . '/Configurations" || exit 1; d="' . VV_DIR_INTERNAL . '/Configurations"; }; '
. 'if [ -f "$d/master.conf" ]; then echo "$d|update"; else echo "$d|seed"; fi';
// Last non-empty line only. Anything the remote's login shell prints of its own accord —
// a profile banner, an MOTD echoed to stdout — arrives ahead of the answer, and taking
// the whole output would build an scp destination out of it.
$probeRaw = trim(shell_exec($sshBase . ' ' . escapeshellarg($remoteCmd)) ?: '');
$probeLines = array_filter(array_map('trim', explode("\n", $probeRaw)), fn($l) => $l !== '');
$probe = $probeLines ? end($probeLines) : '';
if ($probe === '') { if ($probe === '' || !str_contains($probe, '|')) {
$results[] = ['host' => $hostKey, 'ok' => false, 'ready' => false, $results[] = ['host' => $hostKey, 'ok' => false, 'ready' => false,
'error' => 'plugin not installed or Configurations/ missing — skipped']; 'error' => 'no usable conf directory on the remote — SSH failed or /boot is not writable'];
continue; continue;
} }
$remoteConf = rtrim($probe, '/') . '/Configurations'; [$remoteConf, $mode] = explode('|', $probe, 2);
$dest = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf'); $dest = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
$cmd = 'scp -i ' . escapeshellarg($sshKey) $cmd = 'scp -i ' . escapeshellarg($sshKey)
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no' . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
. ' ' . escapeshellarg($localPath) . ' ' . $dest . ' 2>&1'; . ' ' . escapeshellarg($localPath) . ' ' . $dest . ' 2>&1';
// exec() APPENDS to its output array. Left uncleared, a failure on one host would be
// reported again as part of the next host's error.
$out = [];
exec($cmd, $out, $rc); exec($cmd, $out, $rc);
$results[] = [ $results[] = [
'host' => $hostKey, 'host' => $hostKey,
'ok' => $rc === 0, 'ok' => $rc === 0,
'mode' => $mode, // 'seed' = delivered where there was nothing, 'update' = replaced
'path' => $remoteConf,
'error' => $rc !== 0 ? implode('; ', $out) : '', 'error' => $rc !== 0 ? implode('; ', $out) : '',
]; ];
} }
+10
View File
@@ -220,6 +220,16 @@ else
fi fi
# Seed master.conf from template if absent. # Seed master.conf from template if absent.
#
# "If absent" is load-bearing, not defensive. Phase 1 of partnership onboarding pushes the
# owner's master.conf to this host BEFORE the plugin is installed — it lands in exactly this
# directory, which the push creates. That conf already names HOST1 and HOST2, which is how the
# setup wizard knows this machine's identity without asking for it. Overwriting it with the
# template here would replace a populated conf with one whose HOSTn slots are all empty and
# throw the wizard back to the questions the push exists to answer.
#
# The clone above cannot destroy it either: every conf is gitignored, so it is untracked and
# `git reset --hard` leaves it alone.
mkdir -p "$CONF_DIR" mkdir -p "$CONF_DIR"
if [[ ! -f "$CONF_DIR/master.conf" && -f "$DEPLOY_DIR/master.conf.template" ]]; then if [[ ! -f "$CONF_DIR/master.conf" && -f "$DEPLOY_DIR/master.conf.template" ]]; then
cp "$DEPLOY_DIR/master.conf.template" "$CONF_DIR/master.conf" cp "$DEPLOY_DIR/master.conf.template" "$CONF_DIR/master.conf"