CluPilotCloud/deploy/bootstrap/lib/bridge-run.sh

156 lines
6.5 KiB
Bash

#!/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 }')"
# Über den Helfer, nicht mit `awk '{ print $3 }'`: bei `default dev ens3 scope
# link` steht dort der KARTENNAME, und der landete als `gateway ens3` in der
# Strophe. Beides — Karte und Gateway — kommt aus derselben Route.
_gw="$(default_gateway)"
[ "$_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