XinionOS als eigenes Erzeugnis herausgeloest

Die Geraeteschicht — Abbild, zwei Wurzeln, Selbstprueflauf, Ruecksprung —
verlaesst die Ablage der ersten Appliance und wird ein eigenes Erzeugnis
mit eigenem Takt und eigenem Nachweis.

Drei Gruende, nach Gewicht: Der Cyber Resilience Act verpflichtet fuer das
gesamte Produkt einschliesslich der Betriebssystemschicht — vier gewachsene
Basissysteme waeren vier Stuecklisten und vier Schwachstellenprozesse, eine
gemeinsame Basis ist ein Dossier. Der Ruecksprung wird nur einmal richtig
gebaut, weil er nur geprueft wird wenn ihn jemand braucht. Und eine
abgegrenzte Aufgabe laesst sich ausschreiben, was unmittelbar auf das
groesste benannte Risiko des Hauses einzahlt.

Der Einwand der Vertraeglichkeitsmatrix wird durch den Nutzlast-Vertrag
entkraeftet statt weggeredet: Die Nutzlast liegt in der Wurzel und wird mit
ihr getauscht. Eine Fassung, ein Rueckweg, keine Matrix.

Das OS kennt kein Produkt — Regel 1. Alle Namen sind entkoppelt, die
Statusseite ist auf die Standardbibliothek umgestellt, weil jede
Abhaengigkeit im Grund-OS eine Verpflichtung ueber die Produktlebensdauer
ist.

Nichts davon ist gelaufen. Der Inhalt gilt als Bauplan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-08-20 08:02:55 +02:00
commit d9ed1bc2ff
15 changed files with 903 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*.iso
*.iso.sha256
.DS_Store
*.key
*.pem

77
BRIEFING.md Normal file
View file

@ -0,0 +1,77 @@
# Briefing — XinionOS
> Für die Entwicklungsumgebung und für einen neu hinzukommenden Entwickler.
> **Erst lesen, dann anfangen.**
## Was dieses Erzeugnis ist
Das Betriebssystem der Xinion-Appliances. Ein Gerät, das startet, sich selbst prüft, sich sicher
aktualisieren lässt und im Fehlerfall **von allein** auf die vorherige Fassung zurückkehrt.
Es weiß nicht, was auf ihm läuft. Alles Fachliche kommt über den
[Nutzlast-Vertrag](nutzlast/VERTRAG.md).
## Rollenverteilung
| Wer | Was |
|---|---|
| Architektur (CIO und Entwurf) | entscheidet, **was** gebaut wird und warum |
| Entwicklung | baut es, bringt es zum Laufen, **misst** und legt den Nachweis ab |
**Begegnet der Bauplan der Wirklichkeit und verliert, ist das ein Befund und kein Auftrag zum
Umplanen.** Melde zurück, entwirf nicht neu. Faustregel: Musst Du eine Architekturentscheidung
anfassen, um weiterzukommen, ist es eine Entscheidung und keine Reparatur.
## Was zuerst zu bauen ist
**Das Abbild baut reproduzierbar, und der Rücksprung greift nachweislich.**
> Fällt der Selbstprüflauf auf einer Wurzel durch, kehrt das Gerät nach drei Startversuchen **von
> selbst** auf die andere zurück — und man kann hinterher nachlesen, warum.
### Der Nachweis, Schritt für Schritt
1. `abbild/bauen.sh <ubuntu-server.iso>` — Abbild bauen, in einer virtuellen Maschine installieren.
Das Gerät startet in **Wurzel A**.
2. Statusseite auf Anschluss 8080 aufrufen: Wurzel A, Fassung, Selbstprüflauf **bestanden**.
3. Wurzel B befüllen, Starteintrag mit drei Versuchen anlegen.
4. In Wurzel B `/etc/xinionos/selbstpruefung-scheitert` anlegen — der Prüfschalter.
5. Auf B umschalten, neu starten.
6. **Beobachten:** dreimal Fehlschlag, danach startet das Gerät **ohne Zutun** wieder in A.
7. Die Statusseite zeigt unter *Verlauf* drei durchgefallene Läufe auf B und darunter den
bestandenen auf A.
**Schritt 6 ist der Nachweis.** Ergebnis nach `docs/nachweise/`.
### Wo der erste Lauf voraussichtlich scheitert
Beim Anhängen von `autoinstall ds=nocloud;s=/cdrom/xinionos/` an die Startzeile — die Pfade zu
`grub.cfg` unterscheiden sich zwischen Ubuntu-Fassungen. **Startet das Medium und fragt trotzdem
nach der Sprache, liegt es dort und sonst nirgends.** Schnappschuss der VM vor dem ersten Versuch.
## Der Stand: nichts davon ist gelaufen
Der gesamte Inhalt ist auf einem Mac geschrieben worden, auf dem weder ein Ubuntu-Abbild gebaut
noch eine Unit gestartet werden kann. **Behandle ihn als Bauplan, nicht als Zustand.**
## Was Du nicht tust
* **Keine Architekturentscheidung treffen, keine ADR schreiben.** Melde zurück.
* **Keinen Produktnamen aufnehmen.** Weder NEXTgenIT noch OLYMPUS noch sonst einen — siehe Regel 1
in `CLAUDE.md`.
* **Keine Abhängigkeit ohne Begründung im Commit-Text.** Jede steht fünf Jahre in der Stückliste.
* **Keine Zahl erfinden und keine glätten.**
* **Kein `git add -A`.**
## Wo alles steht
| Was | Wo |
|---|---|
| Regeln | `CLAUDE.md` |
| Vertrag zur Nutzlast | `nutzlast/VERTRAG.md` |
| Startverfahren und Rücksprung | `docs/decisions/ADR-0002-startverfahren-und-ruecksprung.md` |
| Gemessene Ergebnisse | `docs/nachweise/` |
**Bei Widerspruch gilt:** ADR schlägt Dokumentation, **Messung schlägt ADR.** Eine Messung, die
einer Entscheidung widerspricht, ist der wertvollste Fund überhaupt — melde ihn, bevor Du
weiterbaust.

62
CLAUDE.md Normal file
View file

@ -0,0 +1,62 @@
# XinionOS — Regeln für diese Ablage
> Verbindlich für jeden, der hier arbeitet — Mensch wie Agent.
## Was hier gebaut wird
Die **Geräteschicht** unter den Appliance-Produkten des Hauses: Abbild, Ersteinrichtung, zwei
Wurzeln, Selbstprüflauf, Rücksprung, Statusseite. **Sonst nichts.**
## Die fünf Regeln
### 1 · Das Betriebssystem kennt kein Produkt
Nirgends in dieser Ablage steht der Name eines Produkts — kein NEXTgenIT, kein OLYMPUS, kein
COSMOS, keine Fachlichkeit. Was ein Produkt braucht, bekommt es über den
[Nutzlast-Vertrag](nutzlast/VERTRAG.md).
Sobald hier ein Produktname auftaucht, ist der Vertrag falsch geschnitten. **Dann wird der Vertrag
geändert, nicht der Name durchgereicht.**
### 2 · Jede Abhängigkeit ist eine Verpflichtung
Was hier eingebaut wird, steht in der Stückliste, muss im Schwachstellenprozess beobachtet und nach
dem Cyber Resilience Act über die Produktlebensdauer versorgt werden — **für jedes Gerät jedes
Kunden.**
Deshalb: **Standardbibliothek vor Paket, Paket vor Fremdablage.** Eine Bequemlichkeit heute ist
fünf Jahre Pflege. Wer eine Abhängigkeit aufnimmt, schreibt in den Commit-Text, warum es ohne sie
nicht ging.
### 3 · Was die Distribution liefert, bleibt unangetastet
Kern, Startlader und Prüfglied kommen aus dem offiziellen Ubuntu-Abbild. Wir legen darauf, wir
ersetzen nicht. Der Grund ist die Zertifizierung durch die Serverhersteller und die signierte
Startkette — ein selbstgebautes System hätte beides nicht.
### 4 · Der Rücksprung ist die Kernzusage
Alles hier dient dem einen Satz: *Kommt eine neue Fassung nicht sauber hoch, kehrt das Gerät von
selbst zur vorherigen zurück.* **Nichts darf ihn umgehen** — kein Aktualisierungsdienst am
Updatekanal vorbei, kein Zustand außerhalb der Wurzel, keine Nutzlast, die sich selbst nachlädt.
Und er wird nur geprüft, wenn ihn jemand auslöst. Deshalb gibt es den Prüfschalter, und deshalb
gehört zu jeder Änderung an diesem Pfad ein neuer Nachweis.
### 5 · Nachweis am Gerät, nicht im Kopf
Was hier geschrieben wird, ist **Bauplan**. Belegt ist nur, was in `docs/nachweise/` steht — mit
Gerät, Zeitpunkt, Ablauf und der **echten Systemmeldung** statt einer Zusammenfassung darüber. Ein
Fehlschlag ist ein vollwertiger Nachweis.
## Sprache
Fachobjekte deutsch — *Wurzel, Fassung, Selbstprüflauf, Rücksprung, Nutzlast, Bestand*. Technik
englisch — *boot, target, unit, service*.
Dokumentation für Menschen: erst ein Absatz Prosa, der sagt worum es geht und warum es zählt, dann
Tabellen. Eine Entscheidung nennt immer ihren Grund.
## Git
Nur eigene Dateien stagen, kein `git add -A`. Commit-Text sagt, **was sich fachlich ändert**.

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# XinionOS — das Betriebssystem der Xinion-Appliances
XinionOS ist die **Geräteschicht** unter den Appliance-Produkten des Hauses. Es liefert ein Gerät,
das startet, sich selbst prüft, sich sicher aktualisieren lässt und im Fehlerfall von allein auf die
vorherige Fassung zurückkehrt — und sonst nichts.
**Es weiß nicht, was auf ihm läuft.** Das ist keine Sparsamkeit, sondern der Zweck: Dieselbe Basis
trägt eine ITSM-Appliance, eine Proxy-Appliance und eine Anwendungsplattform, ohne von einer davon
etwas zu wissen.
## Wo es im Haus steht
| Ebene | Was | Beispiele |
|---|---|---|
| Anwendungen | Fachliche Produkte | COSMOS, INSIGHT, ORION |
| Plattform | Laufzeit- und Betriebsschicht | OLYMPUS · oder die Wirt-Dienste einer Fach-Appliance |
| **Gerät** | **XinionOS** | **diese Ablage** |
| Infrastruktur | Rechenzentrum | GAIA |
Beim Kunden auf Blech ist XinionOS das, worauf die Verkaufsvariante der Plattform läuft.
## Was es leistet
* **Ein Abbild, unbeaufsichtigt installierbar** — der Techniker legt das Medium ein und geht.
* **Zwei abwechselnd beschriebene Wurzeln.** Ein Update schreibt nie in die laufende Fassung.
* **Ein Selbstprüflauf entscheidet über den Start.** Nicht „bootet" gilt als Erfolg, sondern
„ist arbeitsfähig".
* **Rücksprung ohne Zutun.** Kommt eine neue Fassung nicht sauber hoch, kehrt das Gerät nach
begrenzten Versuchen von selbst zurück.
* **Betrieb ohne Internet.** Nichts holt sich selbsttätig Aktualisierungen.
* **Eine Einhängestelle für genau eine Nutzlast** — siehe [`nutzlast/`](nutzlast/VERTRAG.md).
## Was es ausdrücklich nicht leistet
Keine Fachlichkeit, keine Anwendungslogik, keine Benutzerverwaltung, keine Mandantenfähigkeit.
Das alles ist Sache der Nutzlast.
## Einstieg
* **Regeln für die Arbeit hier:** [`CLAUDE.md`](CLAUDE.md)
* **Wenn Du baust:** [`BRIEFING.md`](BRIEFING.md)
* **Der Vertrag zur Nutzlast:** [`nutzlast/VERTRAG.md`](nutzlast/VERTRAG.md)
* **Was wirklich gemessen wurde:** [`docs/nachweise/`](docs/nachweise/README.md)

View file

@ -0,0 +1,60 @@
#cloud-config
# Ersteinrichtung des Grund-Abbilds nach ADR-0004.
#
# Das Plattenbild ist der Kern dieser Datei. Es teilt den Datentraeger so auf, dass ein
# Fassungswechsel nur eine der beiden Wurzeln beschreibt und der Bestandsbereich davon
# unberuehrt bleibt — das ist der Grund, warum die vorgehaltene Vorgaengerfassung 40 GB
# kostet und nicht ueber 100.
autoinstall:
version: 1
locale: de_DE.UTF-8
keyboard:
layout: de
storage:
config:
- {id: platte, type: disk, ptable: gpt, wipe: superblock-recursive, preserve: false, grub_device: false, match: {size: largest}}
# EFI-Startbereich. Bewusst 1 GB statt der ueblichen 512 MB: systemd-boat liest keine
# Linux-Dateisysteme, also liegen hier ZWEI Saetze aus Kern und Startabbild (ADR-0011).
- {id: p-efi, type: partition, device: platte, size: 1G, flag: boot, grub_device: true}
- {id: fs-efi, type: format, volume: p-efi, fstype: fat32, label: EFI}
- {id: m-efi, type: mount, device: fs-efi, path: /boot/efi}
- {id: p-wurzel-a, type: partition, device: platte, size: 40G}
- {id: fs-wurzel-a, type: format, volume: p-wurzel-a, fstype: ext4, label: wurzel-a}
- {id: m-wurzel-a, type: mount, device: fs-wurzel-a, path: /}
# Wurzel B wird bei der Ersteinrichtung angelegt, aber nicht eingehaengt. Sie ist das
# Ziel des ersten Fassungswechsels.
- {id: p-wurzel-b, type: partition, device: platte, size: 40G}
- {id: fs-wurzel-b, type: format, volume: p-wurzel-b, fstype: ext4, label: wurzel-b}
# Bestand: Modelle, Wissensspeicher, Zustandsdatenbank, Vektorindex, Tresor, Protokolle.
# Wird bei einem Fassungswechsel NICHT getauscht und ueberlebt den Ruecksprung.
- {id: p-bestand, type: partition, device: platte, size: -1}
- {id: fs-bestand, type: format, volume: p-bestand, fstype: ext4, label: bestand}
- {id: m-bestand, type: mount, device: fs-bestand, path: /bestand}
packages:
- systemd-boot
- python3
- python3-venv
# Die Schnappschuss-Pakete werden stillgelegt: ihr Dienst sucht selbsttaetig nach
# Aktualisierungen und wuerde damit am Updatekanal, an der Signaturpruefung und am
# Ruecksprung vorbei arbeiten (ADR-0004).
late-commands:
# Die Wurzel-Auflage: Selbstprueflauf, Wurzel-Auskunft und die Units des Dienstes.
- cp -a /cdrom/xinionos/wurzel/. /target/
- curtin in-target -- systemctl enable xinionos-selbstpruefung.service
- curtin in-target -- systemctl enable xinionos-status.service
# Startverwalter nach ADR-0011. bootctl legt systemd-boot auf den EFI-Bereich; die
# Starteintraege mit Zaehler entstehen beim Fassungswechsel, nicht hier.
- curtin in-target -- bootctl install --graceful
# Die Schnappschuss-Pakete werden stillgelegt: ihr Dienst sucht selbsttaetig nach
# Aktualisierungen und wuerde damit am Updatekanal, an der Signaturpruefung und am
# Ruecksprung vorbei arbeiten (ADR-0004).
- curtin in-target -- systemctl mask snapd.service snapd.socket snapd.seeded.service || true
- curtin in-target -- apt-get -y purge snapd || true

77
abbild/bauen.sh Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Baut das Grund-Abbild der Appliance als eigenstaendiges Installationsmedium.
#
# GRUNDSATZ: Wir bauen kein Betriebssystem von Grund auf, sondern schreiben das offizielle
# Ubuntu-Server-Abbild um. Der Grund steht in ADR-0004 — Ubuntu wurde gewaehlt, WEIL die
# Serverhersteller dagegen zertifizieren und weil die signierte Startkette von Canonical
# kommt. Ein selbstgebautes System waere kein zertifiziertes Ubuntu mehr, und damit waere
# genau das Argument weg, aus dem die Wahl getroffen wurde.
#
# Aufruf: ./bauen.sh <ubuntu-server.iso> [ausgabe.iso]
set -euo pipefail
hier=$(cd "$(dirname "$0")" && pwd)
quelle=${1:?Pfad zum offiziellen Ubuntu-Server-Abbild angeben}
ziel=${2:-$hier/xinionos.iso}
kennung=XINIONOS
meldung() { printf '\033[1m==>\033[0m %s\n' "$*"; }
abbruch() { printf '\033[1;31mAbbruch:\033[0m %s\n' "$*" >&2; exit 1; }
# --- Voraussetzungen ------------------------------------------------------------------
[ "$(uname -s)" = Linux ] || abbruch "Der Bau laeuft nur auf Linux. macOS kann kein Ubuntu-Abbild erzeugen."
for werkzeug in xorriso rsync; do
command -v "$werkzeug" >/dev/null || abbruch "$werkzeug fehlt. Nachinstallieren: apt-get install xorriso rsync"
done
[ -r "$quelle" ] || abbruch "Quellabbild nicht lesbar: $quelle"
arbeit=$(mktemp -d)
trap 'rm -rf "$arbeit"' EXIT
inhalt=$arbeit/inhalt
# --- Quellabbild auspacken ------------------------------------------------------------
meldung "Quellabbild auspacken"
xorriso -osirrox on -indev "$quelle" -extract / "$inhalt" >/dev/null 2>&1
chmod -R u+w "$inhalt"
# --- Ersteinrichtung und Wurzel-Auflage beilegen ---------------------------------------
meldung "Ersteinrichtung und Wurzel-Auflage beilegen"
mkdir -p "$inhalt/xinionos"
cp "$hier/autoinstall/user-data" "$inhalt/xinionos/user-data"
: > "$inhalt/xinionos/meta-data"
rsync -a "$hier/../wurzel/" "$inhalt/xinionos/wurzel/"
# --- Startvorgang auf unbeaufsichtigte Einrichtung stellen ------------------------------
# Der Techniker legt das Medium ein und geht. Rueckfragen bei der Ersteinrichtung sind bei
# einem Geraet, das beim Kunden aufgestellt wird, kein Komfortthema, sondern eine Fehlerquelle.
meldung "Startvorgang auf unbeaufsichtigte Einrichtung stellen"
zusatz='autoinstall ds=nocloud\;s=/cdrom/xinionos/'
for datei in "$inhalt/boot/grub/grub.cfg" "$inhalt/EFI/boot/grub.cfg"; do
[ -f "$datei" ] || continue
sed -i "s|\(linux.*vmlinuz\)|\1 ${zusatz}|" "$datei"
sed -i 's/^\(set timeout=\).*/\11/' "$datei"
done
[ -f "$inhalt/isolinux/txt.cfg" ] && sed -i "s|\(append.*initrd=[^ ]*\)|\1 ${zusatz}|" "$inhalt/isolinux/txt.cfg" || true
# --- Neu zusammensetzen -----------------------------------------------------------------
# Die Startbilder eines Ubuntu-Abbilds aendern sich zwischen Fassungen. Statt Schalter fest
# einzutragen und bei der naechsten Fassung stillschweigend ein nicht startendes Medium zu
# erzeugen, fragen wir das Quellabbild, wie es selbst gebaut wurde.
meldung "Abbild neu zusammensetzen"
schalter=$(xorriso -indev "$quelle" -report_el_torito as_mkisofs 2>/dev/null \
| grep -v '^-V' | tr '\n' ' ')
[ -n "$schalter" ] || abbruch "Startbilder des Quellabbilds nicht auslesbar — Abbild vermutlich beschaedigt."
( cd "$inhalt" && eval xorriso -as mkisofs -V "$kennung" $schalter -o "$ziel" . ) >/dev/null 2>&1
# --- Nachweis ---------------------------------------------------------------------------
summe=$(sha256sum "$ziel" | cut -d' ' -f1)
printf '%s %s\n' "$summe" "$(basename "$ziel")" > "$ziel.sha256"
meldung "Fertig"
printf ' Abbild: %s\n Pruefsumme: %s\n' "$ziel" "$summe"
cat <<'HINWEIS'
Dieses Abbild ist NICHT signiert. Die Signaturpruefung vor dem Umschalten
(ADR-0004) gehoert in den naechsten Schnitt, zusammen mit dem Fassungswechsel.
HINWEIS

