# Authoring Steward-friendly Ansible playbooks This is the contract between a playbook and Steward's run UI. Follow it and a playbook drops into Steward with a description, fill-in variable fields, correct targeting, and credentials supplied automatically — no per-playbook wiring. It applies to **any** playbook in a configured source (bundled, the writable local source, or a git source), including third-party ones. --- ## 1. Describe what it does — `# description:` Steward shows a one-line description when a playbook is selected in the run form. ```yaml --- # description: Reclaim disk on Docker/Swarm nodes by pruning unused images and build cache. - name: Docker system prune hosts: all ... ``` - Format: a comment line `# description: ` anywhere in the file. **First match wins.** Case-insensitive on the `description:` key. - Why a comment and not a key: Ansible rejects unknown *play* keys (you can't add `description:` to a play), so a comment is the portable place. It survives `ansible-playbook` untouched. - Fallback: if there's no `# description:` comment, Steward uses the **first play's `name:`**. So always give your play a meaningful `name:` even without the comment. - Keep it to one readable line. Longer "how to use" notes can go in additional normal comments — Steward only reads the `description:` line. ## 2. Declare tunables in `vars:` — they become fill-in fields Every **scalar** entry in a play's `vars:` block becomes an editable field in the run form, with the default shown as the input's placeholder. ```yaml vars: prune_all_images: false # → checkbox-ish text field, placeholder "default: false" keep_last_days: 7 # → field, placeholder "default: 7" registry_url: "" # → field, placeholder "no default" ``` - **Blank field = use the default.** Steward only sends fields the operator actually fills, so an untouched field falls through to the playbook/inventory default rather than overriding it. - Only scalars (string/int/float/bool) surface as fields. Lists/dicts are skipped — set those in the playbook or via inventory group/host vars. - Values are delivered as **extra-vars** (`-e`), which are the **highest** precedence in Ansible — they override the `vars:` defaults. (This is why the default can be empty and still be safely overridden at run time.) ### `vars_prompt:` also works Steward reads `vars_prompt` too. Use it when you want an explicit prompt or a required value: ```yaml vars_prompt: - name: release_tag prompt: "Which release to deploy?" # shown as the field label # no default → Steward marks the field REQUIRED - name: admin_password prompt: "Admin password" private: true # → masked field, never stored ``` ## 3. Secrets — name them so they're masked and not persisted A field is treated as **secret** (rendered masked, and its value is **never written to the DB / run history**) when either: - the variable name contains `password`, `passwd`, `secret`, `token`, `api_key` / `apikey`, `private_key`, or `credential` (case-insensitive), **or** - it's a `vars_prompt` entry with `private: true` (Ansible's default for vars_prompt is private). So name sensitive variables accordingly (`db_password`, `api_token`, `vault_secret`) and they're handled safely with no extra config. Non-secret run-time vars are persisted (so scheduled runs can reuse them); secret ones are passed to the run only. ## 4. Target with `hosts: all` Steward builds the inventory itself from the **target / group** the operator picks in the run form (or the single host on a host page). Your play should: ```yaml hosts: all # run against whatever Steward scoped to ``` - Don't hardcode hostnames or rely on a checked-in inventory for Steward runs (Steward generates a fresh inventory per run). - Per-host connection vars (`ansible_host`, plus anything you set on the target in **Ansible → Inventory**) arrive as inventory host vars. - The run form's **Limit** / **Tags** map to `--limit` / `--tags`. ## 5. Privileges & connection — don't put credentials in the playbook Steward supplies SSH and become for you: - Steady-state runs connect as the managed **`steward`** account using Steward's managed key; that account has **passwordless sudo**. So just use `become: true` where you need root. - First-contact provisioning uses a one-time bootstrap user/password the operator enters (never stored). - Never embed SSH keys, passwords, or `ansible_user`/`ansible_ssh_pass` in the playbook. Connection identity is global (Settings → Ansible) or per-target. ## 6. Be idempotent Steward re-runs playbooks (updates, schedules, retries). Use modules that converge (state-based) rather than ad-hoc `command:`/`shell:` where possible, so re-runs are safe. --- ## What Steward reads from a playbook (summary) | Source in the playbook | What Steward does with it | |---|---| | `# description: ` comment | Description shown on selection (first match) | | first play `name:` | Description fallback | | `# steward:category: ` | Grouping badge in the browse list + run form | | `# steward:confirm: true` | Requires a confirmation tick before the run launches | | `vars:` scalar entries | Run-time fill-in fields (placeholder = default) | | `vars_prompt:` entries | Run-time fields (required if no default) | | secret-looking var name / `private: true` | Field masked + value not persisted | | `hosts:` | Expected to be `all`; Steward provides the inventory | Everything else (SSH user/key, become password, the inventory, `steward_token` etc. for the agent playbooks) is injected by Steward at run time. ## Metadata comments Steward reads metadata from magic comments (Ansible rejects unknown play keys, so comments are the portable place). Two forms: - `# description: ` — the description (see §1). - `# steward:: ` — the namespaced metadata block. First match per key wins; keys are case-insensitive. **Implemented `# steward:` keys:** | Key | Example | Effect | |---|---|---| | `category` | `# steward:category: maintenance` | Free-text grouping label. Shown as a badge in the browse list and on the run form. Purely organizational. | | `confirm` | `# steward:confirm: true` | Marks the playbook as significant/destructive. The run form then requires an explicit confirmation tick before it can be launched. Use for data-loss-capable plays (prune with volumes, resets, etc.). Truthy values: `true`/`yes`/`1`/`on`. | | `description` | `# steward:description: ...` | Alias for `# description:` (the unprefixed form takes precedence if both exist). | ```yaml --- # description: Reclaim disk on Docker/Swarm nodes by pruning unused data. # steward:category: maintenance # steward:confirm: true - name: Docker system prune hosts: all ... ``` Other `# steward::` keys are simply ignored today — the namespace is reserved, so it's safe to add ones Steward doesn't yet understand without breaking anything, but only `category`, `confirm`, and `description` do something. ## Minimal annotated example ```yaml --- # description: Restart a systemd service and confirm it came back up. - name: Restart a service hosts: all become: true gather_facts: false vars: service_name: "" # required-ish: operator fills it in the run form tasks: - name: Validate input ansible.builtin.assert: that: service_name | default('') | length > 0 fail_msg: "Set service_name." - name: Restart ansible.builtin.systemd: name: "{{ service_name }}" state: restarted - name: Confirm active ansible.builtin.command: "systemctl is-active {{ service_name }}" changed_when: false ```