#!/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
