feat: initial Provisioning Agent (check-in loop + ansible-pull)
Erste lauffaehige Version: laedt Credentials, meldet sich per Bearer-Token bei anode/api/v1/agent/checkin, wendet zugewiesene Blueprints per ansible-pull an und schlaeft bis zum naechsten Intervall.
This commit is contained in:
commit
8382a0a949
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# provisioning-agent
|
||||
|
||||
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`.
|
||||
|
||||
## Manuelle Installation (Testgerät)
|
||||
|
||||
Voraussetzung: `ansible-core` ist installiert (`ansible-pull` im PATH).
|
||||
|
||||
1. Gerät aktivieren und Bereitstellungsvorlage zuweisen (bestehender Provisioning-Flow, siehe `provisioning-server`)
|
||||
2. Agent-Secret erzeugen:
|
||||
|
||||
```
|
||||
curl -X POST https://anode.tuxflotte.de/api/v1/agent/bootstrap \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"device_id": "<device-uuid>"}'
|
||||
```
|
||||
|
||||
3. Credentials ablegen:
|
||||
|
||||
```
|
||||
install -d -m 0700 /etc/tuxflotte
|
||||
cat > /etc/tuxflotte/agent.credentials <<EOF
|
||||
{"device_id": "<device-uuid>", "agent_secret": "<agent-secret aus Schritt 2>"}
|
||||
EOF
|
||||
chmod 0600 /etc/tuxflotte/agent.credentials
|
||||
```
|
||||
|
||||
4. Agent-Code ablegen und Dienst einrichten:
|
||||
|
||||
```
|
||||
install -d /opt/tuxflotte/agent
|
||||
cp agent.py /opt/tuxflotte/agent/
|
||||
cp tuxflotte-agent.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now tuxflotte-agent
|
||||
```
|
||||
|
||||
5. Prüfen:
|
||||
|
||||
```
|
||||
journalctl -u tuxflotte-agent -f
|
||||
ls /run/tuxflotte/agent/applied/
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
| Umgebungsvariable | Zweck | Default |
|
||||
|---|---|---|
|
||||
| `TUXFLOTTE_ANODE_URL` | Basis-URL des Provisioning-Servers | `https://anode.tuxflotte.de` |
|
||||
| `TUXFLOTTE_AGENT_CREDENTIALS` | Pfad zur Credentials-Datei | `/etc/tuxflotte/agent.credentials` |
|
||||
101
agent.py
Normal file
101
agent.py
Normal file
@ -0,0 +1,101 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
ANODE_URL = os.environ.get("TUXFLOTTE_ANODE_URL", "https://anode.tuxflotte.de")
|
||||
CREDENTIALS_PATH = os.environ.get(
|
||||
"TUXFLOTTE_AGENT_CREDENTIALS", "/etc/tuxflotte/agent.credentials"
|
||||
)
|
||||
FALLBACK_INTERVAL_SECONDS = 60
|
||||
|
||||
|
||||
def log(message):
|
||||
print(f"[tuxflotte-agent] {message}", flush=True)
|
||||
|
||||
|
||||
def load_credentials():
|
||||
with open(CREDENTIALS_PATH, "r", encoding="utf-8") as f:
|
||||
credentials = json.load(f)
|
||||
|
||||
return credentials["device_id"], credentials["agent_secret"]
|
||||
|
||||
|
||||
def checkin(device_id, agent_secret):
|
||||
body = json.dumps({"device_id": device_id}).encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(
|
||||
ANODE_URL + "/api/v1/agent/checkin",
|
||||
data=body,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {agent_secret}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, timeout=15) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
|
||||
|
||||
def apply_blueprints(ansible_repo, blueprints):
|
||||
roles = ",".join(b["ansible_role"] for b in blueprints)
|
||||
|
||||
log(f"Applying roles via ansible-pull: {roles}")
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ansible-pull",
|
||||
"-U", ansible_repo,
|
||||
"--tags", roles,
|
||||
"-i", "localhost,",
|
||||
"site.yml",
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
log(f"ansible-pull exited with status {result.returncode}")
|
||||
|
||||
|
||||
def run_once(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
|
||||
|
||||
blueprints = result.get("blueprints", [])
|
||||
|
||||
if blueprints:
|
||||
apply_blueprints(result["ansible_repo"], blueprints)
|
||||
else:
|
||||
log("Check-in ok, nothing to do.")
|
||||
|
||||
return result.get("poll_interval_seconds", FALLBACK_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
def main():
|
||||
if shutil.which("ansible-pull") is None:
|
||||
log("ansible-pull nicht gefunden. Bitte ansible-core installieren.")
|
||||
sys.exit(1)
|
||||
|
||||
device_id, agent_secret = load_credentials()
|
||||
|
||||
while True:
|
||||
try:
|
||||
interval = run_once(device_id, agent_secret)
|
||||
except (urllib.error.URLError, OSError) as exc:
|
||||
log(f"Check-in-Fehler: {exc}")
|
||||
interval = FALLBACK_INTERVAL_SECONDS
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
tuxflotte-agent.service
Normal file
13
tuxflotte-agent.service
Normal file
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Tuxflotte Provisioning Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /opt/tuxflotte/agent/agent.py
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Loading…
x
Reference in New Issue
Block a user