147 lines
6.3 KiB
PHP
147 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Models\Datacenter;
|
|
use App\Models\Host;
|
|
use Illuminate\Support\Facades\DB;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Der Name eines Hosts — an dieser einen Stelle gebildet.
|
|
*
|
|
* Vorher gab es zwei: den getippten in `hosts.name`, den die Konsole zeigte,
|
|
* und den aus Rechenzentrum und Nummer gebauten in `hosts.dns_name`, den das
|
|
* DNS führte. Nur der zweite löste auf, und er stand in keiner einzigen
|
|
* Ansicht. Der Betreiber rief den Namen auf, den er selbst vergeben hatte, und
|
|
* stand vor einer Seite, die nicht lädt.
|
|
*
|
|
* Jetzt vergibt CluPilot ihn: `<rz>-<nn>`, fortlaufend je Rechenzentrum und
|
|
* niemals wiederverwendet. Maschine, Proxmox-Node, DNS, /etc/hosts und Konsole
|
|
* benutzen denselben.
|
|
*/
|
|
final class HostName
|
|
{
|
|
/**
|
|
* Der Name, den der nächste Host in diesem Rechenzentrum bekäme.
|
|
*
|
|
* LIEST den Zähler, verbraucht ihn nicht: die Anlegen-Seite zeigt den Namen,
|
|
* bevor gespeichert wird. Zwei gleichzeitig geöffnete Formulare zeigen
|
|
* deshalb beide dieselbe Nummer, und der zweite bekommt beim Speichern die
|
|
* nächste. Eine Reservierung beim Öffnen wäre die schlechtere Antwort — ein
|
|
* abgebrochenes Formular hinterließe eine Lücke im Zähler, die niemand
|
|
* wieder auffüllt.
|
|
*
|
|
* Liest aus `host_name_sequences`, nicht mehr aus der Rechenzentrums-Zeile:
|
|
* der Zähler ist über den Code geführt und übersteht das Löschen und
|
|
* Neuanlegen einer Rechenzentrums-Zeile mit demselben Code (siehe die
|
|
* Migration, die ihn dorthin verschoben hat).
|
|
*/
|
|
public static function preview(string $datacenterCode): string
|
|
{
|
|
$from = (int) (DB::table('host_name_sequences')
|
|
->where('code', $datacenterCode)
|
|
->value('next_host_number') ?? 1);
|
|
|
|
return self::free(self::label($datacenterCode), $from)[1];
|
|
}
|
|
|
|
/**
|
|
* Vergibt den Namen und verbraucht die Nummer.
|
|
*
|
|
* Gehört in eine Transaktion: die Sperre auf die Zeile in
|
|
* `host_name_sequences` hält nur bis zu deren Ende, und ohne sie holten
|
|
* sich zwei gleichzeitige Anlegen-Vorgänge dieselbe Nummer.
|
|
*/
|
|
public static function claim(string $datacenterCode): string
|
|
{
|
|
if (DB::transactionLevel() === 0) {
|
|
// Der Absatz oben VERLANGT eine Transaktion, erzwingt sie bisher
|
|
// aber nicht: außerhalb einer Transaktion hält `lockForUpdate`
|
|
// die Sperre nur bis zum Ende dieser einen Anfrage, nicht bis zum
|
|
// `update()` weiter unten — zwei gleichzeitige Aufrufe holten
|
|
// sich still dieselbe Nummer, ohne dass irgendwo ein Fehler
|
|
// auftaucht. Lieber hier laut scheitern als dort leise falsch
|
|
// vergeben.
|
|
throw new RuntimeException('HostName::claim() muss innerhalb einer Transaktion aufgerufen werden.');
|
|
}
|
|
|
|
if (! Datacenter::query()->where('code', $datacenterCode)->exists()) {
|
|
throw new RuntimeException("Kein Rechenzentrum mit dem Code {$datacenterCode}.");
|
|
}
|
|
|
|
// Der Zähler liegt in einer eigenen Tabelle, unabhängig von der
|
|
// Rechenzentrums-Zeile: ein gelöschtes und unter demselben Code neu
|
|
// angelegtes Rechenzentrum darf seinen Zähler nicht verlieren.
|
|
// insertOrIgnore, weil die Zeile für diesen Code schon bestehen kann
|
|
// — von einem früheren, seither gelöschten Rechenzentrum mit
|
|
// demselben Code — oder hier zum ersten Mal entsteht.
|
|
DB::table('host_name_sequences')->insertOrIgnore(['code' => $datacenterCode, 'next_host_number' => 1]);
|
|
|
|
$sequence = DB::table('host_name_sequences')->where('code', $datacenterCode)->lockForUpdate()->first();
|
|
|
|
[$number, $name] = self::free(self::label($datacenterCode), (int) $sequence->next_host_number);
|
|
|
|
DB::table('host_name_sequences')->where('code', $datacenterCode)->update(['next_host_number' => $number + 1]);
|
|
|
|
return $name;
|
|
}
|
|
|
|
/**
|
|
* `fsn-03` → `fsn-03.node.clupilot.com`.
|
|
*
|
|
* Die PLATTFORM-Zone, nicht die Kundenzone: die zwei sind laut
|
|
* OfficialDomains getrennt, und ein Host gehört auf die Seite der Plattform.
|
|
*/
|
|
public static function fqdn(string $name): string
|
|
{
|
|
return $name.'.node.'.config('provisioning.dns.platform_zone');
|
|
}
|
|
|
|
/**
|
|
* Rechenzentrums-Codes von vor der Verschärfung der Prüfregel können
|
|
* Zeichen tragen, die eine DNS-Bezeichnung nicht darf. Ein Name, den der
|
|
* Anbieter ablehnt, ließe den Host für immer namenlos.
|
|
*/
|
|
public static function label(string $datacenterCode): string
|
|
{
|
|
$label = trim(preg_replace('/[^a-z0-9-]+/', '-', strtolower($datacenterCode)) ?? '', '-');
|
|
|
|
return $label !== '' ? $label : 'node';
|
|
}
|
|
|
|
/**
|
|
* Ab dieser Nummer aufwärts der erste freie Name.
|
|
*
|
|
* Der Zähler allein reicht nicht: zwei Alt-Codes (`eu_west` und `eu-west`)
|
|
* werden zur selben Bezeichnung und führen doch getrennte Zähler. Ohne
|
|
* dieses Weiterzählen bekäme der zweite `eu-west-01` und liefe in den
|
|
* eindeutigen Index — ein Abbruch beim Anlegen statt eines Namens.
|
|
*
|
|
* Nach unten geht es dabei nie: eine entfernte `fsn-02` wird nicht wieder
|
|
* vergeben, weil der Zähler längst darüber steht. Dieses Weiterzählen ist
|
|
* der Riegel, nicht der Zähler — aber nur einfädig. `claim()` sperrt die
|
|
* ZEILE in `host_name_sequences` über den ROHEN Code, und `eu_west` und
|
|
* `eu-west` sind zwei verschiedene Codes (und damit zwei verschiedene
|
|
* Zeilen dort), die zur selben Bezeichnung normalisieren. Laufen beide
|
|
* gleichzeitig durchs Onboarding, sperrt jeder Aufruf nur seine eigene
|
|
* Zeile, beide sehen hier denselben Kandidaten frei, und wer den `INSERT`
|
|
* als Zweiter absetzt, bekommt am `unique('name')`-Index eine
|
|
* unbehandelte `QueryException` statt der nächsten Nummer.
|
|
*
|
|
* `%02d` füllt zweistellig auf und wächst ab hundert von selbst weiter.
|
|
*
|
|
* @return array{0: int, 1: string}
|
|
*/
|
|
private static function free(string $label, int $from): array
|
|
{
|
|
$number = max(1, $from);
|
|
|
|
while (Host::query()->where('name', sprintf('%s-%02d', $label, $number))->exists()) {
|
|
$number++;
|
|
}
|
|
|
|
return [$number, sprintf('%s-%02d', $label, $number)];
|
|
}
|
|
}
|