Race Conditions, Rollbacks und die Architektur, die beides löst

Core 2.0
Lesedauer: ca. 5–6 Min.
Leandro Lerena
Wie aus vier Paradigms of Trust ein Sicherheitsmodell für Netzwerke — und künftige KI-Agenten — wird.
Erster Teil: "Wenn Netzwerke reif werden für KI-Agenten"

Im Beitrag "Wenn Netzwerke reif werden für KI-Agenten" ging es um zwei Probleme, die in historisch gewachsenen, heterogenen Netzwerken zwangsläufig auftreten: Race Conditions, die Änderungen stillschweigend verschwinden lassen, und das Fehlen eines klaren Statusmodells, das zwischen einem sicheren und einem kritischen Fehlschlag unterscheidet. Beide Probleme wurden dort in einem ersten Schritt als Prinzip beschrieben, ganz ohne Code-Beispiele. Diese Woche folgt nun der Deep Dive, der Stoff, aus dem die Lösung tatsächlich gebaut ist: der Code, die Architektur, und die Stellen, an denen es in der Praxis wehtut.

Take aways
  • Trust ist geschichtet: Transactional, Framework, Implementation, Future. Jede Schicht liefert etwas, das ein Skript allein nicht liefern kann.
  • Paradigmen evaluieren, nicht Produkte: Die Frage ist nicht "welches Tool", sondern ob die zugrundeliegenden Prinzipien stimmen.
  • Gut definierte Paradigmen: Pureness, Idempotenz, gesperrter Kontext, strikte Schemas, dieselben Paradigmen, die einer vertrauenswürdigen Automations-Lösung zu Grunde liegen, bilden zugleich das Fundament für den sicheren Einsatz von KI-Agenten. Dieselbe Grenze schützt beides.

INSIGHT

Die vier Paradigms of Trust

Bevor wir in den Code einsteigen: Die technischen Lösungen in diesem Artikel sind keine isolierten Kniffe, sondern Ausprägungen von vier zusammenhängenden Prinzipien, die wir bei der AutoCon 5 als "Paradigms of Trust" vorgestellt haben:

  • Transactional Trust — Netzwerkänderungen als Transaktionen mit definiertem Lifecycle und sauberer Fehlerklassifikation.
  • Framework Trust — FunctionBlocks mit deklariertem Safety-Profil, entkoppelt von der Vendor-Implementierung.
  • Implementation Trust — jede Komponente gegen echte Geräte getestet, nicht nur gemockt.
  • Future-Proofing — dieselben Regeln, die heute Menschen und Skripte absichern, sichern morgen auch KI-Agenten ab.

Die folgenden Abschnitte gehen jedes dieser vier Paradigmen einzeln durch, von der Ausgangslage bis zum Code.

Der Ausgangspunkt: das Skript, das jeder von uns schon geschrieben hat

Der AutoCon-Talk beginnt bewusst mit einem Anti-Pattern, das jedem Engineer bekannt vorkommen dürfte:

import os

HOST, USER, PWD = os.environ["HOST"], os.environ["NET_USER"], os.environ["NET_PWD"]
creds = {"host": HOST, "username": USER, "password": PWD}
try:
   if VENDOR == "cisco_ios":
       conn = ConnectHandler(device_type="cisco_ios", **creds)
       cmd = "shutdown" if STATE == "down" else "no shutdown"
       conn.send_config_set([f"interface {INTERFACE}", cmd])
   elif VENDOR == "nokia_srl":
       conn = ConnectHandler(device_type="nokia_srl", **creds)
       verb = "disable" if STATE == "down" else "enable"
       conn.send_config_set([f"set / interface {INTERFACE} admin-state {verb}"])
   else:
       raise ValueError(f"unsupported vendor: {VENDOR}")
   after = conn.send_command(f"show interface {INTERFACE}")
   conn.disconnect()
   ...
except Exception as e:
   print(f"failed: {e}")
   ...

Funktioniert. Und genau das ist das Problem: Es läuft lange genug fehlerfrei, dass niemand die fehlenden Eigenschaften bemerkt, bis es genau einmal nicht funktioniert. Was hier strukturell fehlt: Parameter-Validierung, ein sauberes Connection-Handling, ein echter Vendor-Abstraktionslayer statt if/elif, definiertes statt verschlucktes Error-Handling, jegliches Testing, ein bekanntes Safety-Profil (ist diese Operation gefahrlos wiederholbar? rückgängig zu machen?) und Auditierbarkeit im Fehlerfall. Am gravierendsten: Parallelausführung ist hier schlicht riskant, weil Race Conditions auf dieser Ebene grundsätzlich nicht erkennbar sind.

