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
+59 -18
View File
@@ -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) : '',
];
}