feat: Self-Heal-Re-Bootstrap bei 401 auf Check-in
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 <noreply@anthropic.com>
This commit is contained in:
parent
03f70e747a
commit
26fabfff03
63
agent.py
63
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user