feat(ansible): structured playbook-variable fields on the schedule form
The Ansible Schedules form only offered a free-form extra-vars textarea,
unlike the browse/host run forms which auto-populate a field per declared
playbook variable (vars:/vars_prompt:) plus the playbook description. Wire
the schedule form to the same shared `_playbook_vars.html` partial:
- Playbook <select> now loads /ansible/playbook-vars?schedule=1 on change
into a #sch-playbook-vars container (mirrors the host detail form).
- Edit mode pre-renders the saved playbook's variables server-side with
their stored values; the extra-vars textarea keeps only leftover
(non-declared) vars.
- Partial gains optional `values` (prefill) and `schedule` flags. In the
schedule context secret vars render disabled ("secrets can't be
scheduled") since they're dropped downstream, and the destructive-confirm
gate is shown but not required.
Backend already parsed var__* fields for schedules, so this is the
front-end/partial parity fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,8 @@ async def playbook_vars():
|
||||
(vars/vars_prompt), loaded when a playbook is chosen from a dropdown."""
|
||||
source_name = (request.args.get("source_name", "") or "").strip()
|
||||
playbook_path = (request.args.get("playbook_path", "") or "").strip()
|
||||
# Schedule form: schedules can't prompt, so secret vars render disabled.
|
||||
schedule = (request.args.get("schedule", "") or "").strip() == "1"
|
||||
source = next((s for s in _get_sources() if s["name"] == source_name), None)
|
||||
variables: list = []
|
||||
meta: dict = {"description": "", "category": "", "confirm": False}
|
||||
@@ -182,7 +184,8 @@ async def playbook_vars():
|
||||
meta = src_module.discover_playbook_meta(contents)
|
||||
return await render_template(
|
||||
"ansible/_playbook_vars.html", variables=variables,
|
||||
description=meta["description"], category=meta["category"], confirm=meta["confirm"])
|
||||
description=meta["description"], category=meta["category"],
|
||||
confirm=meta["confirm"], schedule=schedule)
|
||||
|
||||
|
||||
@ansible_bp.get("/run-form/<source_name>/<path:playbook_path>")
|
||||
@@ -383,11 +386,25 @@ async def schedules():
|
||||
if editing_id and s.id == editing_id:
|
||||
editing = s
|
||||
|
||||
# Edit mode: pre-render the selected playbook's declared variables so their
|
||||
# saved values show (fresh loads fetch these via HTMX from /playbook-vars).
|
||||
edit_variables: list = []
|
||||
edit_meta: dict = {"description": "", "category": "", "confirm": False}
|
||||
if editing:
|
||||
src = next((s for s in _get_sources() if s["name"] == editing.source_name), None)
|
||||
if src:
|
||||
contents = src_module.read_playbook(src["path"], editing.playbook_path)
|
||||
if contents is not None:
|
||||
edit_variables = src_module.discover_playbook_variables(contents)
|
||||
edit_meta = src_module.discover_playbook_meta(contents)
|
||||
|
||||
return await render_template(
|
||||
"ansible/schedules.html",
|
||||
rows=rows, groups=groups, targets=targets,
|
||||
source_data=source_data, all_playbooks=all_playbooks,
|
||||
editing=editing, interval_presets=INTERVAL_PRESETS,
|
||||
edit_variables=edit_variables, edit_description=edit_meta["description"],
|
||||
edit_category=edit_meta["category"], edit_confirm=edit_meta["confirm"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{# Description + discovered variable fields for a selected playbook. Shared by
|
||||
the browse run form, host run form, and schedule form. Expects `variables`
|
||||
and `description`. Rendered standalone by /ansible/playbook-vars. #}
|
||||
and `description`. Rendered standalone by /ansible/playbook-vars.
|
||||
Optional: `values` (dict var-name→saved value, prefills fields for edit
|
||||
forms) and `schedule` (True in the schedule form — schedules can't prompt,
|
||||
so secret vars are shown disabled and the confirm gate isn't `required`). #}
|
||||
{% set _values = values|default({}) %}
|
||||
{% set _schedule = schedule|default(false) %}
|
||||
{% if description or category %}
|
||||
<div style="background:var(--bg);border-left:3px solid var(--accent);border-radius:3px;
|
||||
padding:0.5rem 0.7rem;margin:0.25rem 0 0.6rem;font-size:0.82rem;color:var(--text-muted);">
|
||||
@@ -13,7 +18,7 @@
|
||||
<label style="display:flex;align-items:flex-start;gap:0.5rem;background:color-mix(in srgb,var(--red) 10%,var(--bg-elevated));
|
||||
border:1px solid color-mix(in srgb,var(--red) 35%,var(--border));border-radius:6px;
|
||||
padding:0.6rem 0.8rem;margin:0 0 0.6rem;font-size:0.82rem;font-weight:normal;cursor:pointer;">
|
||||
<input type="checkbox" name="confirmed" required style="margin-top:0.15rem;">
|
||||
<input type="checkbox" name="confirmed" {% if not _schedule %}required{% endif %} style="margin-top:0.15rem;">
|
||||
<span><strong style="color:var(--red);">Confirm:</strong> this playbook is marked as making
|
||||
significant or destructive changes. Tick to enable running it.</span>
|
||||
</label>
|
||||
@@ -33,13 +38,22 @@
|
||||
({{ v.name }}{% if v.required %}, required{% endif %}{% if v.secret %}, secret{% endif %})
|
||||
</span>
|
||||
</label>
|
||||
{% if v.secret and _schedule %}
|
||||
{# Scheduled runs can't prompt, so secret vars are dropped downstream — show
|
||||
the field disabled rather than inviting silent data loss. #}
|
||||
<input type="text" disabled
|
||||
placeholder="secrets can't be scheduled — set via inventory / global creds"
|
||||
style="width:100%;font-family:ui-monospace,monospace;font-size:0.85rem;opacity:0.55;">
|
||||
{% else %}
|
||||
{% if v.secret %}<input type="hidden" name="secret__{{ v.name }}" value="1">{% endif %}
|
||||
<input type="{{ 'password' if v.secret else 'text' }}"
|
||||
name="var__{{ v.name }}"
|
||||
{% if not v.secret %}value="{{ _values.get(v.name, '') }}"{% endif %}
|
||||
{% if v.required %}required{% endif %}
|
||||
{% if v.secret %}autocomplete="new-password"{% endif %}
|
||||
placeholder="{% if v.secret %}(hidden){% elif v.default not in (None, '') %}default: {{ v.default }}{% else %}no default{% endif %}"
|
||||
style="width:100%;font-family:ui-monospace,monospace;font-size:0.85rem;">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
|
||||
{# ── Create / edit form ───────────────────────────────────────────────────── #}
|
||||
{% set p = (editing.params or {}) if editing else {} %}
|
||||
{% set saved_vars = p.get('extra_vars_map') or {} %}
|
||||
{% set declared_names = (edit_variables or []) | map(attribute='name') | list %}
|
||||
<div class="card" id="form">
|
||||
<h2 class="section-title" style="margin-bottom:1rem;">{{ "Edit schedule" if editing else "New schedule" }}</h2>
|
||||
<form method="post" action="{{ '/ansible/schedules/' ~ editing.id if editing else '/ansible/schedules' }}">
|
||||
@@ -96,7 +98,9 @@
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label>Playbook</label>
|
||||
<select name="playbook_path" id="sch-playbook" required>
|
||||
<select name="playbook_path" id="sch-playbook" required
|
||||
hx-get="/ansible/playbook-vars?schedule=1" hx-trigger="change"
|
||||
hx-target="#sch-playbook-vars" hx-swap="innerHTML" hx-include="closest form">
|
||||
<option value="">— choose playbook —</option>
|
||||
{% for pb in all_playbooks %}
|
||||
<option value="{{ pb }}" {% if editing and editing.playbook_path == pb %}selected{% endif %}>{{ pb }}</option>
|
||||
@@ -132,11 +136,29 @@
|
||||
<input type="text" name="tags" value="{{ p.get('tags', '') }}" placeholder="tag1,tag2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top:1rem;">
|
||||
<label>Extra vars (one key=value per line)</label>
|
||||
<textarea name="extra_vars" rows="3" placeholder="prune_volumes=false">{% for k, v in (p.get('extra_vars_map') or {}).items() %}{{ k }}={{ v }}
|
||||
{% endfor %}</textarea>
|
||||
|
||||
{# Declared playbook variables (vars:/vars_prompt:) — loaded via HTMX on
|
||||
playbook change; pre-rendered here in edit mode so saved values show. #}
|
||||
<div id="sch-playbook-vars" style="margin-top:1rem;">
|
||||
{% if editing %}
|
||||
{% set variables = edit_variables %}
|
||||
{% set description = edit_description %}
|
||||
{% set category = edit_category %}
|
||||
{% set confirm = edit_confirm %}
|
||||
{% set values = saved_vars %}
|
||||
{% set schedule = true %}
|
||||
{% include "ansible/_playbook_vars.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<details style="margin-top:0.75rem;">
|
||||
<summary style="cursor:pointer;font-size:0.82rem;color:var(--text-muted);">Extra vars</summary>
|
||||
<div class="form-group" style="margin-top:0.6rem;">
|
||||
<label>Extra vars <span style="color:var(--text-muted);font-weight:normal;">(one key=value per line — for vars not listed above)</span></label>
|
||||
<textarea name="extra_vars" rows="3" placeholder="prune_volumes=false">{% for k, v in saved_vars.items() if k not in declared_names %}{{ k }}={{ v }}
|
||||
{% endfor %}</textarea>
|
||||
</div>
|
||||
</details>
|
||||
<div class="form-group" style="display:flex;align-items:center;gap:0.5rem;">
|
||||
<input type="checkbox" name="check" id="check" style="width:auto;" {% if p.get('check') %}checked{% endif %}>
|
||||
<label for="check" style="margin-bottom:0;">Dry run (--check --diff)</label>
|
||||
|
||||
Reference in New Issue
Block a user