Files
FabledSteward/steward/ansible/origins.py
T
bvandeusenandClaude Opus 5.5 0c72892f58
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 59s
CI / unit (push) Failing after 1m22s
CI / publish (push) Skipped
feat(ansible): schedules read as sentences, each with its own page (#5332)
- The Schedules tab: a verdict line (failing first, paused, next due),
  each schedule as one line (what runs where, how often, when next, how it
  last went); the row opens its page; Run now and Pause stay in the row.
- /ansible/schedules/new and /ansible/schedules/<id>: the form on classes,
  dry run beside the button, the playbook's variables rendered with the
  page; at the foot Delete behind the confirm dialog. ?source=&playbook=
  start a new one on a playbook (Schedule it on the playbook's page).
- A run started by a schedule links to that schedule's page.
- The run page's id guard allows the Run again link's ?from=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:51:15 -04:00

53 lines
2.6 KiB
Python

"""Who or what started each run, as the runs list and run detail say it.
A run records the user who started it (triggered_by) and where it came from
(origin / origin_id, migration 0031): a schedule, an alert rule, or an agent
over MCP. This turns those into "schedule nightly", "alert rule nas down",
"alice via MCP" or "alice", in three queries for a whole page of runs.
"""
from __future__ import annotations
from sqlalchemy import select
from steward.models.alerts import AlertRule
from steward.models.ansible_schedule import AnsibleSchedule
from steward.models.users import User
def describe_origin(run, users: dict, schedules: dict, rules: dict) -> dict:
"""{"text", "url", "kind"} for one run, from id → name maps. A schedule or
rule deleted since is named as such; a run with neither a user nor an
origin predates origins and was started by Steward itself."""
who = users.get(run.triggered_by, "a deleted user") if run.triggered_by else None
if run.origin == "schedule":
name = schedules.get(run.origin_id)
text = f"schedule {name}" if name else "a deleted schedule"
if who:
text += f" (run now by {who})"
return {"text": text, "url": f"/ansible/schedules/{run.origin_id}" if name else None, "kind": "schedule"}
if run.origin == "alert":
name = rules.get(run.origin_id)
return {"text": f"alert rule {name}" if name else "a deleted alert rule",
"url": f"/alerts/history?rule={run.origin_id}" if name else None, "kind": "alert"}
if run.origin == "mcp":
return {"text": f"{who or 'an agent'} via MCP", "url": None, "kind": "mcp"}
if who:
return {"text": who, "url": None, "kind": "user"}
return {"text": "Steward", "url": None, "kind": "system"}
async def run_origins(db, runs) -> dict[str, dict]:
"""{run id: describe_origin(run)} for a page of runs."""
runs = list(runs)
user_ids = {r.triggered_by for r in runs if r.triggered_by}
sched_ids = {r.origin_id for r in runs if r.origin == "schedule" and r.origin_id}
rule_ids = {r.origin_id for r in runs if r.origin == "alert" and r.origin_id}
users = dict((await db.execute(
select(User.id, User.username).where(User.id.in_(user_ids)))).all()) if user_ids else {}
schedules = dict((await db.execute(
select(AnsibleSchedule.id, AnsibleSchedule.name).where(AnsibleSchedule.id.in_(sched_ids)))).all()) \
if sched_ids else {}
rules = dict((await db.execute(
select(AlertRule.id, AlertRule.name).where(AlertRule.id.in_(rule_ids)))).all()) if rule_ids else {}
return {r.id: describe_origin(r, users, schedules, rules) for r in runs}