129 lines
6.4 KiB
PHP
129 lines
6.4 KiB
PHP
<?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)]);
|