bridge-run.sh: der Treiber, und der Zeitgeber steht vor jeder Aenderung
Verdrahtet bridge.sh in der einen Reihenfolge, die stimmen muss: sichern -> Zeitgeber -> uebernehmen -> umstellen -> nachsehen. Alles davor stellt nur fest und veraendert nichts; ab 'sichern' gibt es einen Weg zurueck, und erst ab dann darf ueberhaupt etwas angefasst werden. Abgekoppelt ist hier Bedingung, nicht Optimierung: ifreload -a nimmt die Leitung, ueber die der Befehl laeuft. PID als allererstes, damit ein frueher Poll nicht 'running aber nicht lebendig' liest und einen gesunden Lauf fuer tot erklaert. Der Treiber bestellt den Zeitgeber NIE ab — das tut CluPilot nach dem Wiederverbinden. Ein Test haelt das fest. Dazu die Fremdverwalter-Erkennung in bridge.sh: cloud-init, networkd, NetworkManager werden benannt und entmachtet, Unbekanntes fuehrt zum Abbruch statt zu einem Versuch ins Blaue. Der Zeitgeber faengt diesen Fall NICHT ab — zu seiner Zeit war alles in Ordnung, und die Bruecke verschwaende erst beim naechsten Neustart, mit Kunden darauf. Drei Tests fahren den Treiber in einer Sandkiste WIRKLICH durch, statt nur seinen Text zu lesen: die ip-Attrappe antwortet vor dem ifreload anders als danach. Sie belegen ok, failed-ohne-Bruecke und failed-mit-schalem-Tunnel — und in allen drei Faellen, dass die Rueckfahrkarte stehen bleibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>main
parent
e963821eb4
commit
b0761de1eb
|
|
@ -0,0 +1,152 @@
|
|||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# Treiber für den Brückenbau, wenn er aus der CluPilot-Pipeline kommt.
|
||||
#
|
||||
# `App\Provisioning\Steps\Host\EnsureNetworkBridge` lädt diese Datei zusammen mit
|
||||
# `bridge.sh` auf den Host und startet sie abgekoppelt. Die eigentliche Arbeit
|
||||
# macht `bridge.sh` — hier steht nur, in welcher Reihenfolge, und wie der
|
||||
# Fortschritt zurückgemeldet wird.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# Warum abgekoppelt
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Nicht wegen der Dauer — der Bau ist in Sekunden durch. Sondern weil die
|
||||
# SSH-Verbindung MITTEN im Befehl stirbt: `ifreload -a` nimmt die Leitung, über
|
||||
# die der Befehl läuft. Ein synchroner Aufruf hätte keinen Rückgabewert, sondern
|
||||
# eine Leiche.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# Die Reihenfolge, und warum sie nicht verhandelbar ist
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# sichern → Zeitgeber → übernehmen → umstellen → nachsehen.
|
||||
#
|
||||
# Der Zeitgeber steht VOR jeder Änderung. Er ist der einzige Grund, warum dieses
|
||||
# Skript die Netzkonfiguration überhaupt anfassen darf: kommt der Host nicht
|
||||
# zurück, spielt er den alten Zustand ein, und der Schritt landet in einer
|
||||
# Wiederholung statt auf einem toten Server.
|
||||
#
|
||||
# Abbestellt wird er hier NICHT. Das tut CluPilot, nachdem es sich über den
|
||||
# Tunnel neu verbunden und nachgesehen hat. Dieses Skript kann über seine eigene
|
||||
# Erreichbarkeit von außen nur raten.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rückmeldung
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# state running | ok | failed
|
||||
# pid die PID dieses Skripts — und, weil es per `setsid` gestartet
|
||||
# wird, zugleich die seiner PROZESSGRUPPE
|
||||
# phase die laufende Phase, für die Fortschrittszeile in der Konsole
|
||||
# note der Grund im Fehlerfall
|
||||
# rolled-back vom Zeitgeber angelegt, wenn er zurückgespielt hat
|
||||
# bridge.log alles, samt der geschriebenen Strophe
|
||||
#
|
||||
# `state` allein ist keine Aussage über den Lauf: stirbt das Skript, bleibt dort
|
||||
# für immer `running` stehen, weil niemand mehr da ist, der es ändert. Deshalb
|
||||
# fragt der Schritt zusätzlich `kill -0` gegen `pid`.
|
||||
|
||||
set -u
|
||||
|
||||
CLUPILOT_WORK_DIR="${CLUPILOT_WORK_DIR:-/var/lib/clupilot/bridge}"
|
||||
|
||||
# Als ALLERERSTES, vor jedem Einlesen und jeder Prüfung: der Startbefehl wartet
|
||||
# darauf, dass hier etwas steht, und bis dahin gilt der Lauf als noch nicht
|
||||
# angelaufen. `$$` und nicht `$!` auf der anderen Seite, weil `setsid` dazwischen
|
||||
# liegt — und weil `setsid` daraus eine eigene Sitzung macht, ist diese Zahl
|
||||
# zugleich die Prozessgruppe, an die ein Abbruch geschickt wird.
|
||||
echo $$ > "${CLUPILOT_WORK_DIR}/pid"
|
||||
|
||||
# CLUPILOT_WG_HUB_PUBKEY und alles andere Veränderliche schreibt der Schritt.
|
||||
# shellcheck source=/dev/null
|
||||
. "${CLUPILOT_WORK_DIR}/env"
|
||||
# shellcheck source=lib/bridge.sh
|
||||
. "${CLUPILOT_WORK_DIR}/bridge.sh"
|
||||
|
||||
phase() {
|
||||
printf '%s' "$1" > "${CLUPILOT_WORK_DIR}/phase"
|
||||
log "--- $1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '%s' "$1" > "${CLUPILOT_WORK_DIR}/note"
|
||||
printf 'failed' > "${CLUPILOT_WORK_DIR}/state"
|
||||
log "ABBRUCH: $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ein Abbruch, den keine Zeile hier abgefangen hat (kein Speicher, ein Signal),
|
||||
# darf nicht als `running` liegen bleiben und den Schritt gegen eine Leiche
|
||||
# pollen lassen. Der Schritt erkennt das zwar auch an `kill -0`, aber ein Grund
|
||||
# ist besser als eine Vermutung.
|
||||
on_exit() {
|
||||
_code=$?
|
||||
if [ "$_code" -ne 0 ] && [ "$(cat "${CLUPILOT_WORK_DIR}/state" 2>/dev/null)" = 'running' ]; then
|
||||
printf 'Der Brueckenbau brach unerwartet ab (Rueckgabewert %s); siehe bridge.log' "$_code" \
|
||||
> "${CLUPILOT_WORK_DIR}/note"
|
||||
printf 'failed' > "${CLUPILOT_WORK_DIR}/state"
|
||||
fi
|
||||
}
|
||||
trap on_exit EXIT
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Feststellen — noch wird nichts verändert
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cd "$CLUPILOT_WORK_DIR" || exit 1
|
||||
|
||||
phase 'Zustand feststellen'
|
||||
|
||||
_iface="$(detect_primary_interface)"
|
||||
[ -n "$_iface" ] \
|
||||
|| fail 'Keine Karte traegt die Standardroute — auf dieser Maschine ist nicht abzuleiten, worueber eine Bruecke gehen soll.'
|
||||
|
||||
interface_is_physical "$_iface" \
|
||||
|| fail "Die Karte mit der Standardroute (${_iface}) ist keine physische Karte, sondern eine Bruecke, ein Bond oder ein VLAN. bridge_ports darauf waere falsch."
|
||||
|
||||
_style="$(detect_network_style "$_iface")"
|
||||
_cidr="$("$CLUPILOT_IP" -4 -o addr show dev "$_iface" scope global 2>/dev/null | awk '{ print $4; exit }')"
|
||||
_gw="$("$CLUPILOT_IP" -4 route show default 2>/dev/null | awk '{ print $3; exit }')"
|
||||
|
||||
[ "$_style" = 'dhcp' ] || [ -n "$_cidr" ] \
|
||||
|| fail "Keine globale IPv4-Adresse auf ${_iface} — nichts, was auf eine Bruecke ziehen koennte."
|
||||
|
||||
log "Karte ${_iface}, Form ${_style}, Adresse ${_cidr:-per DHCP}, Gateway ${_gw:-keins}"
|
||||
|
||||
# Unbekanntes wird nicht ins Blaue gebaut. Ein Verwalter, den dieses Skript nicht
|
||||
# entmachten kann, nimmt die Bruecke beim naechsten Neustart wieder weg — und das
|
||||
# faellt erst auf, wenn Kunden darauf liegen.
|
||||
_foreign="$(foreign_network_manager)"
|
||||
case "$_foreign" in
|
||||
''|cloud-init|networkd|network-manager) ;;
|
||||
*) fail "Unbekannter Netzverwalter (${_foreign}) — hier wird nicht ins Blaue gebaut." ;;
|
||||
esac
|
||||
[ -n "$_foreign" ] && log "Fremdverwalter erkannt: ${_foreign}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ab hier wird verändert
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
phase 'sichern'
|
||||
backup_network_config \
|
||||
|| fail 'Die bestehende Netzkonfiguration liess sich nicht sichern — ohne Rueckfahrkarte wird hier nichts umgestellt.'
|
||||
|
||||
phase 'Zeitgeber stellen'
|
||||
schedule_network_rollback "${CLUPILOT_ROLLBACK_MINUTES:-5}"
|
||||
|
||||
phase 'uebernehmen'
|
||||
disown_network_manager "$_foreign" "$_iface"
|
||||
|
||||
phase 'umstellen'
|
||||
build_bridge "$_iface" "$_style" "$_cidr" "$_gw"
|
||||
|
||||
phase 'nachsehen'
|
||||
bridge_proven \
|
||||
|| fail 'Die Bruecke steht, aber der Weg nach draussen oder der Tunnel fehlt. Der Zeitgeber spielt den alten Zustand zurueck.'
|
||||
|
||||
phase 'wartet auf CluPilot'
|
||||
printf 'ok' > "${CLUPILOT_WORK_DIR}/state"
|
||||
log 'Bruecke steht und traegt; der Zeitgeber laeuft weiter, bis CluPilot ihn abbestellt.'
|
||||
exit 0
|
||||
|
|
@ -36,6 +36,7 @@ CLUPILOT_WORK_DIR="${CLUPILOT_WORK_DIR:-/var/lib/clupilot/bridge}"
|
|||
CLUPILOT_INTERFACES_FILE="${CLUPILOT_INTERFACES_FILE:-/etc/network/interfaces}"
|
||||
CLUPILOT_INTERFACES_D="${CLUPILOT_INTERFACES_D:-/etc/network/interfaces.d}"
|
||||
CLUPILOT_SYS_NET="${CLUPILOT_SYS_NET:-/sys/class/net}"
|
||||
CLUPILOT_ETC="${CLUPILOT_ETC:-/etc}"
|
||||
CLUPILOT_IP="${CLUPILOT_IP:-ip}"
|
||||
CLUPILOT_IFRELOAD="${CLUPILOT_IFRELOAD:-ifreload}"
|
||||
CLUPILOT_PROBE_URL="${CLUPILOT_PROBE_URL:-http://deb.debian.org/}"
|
||||
|
|
@ -313,6 +314,84 @@ $(extra_routes "$_iface" "$_gw")${_inet6}
|
|||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fremdverwalter
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Aus dem laufenden Zustand abzuleiten löst das LESEN. Es löst nicht das
|
||||
# SCHREIBEN: führt cloud-init das Netz, ist `/etc/network/interfaces` nicht die
|
||||
# Stelle, an der die Wahrheit steht. Die Brücke griffe entweder sofort nicht
|
||||
# oder — schlimmer — sie griffe jetzt und würde beim nächsten Neustart wieder
|
||||
# eingesammelt. Dann läuft die Übernahme grün durch und der Host verliert seine
|
||||
# Brücke Monate später, mit Kunden darauf.
|
||||
#
|
||||
# Der Zeitgeber fängt diesen Fall NICHT ab: zu seiner Zeit war alles in Ordnung.
|
||||
# Deshalb steht das hier.
|
||||
|
||||
# Wer führt das Netz? Leer heißt: reines ifupdown, und der Weg ist frei.
|
||||
foreign_network_manager() {
|
||||
if [ -f "${CLUPILOT_ETC}/cloud/cloud.cfg" ] || [ -d "${CLUPILOT_ETC}/cloud/cloud.cfg.d" ]; then
|
||||
printf 'cloud-init'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -d "${CLUPILOT_ETC}/systemd/network" ] \
|
||||
&& [ -n "$(ls -A "${CLUPILOT_ETC}/systemd/network" 2>/dev/null)" ]; then
|
||||
printf 'networkd'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -d "${CLUPILOT_ETC}/NetworkManager/system-connections" ] \
|
||||
&& [ -n "$(ls -A "${CLUPILOT_ETC}/NetworkManager/system-connections" 2>/dev/null)" ]; then
|
||||
printf 'network-manager'
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf ''
|
||||
}
|
||||
|
||||
# Entmachtet den erkannten Verwalter — im gesicherten Stand, also unter dem
|
||||
# Zeitgeber. Was hier schiefgeht, holt er zurück.
|
||||
disown_network_manager() {
|
||||
case "${1:-}" in
|
||||
cloud-init)
|
||||
mkdir -p "${CLUPILOT_ETC}/cloud/cloud.cfg.d"
|
||||
printf 'network: {config: disabled}\n' \
|
||||
> "${CLUPILOT_ETC}/cloud/cloud.cfg.d/99-clupilot-disable-network.cfg"
|
||||
log 'cloud-init: Netzteil abgeschaltet'
|
||||
;;
|
||||
networkd)
|
||||
"$CLUPILOT_SYSTEMCTL" disable --now systemd-networkd.socket >/dev/null 2>&1 || true
|
||||
"$CLUPILOT_SYSTEMCTL" mask systemd-networkd >/dev/null 2>&1 || true
|
||||
log 'systemd-networkd maskiert'
|
||||
;;
|
||||
network-manager)
|
||||
"$CLUPILOT_SYSTEMCTL" disable --now NetworkManager >/dev/null 2>&1 || true
|
||||
"$CLUPILOT_SYSTEMCTL" mask NetworkManager >/dev/null 2>&1 || true
|
||||
log 'NetworkManager maskiert'
|
||||
;;
|
||||
'')
|
||||
;;
|
||||
esac
|
||||
|
||||
# Der kleinere Verwandte: die Strophe behält `source interfaces.d/*`. Bleibt
|
||||
# dort eine Datei liegen, die dieselbe Karte beansprucht, hat der Host zwei
|
||||
# Stellen, die seine Adresse vergeben.
|
||||
_iface="${2:-}"
|
||||
if [ -n "$_iface" ] && [ -d "$CLUPILOT_INTERFACES_D" ]; then
|
||||
for _f in "$CLUPILOT_INTERFACES_D"/*; do
|
||||
[ -f "$_f" ] || continue
|
||||
case "$_f" in
|
||||
*.von-clupilot-beiseitegelegt) continue ;;
|
||||
esac
|
||||
if grep -qE "iface[[:space:]]+${_iface}[[:space:]]" "$_f" 2>/dev/null; then
|
||||
mv "$_f" "${_f}.von-clupilot-beiseitegelegt"
|
||||
log "Kollidierende Strophe beiseitegelegt: ${_f}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nachsehen — beide Richtungen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -79,6 +79,22 @@ function verdictBody(string $call): string
|
|||
."if {$call} >/dev/null 2>&1; then echo JA; else echo NEIN; fi";
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft eine Funktion, die etwas ausgibt — und meldet FEHLT, wenn es sie nicht
|
||||
* gibt. Dieselbe Begründung wie bei verdictBody(): eine leere Ausgabe und eine
|
||||
* fehlende Funktion sind von außen nicht zu unterscheiden.
|
||||
*/
|
||||
function callBridgeFn(string $call, array $env = [], string $pre = ''): string
|
||||
{
|
||||
$function = strtok($call, ' ');
|
||||
|
||||
return runBridgeSh(
|
||||
"command -v {$function} >/dev/null 2>&1 || { echo FEHLT; exit 1; }\n{$call}",
|
||||
$env,
|
||||
$pre
|
||||
);
|
||||
}
|
||||
|
||||
/** Ein /sys/class/net-Baum aus Textdateien. */
|
||||
function fakeSysNet(string $dir, string $iface, string $mac, bool $physical = true): string
|
||||
{
|
||||
|
|
@ -646,6 +662,256 @@ it('benutzt an keiner Stelle ping', function () {
|
|||
->not->toMatch('/(^|[^-\w])ping\s/m');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Der Treiber
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('schreibt die PID als allererstes', function () {
|
||||
// Der Startbefehl wartet darauf, dass hier etwas steht, und bis dahin gilt
|
||||
// der Lauf als nicht angelaufen. Steht es weiter unten, liest ein früher
|
||||
// Poll `running`-aber-nicht-lebendig und erklärt einen gesunden Lauf für
|
||||
// tot.
|
||||
$driver = base_path('deploy/bootstrap/lib/bridge-run.sh');
|
||||
expect(file_exists($driver))->toBeTrue('bridge-run.sh fehlt');
|
||||
|
||||
$pidAt = null;
|
||||
foreach (file($driver) ?: [] as $i => $line) {
|
||||
if (str_contains($line, '/pid"') && str_contains($line, '$$')) {
|
||||
$pidAt = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect($pidAt)->not->toBeNull('keine Zeile schreibt $$ in die pid-Datei')
|
||||
->and($pidAt)->toBeLessThan(60);
|
||||
});
|
||||
|
||||
it('stellt den Zeitgeber vor dem ersten verändernden Aufruf', function () {
|
||||
// Dieselbe Zusicherung wie bei bridge.sh, hier auf der Ebene des Treibers:
|
||||
// die Reihenfolge der AUFRUFE im Skripttext. Umgekehrt wäre es die
|
||||
// Reihenfolge, die einen Host dauerhaft unerreichbar macht.
|
||||
$driver = base_path('deploy/bootstrap/lib/bridge-run.sh');
|
||||
expect(file_exists($driver))->toBeTrue('bridge-run.sh fehlt');
|
||||
|
||||
$text = file_get_contents($driver);
|
||||
$backup = strpos($text, 'backup_network_config');
|
||||
$schedule = strpos($text, 'schedule_network_rollback');
|
||||
$disown = strpos($text, 'disown_network_manager');
|
||||
$build = strpos($text, 'build_bridge');
|
||||
|
||||
expect($backup)->not->toBeFalse()
|
||||
->and($schedule)->not->toBeFalse()
|
||||
->and($disown)->not->toBeFalse()
|
||||
->and($build)->not->toBeFalse()
|
||||
->and($backup)->toBeLessThan($schedule)
|
||||
->and($schedule)->toBeLessThan($disown)
|
||||
->and($disown)->toBeLessThan($build);
|
||||
});
|
||||
|
||||
it('bestellt den Zeitgeber nirgends selbst ab', function () {
|
||||
// Nur der Zeitgeber stellt zurück, und nur CluPilot bestellt ihn ab — nach
|
||||
// dem Wiederverbinden. Ein Treiber, der das selbst täte, rät über seine
|
||||
// eigene Erreichbarkeit von außen.
|
||||
$driver = base_path('deploy/bootstrap/lib/bridge-run.sh');
|
||||
expect(file_exists($driver))->toBeTrue('bridge-run.sh fehlt');
|
||||
|
||||
expect(file_get_contents($driver))->not->toContain('cancel_network_rollback');
|
||||
});
|
||||
|
||||
it('lässt den Treiber von einer Shell parsen', function () {
|
||||
$driver = base_path('deploy/bootstrap/lib/bridge-run.sh');
|
||||
expect(file_exists($driver))->toBeTrue('bridge-run.sh fehlt');
|
||||
|
||||
expect(Process::run(['sh', '-n', $driver])->successful())->toBeTrue();
|
||||
});
|
||||
|
||||
it('erkennt cloud-init als Fremdverwalter des Netzes', function () {
|
||||
// Aus dem laufenden Zustand abzuleiten löst das LESEN, nicht das SCHREIBEN.
|
||||
// Führt cloud-init das Netz, griffe die Brücke entweder sofort nicht oder,
|
||||
// schlimmer, sie griffe jetzt und würde beim nächsten Neustart wieder
|
||||
// eingesammelt — mit Kunden darauf. Der Zeitgeber fängt diesen Fall NICHT
|
||||
// ab, denn zu seiner Zeit war alles in Ordnung.
|
||||
$etc = $this->dir.'/etc';
|
||||
mkdir("{$etc}/cloud", 0o755, true);
|
||||
file_put_contents("{$etc}/cloud/cloud.cfg", "datasource_list: [ Hetzner ]\n");
|
||||
|
||||
expect(callBridgeFn('foreign_network_manager', ['CLUPILOT_ETC' => $etc]))
|
||||
->toBe('cloud-init');
|
||||
});
|
||||
|
||||
it('meldet keinen Fremdverwalter auf einer Maschine mit reinem ifupdown', function () {
|
||||
$etc = $this->dir.'/etc';
|
||||
mkdir($etc, 0o755, true);
|
||||
|
||||
expect(callBridgeFn('foreign_network_manager', ['CLUPILOT_ETC' => $etc]))
|
||||
->toBe('');
|
||||
});
|
||||
|
||||
it('entmachtet cloud-init im gesicherten Stand', function () {
|
||||
$etc = $this->dir.'/etc';
|
||||
mkdir("{$etc}/cloud", 0o755, true);
|
||||
file_put_contents("{$etc}/cloud/cloud.cfg", "datasource_list: [ Hetzner ]\n");
|
||||
|
||||
runBridgeSh('disown_network_manager cloud-init ens3', ['CLUPILOT_ETC' => $etc]);
|
||||
|
||||
expect(file_get_contents("{$etc}/cloud/cloud.cfg.d/99-clupilot-disable-network.cfg"))
|
||||
->toContain('network: {config: disabled}');
|
||||
});
|
||||
|
||||
it('legt eine kollidierende interfaces.d-Strophe beiseite', function () {
|
||||
// Die Strophe behält `source interfaces.d/*`. Bleibt dort eine Datei
|
||||
// liegen, die dieselbe Karte beansprucht, hat der Host zwei Stellen, die
|
||||
// seine Adresse vergeben.
|
||||
$etc = $this->dir.'/etc';
|
||||
$d = $this->dir.'/interfaces.d';
|
||||
mkdir($etc, 0o755, true);
|
||||
mkdir($d, 0o755, true);
|
||||
file_put_contents("{$d}/50-cloud-init", "auto ens3\niface ens3 inet static\n address 10.0.0.7/24\n");
|
||||
file_put_contents("{$d}/99-egal", "iface eth9 inet manual\n");
|
||||
|
||||
runBridgeSh('disown_network_manager "" ens3', [
|
||||
'CLUPILOT_ETC' => $etc,
|
||||
'CLUPILOT_INTERFACES_D' => $d,
|
||||
]);
|
||||
|
||||
expect(file_exists("{$d}/50-cloud-init"))->toBeFalse()
|
||||
->and(file_exists("{$d}/50-cloud-init.von-clupilot-beiseitegelegt"))->toBeTrue()
|
||||
// Eine Strophe für eine ANDERE Karte bleibt, wo sie ist.
|
||||
->and(file_exists("{$d}/99-egal"))->toBeTrue();
|
||||
});
|
||||
|
||||
/**
|
||||
* Baut eine Sandkiste, in der bridge-run.sh wirklich durchläuft.
|
||||
*
|
||||
* Der Kniff ist die `ip`-Attrappe: sie antwortet VOR dem `ifreload` anders als
|
||||
* danach, weil die `ifreload`-Attrappe eine Marke setzt. Ohne das könnte
|
||||
* `bridge_proven` nie wahr werden — die Brücke trägt die Standardroute ja erst,
|
||||
* nachdem umgestellt wurde.
|
||||
*
|
||||
* @return array{work:string, calls:string}
|
||||
*/
|
||||
function bridgeSandbox(string $dir, bool $bridgeComesUp = true, int $handshakeAge = 5): array
|
||||
{
|
||||
$work = "{$dir}/work";
|
||||
$bin = "{$dir}/bin";
|
||||
$state = "{$dir}/state";
|
||||
foreach ([$work, $bin, $state, "{$dir}/units", "{$dir}/sbin"] as $d) {
|
||||
mkdir($d, 0o755, true);
|
||||
}
|
||||
|
||||
copy(base_path('deploy/bootstrap/lib/bridge.sh'), "{$work}/bridge.sh");
|
||||
file_put_contents("{$dir}/interfaces", "# die Fassung von vorher\n");
|
||||
|
||||
$up = $bridgeComesUp ? "[ -f '{$state}/bridged' ]" : 'false';
|
||||
|
||||
file_put_contents("{$bin}/ip", <<<SH
|
||||
#!/bin/sh
|
||||
if {$up}; then DEV=vmbr0; else DEV=enp0s31f6; fi
|
||||
case "\$*" in
|
||||
'-4 route show default') echo "default via 10.0.0.1 dev \$DEV" ;;
|
||||
'-4 -o addr show dev enp0s31f6 scope global')
|
||||
echo '2: enp0s31f6 inet 10.0.0.7/24 scope global enp0s31f6' ;;
|
||||
'-4 -o addr show dev vmbr0 scope global')
|
||||
{$up} && echo '5: vmbr0 inet 10.0.0.7/24 scope global vmbr0' ;;
|
||||
'link show vmbr0') {$up} || exit 1 ;;
|
||||
'-4 route show dev enp0s31f6')
|
||||
echo '10.0.0.0/24 proto kernel scope link src 10.0.0.7' ;;
|
||||
'-4 route get 10.0.0.1') echo '10.0.0.1 dev enp0s31f6 src 10.0.0.7' ;;
|
||||
'-6 -o addr show dev enp0s31f6 scope global') ;;
|
||||
'-6 route show default') ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
SH);
|
||||
|
||||
file_put_contents("{$bin}/ifreload", "#!/bin/sh\ntouch '{$state}/bridged'\necho ifreload >> '{$dir}/calls'\n");
|
||||
file_put_contents("{$bin}/systemctl", "#!/bin/sh\necho \"systemctl \$*\" >> '{$dir}/calls'\n");
|
||||
file_put_contents("{$bin}/wg", "#!/bin/sh\nprintf '%s\\t%s\\n' 'HUBKEY=' '".(time() - $handshakeAge)."'\n");
|
||||
file_put_contents("{$bin}/logger", "#!/bin/sh\nexit 0\n");
|
||||
foreach (['ip', 'ifreload', 'systemctl', 'wg', 'logger'] as $f) {
|
||||
chmod("{$bin}/{$f}", 0o755);
|
||||
}
|
||||
|
||||
// `http_get` als Funktion: bridge.sh legt seine eigene nur unter einem
|
||||
// `command -v`-Schutz an, wer zuerst da ist behält recht — und env wird vor
|
||||
// bridge.sh gesourced.
|
||||
file_put_contents("{$work}/env", implode("\n", [
|
||||
"CLUPILOT_WORK_DIR={$work}",
|
||||
"CLUPILOT_IP={$bin}/ip",
|
||||
"CLUPILOT_IFRELOAD={$bin}/ifreload",
|
||||
"CLUPILOT_SYSTEMCTL={$bin}/systemctl",
|
||||
"CLUPILOT_WG={$bin}/wg",
|
||||
"CLUPILOT_INTERFACES_FILE={$dir}/interfaces",
|
||||
"CLUPILOT_INTERFACES_D={$dir}/interfaces.d",
|
||||
"CLUPILOT_NET_BACKUP={$dir}/sicherung",
|
||||
"CLUPILOT_UNIT_DIR={$dir}/units",
|
||||
"CLUPILOT_SBIN_DIR={$dir}/sbin",
|
||||
"CLUPILOT_SYS_NET={$dir}/sys",
|
||||
"CLUPILOT_ETC={$dir}/etc",
|
||||
'CLUPILOT_WG_HUB_PUBKEY=HUBKEY=',
|
||||
'CLUPILOT_HANDSHAKE_TRIES=1',
|
||||
'CLUPILOT_HANDSHAKE_WAIT=0',
|
||||
'http_get() { return 0; }',
|
||||
'',
|
||||
]));
|
||||
|
||||
fakeSysNet($dir, 'enp0s31f6', 'a8:a1:59:00:11:22');
|
||||
mkdir("{$dir}/etc", 0o755, true);
|
||||
|
||||
return ['work' => $work, 'calls' => "{$dir}/calls"];
|
||||
}
|
||||
|
||||
it('fährt den ganzen Ablauf durch und meldet ok', function () {
|
||||
// Bis hierher prüfen die Tests, dass der Treiber DASTEHT — `sh -n` und die
|
||||
// Reihenfolge im Text. Keiner prüft, dass er LÄUFT. Genau das ist aber die
|
||||
// Datei, die auf einem echten Host das Netz umstellt.
|
||||
$box = bridgeSandbox($this->dir);
|
||||
|
||||
$result = Process::path($box['work'])
|
||||
->env(['CLUPILOT_WORK_DIR' => $box['work']])
|
||||
->run('sh '.base_path('deploy/bootstrap/lib/bridge-run.sh'));
|
||||
|
||||
expect(trim((string) file_get_contents($box['work'].'/state')))->toBe('ok')
|
||||
->and(trim((string) file_get_contents($box['work'].'/phase')))->toBe('wartet auf CluPilot')
|
||||
->and($result->exitCode())->toBe(0)
|
||||
// Die Strophe ist geschrieben und trägt die Karte als Port.
|
||||
->and(file_get_contents($this->dir.'/interfaces'))->toContain('bridge-ports enp0s31f6')
|
||||
// Und die Rückfahrkarte steht noch: abbestellen ist CluPilots Sache.
|
||||
->and(file_exists($this->dir.'/units/clupilot-network-rollback.timer'))->toBeTrue()
|
||||
->and(file_get_contents($box['calls']))->not->toContain('stop clupilot-network-rollback.timer');
|
||||
});
|
||||
|
||||
it('meldet failed und lässt den Zeitgeber stehen, wenn die Brücke nicht trägt', function () {
|
||||
// Der Fall, für den die ganze Konstruktion gebaut ist. Der Treiber räumt
|
||||
// NICHT selbst auf — er überlässt es dem Zeitgeber und sagt nur, warum.
|
||||
$box = bridgeSandbox($this->dir, bridgeComesUp: false);
|
||||
|
||||
$result = Process::path($box['work'])
|
||||
->env(['CLUPILOT_WORK_DIR' => $box['work']])
|
||||
->run('sh '.base_path('deploy/bootstrap/lib/bridge-run.sh'));
|
||||
|
||||
expect(trim((string) file_get_contents($box['work'].'/state')))->toBe('failed')
|
||||
->and($result->exitCode())->toBe(1)
|
||||
->and(file_get_contents($box['work'].'/note'))->toContain('Zeitgeber')
|
||||
->and(file_exists($this->dir.'/units/clupilot-network-rollback.timer'))->toBeTrue()
|
||||
->and(file_get_contents($box['calls']))->not->toContain('stop clupilot-network-rollback.timer');
|
||||
});
|
||||
|
||||
it('meldet failed, wenn die Brücke steht, der Tunnel aber schal bleibt', function () {
|
||||
// Öffentlich erreichbar und trotzdem ausgesperrt: die Richtung, die der
|
||||
// Treiber ohne die Handshake-Prüfung für Erfolg gehalten hätte.
|
||||
$box = bridgeSandbox($this->dir, handshakeAge: 3600);
|
||||
|
||||
Process::path($box['work'])
|
||||
->env(['CLUPILOT_WORK_DIR' => $box['work']])
|
||||
->run('sh '.base_path('deploy/bootstrap/lib/bridge-run.sh'));
|
||||
|
||||
expect(trim((string) file_get_contents($box['work'].'/state')))->toBe('failed')
|
||||
->and(file_get_contents($box['calls']))->toContain('restart wg-quick@wg0')
|
||||
->and(file_exists($this->dir.'/units/clupilot-network-rollback.timer'))->toBeTrue();
|
||||
});
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue