Config-Transaktionen: begin/commit/rollback fuer Testaenderungen

Eine Aenderung, die am Geraet steht, aber nicht in der SoT, ist Drift -- und Drift
sieht bisher immer gleich aus, egal ob er ein Versehen ist oder ein laufender
Versuch. Wer testen wollte, hatte die Wahl zwischen "Compliance kaputt" und "gar
nicht testen".

gc-transaktion.py macht die Absicht sichtbar, ohne die Governance aufzuweichen:
der Change geht weiterhin ausschliesslich ueber Golden Config, die Compliance
meldet den Drift weiterhin -- neu ist, dass `status` ihn zuordnen kann,
"erwartet (offene Transaktion)" statt "unerklaert". Und dass er ein Ablaufdatum
hat.

Drei Regeln erzwingt das Werkzeug: --expires ist Pflicht (eine unbefristete
Ausnahme ist undokumentierter Drift mit besserem Gewissen), der Scope ist eine
ComplianceRule und kein Freibrief (Drift ausserhalb wird ausdruecklich als nicht
abgedeckt gemeldet), und es gibt genau eine offene Transaktion je Geraet -- sonst
ist beim Rollback nicht mehr entscheidbar, wohin zurueck. Der Commit wird
verweigert, solange der Scope noch driftet: dann waere die SoT gar nicht
nachgezogen worden.

Zwei bewusste Grenzen: der Rollback pusht nicht selbst, sondern schreibt einen
Vorschlag zum Pruefen (add-Zeilen aus einem /export legen neu an, statt zu
aendern), und der Snapshot enthaelt nur den Scope-Abschnitt. Der Vollexport
enthielt Firewall, Adressen, Benutzerliste und Skriptquellen -- nichts davon
braucht der Rueckbau, und die Akten liegen im Git.

Erste Akte ist der ether3-Trunk: ap1 haengt testweise am Sideboard auf core
ether3, und dieser Port traegt bisher keines der sieben VLANs. Deploy steht noch
aus -- er wurde in dieser Session vom Berechtigungs-Klassifikator blockiert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 20:15:05 +02:00
parent fcd221388f
commit 3f938e8389
5 changed files with 422 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
# Transaktionen — Teständerungen mit Ablaufdatum
Hier liegen die Akten offener und geschlossener **Config-Transaktionen**. Werkzeug:
[`nautobot/gc-transaktion.py`](../../nautobot/gc-transaktion.py).
## Wozu
Eine Änderung, die am Gerät steht, aber nicht in der SoT, ist **Drift** — und Drift sieht bisher immer
gleich aus, egal ob er ein Versehen ist oder ein laufender Versuch. Wer testen wollte, hatte die Wahl
zwischen „Compliance kaputt" und „gar nicht testen".
Eine Transaktion macht die Absicht sichtbar, ohne die Governance aufzuweichen: **der Change geht
weiterhin ausschließlich über Golden Config** (`gc-manual-plan.py`), und die Compliance meldet den
Drift weiterhin. Neu ist nur, dass `status` ihn **zuordnen** kann.
| SQL | hier |
|---|---|
| `BEGIN` | Snapshot des Scope-Abschnitts + Akte mit Gerät, Scope, Grund und **Ablaufdatum** |
| *uncommitted* | Change ist am Gerät wirksam und im Testumfeld sichtbar, aber nicht in der SoT |
| `COMMIT` | Template/Config-Context nachziehen → Compliance grün → Akte schließen |
| `ROLLBACK` | Rückbau-**Vorschlag** aus dem Snapshot, prüfen, deployen, Akte schließen |
## Drei Regeln, die das Werkzeug erzwingt
1. **`--expires` ist Pflicht.** Eine offene Transaktion ohne Frist ist undokumentierter Drift mit
besserem Gewissen. `status` markiert überfällige Akten.
2. **Der Scope ist eine ComplianceRule**, kein Freibrief. Driftet etwas *außerhalb* des Scopes, meldet
`status` das ausdrücklich als **nicht abgedeckt**.
3. **Eine offene Transaktion je Gerät** — sonst ist beim Rollback nicht mehr entscheidbar, wohin zurück.
## Zwei bewusste Grenzen
- **Der Rollback pusht nicht selbst.** Er schreibt einen Vorschlag aus dem Snapshot; `add`-Zeilen aus
einem `/export` legen neu an, statt zu ändern, und brauchen meist ein `set [find …]`. Ein Rückbau,
den niemand gelesen hat, ist genau die Sorte Automatik, die hier schon zweimal teuer war.
- **Der Snapshot enthält nur den Scope-Abschnitt.** Ein voller `/export` enthält Firewall, Adressen,
Benutzerliste und Skriptquellen — nichts davon wird für den Rückbau gebraucht, und diese Akten liegen
im Git. Für den Ernstfall gibt es das binäre `/system backup` auf dem Gerät (`--device-backup`).
## Dateien je Transaktion
| Datei | Inhalt |
|---|---|
| `<change-id>.json` | die Akte: Gerät, Scope, Grund, Fristen, Status |
| `<change-id>.snapshot.rsc` | der Scope-Abschnitt **vor** der Änderung |
| `<change-id>.commands.rsc` | was deployt wurde |
| `<change-id>.rollback.rsc` | erst nach `rollback` — der Vorschlag zum Prüfen |

