Files
FabledSteward/steward/templates/ansible/browse.html
T
bvandeusen 8b62eb2ca3
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 7s
CI / integration (push) Successful in 2m16s
feat(ansible): parameterized runs — extra-vars, limit, tags, dry-run (task 546)
Run form + executor now support optional params, passed as argv (never
shell-interpolated):
- extra_vars: one key=value per line -> repeated -e
- limit -> --limit; tags -> --tags
- dry-run checkbox -> --check --diff

- executor.py: pure build_ansible_command(playbook, inventory, params); start_run
  gains an optional params arg (backward-compatible)
- models/ansible.py + migration 0013: nullable params JSON column
- routes.py: create_run parses + validates (extra-var lines need '='), stores
  params on the run, passes to the executor
- browse.html run form: Extra vars / Limit / Tags / Dry-run fields
- run_detail.html: shows the params used
- tests/test_ansible_command.py: unit coverage of the builder; integration test
  runs a real playbook with extra-var + limit + check and asserts substitution

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:31:16 -04:00

121 lines
6.2 KiB
HTML

{% extends "base.html" %}
{% block title %}Browse Playbooks — Steward{% endblock %}
{% block content %}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
<h1 class="page-title" style="margin-bottom:0;">Browse Playbooks</h1>
<a href="/ansible/" class="btn btn-ghost btn-sm">← Run History</a>
</div>
{% if view_contents is defined %}
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;">
<h3 class="section-title">{{ view_path }}</h3>
<a href="/ansible/browse" class="btn btn-ghost btn-sm">← Back to browse</a>
</div>
<pre style="background:var(--bg);padding:1rem;border-radius:4px;overflow-x:auto;font-size:0.8rem;color:var(--text-muted);max-height:600px;overflow-y:auto;">{{ view_contents }}</pre>
</div>
{% endif %}
{% if not source_data %}
{% if view_contents is not defined %}
<div class="card">
<p style="color:var(--text-muted);">No Ansible sources configured. <a href="/settings/ansible/">Add sources in Settings → Ansible</a>.</p>
</div>
{% endif %}
{% else %}
{% for sd in source_data %}
<div class="card" style="margin-bottom:1.5rem;">
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
<h3 class="section-title">{{ sd.source.name }}</h3>
<span style="color:var(--text-muted);font-size:0.8rem;background:var(--bg-elevated);padding:0.2rem 0.5rem;border-radius:3px;">{{ sd.source.type }}</span>
<span style="color:var(--text-dim);font-size:0.8rem;">{{ sd.source.path }}</span>
</div>
{% if not sd.playbooks %}
<p style="color:#606080;font-size:0.9rem;">No playbooks found at this path.</p>
{% else %}
<table class="table" style="margin-bottom:1rem;">
<thead>
<tr>
<th>Playbook</th>
<th style="width:120px;"></th>
</tr>
</thead>
<tbody>
{% for pb in sd.playbooks %}
<tr>
<td style="font-family:ui-monospace,monospace;font-size:0.88rem;">{{ pb }}</td>
<td class="td-actions">
<a href="/ansible/browse/{{ sd.source.name }}/{{ pb }}" class="btn btn-sm btn-ghost" style="margin-right:0.5rem;">View</a>
<a href="#run-form-{{ sd.source.name }}"
onclick="document.getElementById('run-form-{{ sd.source.name }}').style.display='block';
document.getElementById('run-playbook-{{ sd.source.name }}').value='{{ pb }}';
document.getElementById('run-playbook-display-{{ sd.source.name }}').value='{{ pb }}';
return false;"
class="btn btn-sm btn-ghost">Run</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# Run trigger form for this source #}
<div id="run-form-{{ sd.source.name }}" style="display:none;background:var(--bg-elevated);border-radius:6px;padding:1rem;border:1px solid var(--border-mid);">
<h4 class="section-title" style="margin-bottom:0.75rem;">Trigger Run</h4>
<form hx-post="/ansible/runs" hx-target="#run-result-{{ sd.source.name }}" hx-swap="innerHTML">
<input type="hidden" name="source_name" value="{{ sd.source.name }}">
<input type="hidden" id="run-playbook-{{ sd.source.name }}" name="playbook_path" value="">
<div class="form-group">
<label>Playbook</label>
<input type="text" id="run-playbook-display-{{ sd.source.name }}"
readonly style="color:var(--text-muted);" value="">
</div>
<div class="form-group">
<label>Inventory</label>
<select name="inventory_path">
{% for inv in sd.inventories %}
<option value="{{ inv }}">{{ inv }}</option>
{% endfor %}
<option value="">— Enter manually below —</option>
</select>
</div>
<div class="form-group">
<label>Or enter inventory path manually</label>
<input type="text" id="manual-inv-{{ sd.source.name }}" placeholder="inventories/production/hosts">
</div>
<div class="form-group">
<label>Extra vars <span style="color:var(--text-muted);font-weight:normal;">(optional, one key=value per line)</span></label>
<textarea name="extra_vars" rows="2" placeholder="version=1.2.3&#10;restart=true"
style="width:100%;font-family:ui-monospace,monospace;font-size:0.85rem;"></textarea>
</div>
<div class="form-group">
<label>Limit to host(s) <span style="color:var(--text-muted);font-weight:normal;">(optional, --limit)</span></label>
<input type="text" name="limit" placeholder="web01,db*">
</div>
<div class="form-group">
<label>Tags <span style="color:var(--text-muted);font-weight:normal;">(optional, --tags)</span></label>
<input type="text" name="tags" placeholder="deploy,config">
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:0.5rem;font-weight:normal;">
<input type="checkbox" name="check">
Dry-run (--check --diff) — preview changes without applying
</label>
</div>
<div style="display:flex;gap:0.75rem;align-items:center;">
<button type="button" class="btn"
onclick="var manual=document.getElementById('manual-inv-{{ sd.source.name }}').value.trim();
if(manual){this.closest('form').querySelector('[name=inventory_path]').value=manual;}
htmx.trigger(this.closest('form'),'submit');">Execute</button>
<a href="#" onclick="document.getElementById('run-form-{{ sd.source.name }}').style.display='none';return false;"
class="btn btn-ghost btn-sm">Cancel</a>
</div>
</form>
<div id="run-result-{{ sd.source.name }}" style="margin-top:1rem;"></div>
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% endblock %}