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:
commit
d9ed1bc2ff
15 changed files with 903 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
*.iso
|
||||
*.iso.sha256
|
||||
.DS_Store
|
||||
*.key
|
||||
*.pem
|
||||
77
BRIEFING.md
Normal file
77
BRIEFING.md
Normal 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
62
CLAUDE.md
Normal 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
43
README.md
Normal 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)
|
||||
60
abbild/autoinstall/user-data
Normal file
60
abbild/autoinstall/user-data
Normal 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
77
abbild/bauen.sh
Executable 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
|
||||
54
docs/decisions/ADR-0001-eigenes-betriebssystem.md
Normal file
54
docs/decisions/ADR-0001-eigenes-betriebssystem.md
Normal 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.
|
||||
101
docs/decisions/ADR-0002-startverfahren-und-ruecksprung.md
Normal file
101
docs/decisions/ADR-0002-startverfahren-und-ruecksprung.md
Normal 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
47
docs/nachweise/README.md
Normal 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
75
nutzlast/VERTRAG.md
Normal 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.
|
||||
20
wurzel/etc/systemd/system/xinionos-selbstpruefung.service
Normal file
20
wurzel/etc/systemd/system/xinionos-selbstpruefung.service
Normal 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
|
||||
16
wurzel/etc/systemd/system/xinionos-status.service
Normal file
16
wurzel/etc/systemd/system/xinionos-status.service
Normal 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
|
||||
173
wurzel/opt/xinionos/bin/xinionos-status
Executable file
173
wurzel/opt/xinionos/bin/xinionos-status
Executable 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()
|
||||
62
wurzel/usr/local/sbin/xinionos-selbstpruefung
Executable file
62
wurzel/usr/local/sbin/xinionos-selbstpruefung
Executable 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
|
||||
31
wurzel/usr/local/sbin/xinionos-wurzeln
Executable file
31
wurzel/usr/local/sbin/xinionos-wurzeln
Executable 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
|
||||
Loading…
Add table
Reference in a new issue