feat(ansible): scope picker in run form + create_run() DB inventory generation
This commit is contained in:
@@ -37,6 +37,8 @@ async def index():
|
|||||||
@ansible_bp.get("/browse")
|
@ansible_bp.get("/browse")
|
||||||
@require_role(UserRole.viewer)
|
@require_role(UserRole.viewer)
|
||||||
async def browse():
|
async def browse():
|
||||||
|
from steward.models.ansible_inventory import AnsibleTarget, AnsibleGroup
|
||||||
|
|
||||||
all_sources = _get_sources()
|
all_sources = _get_sources()
|
||||||
source_data = []
|
source_data = []
|
||||||
for source in all_sources:
|
for source in all_sources:
|
||||||
@@ -47,7 +49,21 @@ async def browse():
|
|||||||
"playbooks": playbooks,
|
"playbooks": playbooks,
|
||||||
"inventories": inventories,
|
"inventories": inventories,
|
||||||
})
|
})
|
||||||
return await render_template("ansible/browse.html", source_data=source_data)
|
|
||||||
|
async with current_app.db_sessionmaker() as db:
|
||||||
|
targets = (await db.execute(
|
||||||
|
select(AnsibleTarget).order_by(AnsibleTarget.name)
|
||||||
|
)).scalars().all()
|
||||||
|
groups = (await db.execute(
|
||||||
|
select(AnsibleGroup).order_by(AnsibleGroup.name)
|
||||||
|
)).scalars().all()
|
||||||
|
|
||||||
|
return await render_template(
|
||||||
|
"ansible/browse.html",
|
||||||
|
source_data=source_data,
|
||||||
|
targets=targets,
|
||||||
|
groups=groups,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@ansible_bp.get("/browse/<source_name>/<path:playbook_path>")
|
@ansible_bp.get("/browse/<source_name>/<path:playbook_path>")
|
||||||
@@ -72,19 +88,35 @@ async def view_playbook(source_name: str, playbook_path: str):
|
|||||||
@ansible_bp.post("/runs")
|
@ansible_bp.post("/runs")
|
||||||
@require_role(UserRole.operator)
|
@require_role(UserRole.operator)
|
||||||
async def create_run():
|
async def create_run():
|
||||||
|
import json as _json
|
||||||
|
from steward.ansible.inventory_gen import fetch_scope_targets, generate_inventory
|
||||||
|
|
||||||
form = await request.form
|
form = await request.form
|
||||||
playbook_path = form.get("playbook_path", "").strip()
|
playbook_path = form.get("playbook_path", "").strip()
|
||||||
source_name = form.get("source_name", "").strip()
|
source_name = form.get("source_name", "").strip()
|
||||||
inventory_path = form.get("inventory_path", "").strip()
|
inventory_scope = form.get("inventory_scope", "steward:all").strip()
|
||||||
|
|
||||||
if not playbook_path or not inventory_path:
|
if not playbook_path:
|
||||||
return "playbook_path and inventory_path are required", 400
|
return "playbook_path is required", 400
|
||||||
|
|
||||||
all_sources = _get_sources()
|
all_sources = _get_sources()
|
||||||
source = next((s for s in all_sources if s["name"] == source_name), None)
|
source = next((s for s in all_sources if s["name"] == source_name), None)
|
||||||
if source is None:
|
if source is None:
|
||||||
return "Source not found", 404
|
return "Source not found", 404
|
||||||
|
|
||||||
|
# Resolve inventory for this scope.
|
||||||
|
inventory_content: str | None = None
|
||||||
|
inventory_path: str | None = None
|
||||||
|
if inventory_scope.startswith("steward:"):
|
||||||
|
async with current_app.db_sessionmaker() as db:
|
||||||
|
targets = await fetch_scope_targets(db, inventory_scope)
|
||||||
|
inventory_content = _json.dumps(generate_inventory(targets))
|
||||||
|
elif inventory_scope.startswith("repo:"):
|
||||||
|
parts = inventory_scope.split(":", 2)
|
||||||
|
inventory_path = parts[2] if len(parts) == 3 else ""
|
||||||
|
else:
|
||||||
|
return "Invalid inventory_scope", 400
|
||||||
|
|
||||||
# Optional run parameters (all passed as argv by the executor — no shell).
|
# Optional run parameters (all passed as argv by the executor — no shell).
|
||||||
extra_vars: list[str] = []
|
extra_vars: list[str] = []
|
||||||
for line in (form.get("extra_vars", "") or "").splitlines():
|
for line in (form.get("extra_vars", "") or "").splitlines():
|
||||||
@@ -116,6 +148,7 @@ async def create_run():
|
|||||||
id=run_id,
|
id=run_id,
|
||||||
playbook_path=playbook_path,
|
playbook_path=playbook_path,
|
||||||
inventory_path=inventory_path,
|
inventory_path=inventory_path,
|
||||||
|
inventory_scope=inventory_scope,
|
||||||
source_name=source_name,
|
source_name=source_name,
|
||||||
triggered_by=session["user_id"],
|
triggered_by=session["user_id"],
|
||||||
status=AnsibleRunStatus.running,
|
status=AnsibleRunStatus.running,
|
||||||
@@ -131,9 +164,10 @@ async def create_run():
|
|||||||
current_app._get_current_object(), # type: ignore[attr-defined]
|
current_app._get_current_object(), # type: ignore[attr-defined]
|
||||||
run_id,
|
run_id,
|
||||||
playbook_path,
|
playbook_path,
|
||||||
inventory_path,
|
inventory_path or "",
|
||||||
source["path"],
|
source["path"],
|
||||||
params_or_none,
|
params_or_none,
|
||||||
|
inventory_content,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
task.add_done_callback(
|
task.add_done_callback(
|
||||||
|
|||||||
@@ -71,18 +71,37 @@
|
|||||||
readonly style="color:var(--text-muted);" value="">
|
readonly style="color:var(--text-muted);" value="">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Inventory</label>
|
<label>Run Against</label>
|
||||||
<select name="inventory_path">
|
<select name="inventory_scope">
|
||||||
{% for inv in sd.inventories %}
|
{% if targets %}
|
||||||
<option value="{{ inv }}">{{ inv }}</option>
|
<optgroup label="All Targets">
|
||||||
{% endfor %}
|
<option value="steward:all">All targets ({{ targets | length }})</option>
|
||||||
<option value="">— Enter manually below —</option>
|
</optgroup>
|
||||||
|
{% if groups %}
|
||||||
|
<optgroup label="Group">
|
||||||
|
{% for grp in groups %}
|
||||||
|
<option value="steward:group:{{ grp.id }}">Group: {{ grp.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
{% endif %}
|
||||||
|
<optgroup label="Single Target">
|
||||||
|
{% for tgt in targets %}
|
||||||
|
<option value="steward:target:{{ tgt.id }}">{{ tgt.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
{% endif %}
|
||||||
|
{% if sd.inventories %}
|
||||||
|
<optgroup label="Repo Inventory File">
|
||||||
|
{% for inv in sd.inventories %}
|
||||||
|
<option value="repo:{{ sd.source.name }}:{{ inv }}">{{ sd.source.name }}/{{ inv }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</optgroup>
|
||||||
|
{% endif %}
|
||||||
|
{% if not targets and not sd.inventories %}
|
||||||
|
<option value="" disabled>No targets or inventory files — add targets at /ansible/inventory/targets</option>
|
||||||
|
{% endif %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>Extra vars <span style="color:var(--text-muted);font-weight:normal;">(optional, one key=value per line)</span></label>
|
<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 restart=true"
|
<textarea name="extra_vars" rows="2" placeholder="version=1.2.3 restart=true"
|
||||||
@@ -103,10 +122,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:0.75rem;align-items:center;">
|
<div style="display:flex;gap:0.75rem;align-items:center;">
|
||||||
<button type="button" class="btn"
|
<button type="submit" class="btn">Execute</button>
|
||||||
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;"
|
<a href="#" onclick="document.getElementById('run-form-{{ sd.source.name }}').style.display='none';return false;"
|
||||||
class="btn btn-ghost btn-sm">Cancel</a>
|
class="btn btn-ghost btn-sm">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:0.4rem 1rem;font-size:0.9rem;margin-bottom:1rem;">
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:0.4rem 1rem;font-size:0.9rem;margin-bottom:1rem;">
|
||||||
<span style="color:var(--text-muted);">ID</span><span style="color:var(--text-dim);font-family:monospace;">{{ run.id }}</span>
|
<span style="color:var(--text-muted);">ID</span><span style="color:var(--text-dim);font-family:monospace;">{{ run.id }}</span>
|
||||||
<span style="color:var(--text-muted);">Playbook</span><span>{{ run.playbook_path }}</span>
|
<span style="color:var(--text-muted);">Playbook</span><span>{{ run.playbook_path }}</span>
|
||||||
<span style="color:var(--text-muted);">Inventory</span><span>{{ run.inventory_path }}</span>
|
<span style="color:var(--text-muted);">Scope</span><span style="font-family:ui-monospace,monospace;font-size:0.85rem;">{{ run.inventory_scope }}</span>
|
||||||
<span style="color:var(--text-muted);">Source</span><span>{{ run.source_name }}</span>
|
<span style="color:var(--text-muted);">Source</span><span>{{ run.source_name }}</span>
|
||||||
<span style="color:var(--text-muted);">Triggered by</span><span>{{ triggered_label }}</span>
|
<span style="color:var(--text-muted);">Triggered by</span><span>{{ triggered_label }}</span>
|
||||||
<span style="color:var(--text-muted);">Status</span><span style="color:{{ status_color }};font-weight:bold;">{{ run.status.value.upper() }}</span>
|
<span style="color:var(--text-muted);">Status</span><span style="color:{{ status_color }};font-weight:bold;">{{ run.status.value.upper() }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user