feat: apply and report Auftragskatalog state via ansible-pull
Extends the checkin/apply cycle to cover the "auftraege" list from the check-in response (see ADR-0010) alongside the existing workspace blueprints: both are merged into one ansible-pull invocation, with --tags the union of all involved roles (a catalog role needs to run even when its target state is absent, so its removal branch executes) and a new --extra-vars payload (tuxflotte_auftrag_states, role -> present/absent) that ansible-content's site.yml threads down into each role. After the pull, reports the outcome of each Auftragskatalog entry back via the new POST /api/v1/agent/report endpoint (applied/apply_failed/ removed/remove_failed). This is intentionally coarse: ansible-pull has one exit code for the whole run, not per-role results, so all entries in a given run share that outcome. Workspace blueprints are not reported on, matching ADR-0010's scope. Verified end to end against anode and the QEMU test VM: selecting browser-brave as a catalog entry produced an "applied" event and the marker file; deselecting it removed the marker and produced a "removed" event - both written by the agent's own report call, not simulated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
8382a0a949
commit
9799a3daa0
@ -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/`).
|
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)
|
## Manuelle Installation (Testgerät)
|
||||||
|
|
||||||
|
|||||||
70
agent.py
70
agent.py
@ -26,12 +26,12 @@ def load_credentials():
|
|||||||
return credentials["device_id"], credentials["agent_secret"]
|
return credentials["device_id"], credentials["agent_secret"]
|
||||||
|
|
||||||
|
|
||||||
def checkin(device_id, agent_secret):
|
def api_request(path, agent_secret, body):
|
||||||
body = json.dumps({"device_id": device_id}).encode("utf-8")
|
data = json.dumps(body).encode("utf-8")
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
ANODE_URL + "/api/v1/agent/checkin",
|
ANODE_URL + path,
|
||||||
data=body,
|
data=data,
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": f"Bearer {agent_secret}",
|
"Authorization": f"Bearer {agent_secret}",
|
||||||
@ -43,17 +43,44 @@ def checkin(device_id, agent_secret):
|
|||||||
return json.loads(response.read().decode("utf-8"))
|
return json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def apply_blueprints(ansible_repo, blueprints):
|
def checkin(device_id, agent_secret):
|
||||||
roles = ",".join(b["ansible_role"] for b in blueprints)
|
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(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
"ansible-pull",
|
"ansible-pull",
|
||||||
"-U", ansible_repo,
|
"-U", ansible_repo,
|
||||||
"--tags", roles,
|
"--tags", ",".join(roles),
|
||||||
"-i", "localhost,",
|
"-i", "localhost,",
|
||||||
|
"-e", json.dumps({"tuxflotte_auftrag_states": auftrag_states}),
|
||||||
"site.yml",
|
"site.yml",
|
||||||
],
|
],
|
||||||
check=False,
|
check=False,
|
||||||
@ -62,6 +89,22 @@ def apply_blueprints(ansible_repo, blueprints):
|
|||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
log(f"ansible-pull exited with status {result.returncode}")
|
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):
|
def run_once(device_id, agent_secret):
|
||||||
result = checkin(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
|
return FALLBACK_INTERVAL_SECONDS
|
||||||
|
|
||||||
blueprints = result.get("blueprints", [])
|
blueprints = result.get("blueprints", [])
|
||||||
|
auftraege = result.get("auftraege", [])
|
||||||
|
|
||||||
if blueprints:
|
if blueprints or auftraege:
|
||||||
apply_blueprints(result["ansible_repo"], blueprints)
|
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:
|
else:
|
||||||
log("Check-in ok, nothing to do.")
|
log("Check-in ok, nothing to do.")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user