Let a partner adopt the owner's custom networks from the conf phase 1 already cached, instead of asking for a value it cannot know
This commit is contained in:
@@ -397,6 +397,47 @@ if (!file_exists(CONF_DIR . '/' . $confFile)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Adopt the owner's custom docker networks ─────────────────────────────────────────────────
|
||||||
|
// Sent by the wizard, which read them out of the owner's host conf that onboard Phase 1 cached
|
||||||
|
// into the RAM conf dir. This is the one setup value a fresh mirror has no way to know: the
|
||||||
|
// template ships NETWORK_CONNECT_NETWORKS with its only entry commented out, and the owner then
|
||||||
|
// deploys containers here onto a network named in the *owner's* templates. An empty list is what
|
||||||
|
// left twelve containers created against a network that did not exist.
|
||||||
|
//
|
||||||
|
// Outside the create block above, so it applies to a conf that already exists — the wizard is
|
||||||
|
// re-runnable and a mirror rebuilt against an existing host conf needs this just as much.
|
||||||
|
//
|
||||||
|
// Merged, never replaced: anything already listed here was put there deliberately.
|
||||||
|
$netsRaw = trim((string)($_POST['networks'] ?? ''));
|
||||||
|
if ($netsRaw !== '' && $hostIdLow !== 'host1') {
|
||||||
|
// Written into a file that bash sources, so the name is validated rather than trusted.
|
||||||
|
// Docker's own charset for a network name is a superset of this; anything outside it is
|
||||||
|
// far more likely to be an injection attempt than a real network.
|
||||||
|
$nets = array_values(array_filter(
|
||||||
|
array_map('trim', explode(',', $netsRaw)),
|
||||||
|
fn($n) => $n !== ''
|
||||||
|
&& preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/', $n)
|
||||||
|
&& !preg_match('/^(bridge|host|none|br\d)/i', $n)
|
||||||
|
));
|
||||||
|
if ($nets) {
|
||||||
|
$netVar = $hostId . '_NETWORK_CONNECT_NETWORKS';
|
||||||
|
vv_conf_edit($confFile, function (string $cur) use ($netVar, $nets): ?string {
|
||||||
|
$existing = vv_parse_conf_list($cur, $netVar);
|
||||||
|
$merged = $existing;
|
||||||
|
foreach ($nets as $n) {
|
||||||
|
if (!in_array($n, $merged, true)) $merged[] = $n;
|
||||||
|
}
|
||||||
|
if ($merged === $existing) return null; // already adopted — no write, no audit noise
|
||||||
|
$body = '';
|
||||||
|
foreach ($merged as $n) $body .= ' "' . $n . '"' . "\n";
|
||||||
|
$new = preg_replace(
|
||||||
|
'/^(\s*' . preg_quote($netVar, '/') . '\s*=\s*\()(.*?)(^\s*\))/ms',
|
||||||
|
"\$1\n" . $body . '$3', $cur, 1, $count);
|
||||||
|
return ($count === 1 && $new !== null) ? $new : null;
|
||||||
|
}, [], [$netVar]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write setup state file — lets partner servers know HOST1 is configured.
|
// Write setup state file — lets partner servers know HOST1 is configured.
|
||||||
// Read-modify-write: vv_setup_state_write() replaces the file wholesale, and re-running the
|
// Read-modify-write: vv_setup_state_write() replaces the file wholesale, and re-running the
|
||||||
// wizard must not erase onboarding progress recorded by the partnership phases.
|
// wizard must not erase onboarding progress recorded by the partnership phases.
|
||||||
|
|||||||
@@ -883,6 +883,30 @@ function vv_parse_conf_scalar(string $raw, string $key): string {
|
|||||||
return vv_conf_unquote(ltrim($m[1]));
|
return vv_conf_unquote(ltrim($m[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read a bash array of plain strings out of a conf, skipping commented entries.
|
||||||
|
//
|
||||||
|
// Distinct from vv_parse_conf_array() in scheduler.php, which looks the same but keeps only
|
||||||
|
// entries ending in .sh — it exists to read job lists. Handing it a list of docker networks
|
||||||
|
// returns an empty array, silently, because none of them are scripts. This one makes no
|
||||||
|
// assumption about what the entries mean.
|
||||||
|
//
|
||||||
|
// The closing paren must be at the start of its own line, matching the shape conf_upgrade
|
||||||
|
// writes and the same anchor the scheduler parser uses — a value containing ')' would
|
||||||
|
// otherwise end the array early.
|
||||||
|
function vv_parse_conf_list(string $raw, string $key): array {
|
||||||
|
if (!preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*\((.*?)^\s*\)/ms', $raw, $m)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach (explode("\n", $m[1]) as $line) {
|
||||||
|
if (preg_match('/^\s*#/', $line)) continue; // commented-out entry
|
||||||
|
if (!preg_match('/"([^"]*)"|\'([^\']*)\'/', $line, $e)) continue;
|
||||||
|
$val = trim($e[1] !== '' ? $e[1] : ($e[2] ?? ''));
|
||||||
|
if ($val !== '') $out[] = $val;
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
// Unquote one bash word the way bash does, because the regexes this replaced did not and the
|
// Unquote one bash word the way bash does, because the regexes this replaced did not and the
|
||||||
// conf is read by both. Three separate regexes each handled one quoting style in isolation and
|
// conf is read by both. Three separate regexes each handled one quoting style in isolation and
|
||||||
// none of them handled an escape or two quoted runs in a row, so a value carrying a quote or a
|
// none of them handled an escape or two quoted runs in a row, so a value carrying a quote or a
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ $detectedHostname = vv_get_hostname();
|
|||||||
// Matching is exact and case-insensitive, never a prefix: two hosts called Tower and Tower2 must
|
// Matching is exact and case-insensitive, never a prefix: two hosts called Tower and Tower2 must
|
||||||
// not resolve to each other, and there is no ambiguity to tolerate when the value was written by
|
// not resolve to each other, and there is no ambiguity to tolerate when the value was written by
|
||||||
// the very host it names.
|
// the very host it names.
|
||||||
$vvDetected = ['slot' => '', 'primary' => '', 'role' => ''];
|
$vvDetected = ['slot' => '', 'primary' => '', 'role' => '', 'networks' => [], 'networks_src' => ''];
|
||||||
$vvMasterRaw = vv_read_conf_raw('master.conf');
|
$vvMasterRaw = vv_read_conf_raw('master.conf');
|
||||||
if ($vvMasterRaw !== '' && $detectedHostname !== '') {
|
if ($vvMasterRaw !== '' && $detectedHostname !== '') {
|
||||||
preg_match_all('/^\s*(HOST\d+)\s*=\s*"([^"]*)"/m', $vvMasterRaw, $vvHm, PREG_SET_ORDER);
|
preg_match_all('/^\s*(HOST\d+)\s*=\s*"([^"]*)"/m', $vvMasterRaw, $vvHm, PREG_SET_ORDER);
|
||||||
@@ -70,6 +70,34 @@ if ($vvMasterRaw !== '' && $detectedHostname !== '') {
|
|||||||
// The primary is whatever HOST1 says, and it is only useful to a host that is not HOST1.
|
// The primary is whatever HOST1 says, and it is only useful to a host that is not HOST1.
|
||||||
if ($vvDetected['role'] === 'partner') $vvDetected['primary'] = $vvSlots['host1'] ?? '';
|
if ($vvDetected['role'] === 'partner') $vvDetected['primary'] = $vvSlots['host1'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Custom docker networks, adopted from the owner ───────────────────────────────────────────
|
||||||
|
// master.conf names the hosts; it does not name the networks, which live in each host's own
|
||||||
|
// host*.conf. The owner's copy is available here anyway: onboard Phase 1 caches it into
|
||||||
|
// VV_CONF_RAM_CACHE_DIR as soon as SSH works, before this node has finished setup.
|
||||||
|
//
|
||||||
|
// This is the value a fresh mirror cannot know and cannot be expected to type. host.conf.template
|
||||||
|
// ships NETWORK_CONNECT_NETWORKS with its only entry commented out, so a mirror comes up with an
|
||||||
|
// empty list while the owner deploys containers onto a network named in the owner's templates —
|
||||||
|
// which is exactly how twelve containers ended up created against a network that did not exist.
|
||||||
|
//
|
||||||
|
// Adopting the owner's list also means docker_network_connect.sh will recreate the network here
|
||||||
|
// after an Unraid update wipes it, which is the whole reason that script exists.
|
||||||
|
if ($vvDetected['role'] === 'partner' && $vvDetected['slot'] !== '') {
|
||||||
|
$vvOwnerConf = VV_CONF_RAM_CACHE_DIR . '/host1.conf';
|
||||||
|
if (is_readable($vvOwnerConf)) {
|
||||||
|
$vvOwnerRaw = (string)@file_get_contents($vvOwnerConf);
|
||||||
|
$vvNets = vv_parse_conf_list($vvOwnerRaw, 'HOST1_NETWORK_CONNECT_NETWORKS');
|
||||||
|
// br* is ipvlan/macvlan tied to the owner's own hardware — its parent interface does not
|
||||||
|
// transfer, and adopting the name would attach this host's containers to the wrong thing.
|
||||||
|
$vvNets = array_values(array_filter($vvNets, fn($n) =>
|
||||||
|
$n !== '' && !preg_match('/^(bridge|host|none|br\d)/i', $n)));
|
||||||
|
if ($vvNets) {
|
||||||
|
$vvDetected['networks'] = $vvNets;
|
||||||
|
$vvDetected['networks_src'] = basename($vvOwnerConf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
|
||||||
<style>
|
<style>
|
||||||
@@ -333,6 +361,18 @@ function vvApplyDetectedIdentity() {
|
|||||||
+ `${d.primary}. Change anything below if that is wrong.`
|
+ `${d.primary}. Change anything below if that is wrong.`
|
||||||
: 'Identity read from master.conf — this server is HOST1, the primary.';
|
: 'Identity read from master.conf — this server is HOST1, the primary.';
|
||||||
banner.appendChild(note);
|
banner.appendChild(note);
|
||||||
|
|
||||||
|
// Named separately from identity: this one is adopted from the owner's cached host conf,
|
||||||
|
// not from master.conf, and it is the value the operator would otherwise have to know.
|
||||||
|
if (Array.isArray(d.networks) && d.networks.length) {
|
||||||
|
const nets = document.createElement('div');
|
||||||
|
nets.style.cssText = 'margin-top:6px;padding:7px 10px;border-radius:3px;background:#0d1a28;'
|
||||||
|
+ 'border:1px solid #1a3a5a;color:#7ab;font-size:11px;line-height:1.5;';
|
||||||
|
nets.textContent = `Custom network${d.networks.length > 1 ? 's' : ''} adopted from `
|
||||||
|
+ `${d.primary || 'the primary'}: ${d.networks.join(', ')} — added to this `
|
||||||
|
+ `server's conf on save, so the containers it deploys here have somewhere to land.`;
|
||||||
|
banner.appendChild(nets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,9 +589,13 @@ function vvDoSave() {
|
|||||||
if (mySlot === 'host2') host2 = hostname;
|
if (mySlot === 'host2') host2 = hostname;
|
||||||
}
|
}
|
||||||
vvSetBtn('Saving…', true);
|
vvSetBtn('Saving…', true);
|
||||||
|
// Networks come from the owner's cached conf, not from any field — there is nothing for the
|
||||||
|
// operator to type and nothing to get wrong. Empty on the primary, and on a partner whose
|
||||||
|
// owner conf has not been cached yet; the save handler treats absent as "nothing to adopt".
|
||||||
|
const networks = (vvDetectedIdentity?.networks ?? []).join(',');
|
||||||
fetch('/plugins/varaverk/api/setup.php', {
|
fetch('/plugins/varaverk/api/setup.php', {
|
||||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode})
|
body: new URLSearchParams({action:'save', host1, host2, my_slot:mySlot, my_hostname:hostname, storage_mode:_vvStorageMode, networks})
|
||||||
}).then(r => r.json()).then(d => {
|
}).then(r => r.json()).then(d => {
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
if (d.needs_migration) {
|
if (d.needs_migration) {
|
||||||
|
|||||||
Reference in New Issue
Block a user