feat(host_agent): a root helper the agent can start, for updates, reboots and drains only (#5766)
#5766 "A root helper the agent can start, for a fixed set of maintenance actions and nothing else", milestone 514. The agent runs unprivileged under NoNewPrivileges, so it can't update or reboot its host. It now leaves a request in /var/lib/steward-agent/requests, the one path its unit lets it write; steward-maint.path starts maint.py as root, which reads it without following links, accepts only update, reboot, drain or undrain, deletes it, and writes the outcome to /var/lib/steward-maint/results. The agent (1.17.0) reports each new result state once as a `maintenance` event, re-checks pending updates when an update finishes, and says in its metadata whether the helper is installed. The units and the helper are served from one copy at /plugins/host_agent/files/<name> (a fixed list), and install.sh plus the install, provision and update playbooks all fetch them, replacing three inline copies of the unit. Update agent installs the helper on existing hosts; the Agent panel says under Manage agent whether it is there. Steward handing out jobs is #5767; the cadence is #5768. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -475,6 +475,10 @@ async def ingest():
|
||||
if field in md and getattr(reg, field) != md[field]:
|
||||
setattr(reg, field, md[field])
|
||||
changed = True
|
||||
# Whether the maintenance helper is installed (agent ≥ 1.17.0, #5766).
|
||||
if isinstance(md.get("maint_helper"), bool) and reg.maint_helper != md["maint_helper"]:
|
||||
reg.maint_helper = md["maint_helper"]
|
||||
changed = True
|
||||
host_ip = md.get("host_ip")
|
||||
if host_ip and reg.host_ip != host_ip:
|
||||
reg.host_ip = host_ip
|
||||
@@ -631,6 +635,29 @@ async def agent_source():
|
||||
return Response(body, mimetype="text/x-python")
|
||||
|
||||
|
||||
# What a host installs besides agent.py: the units and the maintenance helper
|
||||
# (#5766), served from one copy so install.sh and every playbook agree.
|
||||
AGENT_FILES_DIR = Path(__file__).parent / "files"
|
||||
AGENT_FILES = {
|
||||
"maint.py": "text/x-python",
|
||||
"steward-agent.service": "text/plain",
|
||||
"steward-maint.service": "text/plain",
|
||||
"steward-maint.path": "text/plain",
|
||||
}
|
||||
|
||||
|
||||
@host_agent_bp.get("/files/<name>")
|
||||
@public_route("fetched by the installer and the agent playbooks; holds no secrets")
|
||||
async def agent_file(name: str):
|
||||
if name not in AGENT_FILES:
|
||||
return _error(404, "not_found")
|
||||
try:
|
||||
body = (AGENT_FILES_DIR / name).read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return _error(500, "agent_missing")
|
||||
return Response(body, mimetype=AGENT_FILES[name])
|
||||
|
||||
|
||||
_FLEET_ORDER = {"stale": 0, "pending": 1, "up": 2}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user