7468806bad
Lightweight stdlib-only Python agent runs on each monitored host, collects CPU / memory / disk / load / uptime from /proc every 30s, and POSTs signed payloads to the Roundtable ingest endpoint. One-line curl-pipe installer creates a hardened systemd unit; admin UI manages host registrations with rotate / revoke. - agent.py: 370 LoC single-file daemon, ring buffer + exponential backoff - ingest route: bearer-token auth, metric expansion into plugin_metrics - install.sh.j2: systemd unit with NoNewPrivileges / ProtectSystem / ProtectHome - settings UI: add host / rotate token / delete registration (admin-only) - dashboard widgets: fleet-glance table + per-host history chart - stale-agent scheduler: 60s log warning for agents past 180s silence Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
79 lines
2.3 KiB
Python
79 lines
2.3 KiB
Python
# plugins/host_agent/scheduler.py
|
|
from __future__ import annotations
|
|
import logging
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Iterable
|
|
|
|
from sqlalchemy import select
|
|
|
|
from roundtable.core.scheduler import ScheduledTask
|
|
from roundtable.models.hosts import Host
|
|
from .models import HostAgentRegistration
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _filter_stale(
|
|
regs: Iterable,
|
|
*,
|
|
now: datetime,
|
|
stale_after_seconds: int,
|
|
) -> list:
|
|
"""Pure staleness filter: returns the subset with last_seen_at strictly
|
|
older than (now - stale_after_seconds). Rows with last_seen_at=None are
|
|
never stale (they are unregistered-in-practice)."""
|
|
cutoff = now - timedelta(seconds=stale_after_seconds)
|
|
return [
|
|
r for r in regs
|
|
if r.last_seen_at is not None and r.last_seen_at < cutoff
|
|
]
|
|
|
|
|
|
async def find_stale_registrations(app, stale_after_seconds: int = 180) -> list[dict]:
|
|
async with app.db_sessionmaker() as session:
|
|
all_regs = (await session.execute(
|
|
select(HostAgentRegistration)
|
|
)).scalars().all()
|
|
stale_rows = _filter_stale(
|
|
all_regs,
|
|
now=datetime.now(timezone.utc),
|
|
stale_after_seconds=stale_after_seconds,
|
|
)
|
|
if not stale_rows:
|
|
return []
|
|
hosts = {
|
|
h.id: h for h in (await session.execute(
|
|
select(Host).where(Host.id.in_([r.host_id for r in stale_rows]))
|
|
)).scalars().all()
|
|
}
|
|
return [
|
|
{
|
|
"host_id": r.host_id,
|
|
"host_name": hosts[r.host_id].name if r.host_id in hosts else "?",
|
|
"last_seen_at": r.last_seen_at,
|
|
}
|
|
for r in stale_rows
|
|
]
|
|
|
|
|
|
def make_task(app) -> ScheduledTask:
|
|
async def _check_stale():
|
|
try:
|
|
stale = await find_stale_registrations(app)
|
|
except Exception:
|
|
logger.exception("host_agent stale check failed")
|
|
return
|
|
if stale:
|
|
logger.info(
|
|
"host_agent: %d stale agent(s): %s",
|
|
len(stale),
|
|
[s["host_name"] for s in stale],
|
|
)
|
|
|
|
return ScheduledTask(
|
|
name="host_agent_stale_check",
|
|
coro_factory=_check_stale,
|
|
interval_seconds=60,
|
|
run_on_startup=False,
|
|
)
|