#5767 "Steward hands a host a maintenance job and follows it to the end, through its own restarts", milestone 514. A host_maintenance_jobs row per job: queued → updating → [draining →] rebooting → [undraining →] done, or failed / timed_out. Steward's answer to an ingest post carries the step that host's agent should carry out now; the agent (1.18.0) leaves each step id for the root helper once, and the helper refuses an id it already ran, so repeats are harmless. Results come back as the #5766 maintenance events, matched by step id; the reboot completes when the host's `started` event has a boot_at after the reboot was handed out. The job reboots only when the update says one is needed (or never, if asked). A swarm node is drained first by itself if it's a manager or by a manager with the helper, and made active after; with no manager to drain it the job is refused. A minute tick times out a job past its state's deadline. A maintenance window over host_agent/<host> lasts the rest of the job. Everything is on the row, so a Steward restart, or the reboot of the host Steward runs on, resumes from the next post. Ingest runs it in its own savepoint so a job going wrong never costs the samples. The Agent panel leads with a running job and says what a finished one did; Manage agent has Update now (with "never reboot"), or says why a job can't start. Integration test: hand off, Steward restarts, the post-boot events close the job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
3.8 KiB
Python
82 lines
3.8 KiB
Python
# plugins/host_agent/__init__.py
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from quart import Quart
|
|
|
|
_app: "Quart | None" = None
|
|
|
|
|
|
def setup(app: "Quart") -> None:
|
|
global _app
|
|
_app = app
|
|
# A host's CPU, memory and load per core learn their normal (#5742), each
|
|
# with its floor from Settings → Thresholds. Parts don't: a host's CPU has
|
|
# a normal, each of its cores doesn't. Disks stay on their fixed cutoffs:
|
|
# a nearly full disk is a failure whatever its history (System 182).
|
|
from steward.core.norms import floor_setting, learn_normal
|
|
learn_normal("host_agent", "cpu_pct", floor_setting(app, "host_cpu_pct"), parts=False)
|
|
learn_normal("host_agent", "mem_used_pct", floor_setting(app, "host_mem_pct"), parts=False)
|
|
learn_normal("host_agent", "load_pct", floor_setting(app, "host_load_pct"), parts=False)
|
|
# "Agent offline" speaks for everything the agent carries: while it is
|
|
# firing, the host's other rules going silent isn't emailed (#5721).
|
|
from steward.core.alerts import agent_fed, agent_heartbeat
|
|
from steward.hosts.service import agent_stale_after_seconds
|
|
agent_heartbeat("host_agent", "seconds_since_seen", lambda: agent_stale_after_seconds(app.config))
|
|
agent_fed("host_agent", lambda resource: resource.split(":", 1)[0])
|
|
# A reboot the agent announced is silence with a reason, for a while (#5756).
|
|
from datetime import datetime, timezone
|
|
from steward.core.alerts import agents_away
|
|
from .downtime import hosts_away, restart_grace_seconds
|
|
agents_away(lambda session: hosts_away(session, datetime.now(timezone.utc),
|
|
restart_grace_seconds(app.config)))
|
|
from steward.core.cleanup import prune_by_age
|
|
from .models import HostDowntime
|
|
prune_by_age(HostDowntime.started_at)
|
|
|
|
|
|
def get_scheduled_tasks() -> list:
|
|
from .scheduler import make_maintenance_task, make_task
|
|
return [make_task(_app), make_maintenance_task(_app)]
|
|
|
|
|
|
def get_blueprint():
|
|
from .routes import host_agent_bp
|
|
return host_agent_bp
|
|
|
|
|
|
def register_mcp_tools(register) -> None:
|
|
from .mcp_tools import register_tools
|
|
register_tools(register)
|
|
|
|
|
|
async def mcp_overview(db) -> list[dict]:
|
|
from .mcp_tools import mcp_overview as findings
|
|
return await findings(db)
|
|
|
|
|
|
def get_alert_metrics() -> dict[str, list[str]]:
|
|
"""Host metrics ingest records for alert rules (resource = the host name;
|
|
disk_used_pct per mount, resource "<host>:<mount>"). seconds_since_seen
|
|
comes from the scheduler, not the agent: "agent offline" is > 180.
|
|
clock_off_secs is how far the host's clock is from Steward's, either way
|
|
(agent ≥ 1.12.0): "clock drifting" is > 30. load_pct is load_1m as a %
|
|
of the host's cores. inodes_used_pct is per mount (agent ≥ 1.13.0), and
|
|
disk_used_pct_worst is the fullest mount by space or inodes.
|
|
mounts_stalled counts the mounts that didn't answer the agent in time
|
|
(agent ≥ 1.14.0): "an NFS share stopped" is > 0. agent_rss_bytes and
|
|
agent_cpu_pct are the agent's own memory and CPU (agent ≥ 1.15.0): a
|
|
healthy agent sits in tens of MB and a few %. updates_pending and
|
|
updates_security are what the package manager has waiting, and
|
|
reboot_required is 1 when the host needs a reboot (agent ≥ 1.16.0):
|
|
"security updates waiting" is updates_security > 0."""
|
|
return {"host_agent": [
|
|
"cpu_pct", "mem_used_pct", "mem_available_bytes", "swap_used_bytes",
|
|
"disk_used_pct_worst", "disk_used_pct", "inodes_used_pct", "load_1m", "load_5m", "load_15m",
|
|
"load_pct",
|
|
"uptime_secs", "seconds_since_seen", "clock_off_secs", "mounts_stalled",
|
|
"agent_rss_bytes", "agent_cpu_pct",
|
|
"updates_pending", "updates_security", "reboot_required",
|
|
]}
|