fix(host_agent): gate "deployed" on agent check-in; fail no-op runs
A failed provision looked successful in two ways: 1. The host panel showed the agent as deployed (metrics + Update/Rotate/Remove) because provision/deploy mint the registration row BEFORE the playbook runs and the panel keyed "installed" on that row. Now gated on the agent actually checking in (reg.last_seen_at). Three states: reporting (metrics + lifecycle), pending (token minted but no check-in → "no metrics yet, deploy may be running/failed" banner + retry + Clear pending registration), and none (install path). 2. The run reported success though nothing ran — ansible-playbook exits 0 on "no hosts matched"/empty inventory. The executor now treats an empty PLAY RECAP (returncode 0 but no hosts executed) as failed, with a clear failure note. Non-zero exits and recap failed/unreachable were already caught. Scribe issue #887. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -548,13 +548,16 @@ async def host_panel(host_id: str):
|
||||
|
||||
hostlvl = latest.get(host.name, {})
|
||||
ls = reg.last_seen_at if reg else None
|
||||
stale = bool(reg) and (
|
||||
ls is None or (datetime.now(timezone.utc) - ls).total_seconds() > _stale_after_seconds())
|
||||
# "Deployed" is gated on the agent actually checking in (last_seen_at), NOT on
|
||||
# the registration row — that row is minted before the playbook runs, so a
|
||||
# failed provision would otherwise look deployed.
|
||||
reporting = bool(reg) and ls is not None
|
||||
stale = reporting and (datetime.now(timezone.utc) - ls).total_seconds() > _stale_after_seconds()
|
||||
|
||||
ansible_cfg = current_app.config.get("ANSIBLE", {})
|
||||
return await render_template(
|
||||
"panel.html",
|
||||
host=host, reg=reg, hostlvl=hostlvl, stale=stale, target=target,
|
||||
host=host, reg=reg, hostlvl=hostlvl, reporting=reporting, stale=stale, target=target,
|
||||
ansible_available=has_capability("ansible.run_playbook"),
|
||||
managed_key_set=bool((ansible_cfg.get("ssh_public_key") or "").strip()),
|
||||
)
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
<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 reg %}
|
||||
{% 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 %}
|
||||
{% if reg.last_seen_at %} · last seen {{ reg.last_seen_at.strftime('%Y-%m-%d %H:%M') }}{% 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 reg %}
|
||||
{# ── Installed: at-a-glance metrics + lifecycle ── #}
|
||||
{% if reporting %}
|
||||
{# ── Reporting: at-a-glance metrics + lifecycle ── #}
|
||||
<div style="display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:0.9rem;">
|
||||
{% set cpu = hostlvl.get('cpu_pct') %}
|
||||
{% set mem = hostlvl.get('mem_used_pct') %}
|
||||
@@ -54,10 +56,21 @@
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{# ── Not installed: provisioning path ── #}
|
||||
{# ── 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>
|
||||
@@ -113,5 +126,12 @@
|
||||
<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>
|
||||
|
||||
@@ -442,10 +442,21 @@ async def start_run(
|
||||
await _flush()
|
||||
|
||||
await proc.wait()
|
||||
recap = parse_recap(_run_lines.get(run_id, []))
|
||||
if run_id in _cancelled:
|
||||
final_status = "cancelled"
|
||||
elif proc.returncode != 0:
|
||||
final_status = "failed"
|
||||
elif not recap:
|
||||
# ansible-playbook exits 0 on "no hosts matched" / empty inventory
|
||||
# — nothing actually ran, so don't report it as a success.
|
||||
final_status = "failed"
|
||||
if len(failures) < _FAILURE_CAP:
|
||||
failures.append(
|
||||
"No hosts matched — nothing executed "
|
||||
"(check the host's Ansible target and inventory).")
|
||||
else:
|
||||
final_status = "success" if proc.returncode == 0 else "failed"
|
||||
final_status = "success"
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user