CluPilotCloud/tests/Feature/Provisioning/BridgeScriptTest.php

195 lines
7.5 KiB
PHP

<?php
use Illuminate\Support\Facades\Process;
/*
|--------------------------------------------------------------------------
| bridge.sh, gegen eine echte Shell gefahren
|--------------------------------------------------------------------------
|
| Der Rest der Testsuite liest die Befehle dieses Schritts als Zeichenketten —
| FakeRemoteShell führt keinen davon aus. Die Strophe, die diese Bibliothek
| schreibt, ist aber genau das, was eine Maschine umbringt, wenn sie falsch
| ist. Deshalb wird bridge.sh hier mit `sh` wirklich ausgeführt, gegen
| aufgezeichnete Ausgaben von `ip` statt gegen einen Host.
|
| `CLUPILOT_IP` zeigt dabei auf ein Attrappen-Skript, `CLUPILOT_SYS_NET` auf
| einen Baum aus Textdateien. Beides ist die einzige Berührung mit dem System,
| die die Bibliothek hat.
*/
/**
* Legt eine `ip`-Attrappe an, die auf feste Argumentmuster feste Ausgaben gibt.
*
* @param array<string, string> $answers Argumentmuster => Ausgabe
*/
function fakeIp(string $dir, array $answers): string
{
$cases = '';
foreach ($answers as $pattern => $output) {
$cases .= " '{$pattern}') cat <<'OUT'\n{$output}\nOUT\n ;;\n";
}
$script = "#!/bin/sh\ncase \"\$*\" in\n{$cases} *) exit 1 ;;\nesac\n";
file_put_contents("{$dir}/ip", $script);
chmod("{$dir}/ip", 0o755);
return "{$dir}/ip";
}
/**
* Fährt eine Zeile Shell mit bridge.sh im Rücken und gibt stdout zurück.
*
* `$pre` läuft VOR dem Sourcen. Damit kann ein Test `log` oder `http_get` selbst
* definieren — bridge.sh legt beide unter einem `command -v`-Schutz an, wer
* zuerst da ist, behält recht. Das ist zugleich die Probe auf diesen Schutz und
* spart dem Test eine Abhängigkeit von curl im Container.
*/
function runBridgeSh(string $body, array $env = [], string $pre = ''): string
{
$lib = base_path('deploy/bootstrap/lib/bridge.sh');
$exports = '';
foreach ($env as $key => $value) {
$exports .= "export {$key}=".escapeshellarg($value)."\n";
}
$result = Process::input("{$exports}{$pre}\n. '{$lib}'\n{$body}\n")->run('sh');
return trim($result->output());
}
/** Ein /sys/class/net-Baum aus Textdateien. */
function fakeSysNet(string $dir, string $iface, string $mac, bool $physical = true): string
{
$root = "{$dir}/sys";
mkdir("{$root}/{$iface}", 0o755, true);
file_put_contents("{$root}/{$iface}/address", $mac."\n");
if ($physical) {
mkdir("{$root}/{$iface}/device", 0o755, true);
}
return $root;
}
beforeEach(function () {
$this->dir = sys_get_temp_dir().'/bridge-'.bin2hex(random_bytes(6));
mkdir($this->dir, 0o755, true);
});
it('nimmt die Karte, über die die Standardroute geht — nicht die erste beste', function () {
// Eine Maschine mit zwei Karten hat oft eine angeschlossene und eine nicht.
$ip = fakeIp($this->dir, [
'-4 route show default' => 'default via 49.12.121.65 dev enp0s31f6 proto static',
]);
expect(runBridgeSh('detect_primary_interface', ['CLUPILOT_IP' => $ip]))
->toBe('enp0s31f6');
});
it('erkennt eine geroutete Einzeladresse an der /32', function () {
// Hetzner dediziert. Das eigene Subnetz besteht aus der eigenen Adresse —
// ein Gateway darin kann es nicht geben, es braucht pointopoint.
$ip = fakeIp($this->dir, [
'-4 route show default' => 'default via 49.12.121.65 dev enp0s31f6',
'-4 -o addr show dev enp0s31f6 scope global' => '2: enp0s31f6 inet 49.12.121.79/32 scope global enp0s31f6\ valid_lft forever preferred_lft forever',
]);
expect(runBridgeSh('detect_network_style enp0s31f6', ['CLUPILOT_IP' => $ip]))
->toBe('routed');
});
it('erkennt DHCP am laufenden Zustand, nicht an der Datei des Anbieters', function () {
// Der Kernel markiert eine geleaste Adresse als `dynamic`. Das steht bei
// jedem Anbieter gleich da — anders als die Datei, in die er es geschrieben
// hat.
$ip = fakeIp($this->dir, [
'-4 route show default' => 'default via 10.0.0.1 dev ens3',
'-4 -o addr show dev ens3 scope global' => '2: ens3 inet 10.0.0.7/24 brd 10.0.0.255 scope global dynamic ens3\ valid_lft 3521sec preferred_lft 3521sec',
]);
expect(runBridgeSh('detect_network_style ens3', [
'CLUPILOT_IP' => $ip,
// Absichtlich auf eine Datei zeigen, die es nicht gibt: die Antwort darf
// nicht davon abhängen.
'CLUPILOT_INTERFACES_FILE' => $this->dir.'/gibtesnicht',
]))->toBe('dhcp');
});
it('hält eine Bridge und einen Bond für keine physische Karte', function () {
// bridge_ports auf einem Bond oder einer bestehenden Bridge ist falsch: die
// Brücke nähme sich ihren eigenen Unterbau als Port.
$sys = fakeSysNet($this->dir, 'bond0', 'aa:bb:cc:dd:ee:ff', physical: false);
mkdir("{$sys}/bond0/bonding", 0o755, true);
$out = runBridgeSh(
'if interface_is_physical bond0; then echo JA; else echo NEIN; fi',
['CLUPILOT_SYS_NET' => $sys]
);
expect($out)->toBe('NEIN');
});
it('hält eine gewöhnliche Karte für eine physische', function () {
$sys = fakeSysNet($this->dir, 'enp0s31f6', 'a8:a1:59:00:11:22');
$out = runBridgeSh(
'if interface_is_physical enp0s31f6; then echo JA; else echo NEIN; fi',
['CLUPILOT_SYS_NET' => $sys]
);
expect($out)->toBe('JA');
});
it('sagt nur dann „Brücke steht", wenn sie Route UND Adresse trägt', function () {
// Eine vmbr0 ohne Adresse und ohne Standardroute ist eine Brücke im Sinne
// von `ip link show` und sonst nichts. Der alte Schritt prüfte genau das
// und warf das Ergebnis weg; sein Kommentar behauptete, er halte die
// Abwesenheit fest, und er hielt nichts fest.
$ip = fakeIp($this->dir, [
'link show vmbr0' => '5: vmbr0: <BROADCAST,MULTICAST,UP> mtu 1500',
'-4 route show default' => 'default via 49.12.121.65 dev enp0s31f6',
'-4 -o addr show dev vmbr0 scope global' => '',
]);
$out = runBridgeSh(
'if bridge_is_up; then echo JA; else echo NEIN; fi',
['CLUPILOT_IP' => $ip]
);
expect($out)->toBe('NEIN');
});
it('hält detect_primary_interface an genau einer Stelle im Repo', function () {
// Zwei Fassungen wären zwei Installationen, die bei jeder Proxmox-Version
// nachgezogen werden müssten — und die zweite fiele erst auf, wenn jemand
// sie benutzt. Genau die Entscheidung, die den Rettungssystem-Weg
// stillgelegt hat.
//
// array_merge, NICHT `+`: bei numerischen Schlüsseln behält der
// Vereinigungsoperator die linke Seite und wirft die rechte still weg. Und
// PHPs glob() kennt kein rekursives `**`, also zwei Muster.
$files = array_merge(
glob(base_path('deploy/bootstrap/lib/*.sh')) ?: [],
glob(base_path('deploy/bootstrap/*.sh')) ?: [],
);
$hits = [];
foreach ($files as $file) {
if (preg_match('/^detect_primary_interface\(\)/m', (string) file_get_contents($file))) {
$hits[] = str_replace(base_path().'/', '', $file);
}
}
expect($hits)->toBe(['deploy/bootstrap/lib/bridge.sh']);
});
it('lässt jede Bibliothek des Bootstraps von einer Shell parsen', function () {
// network.sh und proxmox.sh werden beim Herauslösen beschnitten. Ein
// verrutschter Schnitt fiele sonst erst auf dem Rettungssystem-Weg auf, und
// der wird selten gefahren.
foreach (glob(base_path('deploy/bootstrap/lib/*.sh')) ?: [] as $file) {
expect(Process::run(['sh', '-n', $file])->successful())
->toBeTrue('kein gültiges POSIX-sh: '.$file);
}
});