# 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/` (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//stream` to receive live output --- ## SSE Streaming Run output is streamed to the browser via Server-Sent Events (SSE) at: ``` GET /ansible/runs//stream ``` Each output line is sent as: ``` event: output data: ``` 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 Lifecycle | Status | Description | |---|---| | `queued` | Waiting for a host another run holds, or for a run slot | | `running` | Execution in progress | | `success` | Playbook exited 0 | | `failed` | Playbook exited non-zero | | `interrupted` | App restarted while run was in progress | | `cancelled` | Stopped by an operator, or by Steward shutting down mid-run | On startup, the app marks any runs still `running` or `queued` as `interrupted` (see `_mark_interrupted_runs()` in `app.py`). ### Overlapping runs - **One run per host.** Before it takes a run slot, a run claims every host it touches. Hosts come from the inventory Steward writes, keyed by address. A run on a source's own inventory file claims that file, because Steward doesn't read its hosts. While another run holds one of its hosts, the run shows `queued` and says which run it waits for. After an hour of waiting it fails and says so. - **One run at a time per schedule.** A schedule that falls due while its previous run is still `queued` or `running` skips that firing. The Schedules page shows "Skipped: the previous run was still going" beside that run. The next firing comes an interval later. - **One action run at a time per alert rule.** When a rule fires again while the action run from an earlier firing is still going, no new run starts. The firing's event says it was skipped and names the run. 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 |