- git ships in the image; checkouts live under /data/ansible/sources (STEWARD_ANSIBLE_SOURCE_DIR), on the persistent volume. - One task, ansible_git_pulls, reads the sources every 30 s and pulls each as its interval passes: a source added in Settings is pulled without a restart, a removed one is forgotten. - git_pull: a 120 s timeout ending the process group, GIT_TERMINAL_PROMPT=0, pull --ff-only, a failed clone removed; it raises GitPullError with git's message, so the Sync button stops saying Synced after a failure (and the message is now escaped). - An alert action's vars and the firing's steward_alert_* vars reach ansible-playbook as one JSON extra-vars file, not -e key=value, which Ansible split on whitespace (a rule named 'High CPU on web'). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.6 KiB
Ansible Integration
Steward can browse, trigger, and stream output from Ansible playbooks directly from the web UI. Runs execute as asyncio tasks inside the same process — no Celery, no external workers.
Playbook Sources
Sources are configured at /settings/ under Ansible. Two source types are supported:
Local Filesystem
Points to a directory on the host that already contains playbooks.
# In app_settings (configured via UI)
ansible.sources:
- name: "homelab"
type: local
path: "/opt/playbooks"
Git Repository
The app clones or pulls the repo into a local cache directory on a configurable schedule. All execution uses the local cache.
ansible.sources:
- name: "infra-repo"
type: git
url: "https://github.com/example/infra.git"
branch: "main"
pull_interval_seconds: 300
Each git source is checked out under /data/ansible/sources/<name> (override with STEWARD_ANSIBLE_SOURCE_DIR), on the persistent volume, so a redeploy keeps it. One scheduled task, ansible_git_pulls, ticks every 30 s and pulls each source once its pull_interval_seconds has passed; a source added in Settings is pulled within a tick, with no restart. A clone or pull that runs past 120 s is cut (its process group terminated), a clone that fails is removed so the next attempt starts clean, and git never prompts for credentials.
Inventory Discovery
The app discovers inventory files within the root of the playbook source directory (non-recursive). It looks for files named:
hostsinventoryinventory.ymlinventory.ini
A manual relative path can also be entered in the UI (e.g. inventories/production/hosts) for inventories in subdirectories.
Triggering a Run
From the UI at /ansible/, you can:
- Browse available playbooks across all sources
- View playbook contents before running
- Select an inventory file
- Trigger a run (requires
operatororviewerrole — execution is restricted tooperator/admin)
The run flow:
- UI submits
POST /ansible/runswithplaybook_path,source_name, andinventory_path - An
AnsibleRunrow is created withstatus = running - Playbook execution starts as
asyncio.create_task() - The response returns the
run_idand an HTMX partial that wires the SSE subscription - The browser connects to
GET /ansible/runs/<run_id>/streamto receive live output
SSE Streaming
Run output is streamed to the browser via Server-Sent Events (SSE) at:
GET /ansible/runs/<run_id>/stream
Each output line is sent as:
event: output
data: <line of stdout/stderr>
Run completion:
event: done
data: success|failed|interrupted
Output is flushed to the ansible_runs.output DB column every 50 lines or every 5 seconds (whichever comes first) and always on completion. This means partial output survives a process crash.
If a client connects after the run has already completed, the endpoint immediately sends event: done with the final status and closes the stream. To view stored output, use the run history view at GET /ansible/runs/<run_id>.
Run Lifecycle
| Status | Description |
|---|---|
running |
Execution in progress |
success |
Playbook exited 0 |
failed |
Playbook exited non-zero |
interrupted |
App restarted while run was in progress |
On startup, the app marks any runs still in running state as interrupted (see _mark_interrupted_runs() in app.py).
Output stored in the DB is capped at 1 MB. If truncated, [output truncated] is appended to the DB column, but the live SSE stream continues unaffected.
Data Model
ansible_runs table (defined in steward/models/ansible.py):
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
playbook_path |
str | Relative path to playbook |
inventory_path |
str | Inventory path used |
source_name |
str | Name of the playbook source |
triggered_by |
FK → users | |
status |
enum | running, success, failed, interrupted |
started_at |
timestamp UTC | |
finished_at |
timestamp UTC | Null if still running |
output |
text | Captured stdout/stderr (capped at 1 MB) |
Runs older than data.retention_days (default 90) are pruned by the data_cleanup scheduled task.
Source Files
| File | Purpose |
|---|---|
steward/ansible/sources.py |
Source discovery, git pull logic |
steward/ansible/executor.py |
Subprocess execution and output streaming |
steward/ansible/routes.py |
HTTP routes (browse, trigger, stream, history) |
steward/models/ansible.py |
AnsibleRun model |