136 lines
5.4 KiB
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;
|
|
}
|
|
}
|