diff --git a/README.md b/README.md index e6092f0..0f92f86 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ Der Tuxflotte Provisioning Agent läuft auf installierten Geräten, meldet sich in regelmäßigen Abständen bei `anode` und wendet die ihm zugewiesenen Ansible-Rollen per `ansible-pull` an (siehe `ansible-content/`). -**Stand:** erste lauffähige Version. Die Einrichtung vor dem ersten Reboot (`backend_postinstall()`) ist noch nicht angebunden — Bootstrap läuft aktuell über den Dev-Endpoint `POST /api/v1/agent/bootstrap`. +**Stand:** Kernschleife (Check-in → ansible-pull) sowie Auftragskatalog (present/absent, Ergebnis-Report) laufen. Die Einrichtung vor dem ersten Reboot ist über `backend_postinstall()` im Fedora-Backend angebunden (`tuxflotte-installer/backends/fedora/kickstart.tpl`), ruft dabei denselben `POST /api/v1/agent/bootstrap`-Endpoint auf, der hier für die manuelle Testgerät-Einrichtung beschrieben ist. + +## Auftragskatalog (ADR-0010) + +Neben den additiv über den Workspace zugewiesenen Blueprints liefert der Check-in zusätzlich `auftraege`: den vollständigen present/absent-Zustand aller katalogfähigen Merkmale für das Gerät. Der Agent führt beide Listen in einem gemeinsamen `ansible-pull`-Lauf aus (`--tags` = Vereinigung aller Rollennamen, `--extra-vars` reicht `tuxflotte_auftrag_states` als Rolle→Zustand-Dict durch, siehe `ansible-content/README.md`) und meldet danach das Ergebnis je Auftragskatalog-Eintrag per `POST /api/v1/agent/report` zurück (`applied`/`apply_failed`/`removed`/`remove_failed`). Das Ergebnis ist grob granular — ein `ansible-pull`-Lauf hat einen einzigen Exit-Code für alle enthaltenen Rollen, es gibt noch keine Rolle-für-Rolle-Auswertung. ## Manuelle Installation (Testgerät) diff --git a/agent.py b/agent.py index 5d68fff..8a24c4c 100644 --- a/agent.py +++ b/agent.py @@ -26,12 +26,12 @@ def load_credentials(): return credentials["device_id"], credentials["agent_secret"] -def checkin(device_id, agent_secret): - body = json.dumps({"device_id": device_id}).encode("utf-8") +def api_request(path, agent_secret, body): + data = json.dumps(body).encode("utf-8") req = urllib.request.Request( - ANODE_URL + "/api/v1/agent/checkin", - data=body, + ANODE_URL + path, + data=data, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {agent_secret}", @@ -43,17 +43,44 @@ def checkin(device_id, agent_secret): return json.loads(response.read().decode("utf-8")) -def apply_blueprints(ansible_repo, blueprints): - roles = ",".join(b["ansible_role"] for b in blueprints) +def checkin(device_id, agent_secret): + return api_request( + "/api/v1/agent/checkin", agent_secret, {"device_id": device_id} + ) - log(f"Applying roles via ansible-pull: {roles}") + +def report(device_id, agent_secret, results): + return api_request( + "/api/v1/agent/report", + agent_secret, + {"device_id": device_id, "results": results}, + ) + + +def apply_roles(ansible_repo, blueprints, auftraege): + """ + Führt sowohl die additiv über den Workspace zugewiesenen Blueprints als + auch die Auftragskatalog-Einträge in einem gemeinsamen ansible-pull-Lauf + aus. Auftragskatalog-Rollen müssen unabhängig von present/absent immer + in --tags stehen, sonst laufen ihre Tasks (inklusive des absent-Zweigs, + siehe ADR-0010) gar nicht erst. + """ + + roles = sorted( + {b["ansible_role"] for b in blueprints} + | {a["ansible_role"] for a in auftraege} + ) + auftrag_states = {a["ansible_role"]: a["state"] for a in auftraege} + + log(f"Applying roles via ansible-pull: {','.join(roles)}") result = subprocess.run( [ "ansible-pull", "-U", ansible_repo, - "--tags", roles, + "--tags", ",".join(roles), "-i", "localhost,", + "-e", json.dumps({"tuxflotte_auftrag_states": auftrag_states}), "site.yml", ], check=False, @@ -62,6 +89,22 @@ def apply_blueprints(ansible_repo, blueprints): if result.returncode != 0: log(f"ansible-pull exited with status {result.returncode}") + return result.returncode == 0 + + +def build_report_results(auftraege, pull_succeeded): + results = [] + + for entry in auftraege: + if entry["state"] == "present": + event_type = "applied" if pull_succeeded else "apply_failed" + else: + event_type = "removed" if pull_succeeded else "remove_failed" + + results.append({"merkmal": entry["merkmal"], "event_type": event_type}) + + return results + def run_once(device_id, agent_secret): result = checkin(device_id, agent_secret) @@ -71,9 +114,16 @@ def run_once(device_id, agent_secret): return FALLBACK_INTERVAL_SECONDS blueprints = result.get("blueprints", []) + auftraege = result.get("auftraege", []) - if blueprints: - apply_blueprints(result["ansible_repo"], blueprints) + if blueprints or auftraege: + pull_succeeded = apply_roles(result["ansible_repo"], blueprints, auftraege) + + if auftraege: + try: + report(device_id, agent_secret, build_report_results(auftraege, pull_succeeded)) + except (urllib.error.URLError, OSError) as exc: + log(f"Report-Fehler: {exc}") else: log("Check-in ok, nothing to do.")