CluPilotCloud/app/Support/HostTakeoverCommand.php

136 lines
5.4 KiB
PHP

<?php
namespace App\Support;
use App\Models\Host;
/**
* Die eine Zeile, die der Betreiber im Rettungssystem einfügt.
*
* An einer Stelle gebaut, weil sie an zwei Stellen gezeigt wird (beim Anlegen
* und beim Neuausstellen eines Codes) und weil jeder ihrer sieben Werte aus
* einer anderen Ecke kommt. Zwei Fassungen davon würden auseinanderlaufen, und
* der Unterschied fiele erst auf einem Server auf, der schon bestellt ist.
*
* Sie trägt ALLES, was das Skript vor dem Tunnel braucht (Spec §5): es holt
* nichts ab, es gibt nichts abzurufen. Deshalb steht der private
* WireGuard-Schlüssel darin — mit einer Lebensdauer von Minuten, denn Task 9
* des Skripts tauscht ihn gegen einen auf der Maschine erzeugten.
*/
final class HostTakeoverCommand
{
/** Wohin das Archiv ausgepackt wird. Das Skript sucht sich dort selbst. */
public const INSTALL_DIR = '/opt/clupilot';
/**
* @param array{code: string, private_key: string, public_key: string, wg_ip: string} $enrolment
*/
public static function for(Host $host, array $enrolment): string
{
return implode(' ', [
'mkdir -p '.self::INSTALL_DIR,
'&& curl -fsSL '.self::archiveUrl(),
'| tar xz -C '.self::INSTALL_DIR,
'&& sh '.self::INSTALL_DIR.'/bootstrap/clupilot-bootstrap.sh',
'--code '.$enrolment['code'],
// Einfache Anführungszeichen: WireGuard-Schlüssel sind base64 und
// enthalten +, / und =. Ohne sie zerlegt die Shell die Zeile an
// einer Stelle, die der Betreiber nicht sieht.
"--wg-private '".$enrolment['private_key']."'",
'--wg-ip '.$enrolment['wg_ip'].'/'.self::subnetPrefix(),
"--hub-pubkey '".config('provisioning.wireguard.hub_public_key')."'",
'--hub-endpoint '.config('provisioning.wireguard.endpoint'),
'--api http://'.config('provisioning.wireguard.hub_address'),
'--fqdn '.self::fqdnFor($host),
]);
}
/**
* Immer über den ÖFFENTLICHEN Hostnamen.
*
* Die Konsole läuft unter `admin.…`, aber diese Zeile wird auf einer
* Maschine ausgeführt, die den Adminbereich nicht erreichen darf und soll —
* er ist genau dafür abgeriegelt. `route()` von der Konsole aus lieferte den
* Konsolen-Hostnamen, und die Zeile liefe in eine 404 auf einem Server, an
* den man dann nur noch über die Anbieterkonsole kommt.
*/
public static function archiveUrl(): string
{
$host = (string) config('admin_access.app_host');
// APP_HOST ist auf den meisten Installationen NICHT gesetzt — leer heißt
// dort „das Portal antwortet auf jedem Hostnamen", und das ist die
// Vorgabe. Der Rückfall auf `url()` war deshalb falsch: aufgerufen wird
// diese Methode aus der Konsole, also hätte `url()` den KONSOLEN-Namen
// geliefert, und die Zeile liefe auf einer Maschine, die den
// Adminbereich nicht erreichen darf, in eine 404.
//
// Also aus APP_URL, denn das ist die Adresse der Plattform und auf jeder
// Installation gesetzt.
if ($host === '') {
$host = (string) (parse_url((string) config('app.url'), PHP_URL_HOST) ?: '');
}
return $host === ''
? url('/bootstrap.tar.gz')
: 'https://'.$host.'/bootstrap.tar.gz';
}
/**
* Der Name, den CluPilot für diesen Host führt: `<rz>-<nn>.node.<zone>`.
*
* Mitgegeben statt vom Skript geraten. Die erste Fassung leitete ihn auf der
* Maschine aus der Rückwärtsauflösung ab und fiel notfalls auf einen fest
* verdrahteten Namen zurück — der Installer verlangt einen FQDN, und ein
* geratener steht danach in `/etc/hosts`, im Zertifikatsantrag und in jeder
* Protokollzeile des Hosts.
*
* Die PLATTFORM-Zone, nicht die Kundenzone: `clupilot.com`, nicht
* `clupilot.cloud`. Die zwei sind laut OfficialDomains getrennt, und ein
* Host gehört auf die Seite der Plattform.
*/
public static function fqdnFor(Host $host): string
{
$name = $host->dns_name ?: $host->name;
return $name.'.node.'.config('provisioning.dns.platform_zone');
}
/** Die Präfixlänge des Tunnel-Subnetzes, damit `--wg-ip` vollständig ist. */
private static function subnetPrefix(): string
{
$subnet = (string) config('provisioning.wireguard.subnet', '10.66.0.0/24');
return str_contains($subnet, '/') ? explode('/', $subnet)[1] : '24';
}
/**
* Was in der Zeile fehlt, damit sie überhaupt funktionieren kann.
*
* Ein leerer Hub-Schlüssel oder ein leerer Endpunkt ergibt eine Zeile, die
* sauber aussieht, kopiert wird, läuft — und in einem Tunnel endet, der nie
* einen Handshake hat. Das fiele erst auf der Maschine auf, nach der
* Installation. Lieber hier sagen, was fehlt.
*
* @return list<string>
*/
public static function missingSettings(): array
{
$missing = [];
if (blank(config('provisioning.wireguard.hub_public_key'))) {
$missing[] = 'CLUPILOT_WG_HUB_PUBKEY';
}
if (blank(config('provisioning.wireguard.endpoint'))) {
$missing[] = 'CLUPILOT_WG_ENDPOINT';
}
if (blank(config('provisioning.wireguard.hub_address'))) {
$missing[] = 'CLUPILOT_WG_HUB_ADDRESS';
}
return $missing;
}
}