Files
XC_VM/tests/Unit/LbInstallExtensionTest.php
T
obscuremind 52c921e7aa fix(install): a load balancer install checks what it needs first
- MAIN without bin/install/install_xcvm_core.sh (an update from before
  the updater copied bin/install) takes it from GitHub, this release's
  tag first. The install log showed "bash: /tmp/install_xcvm_core.sh:
  No such file or directory".
- That script is looked for before anything is removed on the node.
- The SSH user must be root or have sudo without a password: asked right
  after the login, with a message that says so, instead of failing
  halfway. sudo is installed for a root login that lacks it.
- A file a sudo user cannot write directly is staged in a private
  directory and copied with sudo; its checksum is compared afterwards.
- A reinstall left two fanout supervisors running: the service is
  stopped and the node user's processes are ended before the restart.
- Debian 13 has no /etc/systemd/system.conf and user.conf: status
  stopped on the missing file, and the install wrote the limits outside
  any section, where systemd ignores them. Both start the file with its
  [Manager] header, also where 2.6.0 left one without it.
2026-10-03 18:34:20 +00:00

174 lines
7.5 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Cli\Commands\LbInstallFlow;
/**
* LbInstallFlow::installExtension — a load balancer cannot run without
* xcvm_core, and neither the binaries bundle nor the archive carries it. An
* install whose installer script could not be sent, or failed, used to carry
* on: it failed later at "Failed to read install_id", or left the node on
* whatever older extension an old bundle still had.
*/
final class LbInstallExtensionTest extends TestCase {
/** @var list<string> */
private array $rRan = [];
public static function setUpBeforeClass(): void {
foreach (['GIT_OWNER' => 'Vateron-Media', 'GIT_REPO_BIN' => 'XC_VM_Binaries', 'GIT_REPO_MAIN' => 'XC_VM', 'XC_VM_VERSION' => '2.6.0'] as $rName => $rValue) {
if (!defined($rName)) {
define($rName, $rValue);
}
}
}
/** Where the script was sent on the node. */
private string $rSentTo = '';
/** Run the step against a node whose installer prints $rScript, whose PHP answers $rPhp and whose mktemp gives $rDir. */
private function install(bool $rSent, string $rScript = "xcvm_core installed: x.so\nexit=0", string $rPhp = 'CORE_OK', string $rDir = '/tmp/xcvm.aB3dE6gH9k'): bool {
$this->rRan = [];
$this->rSentTo = '';
$rRunSSH = function ($rConn, string $rCommand) use ($rScript, $rPhp, $rDir): array {
$this->rRan[] = $rCommand;
return ['output' => match (true) {
str_starts_with($rCommand, 'mktemp') => $rDir . "\n",
str_starts_with($rCommand, 'sudo bash') => $rScript . "\n",
default => $rPhp,
}, 'error' => ''];
};
ob_start();
try {
return LbInstallFlow::installExtension(null, $rRunSSH, function ($rConn, string $rPath, string $rOutput, bool $rWarn = false) use ($rSent): bool {
$this->rSentTo = $rOutput;
return $rSent;
});
} finally {
ob_end_clean();
}
}
public function testTheInstallGoesOnOnlyWithTheExtensionLoaded(): void {
$this->assertTrue($this->install(true));
$this->assertCount(3, $this->rRan, 'a private directory, the installer, then PHP asked for the class');
$this->assertStringContainsString('echo "exit=$?"', $this->rRan[1], "the installer's own exit status is read");
$this->assertStringContainsString('class_exists("XC_VM", false)', $this->rRan[2]);
}
public function testTheScriptRootRunsIsWhereNoOtherUserCanReachIt(): void {
// At a fixed path in /tmp another user of the node could own the file
// first, and change it between the transfer and the sudo.
$this->assertTrue($this->install(true));
$this->assertSame('mktemp -d /tmp/xcvm.XXXXXXXXXX', $this->rRan[0]);
$this->assertSame('/tmp/xcvm.aB3dE6gH9k/install_xcvm_core.sh', $this->rSentTo);
$this->assertStringStartsWith('sudo bash /tmp/xcvm.aB3dE6gH9k/install_xcvm_core.sh ', $this->rRan[1]);
$this->assertStringEndsWith('rm -rf /tmp/xcvm.aB3dE6gH9k', $this->rRan[1], 'and is removed with its directory');
// Anything but the directory mktemp names is refused: it goes into shell commands.
foreach (['', '/tmp/install_xcvm_core.sh', '/tmp/xcvm.aB3dE6gH9k; id', "mktemp: failed to create directory"] as $rBad) {
$this->assertFalse($this->install(true, "xcvm_core installed: x.so\nexit=0", 'CORE_OK', $rBad), var_export($rBad, true));
$this->assertSame('', $this->rSentTo, 'nothing is sent');
$this->assertCount(1, $this->rRan, 'and nothing is run');
}
}
public function testTheArchiveRootUnpacksIsFetchedWhereNoOtherUserCanReachIt(): void {
$rDb = new class {
/** @var list<string> */
public array $rQueries = [];
public function query(string $rSql, mixed ...$rArgs): bool {
$this->rQueries[] = $rSql;
return true;
}
};
$rInstall = function (string $rDir) use ($rDb): bool {
$this->rRan = [];
$rNode = function ($rConn, string $rCommand) use ($rDir): array {
$this->rRan[] = $rCommand;
return ['output' => match (true) {
str_starts_with($rCommand, 'mktemp') => $rDir . "\n",
str_contains($rCommand, 'md5sum') => "0123abc\n",
str_starts_with($rCommand, 'test -f') => "OK\n",
default => '',
}, 'error' => ''];
};
ob_start();
try {
return LbInstallFlow::installArchive(null, $rNode, 'https://example.invalid/loadbalancer.tar.gz', '0123abc', 2, $rDb);
} finally {
ob_end_clean();
}
};
$this->assertTrue($rInstall('/tmp/xcvm.aB3dE6gH9k'));
$this->assertSame('mktemp -d /tmp/xcvm.XXXXXXXXXX', $this->rRan[0]);
$this->assertStringContainsString(' -O /tmp/xcvm.aB3dE6gH9k/XC_VM.tar.gz ', $this->rRan[1]);
$this->assertContains('sudo tar -zxvf /tmp/xcvm.aB3dE6gH9k/XC_VM.tar.gz -C "' . MAIN_HOME . '"', $this->rRan);
$this->assertContains('sudo rm -rf /tmp/xcvm.aB3dE6gH9k', $this->rRan, 'and is removed with its directory');
$this->assertSame([], $rDb->rQueries, 'the server is not marked failed');
$this->assertFalse($rInstall('mktemp: failed to create directory'));
$this->assertCount(1, $this->rRan, 'nothing is downloaded or unpacked');
$this->assertCount(1, $rDb->rQueries, 'and the server is marked failed (status 4)');
}
public function testAMainWithoutTheInstallerTakesItFromGitHub(): void {
// A MAIN that reached 2.6.0 by updating has no bin/install/install_xcvm_core.sh
// (the updater left bin/install out): every install it ran printed
// "bash: /tmp/install_xcvm_core.sh: No such file or directory".
$rDir = sys_get_temp_dir() . '/xcvm-installer-' . bin2hex(random_bytes(4));
mkdir($rDir);
$rPath = $rDir . '/install_xcvm_core.sh';
$rAsked = [];
$rGitHub = static function (string $rUrl, string $rDest) use (&$rAsked): bool {
$rAsked[] = $rUrl;
if (!str_contains($rUrl, '/main/')) {
return false; // a build whose release has no tag
}
return (bool) file_put_contents($rDest, "#!/bin/bash\necho ok\n");
};
$rTake = static function (callable $rDownload) use ($rPath): ?string {
ob_start();
try {
return LbInstallFlow::extensionInstaller($rPath, $rDownload);
} finally {
ob_end_clean();
}
};
try {
$rRaw = 'https://raw.githubusercontent.com/' . GIT_OWNER . '/' . GIT_REPO_MAIN . '/';
$this->assertSame($rPath, $rTake($rGitHub));
$this->assertSame("#!/bin/bash\necho ok\n", file_get_contents($rPath));
$this->assertSame([$rRaw . XC_VM_VERSION . '/src/bin/install/install_xcvm_core.sh', $rRaw . 'main/src/bin/install/install_xcvm_core.sh'], $rAsked, "MAIN's release first, then main");
$rAsked = [];
$this->assertSame($rPath, $rTake($rGitHub));
$this->assertSame([], $rAsked, 'one that is there is not fetched again');
unlink($rPath);
$this->assertNull($rTake(static fn(string $rUrl, string $rDest): bool => (bool) file_put_contents($rDest, '<html>404</html>')), 'an error page is not a script root may run');
$this->assertFileDoesNotExist($rPath);
$this->assertFileDoesNotExist($rPath . '.part');
} finally {
@unlink($rPath);
@unlink($rPath . '.part');
@rmdir($rDir);
}
}
public function testAScriptThatCouldNotBeSentStopsTheInstall(): void {
$this->assertFalse($this->install(false));
$this->assertSame(['mktemp -d /tmp/xcvm.XXXXXXXXXX', 'rm -rf /tmp/xcvm.aB3dE6gH9k'], $this->rRan, 'only the directory is made and removed');
}
public function testAFailedInstallerStopsTheInstallEvenWithAnOlderExtensionLoaded(): void {
$this->assertFalse($this->install(true, "curl: (6) Could not resolve host: raw.githubusercontent.com\nexit=6", 'CORE_OK'));
}
public function testAnExtensionPhpDoesNotLoadStopsTheInstall(): void {
$this->assertFalse($this->install(true, "xcvm_core installed: x.so\nexit=0", 'CORE_MISSING'));
$this->assertFalse($this->install(true, "xcvm_core installed: x.so\nexit=0", ''), 'PHP that does not even start');
}
}