From 26fabfff03c5e6f5a9a12f4daa48e11d859be6fd Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Mon, 10 Aug 2026 13:04:55 +0200 Subject: [PATCH] feat: Self-Heal-Re-Bootstrap bei 401 auf Check-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ermöglicht die neue Reprovisionieren-Aktion im Kundenportal: wenn das lokale Secret serverseitig ungültig gemacht wurde (Deprovisionierung), holt sich der Agent beim nächsten Poll automatisch ein neues über den bestehenden /api/v1/agent/bootstrap-Endpoint statt dauerhaft mit 401 hängenzubleiben. Co-Authored-By: Claude Sonnet 5 --- agent.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/agent.py b/agent.py index bbdd060..58258c5 100644 --- a/agent.py +++ b/agent.py @@ -26,6 +26,11 @@ def load_credentials(): return credentials["device_id"], credentials["agent_secret"] +def save_credentials(device_id, agent_secret): + with open(CREDENTIALS_PATH, "w", encoding="utf-8") as f: + json.dump({"device_id": device_id, "agent_secret": agent_secret}, f) + + def api_request(path, agent_secret, body): data = json.dumps(body).encode("utf-8") @@ -43,6 +48,29 @@ def api_request(path, agent_secret, body): return json.loads(response.read().decode("utf-8")) +def bootstrap(device_id): + """ + Holt ein frisches agent_secret über /api/v1/agent/bootstrap. Braucht + keinen Authorization-Header (der Endpoint kannte von Anfang an keine + andere Authentifizierung als die device_id, siehe provisioning-server + app.py) - macht sich zunutze, dass so ein Re-Bootstrap nach einer + Reprovisionierung im Kundenportal möglich ist, ohne physisch am Gerät + etwas tun zu müssen. + """ + + data = json.dumps({"device_id": device_id}).encode("utf-8") + + req = urllib.request.Request( + ANODE_URL + "/api/v1/agent/bootstrap", + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + + with urllib.request.urlopen(req, timeout=15) as response: + return json.loads(response.read().decode("utf-8")) + + def checkin(device_id, agent_secret): return api_request( "/api/v1/agent/checkin", agent_secret, {"device_id": device_id} @@ -111,11 +139,40 @@ def build_report_results(auftraege, pull_succeeded): def run_once(device_id, agent_secret): + """ + Rückgabe: (poll_interval_seconds, agent_secret) - der Aufrufer muss das + zurückgegebene agent_secret für den nächsten Zyklus übernehmen, falls + hier ein Re-Bootstrap stattgefunden hat (sonst würde main() dauerhaft mit + dem alten, ungültigen Secret weiterlaufen und bei jedem Zyklus erneut + bootstrappen). + """ + result = checkin(device_id, agent_secret) + if not result.get("success") and result.get("error") == "unauthorized": + # Passiert nach einer Deprovisionierung im Kundenportal - das lokale + # Secret wurde serverseitig ungültig gemacht. Einmalig neu + # bootstrappen und den Check-in wiederholen, statt für immer 401 zu + # loggen (siehe bootstrap()-Kommentar). + log("Check-in unauthorized, versuche Re-Bootstrap.") + + try: + bootstrap_result = bootstrap(device_id) + except (urllib.error.URLError, OSError) as exc: + log(f"Re-Bootstrap-Fehler: {exc}") + return FALLBACK_INTERVAL_SECONDS, agent_secret + + if not bootstrap_result.get("success"): + log(f"Re-Bootstrap failed: {bootstrap_result.get('message', bootstrap_result.get('error'))}") + return FALLBACK_INTERVAL_SECONDS, agent_secret + + agent_secret = bootstrap_result["agent_secret"] + save_credentials(device_id, agent_secret) + result = checkin(device_id, agent_secret) + if not result.get("success"): log(f"Check-in failed: {result.get('message', result.get('error'))}") - return FALLBACK_INTERVAL_SECONDS + return FALLBACK_INTERVAL_SECONDS, agent_secret blueprints = result.get("blueprints", []) auftraege = result.get("auftraege", []) @@ -131,7 +188,7 @@ def run_once(device_id, agent_secret): else: log("Check-in ok, nothing to do.") - return result.get("poll_interval_seconds", FALLBACK_INTERVAL_SECONDS) + return result.get("poll_interval_seconds", FALLBACK_INTERVAL_SECONDS), agent_secret def main(): @@ -143,7 +200,7 @@ def main(): while True: try: - interval = run_once(device_id, agent_secret) + interval, agent_secret = run_once(device_id, agent_secret) except (urllib.error.URLError, OSError) as exc: log(f"Check-in-Fehler: {exc}") interval = FALLBACK_INTERVAL_SECONDS