Rebuild the Fallback tab around what the daemon is actually doing, not what its last state file said
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Start and stop the fallback daemon, its dry-run preview, and the failover test harness, on
|
||||
// this host or on a partner. The Fallback tab's only write surface.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Every action shells out to the script that already owns the operation — fallback.sh --stop
|
||||
// and fallback_test.sh --stop — rather than signalling PIDs from PHP. Those two know things
|
||||
// this layer must not have to: which lock holds the process, how long to wait, and, for the
|
||||
// test, that SIGKILL must never be used because only its EXIT trap removes the iptables rule
|
||||
// it installed.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// The scripts own stopping; this endpoint owns routing.
|
||||
// Duplicating the escalation logic here would put a second, divergent implementation of
|
||||
// "how to stop fallback safely" in a language that cannot run its traps.
|
||||
//
|
||||
// Start is dry-run only.
|
||||
// The live daemon is started by array_started.sh at array start, which is the one context
|
||||
// where the tier delays and the state file mean what they say. A button that launched a
|
||||
// live failover monitor mid-session — against a partner mid-maintenance, from a page
|
||||
// someone was reading — is not a button this page should have. Previewing is safe and is
|
||||
// what the page is for.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// POST only. Unraid's CSRF token injector is jQuery-only, so a native fetch() GET would fail
|
||||
// silently anyway; making these POST means a link or prefetch cannot stop a daemon.
|
||||
//
|
||||
// host is matched against the configured host list, never used as a path or a shell word.
|
||||
// The slot resolves to a hostname from conf, then to a Tailscale IP through the same
|
||||
// unambiguous-prefix resolver the rest of the mesh uses. A value that does not name a
|
||||
// configured host is refused before anything runs.
|
||||
//
|
||||
// setsid, not nohup+&, for the dry run.
|
||||
// A backgrounded child stays in php-fpm's process group, and the group kill that ends a
|
||||
// request takes it with it. setsid detaches it into its own session so it survives the
|
||||
// response — the same fix the Scheduler's Stop button needed.
|
||||
//
|
||||
// Output is captured and returned, not discarded.
|
||||
// These scripts report refusals in words — "did not exit within 30s", "NOT force-killing"
|
||||
// — and a boolean would throw away the only explanation the operator gets.
|
||||
//
|
||||
// REQUEST
|
||||
// POST action=stop|stop_test|start_dry|clear_lock host=<slot>
|
||||
//
|
||||
// RESPONSE
|
||||
// {"ok":true,"output":string} action ran; output is the script's own report
|
||||
// {"ok":false,"error":string} bad method, unknown action, or unresolvable host
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/fallback.php vv_pt_peer_lookup(), vv_pt_ts_peers(), vv_pt_ssh()
|
||||
// Fallback/fallback.sh --stop
|
||||
// Fallback/fallback_test.sh --stop
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
require_once dirname(__DIR__) . '/include/fallback.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
$slot = strtolower((string)($_POST['host'] ?? ''));
|
||||
|
||||
$allowed = ['stop', 'stop_test', 'start_dry', 'clear_lock'];
|
||||
if (!in_array($action, $allowed, true)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown action']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$hosts = vv_fb_known_hosts();
|
||||
if (!isset($hosts[$slot])) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown host']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$isMe = ($slot === vv_detect_host());
|
||||
$scripts = rtrim(SCRIPTS_DIR, '/');
|
||||
|
||||
// The command, as the script that owns the operation would be invoked by hand.
|
||||
$cmds = [
|
||||
'stop' => 'bash ' . escapeshellarg("$scripts/Fallback/fallback.sh") . ' --stop 2>&1',
|
||||
'stop_test' => 'bash ' . escapeshellarg("$scripts/Fallback/fallback_test.sh") . ' --stop 2>&1',
|
||||
// setsid so it outlives this request; own log so the page can show what the preview said.
|
||||
'start_dry' => 'setsid bash ' . escapeshellarg("$scripts/Fallback/fallback.sh")
|
||||
. ' --dry-run --log > /tmp/varaverk/fallback_dryrun.log 2>&1 < /dev/null & echo started',
|
||||
'clear_lock' => 'rm -f /tmp/unraid_locks/fallback.lock /tmp/unraid_locks/fallback_test.lock && echo cleared',
|
||||
];
|
||||
|
||||
if ($isMe) {
|
||||
@mkdir('/tmp/varaverk', 0755, true);
|
||||
$out = (string)shell_exec($cmds[$action]);
|
||||
echo json_encode(['ok' => true, 'output' => trim($out)]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Remote: same command, same script, over the SSH this file's neighbours already use.
|
||||
$tsPeers = vv_pt_ts_peers();
|
||||
$ts = vv_pt_peer_lookup($tsPeers, $hosts[$slot]);
|
||||
$ip = $ts['ip'] ?? null;
|
||||
|
||||
$myId = strtoupper(vv_detect_host());
|
||||
$sshKey = vv_fb_scalar(vv_read_conf_raw(vv_detect_host() . '.conf'), $myId . '_SSH_KEY');
|
||||
|
||||
if (!$ip || !$sshKey) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Partner not resolvable — no Tailscale IP or no SSH key']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// The remote's SCRIPTS_DIR is not this host's: appdata mode on one side and flash on the other
|
||||
// is the normal case on this mesh, so ask the partner where it keeps them.
|
||||
$remoteDir = trim((string)vv_pt_ssh($ip, $sshKey,
|
||||
'sed -n \'s/^SCRIPTS_DIR="\(.*\)"$/\1/p\' /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null'));
|
||||
if ($remoteDir === '') $remoteDir = '/boot/config/plugins/varaverk';
|
||||
|
||||
$remoteCmds = [
|
||||
'stop' => "bash '$remoteDir/Fallback/fallback.sh' --stop 2>&1",
|
||||
'stop_test' => "bash '$remoteDir/Fallback/fallback_test.sh' --stop 2>&1",
|
||||
'start_dry' => "mkdir -p /tmp/varaverk; setsid bash '$remoteDir/Fallback/fallback.sh'"
|
||||
. " --dry-run --log > /tmp/varaverk/fallback_dryrun.log 2>&1 < /dev/null & echo started",
|
||||
'clear_lock' => 'rm -f /tmp/unraid_locks/fallback.lock /tmp/unraid_locks/fallback_test.lock && echo cleared',
|
||||
];
|
||||
|
||||
$out = vv_pt_ssh($ip, $sshKey, $remoteCmds[$action]);
|
||||
echo json_encode(['ok' => true, 'output' => trim((string)$out)]);
|
||||
Reference in New Issue
Block a user