View file

@ -0,0 +1,54 @@
# ADR-0001 — Das Betriebssystem wird ein eigenes Erzeugnis
**Stand:** Angenommen, 20.08.2026 · **Entscheidet:** CIO Thomas Reitz
---
## Worum es geht
Die Geräteschicht — Abbild, zwei Wurzeln, Selbstprüflauf, Rücksprung — war Teil der ersten
Appliance, für die sie gebraucht wurde. Inzwischen ist absehbar, dass mehrere Erzeugnisse des
Hauses auf Blech beim Kunden stehen sollen: eine Fach-Appliance, eine Proxy-Appliance, die
Verkaufsvariante der Anwendungsplattform.
Diese Entscheidung trennt die Geräteschicht heraus.
## Die Entscheidung
**XinionOS ist ein eigenes Erzeugnis mit eigener Ablage, eigenem Takt und eigenem Nachweis.** Es
kennt kein Produkt; jede Fachlichkeit kommt über den Nutzlast-Vertrag.
## Warum, in der Reihenfolge des Gewichts
**Der Cyber Resilience Act.** Für mehrere Erzeugnisse des Hauses liegen bereits
CRA-Konformitätsdossiers. Die Verordnung verpflichtet den Hersteller, Schwachstellen zu behandeln
und Sicherheitsaktualisierungen zu liefern — **für das gesamte Produkt einschließlich der
Betriebssystemschicht.** Vier gewachsene Basissysteme sind vier Stücklisten, vier
Schwachstellenprozesse, vier Aktualisierungswege. Eine gemeinsame Basis ist **ein Dossier, auf das
jedes Produkt verweist.**
**Der Rücksprung wird nur einmal richtig gebaut.** Er wird ausschließlich dann geprüft, wenn ihn
jemand braucht — eine zweite handgebaute Umsetzung wäre eine zweite ungeprüfte.
**Und er lässt sich besetzen.** Eine abgegrenzte Aufgabe „Betriebssystem für Appliances" ist eine
Stelle, die man ausschreiben kann. „Jemand, der alles kann" ist keine. Das zahlt unmittelbar auf
das größte benannte Risiko des Hauses ein: die Abhängigkeit von einer einzelnen Person.
## Was dagegen sprach
**Verfrühte Verallgemeinerung.** Eine Plattform aus einem einzigen Fall abzuleiten geht meistens
schief; die richtige Abstraktion zeigt sich am zweiten. Der Einwand trägt hier nicht mehr — die
Verkaufsvariante der Anwendungsplattform ist gebaut und läuft.
**Die Verträglichkeitsmatrix.** Zwei getrennt versionierte Erzeugnisse müssen zueinander passen.
Dieser Einwand ist ernst und wird durch den Nutzlast-Vertrag entkräftet, nicht weggeredet: **Die
Nutzlast liegt in der Wurzel und wird mit ihr getauscht** — eine Fassung, ein Rückweg, keine Matrix.
## Was daraus folgt
Die Geräteschicht verlässt die Ablage der ersten Appliance. Dort bleibt ein Verweis und die
Feststellung, dass die Appliance künftig eine **Nutzlast** ist.
Der Name ist vorläufig. Sobald er auf einem Datenblatt oder in einem CRA-Dossier steht, ist er
marktgerichtet und braucht eine Markenrecherche — und ein Betriebssystem, das den Hausnamen trägt,
steht auf jedem Kundengerät. Eine spätere Umbenennung wäre entsprechend teuer.

View file

