Files
FabledSteward/docs/core/ansible.md
T
bvandeusenandClaude Opus 5.5 edcfa4d3db
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 49s
CI / unit (push) Successful in 1m11s
CI / publish (push) Successful in 1m3s
fix(ansible): git sources work in the image and survive a redeploy; pulls time out; alert vars go in a JSON file (#5157)
- 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>
2026-10-06 21:13:14 -04:00

143 lines
4.6 KiB
Markdown

# 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.
```yaml
# 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.
```yaml
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:
- `hosts`
- `inventory`
- `inventory.yml`
- `inventory.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:
1. Browse available playbooks across all sources
2. View playbook contents before running
3. Select an inventory file
4. Trigger a run (requires `operator` or `viewer` role — execution is restricted to `operator`/`admin`)
The run flow:
1. UI submits `POST /ansible/runs` with `playbook_path`, `source_name`, and `inventory_path`
2. An `AnsibleRun` row is created with `status = running`
3. Playbook execution starts as `asyncio.create_task()`
4. The response returns the `run_id` and an HTMX partial that wires the SSE subscription
5. The browser connects to `GET /ansible/runs/<run_id>/stream` to 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 |