Route every conf writer through the guarded path
Eleven call sites wrote master.conf with tmp+rename and nothing else — no backup, no parse check, no audit — including the two toggles the UI uses most and the raw editor that installs a whole hand-edited file.
This commit is contained in:
@@ -43,10 +43,17 @@
|
|||||||
// outside one, so a matching string in a comment or an unrelated variable cannot be
|
// outside one, so a matching string in a comment or an unrelated variable cannot be
|
||||||
// rewritten.
|
// rewritten.
|
||||||
//
|
//
|
||||||
// The write is atomic.
|
// The write is atomic, backed up, verified and logged.
|
||||||
// vv_conf_toggle_script() writes through vv_write_conf_raw() (tmp + rename). Every
|
// vv_conf_toggle_script() goes through vv_conf_edit(), the one guarded conf write path:
|
||||||
// script sources master.conf; a truncated write here would be a system-wide outage
|
// an exclusive lock, a timestamped copy into CONF_BACKUP_DIR, bash -n on the candidate,
|
||||||
// rather than a lost toggle.
|
// tmp + rename to install it, then the installed file is sourced to prove it still loads.
|
||||||
|
// There is no single key to read back for a commented array member, so a clean source is
|
||||||
|
// the whole assertion. Every script sources master.conf; a truncated or unparseable write
|
||||||
|
// here would be a system-wide outage rather than a lost toggle.
|
||||||
|
//
|
||||||
|
// A script in no array writes nothing at all.
|
||||||
|
// The rewrite returns the contents unchanged, which reports success without taking a
|
||||||
|
// backup or touching the file. "Already in the requested state" is not a write.
|
||||||
//
|
//
|
||||||
// REQUEST
|
// REQUEST
|
||||||
// POST id=<Category/script.sh> enabled=0|1
|
// POST id=<Category/script.sh> enabled=0|1
|
||||||
@@ -56,7 +63,7 @@
|
|||||||
// {"ok":false,"error":"POST only"|"Invalid id"|"Failed to write master.conf"}
|
// {"ok":false,"error":"POST only"|"Invalid id"|"Failed to write master.conf"}
|
||||||
//
|
//
|
||||||
// DEPENDS ON
|
// DEPENDS ON
|
||||||
// include/scheduler.php vv_conf_toggle_script() → vv_write_conf_raw()
|
// include/scheduler.php vv_conf_toggle_script() → vv_conf_edit() → vv_write_conf_raw()
|
||||||
// Configurations/master.conf the *_SCRIPTS arrays
|
// Configurations/master.conf the *_SCRIPTS arrays
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|||||||
@@ -38,13 +38,17 @@
|
|||||||
// is the difference between a rejected save and a silent, total outage, so a file that
|
// is the difference between a rejected save and a silent, total outage, so a file that
|
||||||
// does not parse is refused and the previous version is left untouched.
|
// does not parse is refused and the previous version is left untouched.
|
||||||
//
|
//
|
||||||
// The temp copy is created with tempnam() and always removed.
|
// Checked here via vv_conf_syntax_error() only so the editor can show bash's own
|
||||||
// The candidate is never written next to the real conf and never under a predictable
|
// complaint with a line number. vv_conf_edit() checks again before installing; this one
|
||||||
// name, so a failed validation cannot leave a stray file for a script to source.
|
// is for the message, not the decision.
|
||||||
//
|
//
|
||||||
// The real write is atomic.
|
// The write goes through the one guarded conf path.
|
||||||
// vv_write_conf_raw() writes .vv.tmp and rename()s, so a script sourcing the conf
|
// vv_conf_edit() takes an exclusive lock, copies the previous file into CONF_BACKUP_DIR,
|
||||||
// during the save reads either the old file or the new one, never a half-written one.
|
// re-checks the syntax, installs via .vv.tmp + rename() so a concurrent reader sees the
|
||||||
|
// old file or the new one but never a half-written one, then sources the installed file
|
||||||
|
// to prove it still loads and restores the backup if it does not. The whole-file nature
|
||||||
|
// of this endpoint is why that matters most here: there is no key to verify, so a clean
|
||||||
|
// source is the only assertion available.
|
||||||
//
|
//
|
||||||
// REQUEST
|
// REQUEST
|
||||||
// POST file=<allowed conf name> content=<full file text>
|
// POST file=<allowed conf name> content=<full file text>
|
||||||
@@ -54,10 +58,12 @@
|
|||||||
// {"ok":false,"error":"POST only"|"File not permitted"|"Syntax error: …"|"Failed to write file"}
|
// {"ok":false,"error":"POST only"|"File not permitted"|"Syntax error: …"|"Failed to write file"}
|
||||||
//
|
//
|
||||||
// DEPENDS ON
|
// DEPENDS ON
|
||||||
// include/config.php vv_get_conf_files(), vv_write_conf_raw(), CONF_DIR
|
// include/config.php vv_get_conf_files(), CONF_DIR
|
||||||
|
// include/confform.php vv_conf_syntax_error(), vv_conf_edit()
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once dirname(__DIR__) . '/include/config.php';
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
require_once dirname(__DIR__) . '/include/confform.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||||
@@ -75,22 +81,14 @@ if (!$file || !in_array($file, $allowed)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Every script sources these. A syntax error here takes the whole system down, so the
|
// Every script sources these. A syntax error here takes the whole system down, so the
|
||||||
// candidate is parsed before it is allowed to replace a working file.
|
// candidate is parsed before it is allowed to replace a working file. Checked here as well as
|
||||||
$check = tempnam(sys_get_temp_dir(), 'vvconf');
|
// inside vv_conf_edit() so the editor can show bash's own complaint; the write path only knows
|
||||||
if ($check !== false) {
|
// whether to proceed, not what to tell the person typing.
|
||||||
file_put_contents($check, $content);
|
$syntax = vv_conf_syntax_error($content, $file);
|
||||||
$out = []; $rc = 0;
|
if ($syntax !== null) {
|
||||||
exec('bash -n ' . escapeshellarg($check) . ' 2>&1', $out, $rc);
|
echo json_encode(['ok' => false, 'error' => 'Syntax error: ' . $syntax]);
|
||||||
@unlink($check);
|
exit;
|
||||||
if ($rc !== 0) {
|
|
||||||
$msg = implode(' ', array_filter(array_map('trim', $out)));
|
|
||||||
echo json_encode([
|
|
||||||
'ok' => false,
|
|
||||||
'error' => 'Syntax error: ' . str_replace($check, $file, $msg ?: 'conf does not parse'),
|
|
||||||
]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$ok = vv_write_conf_raw($file, $content);
|
$ok = vv_conf_edit($file, fn() => $content, [], ['whole-file']);
|
||||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write file']);
|
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write file']);
|
||||||
|
|||||||
@@ -48,9 +48,12 @@
|
|||||||
// missing, malformed, or unexpected parameter disables rather than enables. Failing
|
// missing, malformed, or unexpected parameter disables rather than enables. Failing
|
||||||
// toward off is the safe direction for a flag that starts data movement.
|
// toward off is the safe direction for a flag that starts data movement.
|
||||||
//
|
//
|
||||||
// The conf write is atomic.
|
// The conf write is atomic, backed up, verified and logged.
|
||||||
// vv_conf_flag_set() writes through vv_write_conf_raw() (tmp + rename). Every script
|
// vv_conf_flag_set() goes through vv_conf_edit(), the one guarded conf write path: an
|
||||||
// sources master.conf, so a truncated write would be a system-wide outage.
|
// exclusive lock, a timestamped copy into CONF_BACKUP_DIR, bash -n on the candidate,
|
||||||
|
// tmp + rename to install it, then the file is sourced and the flag read back — a value
|
||||||
|
// that does not come back as asked restores the backup. Every script sources master.conf,
|
||||||
|
// so a truncated or unparseable write would be a system-wide outage.
|
||||||
//
|
//
|
||||||
// The push only happens after a confirmed local write.
|
// The push only happens after a confirmed local write.
|
||||||
// Guarded on $ok, so a failed edit cannot distribute a stale or partly-written conf to
|
// Guarded on $ok, so a failed edit cannot distribute a stale or partly-written conf to
|
||||||
@@ -69,7 +72,7 @@
|
|||||||
// "push":[]}
|
// "push":[]}
|
||||||
//
|
//
|
||||||
// DEPENDS ON
|
// DEPENDS ON
|
||||||
// include/scheduler.php vv_conf_flag_set() → vv_write_conf_raw()
|
// include/scheduler.php vv_conf_flag_set() → vv_conf_edit() → vv_write_conf_raw()
|
||||||
// include/config.php vv_push_master_conf(), vv_push_setup_state()
|
// include/config.php vv_push_master_conf(), vv_push_setup_state()
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once dirname(__DIR__) . '/include/config.php';
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
require_once dirname(__DIR__) . '/include/confform.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||||
@@ -106,6 +107,9 @@ if (!file_exists($confPath)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||||
|
// Captured before anything is rebuilt from it — this is what the write compares against to
|
||||||
|
// prove master.conf did not change while the move was being worked out.
|
||||||
|
$origConf = implode('', $lines ?: []);
|
||||||
if (!$lines) {
|
if (!$lines) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
||||||
exit;
|
exit;
|
||||||
@@ -148,7 +152,14 @@ if ($toArray) {
|
|||||||
$newLines = $resultLines;
|
$newLines = $resultLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vv_write_conf_raw('master.conf', implode('', $newLines))) {
|
// The array was rebuilt from a copy read before the lock was taken. Handing vv_conf_edit() a
|
||||||
|
// closure that compares against the current contents turns that into an optimistic check: if
|
||||||
|
// anything changed master.conf in between, the move is abandoned rather than written over the
|
||||||
|
// top of it. Everything else — backup, bash -n, read-back, audit — comes with the shared path.
|
||||||
|
$ok = vv_conf_edit('master.conf', fn(string $cur): ?string =>
|
||||||
|
$cur === $origConf ? implode('', $newLines) : null, [], [$script]);
|
||||||
|
|
||||||
|
if (!$ok) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,13 @@
|
|||||||
// The temp copy is created with tempnam() and always removed, so a rejected save cannot
|
// The temp copy is created with tempnam() and always removed, so a rejected save cannot
|
||||||
// leave a stray file beside the real conf for a script to source.
|
// leave a stray file beside the real conf for a script to source.
|
||||||
//
|
//
|
||||||
// The real write is atomic.
|
// The write goes through the one guarded conf path.
|
||||||
// vv_write_conf_raw() writes .vv.tmp and rename()s, so a script sourcing the conf during
|
// vv_conf_edit() takes an exclusive lock, copies the previous file into CONF_BACKUP_DIR,
|
||||||
// the save reads either the old file or the new one, never a half-written one.
|
// re-checks the syntax, installs via .vv.tmp + rename() so a concurrent reader sees the
|
||||||
|
// old file or the new one but never a half-written one, then sources the installed file
|
||||||
|
// to prove it still loads and restores the backup if it does not. That backup matters
|
||||||
|
// more here than anywhere else: this endpoint replaces a whole hand-edited file, and the
|
||||||
|
// confs are gitignored, so before it existed a bad paste had nothing to go back to.
|
||||||
//
|
//
|
||||||
// The push only happens after a confirmed write.
|
// The push only happens after a confirmed write.
|
||||||
// Guarded on $written, so a failed save cannot distribute a stale or partly-written
|
// Guarded on $written, so a failed save cannot distribute a stale or partly-written
|
||||||
@@ -68,11 +72,13 @@
|
|||||||
// {"ok":false,"error":"Not allowed"|"Syntax error: …"|"Method not allowed"}
|
// {"ok":false,"error":"Not allowed"|"Syntax error: …"|"Method not allowed"}
|
||||||
//
|
//
|
||||||
// DEPENDS ON
|
// DEPENDS ON
|
||||||
// include/config.php vv_get_conf_files(), vv_read_conf_raw(), vv_write_conf_raw(),
|
// include/config.php vv_get_conf_files(), vv_read_conf_raw(),
|
||||||
// vv_push_master_conf(), vv_push_setup_state()
|
// vv_push_master_conf(), vv_push_setup_state()
|
||||||
|
// include/confform.php vv_conf_syntax_error(), vv_conf_edit()
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once dirname(__DIR__) . '/include/config.php';
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
require_once dirname(__DIR__) . '/include/confform.php';
|
||||||
|
|
||||||
$allowed = vv_get_conf_files();
|
$allowed = vv_get_conf_files();
|
||||||
|
|
||||||
@@ -95,24 +101,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
// Every script sources these, and master.conf is pushed to every partner from here — a
|
// Every script sources these, and master.conf is pushed to every partner from here — a
|
||||||
// syntax error saved through this endpoint would propagate the outage across the mesh.
|
// syntax error saved through this endpoint would propagate the outage across the mesh.
|
||||||
$check = tempnam(sys_get_temp_dir(), 'vvconf');
|
// Checked here as well as inside vv_conf_edit() so the editor can show bash's own complaint;
|
||||||
if ($check !== false) {
|
// the write path only knows whether to proceed, not what to tell the person typing.
|
||||||
file_put_contents($check, $content);
|
$syntax = vv_conf_syntax_error($content, $file);
|
||||||
$out = []; $rc = 0;
|
if ($syntax !== null) {
|
||||||
exec('bash -n ' . escapeshellarg($check) . ' 2>&1', $out, $rc);
|
echo json_encode(['ok' => false, 'error' => 'Syntax error: ' . $syntax, 'push' => []]);
|
||||||
@unlink($check);
|
exit;
|
||||||
if ($rc !== 0) {
|
|
||||||
$msg = implode(' ', array_filter(array_map('trim', $out)));
|
|
||||||
echo json_encode([
|
|
||||||
'ok' => false,
|
|
||||||
'error' => 'Syntax error: ' . str_replace($check, $file, $msg ?: 'conf does not parse'),
|
|
||||||
'push' => [],
|
|
||||||
]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$written = vv_write_conf_raw($file, $content);
|
$written = vv_conf_edit($file, fn() => $content, [], ['whole-file']);
|
||||||
$push = [];
|
$push = [];
|
||||||
if ($written && $file === 'master.conf') {
|
if ($written && $file === 'master.conf') {
|
||||||
$push = vv_push_master_conf();
|
$push = vv_push_master_conf();
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once dirname(__DIR__) . '/include/config.php';
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
require_once dirname(__DIR__) . '/include/confform.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||||
@@ -122,6 +123,9 @@ if (!file_exists($confPath)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
||||||
|
// Captured before array_splice rewrites the block in place — this is what the write compares
|
||||||
|
// against to prove master.conf did not change while the new order was being assembled.
|
||||||
|
$origConf = implode('', $lines ?: []);
|
||||||
if (!$lines) {
|
if (!$lines) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
echo json_encode(['ok' => false, 'error' => 'Could not read master.conf']);
|
||||||
exit;
|
exit;
|
||||||
@@ -183,7 +187,13 @@ $newBlockLines[] = $lines[$blockEnd];
|
|||||||
// Replace the original block in $lines
|
// Replace the original block in $lines
|
||||||
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines);
|
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines);
|
||||||
|
|
||||||
if (!vv_write_conf_raw('master.conf', implode('', $lines))) {
|
// See movescript.php: the block was rebuilt from a copy read before the lock, so the closure
|
||||||
|
// compares against the current contents and abandons the reorder if anything changed in
|
||||||
|
// between. Backup, bash -n, read-back and audit come with the shared path.
|
||||||
|
$ok = vv_conf_edit('master.conf', fn(string $cur): ?string =>
|
||||||
|
$cur === $origConf ? implode('', $lines) : null, [], [$arrayName]);
|
||||||
|
|
||||||
|
if (!$ok) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (is_array($scripts)) {
|
if (is_array($scripts)) {
|
||||||
$confPath = CONF_DIR . '/master.conf';
|
$confPath = CONF_DIR . '/master.conf';
|
||||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES) ?: [];
|
$lines = file($confPath, FILE_KEEP_BLANK_LINES) ?: [];
|
||||||
|
// Captured before array_splice rewrites the block in place — the write compares against
|
||||||
|
// it to prove master.conf did not change while the new block was being assembled.
|
||||||
|
$origConf = implode('', $lines);
|
||||||
$esc = preg_quote($scriptsVar, '/');
|
$esc = preg_quote($scriptsVar, '/');
|
||||||
$blockStart = $blockEnd = null;
|
$blockStart = $blockEnd = null;
|
||||||
$depth = 0;
|
$depth = 0;
|
||||||
@@ -213,9 +216,13 @@ if ($action === 'save' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
$newBlock[] = $lines[$blockEnd];
|
$newBlock[] = $lines[$blockEnd];
|
||||||
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlock);
|
array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlock);
|
||||||
// tmp+rename — every script sources master.conf, so a truncated write here is a
|
// Shared guarded path: lock, backup, bash -n, atomic install, read-back, audit. The
|
||||||
// system-wide outage, not a lost edit.
|
// closure compares against the current contents first, so a master.conf that changed
|
||||||
if (!vv_write_conf_raw('master.conf', implode('', $lines))) {
|
// while this block was being assembled abandons the write instead of clobbering it.
|
||||||
|
$wrote = vv_conf_edit('master.conf', fn(string $cur): ?string =>
|
||||||
|
$cur === $origConf ? implode('', $lines) : null, [], [$scriptsVar]);
|
||||||
|
|
||||||
|
if (!$wrote) {
|
||||||
$errors[] = 'scripts write failed';
|
$errors[] = 'scripts write failed';
|
||||||
} else {
|
} else {
|
||||||
// master.conf is shared — mirrors reorderarray/movescript/rawconf.
|
// master.conf is shared — mirrors reorderarray/movescript/rawconf.
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require_once dirname(__DIR__) . '/include/config.php';
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
require_once dirname(__DIR__) . '/include/confform.php';
|
||||||
|
|
||||||
$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
|
$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
|
||||||
? trim($_GET['action'] ?? '')
|
? trim($_GET['action'] ?? '')
|
||||||
@@ -277,7 +278,10 @@ if ($action === 'pull') {
|
|||||||
'${1}"' . $sshKey . '"', $conf);
|
'${1}"' . $sshKey . '"', $conf);
|
||||||
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||||
'${1}' . $storageInternal2, $conf);
|
'${1}' . $storageInternal2, $conf);
|
||||||
vv_write_conf_raw($confFile, $conf);
|
// allowCreate: this is first-run setup, so the host conf does not exist yet. There
|
||||||
|
// is no prior content to back up, and a candidate that fails bash -n is removed
|
||||||
|
// rather than restored.
|
||||||
|
vv_conf_edit($confFile, fn(): string => $conf, [], ["{$hostId}_SSH_KEY"], true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +317,9 @@ if (!preg_match('/^host\d+$/', $mySlot)) {
|
|||||||
|
|
||||||
// Write HOST1 / HOST2 into master.conf
|
// Write HOST1 / HOST2 into master.conf
|
||||||
$master = vv_read_conf_raw('master.conf');
|
$master = vv_read_conf_raw('master.conf');
|
||||||
|
// Captured before the substitutions below — the write compares against it so a master.conf that
|
||||||
|
// changed while setup was being filled in is not silently overwritten.
|
||||||
|
$origMaster = $master;
|
||||||
if ($master === '') {
|
if ($master === '') {
|
||||||
echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
|
echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']);
|
||||||
exit;
|
exit;
|
||||||
@@ -333,7 +340,8 @@ if ($slotNum > 2 && !empty($myHostname)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vv_write_conf_raw('master.conf', $master)) {
|
if (!vv_conf_edit('master.conf', fn(string $cur): ?string => $cur === $origMaster ? $master : null,
|
||||||
|
[], ['HOST1', 'HOST2'])) {
|
||||||
echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
|
echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -368,7 +376,8 @@ if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
|||||||
'${1}"' . $sshKeyPath . '"', $conf);
|
'${1}"' . $sshKeyPath . '"', $conf);
|
||||||
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
$conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m',
|
||||||
'${1}' . $storageInternal, $conf);
|
'${1}' . $storageInternal, $conf);
|
||||||
if (!vv_write_conf_raw($confFile, $conf)) {
|
// allowCreate — see the sibling write above; this is the same first-run create.
|
||||||
|
if (!vv_conf_edit($confFile, fn(): string => $conf, [], ["{$hostId}_SSH_KEY"], true)) {
|
||||||
echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
|
echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
// Reads/writes HOST*_DOCKER_FOLDER_MAP in host*.conf (used by onboard scripts)
|
// Reads/writes HOST*_DOCKER_FOLDER_MAP in host*.conf (used by onboard scripts)
|
||||||
|
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
|
require_once __DIR__ . '/confform.php';
|
||||||
|
|
||||||
define('VV_DOCKER_JSON', SCRIPTS_DIR . '/docker_folders.json');
|
define('VV_DOCKER_JSON', SCRIPTS_DIR . '/docker_folders.json');
|
||||||
define('VV_FV3_JSON', '/boot/config/plugins/folder.view3/docker.json');
|
define('VV_FV3_JSON', '/boot/config/plugins/folder.view3/docker.json');
|
||||||
@@ -344,17 +345,19 @@ function vv_dk_rename_folder(string $folderId, string $newName): array {
|
|||||||
$data[$folderId]['name'] = $newName;
|
$data[$folderId]['name'] = $newName;
|
||||||
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
||||||
|
|
||||||
// Update conf: replace old folder name with new name in the map
|
// Update conf: replace old folder name with new name in the map.
|
||||||
|
// The rebuild is a pure function of the current contents, so it runs inside vv_conf_edit()'s
|
||||||
|
// lock rather than against a copy read beforehand — there is no window to lose an edit in.
|
||||||
$currentHost = vv_detect_host();
|
$currentHost = vv_detect_host();
|
||||||
$myId = strtoupper($currentHost);
|
$myId = strtoupper($currentHost);
|
||||||
$raw = vv_read_conf_raw($currentHost . '.conf');
|
vv_conf_edit($currentHost . '.conf', function (string $raw) use ($myId, $oldName, $newName): string {
|
||||||
$map = vv_dk_read_conf_map($raw, $myId);
|
$map = vv_dk_read_conf_map($raw, $myId);
|
||||||
foreach ($map as &$v) {
|
foreach ($map as &$v) {
|
||||||
if ($v === $oldName) $v = $newName;
|
if ($v === $oldName) $v = $newName;
|
||||||
}
|
}
|
||||||
unset($v);
|
unset($v);
|
||||||
$updated = vv_dk_write_conf_map($raw, $myId, $map);
|
return vv_dk_write_conf_map($raw, $myId, $map);
|
||||||
vv_write_conf_raw($currentHost . '.conf', $updated);
|
}, [], ["{$myId}_DOCKER_FOLDER_MAP"]);
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,14 +370,14 @@ function vv_dk_delete_folder(string $folderId): array {
|
|||||||
unset($data[$folderId]);
|
unset($data[$folderId]);
|
||||||
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
if (!vv_dk_write_json($data)) return ['ok' => false, 'error' => 'JSON write failed'];
|
||||||
|
|
||||||
// Remove from conf map
|
// Remove from conf map — rebuilt inside the lock, see vv_dk_rename_folder() above.
|
||||||
$currentHost = vv_detect_host();
|
$currentHost = vv_detect_host();
|
||||||
$myId = strtoupper($currentHost);
|
$myId = strtoupper($currentHost);
|
||||||
$raw = vv_read_conf_raw($currentHost . '.conf');
|
vv_conf_edit($currentHost . '.conf', function (string $raw) use ($myId, $folderName): string {
|
||||||
$map = vv_dk_read_conf_map($raw, $myId);
|
$map = vv_dk_read_conf_map($raw, $myId);
|
||||||
$map = array_filter($map, fn($v) => $v !== $folderName);
|
$map = array_filter($map, fn($v) => $v !== $folderName);
|
||||||
$updated = vv_dk_write_conf_map($raw, $myId, $map);
|
return vv_dk_write_conf_map($raw, $myId, $map);
|
||||||
vv_write_conf_raw($currentHost . '.conf', $updated);
|
}, [], ["{$myId}_DOCKER_FOLDER_MAP"]);
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,22 +424,24 @@ function vv_dk_sync_json_to_conf(): array {
|
|||||||
$data = vv_dk_read_json();
|
$data = vv_dk_read_json();
|
||||||
$currentHost = vv_detect_host();
|
$currentHost = vv_detect_host();
|
||||||
$myId = strtoupper($currentHost);
|
$myId = strtoupper($currentHost);
|
||||||
$raw = vv_read_conf_raw($currentHost . '.conf');
|
_vv_dk_sync_conf_from_json($data, $currentHost, $myId);
|
||||||
_vv_dk_sync_conf_from_json($data, $raw, $currentHost, $myId);
|
|
||||||
return ['ok' => true];
|
return ['ok' => true];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal: rebuild conf map from current json state and write it
|
// Internal: rebuild conf map from current json state and write it.
|
||||||
function _vv_dk_sync_conf_from_json(array $data, string $raw = '', string $host = '', string $id = ''): void {
|
// The contents to splice into are read inside vv_conf_edit()'s lock. The caller used to be able
|
||||||
|
// to hand in a copy it had already read; that was only ever an optimisation, and passing a stale
|
||||||
|
// copy would have written the rest of the conf back as it looked before the lock was taken.
|
||||||
|
function _vv_dk_sync_conf_from_json(array $data, string $host = '', string $id = ''): void {
|
||||||
if (!$host) $host = vv_detect_host();
|
if (!$host) $host = vv_detect_host();
|
||||||
if (!$id) $id = strtoupper($host);
|
if (!$id) $id = strtoupper($host);
|
||||||
if (!$raw) $raw = vv_read_conf_raw($host . '.conf');
|
|
||||||
|
|
||||||
$map = [];
|
$map = [];
|
||||||
foreach ($data as $f) {
|
foreach ($data as $f) {
|
||||||
$name = $f['name'] ?? '';
|
$name = $f['name'] ?? '';
|
||||||
foreach ($f['containers'] ?? [] as $c) $map[$c] = $name;
|
foreach ($f['containers'] ?? [] as $c) $map[$c] = $name;
|
||||||
}
|
}
|
||||||
$updated = vv_dk_write_conf_map($raw, $id, $map);
|
|
||||||
vv_write_conf_raw($host . '.conf', $updated);
|
vv_conf_edit($host . '.conf', fn(string $raw): string => vv_dk_write_conf_map($raw, $id, $map),
|
||||||
|
[], ["{$id}_DOCKER_FOLDER_MAP"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -598,50 +598,56 @@ function vv_conf_flag_value(string $name): bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write a boolean flag value to master.conf.
|
// Write a boolean flag value to master.conf.
|
||||||
|
// Goes through vv_conf_edit() for the lock, the pre-write backup, the syntax check and the audit
|
||||||
|
// line. This used to call vv_write_conf_raw() directly, which gave it tmp+rename atomicity and
|
||||||
|
// nothing else — no backup, and no check that the file still sourced afterwards.
|
||||||
function vv_conf_flag_set(string $name, bool $value): bool {
|
function vv_conf_flag_set(string $name, bool $value): bool {
|
||||||
$confPath = CONF_DIR . '/master.conf';
|
if (!vv_conf_key_valid($name)) return false;
|
||||||
$content = file_get_contents($confPath);
|
$val = $value ? 'true' : 'false';
|
||||||
if ($content === false) return false;
|
|
||||||
$val = $value ? 'true' : 'false';
|
return vv_conf_edit('master.conf', function (string $content) use ($name, $val): ?string {
|
||||||
$new = preg_replace(
|
$new = preg_replace(
|
||||||
'/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m',
|
'/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m',
|
||||||
'${1}' . $val . '${3}',
|
'${1}' . $val . '${3}',
|
||||||
$content, -1, $count
|
$content, -1, $count
|
||||||
);
|
);
|
||||||
if (!$count) return false;
|
// A name that matches no true/false line is a caller error, not an already-correct
|
||||||
// tmp+rename — every script sources master.conf, so a truncated write here is a
|
// state — unlike the membership toggle below, where absence genuinely means nothing
|
||||||
// system-wide outage, not a lost toggle.
|
// to do. Returning null keeps the write from happening and logs reason=no-match.
|
||||||
return vv_write_conf_raw('master.conf', $new);
|
return $count ? $new : null;
|
||||||
|
}, [$name => $val]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment or uncomment a script's line in the first master.conf array that contains it.
|
// Comment or uncomment a script's line in the first master.conf array that contains it.
|
||||||
|
// Goes through vv_conf_edit() for the lock, the pre-write backup, the syntax check and the audit
|
||||||
|
// line — see vv_conf_flag_set() above for what that replaced. There is no key to verify here,
|
||||||
|
// so the audit subject is the script id and a clean source is the whole assertion.
|
||||||
function vv_conf_toggle_script(string $rel, bool $enable): bool {
|
function vv_conf_toggle_script(string $rel, bool $enable): bool {
|
||||||
$confPath = CONF_DIR . '/master.conf';
|
return vv_conf_edit('master.conf', function (string $content) use ($rel, $enable): ?string {
|
||||||
$lines = file($confPath, FILE_KEEP_BLANK_LINES);
|
$lines = preg_split('/(?<=\n)/', $content) ?: [];
|
||||||
if (!$lines) return false;
|
$changed = false;
|
||||||
$changed = false;
|
$inArray = false;
|
||||||
$inArray = false;
|
$relEsc = preg_quote($rel, '/');
|
||||||
$relEsc = preg_quote($rel, '/');
|
foreach ($lines as &$line) {
|
||||||
foreach ($lines as &$line) {
|
if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
|
||||||
if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
|
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
||||||
if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
|
if (!$inArray) continue;
|
||||||
if (!$inArray) continue;
|
if (!preg_match('/^\s*(?:#\s*)?"' . $relEsc . '(?:\s[^"]*)?"/', $line)) continue;
|
||||||
if (!preg_match('/^\s*(?:#\s*)?"' . $relEsc . '(?:\s[^"]*)?"/', $line)) continue;
|
$isCommented = (bool)preg_match('/^\s*#/', $line);
|
||||||
$isCommented = (bool)preg_match('/^\s*#/', $line);
|
if ($enable && $isCommented) {
|
||||||
if ($enable && $isCommented) {
|
$line = preg_replace('/^(\s*)#\s*("' . $relEsc . ')/', '$1$2', $line);
|
||||||
$line = preg_replace('/^(\s*)#\s*("' . $relEsc . ')/', '$1$2', $line);
|
$changed = true;
|
||||||
$changed = true;
|
} elseif (!$enable && !$isCommented) {
|
||||||
} elseif (!$enable && !$isCommented) {
|
$line = preg_replace('/^(\s*)("' . $relEsc . ')/', '$1# $2', $line);
|
||||||
$line = preg_replace('/^(\s*)("' . $relEsc . ')/', '$1# $2', $line);
|
$changed = true;
|
||||||
$changed = true;
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
unset($line);
|
||||||
}
|
// A script in no array has nothing to toggle and the conf already reads the way the
|
||||||
unset($line);
|
// caller asked. Returning the content unchanged reports success without a write.
|
||||||
if (!$changed) return true;
|
return $changed ? implode('', $lines) : $content;
|
||||||
// tmp+rename — every script sources master.conf, so a truncated write here is a
|
}, [], [$rel]);
|
||||||
// system-wide outage, not a lost toggle.
|
|
||||||
return vv_write_conf_raw('master.conf', implode('', $lines));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse an orchestrator script to find which child scripts it calls.
|
// Parse an orchestrator script to find which child scripts it calls.
|
||||||
|
|||||||
Reference in New Issue
Block a user