This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Roundtable-plugins/host_agent/scheduler.py
T
bvandeusen 7468806bad feat: add host_agent plugin — push-model host resource monitoring
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>
2026-04-15 01:04:51 -04:00

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,
)