Ansible schedule form: structured playbook-variable fields + first-item dropdown defaults #2
@@ -375,10 +375,9 @@ async def _fleet_rows(session) -> list[dict]:
|
||||
@host_agent_bp.get("/")
|
||||
@require_role(UserRole.viewer)
|
||||
async def index():
|
||||
"""Fleet overview page — cards per host linking to the detail view."""
|
||||
async with current_app.db_sessionmaker() as session:
|
||||
rows = await _fleet_rows(session)
|
||||
return await render_template("host_list.html", rows=rows)
|
||||
"""The fleet view folded into the Hosts hub — kept as a redirect so old
|
||||
links/widgets don't 404."""
|
||||
return redirect("/hosts/")
|
||||
|
||||
|
||||
@host_agent_bp.get("/widget")
|
||||
@@ -571,7 +570,14 @@ async def host_panel(host_id: str):
|
||||
|
||||
@host_agent_bp.get("/settings/")
|
||||
@require_role(UserRole.admin)
|
||||
async def settings_list():
|
||||
async def settings_redirect():
|
||||
"""Old management URL — agent management moved into the Hosts section."""
|
||||
return redirect(url_for("host_agent.fleet"))
|
||||
|
||||
|
||||
@host_agent_bp.get("/fleet/")
|
||||
@require_role(UserRole.admin)
|
||||
async def fleet():
|
||||
from steward.core.capabilities import has_capability
|
||||
from steward.models.ansible_inventory import AnsibleTarget, AnsibleGroup
|
||||
|
||||
@@ -641,7 +647,7 @@ async def add_host():
|
||||
await session.commit()
|
||||
host_id = host.id
|
||||
|
||||
return redirect(url_for("host_agent.settings_list") + f"?new_token={raw}&host_id={host_id}")
|
||||
return redirect(url_for("host_agent.fleet") + f"?new_token={raw}&host_id={host_id}")
|
||||
|
||||
|
||||
@host_agent_bp.post("/settings/<host_id>/rotate-token")
|
||||
@@ -657,7 +663,7 @@ async def rotate_token(host_id: str):
|
||||
reg.token_hash = hashed
|
||||
reg.token_created_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
return redirect(url_for("host_agent.settings_list") + f"?new_token={raw}&host_id={host_id}")
|
||||
return redirect(url_for("host_agent.fleet") + f"?new_token={raw}&host_id={host_id}")
|
||||
|
||||
|
||||
@host_agent_bp.post("/settings/<host_id>/delete")
|
||||
@@ -670,7 +676,7 @@ async def delete_registration(host_id: str):
|
||||
if reg is not None:
|
||||
await session.delete(reg)
|
||||
await session.commit()
|
||||
return redirect(url_for("host_agent.settings_list"))
|
||||
return redirect(url_for("host_agent.fleet"))
|
||||
|
||||
|
||||
# ── Deploy via Ansible (plugin↔core synergy via the capability registry) ──────
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import crumbs %}
|
||||
{% block title %}Host Agent — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Host Agents", "/plugins/host_agent/"), ("Settings", "")]) }}{% endblock %}
|
||||
{% block title %}Agent fleet — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), ("Agent fleet", "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
<h1 class="page-title">Host Agent — Registered Hosts</h1>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;gap:1rem;flex-wrap:wrap;">
|
||||
<h1 class="page-title" style="margin-bottom:0;">Agent fleet</h1>
|
||||
<a href="/hosts/" class="btn btn-ghost btn-sm">← Hosts</a>
|
||||
</div>
|
||||
<p style="color:var(--text-muted);font-size:0.84rem;margin:-0.4rem 0 1.25rem;">
|
||||
Bulk agent operations across your inventory. For a single host, use its
|
||||
<a href="/hosts/">host page</a> — provisioning and metrics live there.
|
||||
</p>
|
||||
|
||||
{% if new_token %}
|
||||
<div class="card" style="background:var(--accent-bg);">
|
||||
@@ -58,7 +65,7 @@
|
||||
No managed SSH key yet — Steward needs one to log into hosts as <code>steward</code>.
|
||||
</span>
|
||||
<form method="post" action="/settings/ansible/generate-key" style="margin:0;">
|
||||
<input type="hidden" name="next" value="/plugins/host_agent/settings/">
|
||||
<input type="hidden" name="next" value="/plugins/host_agent/fleet/">
|
||||
<button type="submit" class="btn btn-sm">Generate managed key</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,42 @@ async def _compute_uptime(db) -> dict[str, dict]:
|
||||
return stats
|
||||
|
||||
|
||||
async def _agent_metrics_by_host(db, host_names: list[str]) -> dict[str, dict]:
|
||||
"""Latest agent cpu/mem per host name from the generic PluginMetric table.
|
||||
|
||||
Read directly (PluginMetric is a core model) so the Hosts list can show an
|
||||
at-a-glance agent column without importing the host_agent plugin.
|
||||
"""
|
||||
from steward.models.metrics import PluginMetric
|
||||
if not host_names:
|
||||
return {}
|
||||
wanted = ("cpu_pct", "mem_used_pct")
|
||||
subq = (
|
||||
select(
|
||||
PluginMetric.resource_name, PluginMetric.metric_name,
|
||||
func.max(PluginMetric.recorded_at).label("m"),
|
||||
)
|
||||
.where(
|
||||
PluginMetric.source_module == "host_agent",
|
||||
PluginMetric.metric_name.in_(wanted),
|
||||
PluginMetric.resource_name.in_(host_names),
|
||||
)
|
||||
.group_by(PluginMetric.resource_name, PluginMetric.metric_name)
|
||||
).subquery()
|
||||
rows = (await db.execute(
|
||||
select(PluginMetric).join(
|
||||
subq,
|
||||
(PluginMetric.resource_name == subq.c.resource_name)
|
||||
& (PluginMetric.metric_name == subq.c.metric_name)
|
||||
& (PluginMetric.recorded_at == subq.c.m),
|
||||
)
|
||||
)).scalars().all()
|
||||
out: dict[str, dict] = {}
|
||||
for r in rows:
|
||||
out.setdefault(r.resource_name, {})[r.metric_name] = r.value
|
||||
return out
|
||||
|
||||
|
||||
@hosts_bp.get("/")
|
||||
@require_role(UserRole.viewer)
|
||||
async def list_hosts():
|
||||
@@ -105,6 +141,7 @@ async def list_hosts():
|
||||
)
|
||||
latest_dns = {r.host_id: r for r in dr.scalars()}
|
||||
uptime = await _compute_uptime(db)
|
||||
agent_metrics = await _agent_metrics_by_host(db, [h.name for h in hosts])
|
||||
|
||||
return await render_template(
|
||||
"hosts/list.html",
|
||||
@@ -112,6 +149,7 @@ async def list_hosts():
|
||||
latest_pings=latest_pings,
|
||||
latest_dns=latest_dns,
|
||||
uptime=uptime,
|
||||
agent_metrics=agent_metrics,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<h1 class="page-title" style="margin-bottom:0;">Hosts</h1>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<a class="btn btn-ghost" href="/hosts/uptime">SLA</a>
|
||||
{% if session.user_role == 'admin' %}
|
||||
<a class="btn btn-ghost" href="/plugins/host_agent/fleet/">Agent fleet</a>
|
||||
{% endif %}
|
||||
<a class="btn" href="/hosts/new">Add Host</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,6 +25,7 @@
|
||||
<th style="text-align:center;" title="Uptime last 24 hours">24h</th>
|
||||
<th style="text-align:center;" title="Uptime last 7 days">7d</th>
|
||||
<th style="text-align:center;" title="Uptime last 30 days">30d</th>
|
||||
<th style="text-align:center;" title="Agent CPU / memory (latest)">Agent</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -89,6 +93,16 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% set am = agent_metrics.get(host.name) %}
|
||||
<td style="text-align:center;font-size:0.8rem;font-family:ui-monospace,monospace;">
|
||||
{% if am %}
|
||||
<span style="color:var(--text-muted);" title="CPU / memory">
|
||||
{{ '%.0f'|format(am.get('cpu_pct', 0)) }}% / {{ '%.0f'|format(am.get('mem_used_pct', 0)) }}%
|
||||
</span>
|
||||
{% else %}
|
||||
<span style="color:var(--text-dim);">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="td-actions">
|
||||
<a href="/hosts/{{ host.id }}/edit" class="btn btn-sm btn-ghost" style="margin-right:0.5rem;">Edit</a>
|
||||
<form method="post" action="/hosts/{{ host.id }}/delete" style="display:inline;">
|
||||
|
||||
Reference in New Issue
Block a user