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:
@@ -535,17 +535,22 @@ if [[ "$PHASE1_ONLY" == true ]]; then
|
||||
echo " Duration: $(format_duration $(( END - START )))"
|
||||
echo ""
|
||||
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
|
||||
# exist on the far side. Before the plugin is installed there is nowhere to put the file, so
|
||||
# a skip here is the expected result of running Phase 1 first, not a fault to chase.
|
||||
# The push no longer waits for a Varaverk install on the far side. It resolves the partner's
|
||||
# conf directory across both layouts and creates the internal one if neither exists, so the
|
||||
# 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
|
||||
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
|
||||
echo " master.conf was NOT delivered — $MIRROR has no Varaverk install to receive it yet."
|
||||
echo " That is expected at this stage. Once the plugin is installed there, either:"
|
||||
echo " • re-run: bash Partnership/partnership_onboard.sh --phase1-only --skip-ssh"
|
||||
echo " master.conf was NOT delivered. Phase 1 seeds it into a bare host, so this is a"
|
||||
echo " real failure, not the pre-install state — check SSH and that /boot is writable"
|
||||
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, 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
|
||||
echo " When HOST2 completes its onboard, it will automatically trigger Phase 2 here."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
@@ -412,7 +412,12 @@ require_once '/usr/local/emhttp/plugins/varaverk/include/config.php';
|
||||
if (empty(\$results)) { echo 'no remote hosts'; exit(0); }
|
||||
\$failed = 0;
|
||||
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++;
|
||||
}
|
||||
exit(\$failed > 0 ? 1 : 0);
|
||||
|
||||
@@ -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') {
|
||||
$state = vv_setup_state_read();
|
||||
$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[] = [
|
||||
'id' => 'master_conf',
|
||||
'label' => 'master.conf',
|
||||
'ok' => $pulled,
|
||||
'ok' => $pulled || $pushed,
|
||||
'detail' => $pulled
|
||||
? 'Synced from HOST1'
|
||||
: ($host1 ? "Not yet pulled from $host1" : 'HOST1 hostname not set in master.conf'),
|
||||
'action' => (!$pulled && $host1) ? 'pull_master' : null,
|
||||
? "Pulled from $host1"
|
||||
: ($pushed
|
||||
? "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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,20 @@ unset($_vv_cfg);
|
||||
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
// 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,
|
||||
@@ -296,18 +310,32 @@ function vv_push_master_conf(): array {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Single SSH call: get remote SCRIPTS_DIR and verify the plugin is installed and
|
||||
// Configurations/ exists. Any missing piece means the remote isn't ready — skip rather
|
||||
// than push blind.
|
||||
// Single SSH call decides which directory on the remote receives the conf. Two layouts
|
||||
// exist and a third state — not installed yet — is the one this has to serve first.
|
||||
//
|
||||
// It deliberately does NOT require master.conf to already be there. Requiring it meant
|
||||
// this could only ever *update* a conf, never deliver one — and delivering is what Step 10
|
||||
// of partnership_onboard.sh exists to do. Every conf is gitignored, so a freshly installed
|
||||
// node has varaverk.cfg and Configurations/ (README.md is tracked, so the clone creates the
|
||||
// dir) but no master.conf, and the push skipped exactly the node it was meant to seed.
|
||||
// 1. varaverk.cfg names SCRIPTS_DIR and that install has a Configurations/ — the
|
||||
// remote has told us where it lives, and it outranks anything found by looking.
|
||||
// A node migrated to appdata can still have a stale master.conf on /boot; picking
|
||||
// the file over the declaration would write to the copy nothing reads.
|
||||
// 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
|
||||
// pushed into a directory that is not a Varaverk install.
|
||||
// Case 3 is the point of the whole function during Phase 1 of onboarding. The partner
|
||||
// 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
|
||||
// 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.
|
||||
$sshBase = 'ssh -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
|
||||
$remoteCmd = 'cfg=$(grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null)'
|
||||
. ' && sd=$(echo "$cfg" | grep -oP \'(?<=SCRIPTS_DIR=")[^"]+\')'
|
||||
. ' && test -d "${sd}/Configurations"'
|
||||
. ' && echo "$sd"';
|
||||
$probe = trim(shell_exec($sshBase . ' ' . escapeshellarg($remoteCmd)) ?: '');
|
||||
$remoteCmd = 'sd=$(grep -oP \'(?<=SCRIPTS_DIR=")[^"]+\' /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null); '
|
||||
. 'd=""; '
|
||||
. '[ -n "$sd" ] && [ -d "$sd/Configurations" ] && d="$sd/Configurations"; '
|
||||
. '[ -z "$d" ] && [ -f "' . VV_DIR_INTERNAL . '/Configurations/master.conf" ] && d="' . VV_DIR_INTERNAL . '/Configurations"; '
|
||||
. '[ -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,
|
||||
'error' => 'plugin not installed or Configurations/ missing — skipped'];
|
||||
'error' => 'no usable conf directory on the remote — SSH failed or /boot is not writable'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$remoteConf = rtrim($probe, '/') . '/Configurations';
|
||||
[$remoteConf, $mode] = explode('|', $probe, 2);
|
||||
$dest = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf');
|
||||
$cmd = 'scp -i ' . escapeshellarg($sshKey)
|
||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no'
|
||||
. ' ' . 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);
|
||||
$results[] = [
|
||||
'host' => $hostKey,
|
||||
'ok' => $rc === 0,
|
||||
'mode' => $mode, // 'seed' = delivered where there was nothing, 'update' = replaced
|
||||
'path' => $remoteConf,
|
||||
'error' => $rc !== 0 ? implode('; ', $out) : '',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -220,6 +220,16 @@ else
|
||||
fi
|
||||
|
||||
# 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"
|
||||
if [[ ! -f "$CONF_DIR/master.conf" && -f "$DEPLOY_DIR/master.conf.template" ]]; then
|
||||
cp "$DEPLOY_DIR/master.conf.template" "$CONF_DIR/master.conf"
|
||||
|
||||
Reference in New Issue
Block a user