Files
XC_VM/tests/Unit/LbInstallFlowOpensslExtraTest.php
T
Claude 33d1aa7188 fix(servers): add server:sync-openssl-extra to repair mismatched LBs
Load balancers added before provisioning shipped config/openssl_extra
run on the built-in OPENSSL_EXTRA while their main holds a random one,
so they reject every token the main mints for them. server:diagnose now
shows this; nothing could fix it short of a manual copy.

server:sync-openssl-extra <server_id>|--all [--force], run on the main,
queues one root signal carrying the main's value for each streaming LB
whose published fingerprint differs. Nodes that publish none (not
updated), offline nodes and nodes already in sync need --force; proxies
and the main are never sent it. A main without config/openssl_extra
runs on the default like its LBs, and the command sends nothing.

The LB's cron:root_signals handles 'set_openssl_extra' in its own case
(ignored on the main, whose value keys hmac_keys and image names) with
OpensslExtra::install: it writes the value it replaces to
openssl_extra.prev, accepted for 600 s by Encryption::readToken, then
swaps openssl_extra in through a 0600 temporary file owned like
config/, so php-fpm never sees a file it cannot read. php-fpm picks the
new value up on its next request. Installing the value already in use
keeps an earlier .prev. server:install now also clears a stale .prev,
and server:diagnose names the command in its mismatch report.

The value sits in the signals table until the LB's root cron deletes the
row, about a minute; a legacy LB reads that database in full anyway.
The command is MAIN-only: it is stripped from the LB archive and listed
in verify-lb-archive.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQHeypQpfAXTdCysmi7b6a
2026-09-25 00:34:59 +00:00

98 lines
3.3 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Cli\Commands\LbInstallFlow;
/**
* LbInstallFlow::provisionOpensslExtra — an LB added with server:install must
* end up with the MAIN's OPENSSL_EXTRA, or every token MAIN mints for a
* redirect is rejected there. The MAIN's config/openssl_extra is shipped when
* it holds a value; otherwise both sides run on the built-in default. Whatever
* a previous MAIN left on a reused host is removed first.
*/
final class LbInstallFlowOpensslExtraTest extends TestCase {
private string $rLocal;
/** @var array<int,array<int,string>> */
private array $rCalls = [];
public static function setUpBeforeClass(): void {
// The value init() gives it; the module loader's fallbacks resolve the same directory.
if (!defined('CONFIG_PATH')) {
define('CONFIG_PATH', MAIN_HOME . 'config/');
}
}
protected function setUp(): void {
$this->rLocal = sys_get_temp_dir() . '/xcvm_lb_openssl_extra_' . uniqid();
$this->rCalls = [];
}
protected function tearDown(): void {
@unlink($this->rLocal);
}
private function provision(bool $rSendOk = true): bool {
$rRunSSH = function ($rConn, string $rCommand): array {
$this->rCalls[] = ['run', $rCommand];
return ['output' => '', 'error' => ''];
};
$rSendFileSSH = function ($rConn, string $rPath, string $rOutput, bool $rWarn = false) use ($rSendOk): bool {
$this->rCalls[] = ['send', $rPath, $rOutput];
return $rSendOk;
};
return LbInstallFlow::provisionOpensslExtra(null, $rRunSSH, $rSendFileSSH, $this->rLocal);
}
private function clear(): array {
return ['run', 'sudo rm -f ' . CONFIG_PATH . 'openssl_extra ' . CONFIG_PATH . 'openssl_extra.prev'];
}
public function testShipsTheMainsValueAndLocksItDown(): void {
file_put_contents($this->rLocal, 'main-extra');
$this->assertTrue($this->provision());
$this->assertSame([
$this->clear(),
['send', $this->rLocal, CONFIG_PATH . 'openssl_extra'],
['run', 'sudo chmod 600 ' . CONFIG_PATH . 'openssl_extra'],
], $this->rCalls);
}
public function testWithoutAValueOnTheMainOnlyAStaleFileIsRemoved(): void {
$this->assertTrue($this->provision(), 'no file');
$this->assertSame([$this->clear()], $this->rCalls);
foreach (['', " \n"] as $rEmpty) {
$this->rCalls = [];
file_put_contents($this->rLocal, $rEmpty);
$this->assertTrue($this->provision());
$this->assertSame([$this->clear()], $this->rCalls);
}
}
public function testAFailedUploadFailsTheStep(): void {
file_put_contents($this->rLocal, 'main-extra');
$this->assertFalse($this->provision(false));
$this->assertNotContains(['run', 'sudo chmod 600 ' . CONFIG_PATH . 'openssl_extra'], $this->rCalls);
}
/** Shipped as root before config/ is handed to xc_vm, so FPM can read it. */
public function testProvisionConfigShipsTheValueBeforeHandingConfigToXcVm(): void {
$rSource = (string) file_get_contents(MAIN_HOME . 'Cli/Commands/LbInstallFlow.php');
$rStart = strpos($rSource, 'function provisionConfig(');
$this->assertNotFalse($rStart);
$rEnd = strpos($rSource, "\n\t}\n", $rStart);
$rBody = substr($rSource, $rStart, $rEnd - $rStart);
$rProvision = strpos($rBody, 'self::provisionOpensslExtra(');
$rChown = strpos($rBody, "'sudo chown -R xc_vm:xc_vm '");
$this->assertNotFalse($rProvision, 'provisionConfig ships OPENSSL_EXTRA');
$this->assertNotFalse($rChown);
$this->assertLessThan($rChown, $rProvision);
}
}