88afad9de4
- Host agent panel (reporting state) gains a "Re-provision" collapsible (admin + linked target + managed key): bootstrap user/password → provision.yml, which reinstalls the steward account + managed key + agent. This is the missing path after regenerating the managed key — Update alone can't fix a broken key. - Remove the Ansible sections (run-playbook + target link) from the host EDIT page — they were the stale free-text version and the wrong place. They live on the host detail hub (dropdown + discovered variables). Edit page now links to the host page; edit_host route simplified (no ansible fetch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
9.8 KiB
HTML
177 lines
9.8 KiB
HTML
{# Per-host agent panel — embedded into /hosts/<id> via HTMX. Self-contained. #}
|
|
{% set scope = "steward:target:" ~ target.id if target else "" %}
|
|
<div class="card">
|
|
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.75rem;gap:1rem;flex-wrap:wrap;">
|
|
<h3 class="section-title" style="margin-bottom:0;">Agent</h3>
|
|
{% if reporting %}
|
|
<span style="font-size:0.78rem;color:{{ 'var(--yellow)' if stale else 'var(--green)' }};">
|
|
{{ 'stale' if stale else 'reporting' }}{% if reg.agent_version %} · v{{ reg.agent_version }}{% endif %}
|
|
· last seen {{ reg.last_seen_at.strftime('%Y-%m-%d %H:%M') }}
|
|
</span>
|
|
{% elif reg %}
|
|
<span style="font-size:0.78rem;color:var(--text-muted);">pending — no check-in yet</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if reporting %}
|
|
{# ── Reporting: at-a-glance metrics (+ sparkline trend) + lifecycle ── #}
|
|
{% set cpu = hostlvl.get('cpu_pct') %}
|
|
{% set mem = hostlvl.get('mem_used_pct') %}
|
|
{% set lbl = "font-size:0.72rem;color:var(--text-muted);text-transform:uppercase;" %}
|
|
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:0.6rem;">
|
|
<div title="Average CPU utilization across all cores">
|
|
<div style="{{ lbl }}">CPU</div>
|
|
<div style="font-weight:600;">{{ '%.0f%%'|format(cpu) if cpu is not none else '—' }}</div>
|
|
{{ sparks.cpu | safe }}</div>
|
|
<div title="RAM in use (total minus available; cache/buffers count as free)">
|
|
<div style="{{ lbl }}">Memory</div>
|
|
<div style="font-weight:600;">{{ '%.0f%%'|format(mem) if mem is not none else '—' }}</div>
|
|
{{ sparks.mem | safe }}</div>
|
|
<div title="Root filesystem (/) usage — see Full metrics for every mount">
|
|
<div style="{{ lbl }}">Disk /</div>
|
|
<div style="font-weight:600;">{{ '%.0f%%'|format(disk_root) if disk_root is not none else '—' }}</div>
|
|
{{ sparks.disk | safe }}</div>
|
|
<div title="1-minute load average ÷ {{ cores or '?' }} CPU cores (100% = run queue matches capacity). Raw 1m load: {{ '%.2f'|format(load1) if load1 is not none else '—' }}.">
|
|
<div style="{{ lbl }}">Load /core</div>
|
|
<div style="font-weight:600;">{{ '%d%%'|format(load_per_core) if load_per_core is not none else ('%.2f'|format(load1) if load1 is not none else '—') }}</div>
|
|
{{ sparks.load | safe }}</div>
|
|
</div>
|
|
{% if psi.cpu is not none or psi.mem is not none or psi.io is not none %}
|
|
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:0.9rem;"
|
|
title="Pressure Stall Information — % of the last 10s that tasks were stalled waiting for the resource. Hardware-independent, comparable across hosts.">
|
|
<span style="text-transform:uppercase;font-size:0.7rem;letter-spacing:0.04em;color:var(--text-dim);">Pressure 10s</span>
|
|
CPU {{ '%.0f%%'|format(psi.cpu) if psi.cpu is not none else '—' }}
|
|
· Mem {{ '%.0f%%'|format(psi.mem) if psi.mem is not none else '—' }}
|
|
· IO {{ '%.0f%%'|format(psi.io) if psi.io is not none else '—' }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
|
|
<a href="/plugins/host_agent/{{ host.id }}/" class="btn btn-sm btn-ghost">Full metrics →</a>
|
|
{% if session.user_role == 'admin' %}
|
|
{% if ansible_available and target %}
|
|
<form method="post" action="/plugins/host_agent/update" style="margin:0;">
|
|
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
|
<button type="submit" class="btn btn-sm" title="Refresh agent.py + restart (token preserved)">Update agent</button>
|
|
</form>
|
|
{% endif %}
|
|
<form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/rotate-token" style="margin:0;"
|
|
onsubmit="return confirm('Rotate token? The agent stops reporting until reinstalled/updated with the new token.');">
|
|
<button type="submit" class="btn btn-sm btn-ghost">Rotate token</button>
|
|
</form>
|
|
<form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/delete" style="margin:0;"
|
|
onsubmit="return confirm('Remove this agent registration? Metrics stop until re-registered.');">
|
|
<button type="submit" class="btn btn-sm btn-danger">Remove</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% if ansible_available and not target %}
|
|
<p style="color:var(--text-dim);font-size:0.8rem;margin:0.6rem 0 0;">
|
|
Link an Ansible target (above) to enable one-click agent updates.
|
|
</p>
|
|
{% endif %}
|
|
|
|
{% if session.user_role == 'admin' and ansible_available and target and managed_key_set %}
|
|
<details style="margin-top:0.7rem;">
|
|
<summary style="cursor:pointer;font-size:0.8rem;color:var(--text-muted);">Re-provision (reinstall the steward account + managed key, then the agent)</summary>
|
|
<p style="font-size:0.78rem;color:var(--text-dim);margin:0.5rem 0;">
|
|
Use after regenerating the managed key, or if SSH auth as <code>steward</code> breaks.
|
|
Connects over a one-time bootstrap user + password (not stored).
|
|
</p>
|
|
<form method="post" action="/plugins/host_agent/provision"
|
|
style="display:flex;gap:0.6rem;align-items:flex-end;flex-wrap:wrap;">
|
|
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
|
<div class="form-group" style="margin:0;">
|
|
<label>Bootstrap user</label>
|
|
<input type="text" name="bootstrap_user" required placeholder="root" style="width:8rem;" autocomplete="off">
|
|
</div>
|
|
<div class="form-group" style="margin:0;">
|
|
<label>Bootstrap password</label>
|
|
<input type="password" name="bootstrap_password" required style="width:10rem;" autocomplete="new-password">
|
|
</div>
|
|
<button type="submit" class="btn btn-sm">Re-provision</button>
|
|
</form>
|
|
</details>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
{# ── Not reporting: pending (token minted, no check-in) or never installed ── #}
|
|
{% if reg %}
|
|
<div style="background:color-mix(in srgb,var(--yellow) 10%,var(--bg-elevated));
|
|
border:1px solid color-mix(in srgb,var(--yellow) 30%,var(--border));
|
|
border-radius:6px;padding:0.7rem 0.9rem;margin-bottom:0.75rem;font-size:0.84rem;">
|
|
A token was minted for this host but <strong>no metrics have arrived yet</strong> — the
|
|
deploy may still be running, or it failed. Open the latest
|
|
<a href="/ansible/">Ansible run</a> to check, then retry below. Live metrics appear here
|
|
once the agent checks in.
|
|
</div>
|
|
{% else %}
|
|
<p style="color:var(--text-muted);font-size:0.88rem;margin-bottom:0.75rem;">
|
|
No agent installed. The agent reports CPU, memory, disk, network and more back to Steward.
|
|
</p>
|
|
{% endif %}
|
|
|
|
{% if session.user_role != 'admin' %}
|
|
<p style="color:var(--text-dim);font-size:0.85rem;">An admin can install the agent here.</p>
|
|
|
|
{% elif not ansible_available %}
|
|
<p style="color:var(--text-dim);font-size:0.85rem;">
|
|
Ansible isn't available, so the agent can't be deployed from here. Install it manually with the
|
|
<a href="/plugins/host_agent/fleet/">curl install command</a>.
|
|
</p>
|
|
|
|
{% elif not managed_key_set %}
|
|
<div style="background:color-mix(in srgb,var(--yellow) 12%,var(--bg-elevated));
|
|
border:1px solid color-mix(in srgb,var(--yellow) 35%,var(--border));
|
|
border-radius:6px;padding:0.7rem 0.9rem;display:flex;align-items:center;gap:0.8rem;flex-wrap:wrap;">
|
|
<span style="color:var(--yellow);">⚠</span>
|
|
<span style="font-size:0.84rem;flex:1;min-width:13rem;">No managed SSH key yet — Steward needs one to log into hosts.</span>
|
|
<form method="post" action="/settings/ansible/generate-key" style="margin:0;">
|
|
<input type="hidden" name="next" value="/hosts/{{ host.id }}">
|
|
<button type="submit" class="btn btn-sm">Generate managed key</button>
|
|
</form>
|
|
</div>
|
|
|
|
{% elif not target %}
|
|
<p style="color:var(--text-dim);font-size:0.85rem;">
|
|
Link or create an Ansible target for this host (in the Ansible section below) first — provisioning
|
|
needs an SSH connection.
|
|
</p>
|
|
|
|
{% else %}
|
|
<p style="font-size:0.82rem;color:var(--text-muted);margin-bottom:0.75rem;">
|
|
Deploys the agent to <code>{{ target.name }}</code> by running an Ansible playbook. Steward mints
|
|
the host's API token automatically; you'll watch the run live.
|
|
</p>
|
|
{# Provision: brand-new host (creates steward account + key over a one-time password) #}
|
|
<form method="post" action="/plugins/host_agent/provision"
|
|
style="display:flex;gap:0.6rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:0.75rem;">
|
|
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
|
<div style="font-size:0.78rem;color:var(--text-muted);width:100%;">Provision (first contact — fresh host):</div>
|
|
<div class="form-group" style="margin:0;">
|
|
<label>Bootstrap user</label>
|
|
<input type="text" name="bootstrap_user" required placeholder="root" style="width:8rem;" autocomplete="off">
|
|
</div>
|
|
<div class="form-group" style="margin:0;">
|
|
<label>Bootstrap password</label>
|
|
<input type="password" name="bootstrap_password" required style="width:10rem;" autocomplete="new-password">
|
|
</div>
|
|
<button type="submit" class="btn btn-sm">Provision</button>
|
|
</form>
|
|
{# Install: host already has the steward account (managed key works) #}
|
|
<form method="post" action="/plugins/host_agent/deploy" style="display:flex;gap:0.6rem;align-items:center;">
|
|
<input type="hidden" name="inventory_scope" value="{{ scope }}">
|
|
<span style="font-size:0.78rem;color:var(--text-muted);">Already provisioned?</span>
|
|
<button type="submit" class="btn btn-sm btn-ghost">Install agent (managed key)</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% if reg and session.user_role == 'admin' %}
|
|
<form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/delete" style="margin:0.75rem 0 0;"
|
|
onsubmit="return confirm('Clear this pending registration? Its token is discarded.');">
|
|
<button type="submit" class="btn btn-sm btn-ghost">Clear pending registration</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|