Warum Atomarität ein unerreichbares Ziel ist

Die Mechanik dahinter, wie sie im SwiNOG-Talk hergeleitet wird: Zwei Akteure A und B lesen den Zustand eines Geräts (Port 1: down, Port 2: down), bevor sie unabhängig voneinander schreiben. A will Port 1 auf "up" setzen, B will Port 2 auf "up" setzen. Weil beide Operationen nicht atomar sind (lesen, verändern, schreiben sind getrennte Schritte), kann B's Schreibvorgang auf Basis des ursprünglich gelesenen Zustands A's zwischenzeitliche Änderung überschreiben. Ergebnis: Port 1 fällt zurück auf "down", ohne dass A je einen Fehler sieht.

Das ist keine Netzwerk-Eigenart, es ist exakt das Problem, das relationale Datenbanken seit Jahrzehnten mit Transaktionsisolation lösen. Die Konsequenz, die daraus gezogen wird: Für die meisten komplexen Operationen ist Atomarität auf Ebene der Einzeloperation unerreichbar; man braucht einen zentralen Orchestrator, der Zustand exklusiv sperrt, statt zu hoffen, dass schon nichts kollidiert.

Transactional Trust: Der Transaktions-Lifecycle

Daraus ergibt sich der Lifecycle, den neops Workflows durchlaufen:

NEW → VALID → ... → SCHEDULED → LOCKING → ... → RUNNING → COMPLETED
                                                       ↘ FAILED_SAFE
                                                       ↘ FAILED_UNSAFE

LOCKING verschafft exklusiven Zugriff auf den betroffenen Kontext und lädt ihn, erst danach beginnt RUNNING. Schlägt ein Workflow fehl, wird zwischen zwei Fällen unterschieden: FAILED_SAFE, wenn ausschliesslich reine (pure) Schritte betroffen waren oder ein Rollback nachweislich gelungen ist, und FAILED_UNSAFE, wenn möglicherweise Seiteneffekte aufgetreten sind. In diesem Fall bleiben Ressourcen bewusst gesperrt, um zu verhindern, dass weitere Workflows auf einem unbekannten Zustand aufsetzen, bis eine manuelle Prüfung erfolgt ist. Diese Unterscheidung ist nur möglich, weil jeder einzelne FunctionBlock ein deklariertes Safety-Profil trägt: pure, idempotent, reversible.

Framework Trust: ein FunctionBlock in echt

Das Demo-Beispiel aus beiden Talks ist bewusst simpel gehalten, ein vendor-agnostischer SetAdminState:

@register_function_block(Registration(
   name="set_admin_state",
   description="Set the admin state of an interface.",
   package="fb.examples.neops.io",
   version=(1, 0, 0),
   run_on="device",
   fb_type="configure",
   param_cls=SetAdminStateParams,
   result_cls=SetAdminStateResult,
   is_idempotent=True,  
# -> bei Fehlschlag kann der Orchestrator sicher retryen
))
class SetAdminState(FunctionBlock[SetAdminStateParams, SetAdminStateResult]):
   async def acquire(self, params: AcquireParams[SetAdminStateParams]) -> FunctionBlockAcquireResult:
       return FunctionBlockAcquireResult(success=True, message="Kein Zusatzkontext nötig.")

   async def run(self, p, ctx):
       with AdminStateProxy.connect(ctx.device) as conn:
           before = conn.get_admin_state(p.interface)
           conn.set_admin_state(p.interface, p.desired_state)
           after = conn.get_admin_state(p.interface)
       return FunctionBlockResult(
           success=after is p.desired_state,
           message=f"{p.interface}: {before} -> {after}",
           data=SetAdminStateResult(interface=p.interface, state_before=before, state_after=after),
       )

Semantic Versioning, Scope und Safety-Profil sind Teil der Deklaration, nicht Kommentar. is_idempotent=True ist keine Doku-Notiz. Der Orchestrator liest dieses Flag und entscheidet danach, ob ein automatischer Retry sicher ist.

Die eigentliche Pointe steckt im AdminStateProxy: Der FunctionBlock kennt keinen einzigen Hersteller. Die Vendor-Logik steckt vollständig in austauschbaren Plugins hinter einer gemeinsamen Capability-Schnittstelle:

class AdminStateCapability(CapabilityInterface):
   @abstractmethod
   def get_admin_state(self, interface: str) -> AdminState: ...
   @abstractmethod
   def set_admin_state(self, interface: str, state: AdminState) -> None: ...

# Cisco IOS/IOL via Scrapli
def set_admin_state(self, iface, state):
   self.require_raw_connection().send_configs(
       [f"interface {iface}", "shutdown" if state == DOWN else "no shutdown"]
   )