@ -0,0 +1,101 @@
# ADR-0011 — systemd-boot trägt den Rücksprung, der Selbstprüflauf entscheidet ihn
**Stand:** Angenommen, 17.08.2026 · **Entscheidet:** CIO Thomas Reitz
**Setzt um:** `ADR-0004` (A/B-Wurzel) · **Betrifft:** Abbildbau, Updatekanal, offene Frage O-8
**Nachweis steht aus** — die Punkte unter „Was am Gerät zu bestätigen ist"
---
## Worum es geht
`ADR-0004` verspricht einen Rücksprung, der mehr kann als „bootet nicht": Nach dem Umschalten
bekommt die neue Fassung eine begrenzte Zahl von Versuchen, gilt erst nach einem bestandenen
**Selbstprüflauf** als bestätigt, und läuft der Zähler ab, kehrt das Gerät **von selbst** zur
vorherigen Wurzel zurück.
Das ist eine Zusage über Zähler, Schwellen und Zustände — also über Mechanik, die entweder korrekt
ist oder still versagt. Und sie versagt an einer unangenehmen Stelle: **Ein Rücksprung, der nicht
funktioniert, fällt erst auf, wenn man ihn braucht.**
Diese Entscheidung legt fest, womit er gebaut wird.
## Die Entscheidung
**Der Startvorgang läuft über systemd-boot mit dessen Automatic Boot Assessment. Der
Selbstprüflauf hängt vor `boot-complete.target`.**
Der Grund ist, dass genau diese Mechanik dort bereits existiert und gepflegt wird:
| Was ADR-0004 verlangt | Was systemd-boot mitbringt |
|---|---|
| begrenzte Zahl von Startversuchen | Zähler im Dateinamen des Starteintrags — `…+3.conf` |
| Herunterzählen bei jedem Versuch | erledigt der Startverwalter selbst, vor dem Kern |
| Bestätigung erst nach bestandener Prüfung | `systemd-bless-boot` löscht den Zähler — und läuft erst nach `boot-complete.target` |
| Rückfall nach erschöpften Versuchen | erschöpfte Einträge werden ans Ende sortiert |
Unser Anteil daran ist eine einzige Unit: der Selbstprüflauf, der **vor** `boot-complete.target`
laufen muss und von ihm **benötigt** wird. Fällt er durch, wird das Ziel nie erreicht, der Zähler
nie gelöscht — und der dritte Fehlversuch stuft die Fassung ab.
> **Der eigentliche Gewinn ist, was wir dadurch nicht bauen.** Ein selbstgeschriebener Startzähler
> in einer Umgebungsvariablen des Startverwalters ist erfahrungsgemäß genau die Stelle, an der
> Rücksprünge versagen: Er wird bei einem unsauberen Abschalten nicht zurückgesetzt, er verzählt
> sich bei einem Neustart aus der laufenden Sitzung, und niemand merkt es, weil der Normalbetrieb
> davon nichts sieht. Bei einem Produkt, dessen Verkaufsargument der funktionierende Rückweg ist,
> ist geerbte und gepflegte Mechanik mehr wert als eigener Code.
## Was daraus für den Aufbau folgt
**Kern und Startabbild liegen auf dem EFI-Bereich.** systemd-boot liest keine Linux-Dateisysteme;
was es starten soll, muss auf dem EFI-Bereich liegen. Je Wurzel ein eigenes Verzeichnis, also zwei
Sätze aus Kern und Startabbild. Das ist der Grund, warum der EFI-Bereich in ADR-0004 mit einem
Gigabyte großzügiger bemessen ist als üblich.
**Der Selbstprüflauf wächst mit dem Produkt.** Heute prüft er, was es gibt: richtige Wurzel
eingehängt, Bestandsbereich vorhanden, Dienste gestartet. Später kommen die Punkte aus ADR-0004
dazu — Datenbank antwortet, jede Aufgabenklasse ist bedienbar, der Wissensspeicher ist erreichbar.
**Die Verdrahtung ändert sich dabei nicht**, nur die Prüfliste. Das ist der Grund, ihn früh zu bauen.
**Und er braucht einen Prüfschalter.** Ein Rücksprung, den man nicht auslösen kann, ist nicht
prüfbar. Deshalb bringt der Selbstprüflauf einen ausdrücklichen Weg mit, absichtlich
durchzufallen — sonst lässt sich die Abnahmebedingung nicht messen.
## Was am Gerät zu bestätigen ist, bevor diese Entscheidung als belegt gilt
Vier Punkte, und keiner davon lässt sich herleiten:
**Greift der Rückfall wirklich?** Drei Fehlversuche auf Wurzel B müssen das Gerät ohne Zutun nach A
bringen. Das ist die Abnahmebedingung, alles andere ist Vorarbeit.
**Trägt die signierte Startkette?** Ubuntu startet regulär über GRUB mit vorgelagertem Prüfglied.
Ob systemd-boot dort mit eingeschaltetem Secure Boot sauber läuft, ist am Gerät zu prüfen. Fällt
das negativ aus, ist der zweite Weg aus der Bewertung unten zu nehmen — die übrige Bauweise bleibt.
**Überlebt der Tresorschlüssel den Fassungswechsel?** Bindet er an den Sicherheitsbaustein, hängt er
an einer Messung des Startzustands — und der ändert sich mit dem Startverwalter. Das ist die offene
Frage O-8, und sie wird durch diese Entscheidung **berührt, nicht gelöst**.
**Verhält sich ein erschöpfter Eintrag wie erwartet?** Dass abgestufte Einträge ans Ende sortiert
werden, ist dokumentiert. Wie sich das mit einem ausdrücklich gesetzten Vorgabeeintrag verträgt,
gehört nachgesehen und nicht angenommen.
> Bis diese vier Punkte gemessen sind, gilt für diese Entscheidung dieselbe Regel wie für alles
> andere in diesem Produkt: **Sie ist begründet, nicht belegt.**
## Die verworfene Alternative
**GRUB mit eigenem Zähler.** Ubuntus Standardweg bliebe unangetastet, die signierte Kette
funktioniert nachweislich, und die Mechanik wäre über `GRUB_DEFAULT=saved` und einen Zähler in der
Umgebungsdatei des Startverwalters aufgebaut.
Der Vorzug ist real und war der Grund, überhaupt abzuwägen: Bei eingeschaltetem Secure Boot ist das
der Weg, der garantiert läuft.
Dagegen steht, dass **der Zähler dann unser Code ist** — und zwar Code, der bei jedem unsauberen
Abschalten, jedem Neustart aus der laufenden Sitzung und jedem Stromausfall das Richtige tun muss.
Getestet wird er nur, wenn jemand einen Rücksprung auslöst, also fast nie. Ein stiller Fehler darin
zeigt sich zum ersten Mal an dem Tag, an dem ein Kunde ihn braucht.
**Fällt die Secure-Boot-Prüfung negativ aus, wird diese Alternative wieder aufgenommen** — dann
allerdings mit der ausdrücklichen Auflage, den Zähler gegen unsauberes Abschalten zu prüfen, nicht
nur gegen den Normalfall.

47
docs/nachweise/README.md Normal file
View file

@ -0,0 +1,47 @@
# Nachweise — hier steht, was wirklich gemessen wurde
Dieses Verzeichnis ist die Ablage für **gemessene Ergebnisse**. Es existiert, weil das Produkt mit
dem Satz „kein erledigt ohne Nachweis" verkauft wird und diese Regel für die eigene Entwicklung
genauso gilt wie für das Gerät beim Kunden.
Alles andere im Repo ist Absicht, Entwurf oder Bauplan. **Nur was hier liegt, ist belegt.**
## Wann ein Nachweis fällig ist
Immer dann, wenn eine Architekturentscheidung oder eine Zusage aus dem Konzept zum ersten Mal
wirklich läuft — und immer dann, wenn eine Zahl entsteht, die später jemand in ein Angebot oder ein
Datenblatt schreiben könnte.
Ein Nachweis ist **kein Protokoll der Arbeit**. Er beantwortet eine vorher gestellte Frage mit Ja
oder Nein.
## Wie ein Nachweis aussieht
Eine Datei je Nachweis, benannt nach Datum und Sache: `2026-08-20-ruecksprung-greift.md`.
| Abschnitt | Inhalt |
|---|---|
| **Behauptung** | Was geprüft wurde, wörtlich — und woher sie stammt (ADR, Backlog-Punkt, Konzeptseite) |
| **Gerät** | Auf welcher Maschine gemessen wurde, mit Ausstattung. Virtuelle Maschine oder Blech ist ein Unterschied |
| **Zeitpunkt** | Datum, und bei Zeitmessungen die Uhrzeit |
| **Ablauf** | Was getan wurde, Schritt für Schritt, so dass es jemand anderes wiederholen kann |
| **Beobachtung** | Was herauskam — **roh**, mit den echten Ausgaben. Keine beschönigende Zusammenfassung |
| **Urteil** | `belegt` · `nicht belegt` · `teilweise belegt`, mit einem Satz Begründung |
## Drei Regeln, die den Unterschied machen
**Ein Fehlschlag ist ein Nachweis.** „Nicht belegt" ist ein vollwertiges Ergebnis und gehört
genauso hierher wie ein Erfolg. Ein Verzeichnis, in dem nur Erfolge stehen, ist kein Nachweis-,
sondern ein Werbeordner.
**Die echte Systemmeldung, nicht die Zusammenfassung darüber.** Dieselbe Regel, die für das Gerät
gilt: Wer eine Fehlermeldung glättet, nimmt dem Nächsten die Information, die er braucht.
**Das Gerät gehört zur Zahl.** Eine in einer virtuellen Maschine gemessene Startzeit ist keine
Aussage über ein Kundengerät. Eine auf einer Grafikkarte gemessene Antwortzeit keine über eine
andere. Ohne Gerät ist eine Zahl nicht unvollständig, sondern irreführend.
## Was hier nicht hineingehört
Vermutungen, Planungen und Absichten — die stehen in `docs/decisions/` und `docs/auslegung/`.
Und Zwischenstände einer laufenden Arbeit; die gehören in den Commit-Text.