View File

@@ -0,0 +1,7 @@
/interface bridge vlan set [find where vlan-ids=10 and !dynamic] tagged=ether1,ether3,ether8,ether13,ether15
/interface bridge vlan set [find where vlan-ids=20 and !dynamic] tagged=ether3,ether8,ether13,ether15
/interface bridge vlan set [find where vlan-ids=30 and !dynamic] tagged=ether1,ether3,ether8,ether15
/interface bridge vlan set [find where vlan-ids=40 and !dynamic] tagged=ether1,ether3,ether8,ether13,ether15
/interface bridge vlan set [find where vlan-ids=50 and !dynamic] tagged=ether1,ether3,ether8,ether15
/interface bridge vlan set [find where vlan-ids=90 and !dynamic] tagged=ether1,ether3,ether8,ether15
/interface bridge vlan set [find where vlan-ids=99 and !dynamic] tagged=ether3,ether8,ether15,bridge

View File

@@ -0,0 +1,14 @@
{
"change_id": "TEST-ether3-trunk-ap1-sideboard",
"device": "crs326-core",
"scope": "bridge-vlan",
"match_config": [
"/interface bridge vlan"
],
"reason": "ap1 testweise am Sideboard, Uplink auf core ether3 statt ether15 (TP25 Standorttest)",
"started": "2026-08-21",
"expires": "2026-09-04",
"state": "open",
"snapshot": "docs/transaktionen/TEST-ether3-trunk-ap1-sideboard.snapshot.rsc",
"device_backup": "vor-ether3-trunk-20260821.backup"
}

View File

@@ -0,0 +1,14 @@
# Snapshot des Scopes 'bridge-vlan' auf crs326-core, 2026-08-21 (Transaktion TEST-ether3-trunk-ap1-sideboard)
# Bewusst NUR dieser Abschnitt — der Vollexport gehoert nicht ins Repo.
/interface bridge vlan
add bridge=bridge untagged=ether1,ether2 vlan-ids=1
add bridge=bridge tagged=ether1,ether8,ether13,ether15 vlan-ids=10
add bridge=bridge tagged=ether8,ether13,ether15 vlan-ids=20
add bridge=bridge tagged=ether1,ether8,ether15 untagged=ether14 vlan-ids=30
add bridge=bridge tagged=ether1,ether8,ether13,ether15 untagged=\
ether2,ether3,ether4,ether5 vlan-ids=40
add bridge=bridge tagged=ether1,ether8,ether15 vlan-ids=50
add bridge=bridge tagged=ether1,ether8,ether15 vlan-ids=90
add bridge=bridge tagged=ether8,ether15,bridge vlan-ids=99
add bridge=bridge tagged=ether1 untagged=ether16 vlan-ids=100