# Nokia SR Linux via Netmiko
def set_admin_state(self, interface, state):
   cmd = "enable" if state == UP else "disable"
   conn = self.require_raw_connection()
   conn.send_command(f"set / interface {interface} admin-state {cmd}")
   conn.send_command("commit now")

Zwei Hersteller, zwei grundverschiedene CLI-Dialekte, ein gemeinsamer Vertrag — Vendor-Agnostizismus als direkte Konsequenz der Abstraktion, nicht als Behauptung auf einer Slide. Ein dritter oder vierter Hersteller kommt über ein weiteres Plugin hinzu (Scrapli, Netmiko, NAPALM und pynetbox bringt das SDK als Basisklassen mit), ohne dass am FunctionBlock selbst etwas verändert werden muss, genau das zahlt sich in Brownfield-Umgebungen aus, wo Herstellerwechsel eher Realität als Ausnahme sind.

Implementation Trust: gegen echte Geräte getestet, nicht nur gemockt

Der zweite Teil des SwiNOG-Talks (Andy Griesbeck) zeigt drei Testebenen für genau diesen FunctionBlock:

  • Unit-Test ohne Gerät — reine Logik, z. B. der Fall "kein Gerät vorhanden" muss kontrolliert fehlschlagen.
  • Lab-Test gegen Cisco IOL — reale CLI-Interaktion, Assertion: Port ist nach der Operation tatsächlich down.
  • Lab-Test gegen Nokia SR Linux — dieselbe FunctionBlock-Logik, andere Plattform, Assertion: Port ist tatsächlich up.

@fb_test_case_with_lab(
   "Shut Ethernet0/1 on Cisco IOL",
   params=SetAdminStateParams(interface="Ethernet0/1", desired_state=AdminState.DOWN),
   remote_lab_fixture="simple_iol",
   assertions=[lambda r: r.data.state_after == AdminState.DOWN],
)
class SetAdminState(FunctionBlock[SetAdminStateParams, SetAdminStateResult]):

Möglich macht das der Remote Lab Manager: ein FastAPI-Service, der mit netlab und Containerlab virtuelle Topologien aus einer topology.yml on-the-fly hochfährt, den FunctionBlock gegen diese Live-Fixture ausführt und danach vollständig zurückbaut, zero residual state. Ablauf: pytest entdeckt den generierten Testfall → Remote Lab koordiniert den Lab-Lifecycle → netlab + Containerlab spannen die Topologie auf → der Test läuft gegen echte CLI (Cisco IOL bzw. Nokia SR Linux) → Teardown. Nutzbar lokal in der IDE genauso wie in der CI/CD-Pipeline, über einen einzigen Python-Decorator angesteuert.

Future-Proofing: dieselben Paradigmen als Sicherheitsfundament für Agentic AI

Der AutoCon-Talk führt die drei bisherigen Paradigmen (Transactional, Framework, Implementation Trust) zu einem vierten zusammen: einem 3-Tier-Modell dafür, wie KI-Agenten kontrolliert auf diese Infrastruktur zugreifen dürfen.

  • Tier 1 — autonom, risikofrei: Der Agent darf ausschliesslich auf FunctionBlocks ohne Seiteneffekte ("pure") zugreifen. Er kann Zustand lesen, Kontext sammeln, Hypothesen formulieren — verändern kann er per Definition nichts.
  • Tier 2 — autonom, vorab genehmigt: Der Agent darf innerhalb einer engen, vom Team explizit freigegebenen Allow-List handeln — bevorzugt idempotente, reversible FunctionBlocks —, immer unter vollem Transaktionsschutz mit gesperrtem Kontext und automatischem Rollback bei Problemen.
  • Tier 3 — supervised: Der Agent darf jeden verfügbaren FunctionBlock zu einem Workflow-Vorschlag kombinieren. Ein Mensch prüft die deklarierten Eigenschaften und den gesperrten Kontext und startet die Ausführung explizit.

Durchgängig gilt: keine rohen Operationen, keine Umgehung von Logging und Auditierbarkeit, und der Agent hat zu keinem Zeitpunkt direkten Kontakt zu Geräte-Credentials. Die Sicherheitsgrenze ist dieselbe, unabhängig davon, ob ein Mensch oder ein Agent den Workflow anstösst und genau das ist der Kernpunkt: Wer heute schon sauber zwischen pure/idempotent/reversible unterscheidet und Kontext transaktional sperrt, hat morgen keine separate "AI-Safety-Architektur" nachzurüsten.

what we talk about

Read the most recent articles