75
nutzlast/VERTRAG.md Normal file
View file

@ -0,0 +1,75 @@
# Der Nutzlast-Vertrag
XinionOS liefert ein Gerät, das startet, sich prüft und zurückspringt. Was darauf arbeitet, ist die
**Nutzlast** — und dieser Vertrag ist alles, was die beiden voneinander wissen.
Er ist bewusst klein. Je mehr das Betriebssystem über die Nutzlast weiß, desto weniger Produkte
kann es tragen.
## Der Grundsatz: eine Fassung, ein Rückweg
**Die Nutzlast liegt in der Wurzel und wird mit ihr getauscht.** Damit erbt sie den A/B-Wechsel und
den Rücksprung, ohne selbst etwas dafür zu tun.
Das ist die wichtigste Festlegung dieses Dokuments, und sie ist eine Entscheidung gegen eine
naheliegende Alternative: Nutzlast und Betriebssystem **getrennt** zu versionieren. Das klingt
flexibler und erzeugt eine Verträglichkeitsmatrix — welche Nutzlastfassung läuft auf welcher
OS-Fassung —, die gepflegt und geprüft werden muss. Vor allem aber gäbe es **zwei Rücksprünge**,
die zueinander passen müssten.
> **Ein Abbild, eine Fassung, ein Rückweg.** Wer die Nutzlast aktualisieren will, baut ein neues
> Abbild.
## Was die Nutzlast bereitstellt
| Pfad | Pflicht | Zweck |
|---|---|---|
| `/opt/nutzlast/bin/selbstpruefung` | **ja** | Meldet mit Rückgabewert 0, dass die Nutzlast arbeitsfähig ist |
| `/opt/nutzlast/nutzlast.target` | **ja** | systemd-Ziel, unter dem alle Dienste der Nutzlast hängen |
| `/opt/nutzlast/kennung` | **ja** | Name und Fassung, einzeilig — erscheint auf der Statusseite |
| `/opt/nutzlast/bin/einrichten` | nein | Wird bei der Ersteinrichtung einmalig aufgerufen |
## Wie der Selbstprüflauf zusammenspielt
Der Selbstprüflauf des Betriebssystems prüft **seine** Sachen — Wurzel erkennbar, Bestandsbereich
eingehängt und beschreibbar, Statusseite antwortet. Danach ruft er
`/opt/nutzlast/bin/selbstpruefung` auf.
**Fällt einer von beiden durch, gilt der Start als misslungen.** Das ist der ganze Mechanismus: Die
Nutzlast entscheidet über den Rücksprung mit, ohne dass das Betriebssystem wissen muss, woran sie
das erkennt.
Was die Nutzlast dort prüft, ist ihre Sache. Eine Fach-Appliance fragt ihre Datenbank; eine
Anwendungsplattform fragt, ob ihre Dienste bereit sind; ein Proxy prüft, ob er lauscht.
## Was die Nutzlast nicht darf
**Sich selbst aktualisieren.** Kein Dienst, der im Hintergrund neue Fassungen holt. Das umginge
Updatekanal, Signaturprüfung und Rücksprung — also alles, wofür dieses Betriebssystem da ist.
**Zustand außerhalb des Bestandsbereichs ablegen.** Alles Dauerhafte gehört nach `/bestand`. Was in
der Wurzel liegt, ist beim nächsten Fassungswechsel weg — das ist Absicht.
**In die andere Wurzel schreiben.** Die gehört dem Fassungswechsel.
## Drei Beispiele
**Eine Anwendungsplattform** (Helm-Bündel, Kubernetes): Die Nutzlast bringt die Laufzeitumgebung und
das Bündel mit; ihr Selbstprüflauf fragt, ob die Freigabe ausgerollt und erreichbar ist.
**Eine Fach-Appliance mit Wirt-Diensten:** Die Nutzlast bringt ihre Dienste als systemd-Units unter
`nutzlast.target`; ihr Selbstprüflauf fragt Datenbank und Dienste ab.
**Ein Proxy:** Ein einziger Dienst, ein dreizeiliger Selbstprüflauf.
**Keines der drei erfordert eine Änderung am Betriebssystem.** Wenn doch, ist der Vertrag falsch
geschnitten — dann wird er geändert und nicht umgangen.
## Was noch offen ist
* **Wie die Nutzlast ins Abbild kommt.** Als Verzeichnis beim Bau oder als signiertes Paket, das
das Bauwerkzeug einliest. Zu entscheiden, bevor das zweite Produkt gebaut wird.
* **Die Signatur.** Nutzlast und Abbild müssen gemeinsam signiert und vor dem Umschalten geprüft
werden.
* **Ob die Statusseite Angaben der Nutzlast anzeigt** — und wenn ja, wie sie sie bekommt, ohne dass
das Betriebssystem sie versteht.

View file

@ -0,0 +1,20 @@
[Unit]
Description=XinionOS Selbstprueflauf — entscheidet, ob dieser Start als gelungen gilt
Documentation=file:///usr/share/doc/xinionos/ADR-0002
After=network-online.target xinionos-status.service
Wants=network-online.target
Requires=xinionos-status.service
# Das ist die ganze Verdrahtung des Ruecksprungs: Diese Unit muss VOR boot-complete.target
# laufen und wird von ihm BENOETIGT. Faellt sie durch, wird das Ziel nie erreicht,
# systemd-bless-boot laeuft nicht, der Startzaehler bleibt stehen — und der dritte
# Fehlversuch stuft die Fassung ab (ADR-0011).
Before=boot-complete.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/xinionos-selbstpruefung
[Install]
RequiredBy=boot-complete.target

View file

@ -0,0 +1,16 @@
[Unit]
Description=XinionOS Statusseite — laufende Wurzel, Fassung, Selbstprueflauf
After=local-fs.target
[Service]
Type=exec
ExecStart=/opt/xinionos/bin/xinionos-status
Restart=on-failure
RestartSec=2
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""Zeigt den Betriebszustand des Geraets — laufende Wurzel, Fassung, Selbstprueflauf.
Bewusst **nur Standardbibliothek**. Jede Abhaengigkeit im Grund-OS ist eine Zeile mehr in
der Stueckliste, ein Posten mehr im Schwachstellenprozess und damit eine Verpflichtung nach
dem Cyber Resilience Act. Eine Statusseite ist das nicht wert.
Es wird nichts gemerkt: Jede Auskunft entsteht beim Abruf aus der Wirklichkeit. Ein
gemerkter Zustand kann nach einem Ruecksprung falsch sein, ein gemessener nie.
"""
from __future__ import annotations
import json
import subprocess
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
PROTOKOLL = Path("/bestand/xinionos/protokoll/selbstpruefung.jsonl")
LETZTE = Path("/run/xinionos-selbstpruefung.json")
FASSUNG = Path("/etc/xinionos/fassung")
EINTRAEGE = Path("/boot/efi/loader/entries")
ANSCHLUSS = 8080
UNBEKANNT = "unbekannt"
def _wurzel() -> str:
try:
return subprocess.run(
["xinionos-wurzeln", "aktiv"], capture_output=True, text=True, timeout=5, check=True
).stdout.strip()
except (OSError, subprocess.SubprocessError):
return UNBEKANNT
def _startzaehler(wurzel: str) -> tuple[int | None, bool]:
"""Der Zaehler steht im Namen des Starteintrags: 'xinionos-a+2-1.conf'."""
if not EINTRAEGE.is_dir() or wurzel == UNBEKANNT:
return None, False
for eintrag in EINTRAEGE.glob(f"xinionos-{wurzel}*.conf"):
rest = eintrag.stem.removeprefix(f"xinionos-{wurzel}")
if not rest:
return None, True
if rest.startswith("+"):
offen = rest[1:].split("-", 1)[0]
return (int(offen) if offen.isdigit() else None), False
return None, False
def _lies(pfad: Path) -> str:
try:
return pfad.read_text(encoding="utf-8").strip()
except OSError:
return ""
def _verlauf(anzahl: int = 20) -> list[dict]:
roh = _lies(PROTOKOLL)
if not roh:
return []
saetze, puffer = [], ""
for zeile in roh.splitlines(keepends=True):
if zeile.startswith("{") and puffer:
saetze.append(puffer)
puffer = ""
puffer += zeile
if puffer:
saetze.append(puffer)
ergebnis = []
for satz in reversed(saetze[-anzahl:]):
try:
ergebnis.append(json.loads(satz))
except json.JSONDecodeError:
continue
return ergebnis
def zustand() -> dict:
wurzel = _wurzel()
offen, bestaetigt = _startzaehler(wurzel)
try:
pruefung = json.loads(_lies(LETZTE) or "{}")
except json.JSONDecodeError:
pruefung = {}
return {
"wurzel": {
"aktiv": wurzel,
"fassung": _lies(FASSUNG) or UNBEKANNT,
"versuche_offen": offen,
"bestaetigt": bestaetigt,
},
"selbstpruefung": pruefung,
}
_STIL = (
":root{color-scheme:light dark}body{font-family:system-ui,sans-serif;max-width:60rem;"
"margin:2rem auto;padding:0 1rem;line-height:1.5}table{border-collapse:collapse;width:100%;"
"margin:1rem 0}th,td{text-align:left;padding:.4rem .6rem;"
"border-bottom:1px solid rgba(128,128,128,.35)}th{width:16rem}"
".gut{color:#1a7f37;font-weight:600}.schlecht{color:#b42318;font-weight:600}"
".offen{color:#9a6700;font-weight:600}"
)
def _klasse(wert: str) -> str:
return {"bestanden": "gut", "durchgefallen": "schlecht"}.get(wert, "offen")
def seite() -> str:
from html import escape as e
z = zustand()
w = z["wurzel"]
if w["bestaetigt"]:
stand = '<span class="gut">bestätigt</span>'
elif w["versuche_offen"] is not None:
stand = f'<span class="offen">auf Probe — {w["versuche_offen"]} Startversuche offen</span>'
else:
stand = '<span class="offen">unbekannt</span>'
ergebnis = str(z["selbstpruefung"].get("ergebnis", UNBEKANNT))
schritte = "".join(
f"<tr><th>{e(k)}</th><td class='{_klasse(str(v))}'>{e(str(v))}</td></tr>"
for k, v in z["selbstpruefung"].get("schritte", {}).items()
) or "<tr><td>Kein Ergebnis vorhanden.</td></tr>"
laeufe = "".join(
f"<tr><td>{e(str(v.get('zeit','')))}</td><td>Wurzel {e(str(v.get('wurzel','?')))}</td>"
f"<td>{e(str(v.get('fassung','?')))}</td>"
f"<td class='{_klasse(str(v.get('ergebnis','')))}'>{e(str(v.get('ergebnis','')))}</td></tr>"
for v in _verlauf()
) or "<tr><td colspan='4'>Noch kein Lauf aufgezeichnet.</td></tr>"
return f"""<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>XinionOS — Betriebszustand</title><style>{_STIL}</style></head><body>
<h1>Betriebszustand</h1>
<table>
<tr><th>Laufende Wurzel</th><td><strong>{e(w["aktiv"])}</strong></td></tr>
<tr><th>Fassung</th><td>{e(w["fassung"])}</td></tr>
<tr><th>Stand dieser Fassung</th><td>{stand}</td></tr>
<tr><th>Letzter Selbstprüflauf</th><td class="{_klasse(ergebnis)}">{e(ergebnis)}</td></tr>
</table>
<h2>Die Schritte des letzten Laufs</h2><table>{schritte}</table>
<h2>Verlauf</h2>
<p>Ein Rücksprung wird hier sichtbar: ein durchgefallener Lauf auf der einen Wurzel, danach ein
bestandener auf der anderen. Das Protokoll liegt im Bestandsbereich und überlebt den
Fassungswechsel.</p>
<table><tr><th>Zeitpunkt</th><th>Wurzel</th><th>Fassung</th><th>Ergebnis</th></tr>{laeufe}</table>
</body></html>"""
class Griff(BaseHTTPRequestHandler):
server_version = "XinionOS"
def do_GET(self) -> None: # noqa: N802
if self.path.startswith("/zustand"):
leib, art = json.dumps(zustand(), ensure_ascii=False).encode(), "application/json"
elif self.path in ("/", "/index.html"):
leib, art = seite().encode(), "text/html; charset=utf-8"
else:
self.send_error(404)
return
self.send_response(200)
self.send_header("Content-Type", art)
self.send_header("Content-Length", str(len(leib)))
self.end_headers()
self.wfile.write(leib)
def log_message(self, *_: object) -> None:
pass # das Journal fuehrt systemd
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", ANSCHLUSS), Griff).serve_forever()

View file

@ -0,0 +1,62 @@
#!/bin/sh
# Der Selbstpruefflauf nach ADR-0004 und ADR-0011.
#
# Er entscheidet, ob ein Start als gelungen gilt. Besteht er, wird `boot-complete.target`
# erreicht, systemd loescht den Startzaehler und die Fassung ist bestaetigt. Faellt er durch,
# bleibt der Zaehler stehen — und der dritte Fehlversuch stuft die Fassung ab, sodass das
# Geraet von selbst auf die andere Wurzel zurueckkehrt.
#
# WICHTIG: Diese Pruefliste waechst mit dem Produkt. Heute prueft sie, was es gibt. Spaeter
# kommen die Punkte aus ADR-0004 dazu — Datenbank antwortet, jede Aufgabenklasse ist
# bedienbar, der Wissensspeicher ist erreichbar. Die Verdrahtung aendert sich dabei nicht.
set -u
PROTOKOLL=/bestand/xinionos/protokoll
FASSUNG_DATEI=/etc/xinionos/fassung
SCHALTER=/etc/xinionos/selbstpruefung-scheitert
fehler=0
zeilen=""
pruefe() {
name=$1; shift
if "$@" >/dev/null 2>&1; then
zeilen="${zeilen} \"${name}\": \"bestanden\",\n"
else
zeilen="${zeilen} \"${name}\": \"durchgefallen\",\n"
fehler=$((fehler + 1))
echo "Selbstpruefung: ${name} durchgefallen" >&2
fi
}
# --- die Pruefliste -------------------------------------------------------------------
pruefe "wurzel_erkennbar" xinionos-wurzeln aktiv
pruefe "bestand_eingehaengt" mountpoint -q /bestand
pruefe "bestand_beschreibbar" sh -c 'touch /bestand/.schreibprobe && rm -f /bestand/.schreibprobe'
pruefe "dienst_antwortet" sh -c 'curl -fsS --max-time 5 http://127.0.0.1:8080/zustand'
# Der Pruefschalter. Ein Ruecksprung, den man nicht ausloesen kann, ist nicht nachweisbar —
# und ohne Nachweis gilt er in diesem Produkt nicht. Deshalb gibt es einen ausdruecklichen
# Weg, absichtlich durchzufallen.
if [ -f "$SCHALTER" ]; then
zeilen="${zeilen} \"pruefschalter\": \"absichtlicher Fehlschlag\",\n"
fehler=$((fehler + 1))
echo "Selbstpruefung: absichtlicher Fehlschlag durch $SCHALTER" >&2
fi
# --------------------------------------------------------------------------------------
wurzel=$(xinionos-wurzeln aktiv 2>/dev/null || echo "unbekannt")
fassung=$(cat "$FASSUNG_DATEI" 2>/dev/null || echo "unbekannt")
zeit=$(date -Is)
[ "$fehler" -eq 0 ] && ergebnis=bestanden || ergebnis=durchgefallen
# Das Protokoll liegt im Bestandsbereich und ueberlebt damit den Ruecksprung. Das ist kein
# Zufall, sondern der Grund fuer die eigene Partition: Wer nach einem Rueckfall wissen will,
# WARUM die neue Fassung nicht hochkam, findet es nur dort.
mkdir -p "$PROTOKOLL" 2>/dev/null || true
printf '{\n "zeit": "%s",\n "wurzel": "%s",\n "fassung": "%s",\n "ergebnis": "%s",\n "schritte": {\n%b }\n}\n' \
"$zeit" "$wurzel" "$fassung" "$ergebnis" "$(printf '%b' "$zeilen" | sed '$ s/,$//')" \
> /run/xinionos-selbstpruefung.json
cat /run/xinionos-selbstpruefung.json >> "$PROTOKOLL/selbstpruefung.jsonl" 2>/dev/null || true
[ "$fehler" -eq 0 ] || exit 1

View file

@ -0,0 +1,31 @@
#!/bin/sh
# Sagt, welche der beiden Wurzeln gerade laeuft und welche die andere ist.
#
# Alles im Fassungswechsel haengt an dieser Auskunft: Ein Update schreibt immer in die
# INAKTIVE Wurzel. Wer sich hier irrt, ueberschreibt die laufende Fassung — und damit den
# Rueckweg, den das ganze Verfahren verspricht.
#
# Aufruf:
# xinionos-wurzeln aktiv -> a | b
# xinionos-wurzeln andere -> b | a
# xinionos-wurzeln geraet a -> /dev/disk/by-label/wurzel-a
set -eu
aktive_wurzel() {
# Die Wahrheit steht im eingehaengten Wurzeldateisystem, nicht in einer Merkdatei:
# eine Merkdatei kann nach einem Ruecksprung falsch sein, das Dateisystem nie.
quelle=$(findmnt -n -o SOURCE / | sed 's/\[.*\]//')
kennung=$(blkid -o value -s LABEL "$quelle" 2>/dev/null || echo "")
case "$kennung" in
wurzel-a) echo a ;;
wurzel-b) echo b ;;
*) echo "Wurzel nicht erkennbar: $quelle (Bezeichner '$kennung')" >&2; exit 1 ;;
esac
}
case "${1:-}" in
aktiv) aktive_wurzel ;;
andere) [ "$(aktive_wurzel)" = a ] && echo b || echo a ;;
geraet) echo "/dev/disk/by-label/wurzel-${2:?Wurzel a oder b angeben}" ;;
*) echo "Aufruf: $0 aktiv|andere|geraet <a|b>" >&2; exit 64 ;;
esac