#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>
88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
"""Pure-function tests for install.sh.j2 rendering and agent.py parsing."""
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
from plugins.host_agent.routes import _agent_version, AGENT_SOURCE_PATH
|
|
|
|
|
|
TEMPLATE_DIR = Path(__file__).resolve().parents[3] / "plugins" / "host_agent" / "templates"
|
|
|
|
|
|
def _render(**overrides) -> str:
|
|
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
|
defaults = dict(
|
|
url="https://r.example",
|
|
token="tok-abc",
|
|
agent_version="1.0.0",
|
|
host_name="testhost",
|
|
host_address="10.0.0.1",
|
|
)
|
|
defaults.update(overrides)
|
|
return env.get_template("install.sh.j2").render(**defaults)
|
|
|
|
|
|
def test_render_contains_token_and_url():
|
|
out = _render()
|
|
assert "tok-abc" in out
|
|
assert "https://r.example" in out
|
|
assert "testhost" in out
|
|
|
|
|
|
UNIT_DIR = TEMPLATE_DIR.parent / "files"
|
|
|
|
|
|
def test_the_agent_unit_is_hardened_and_may_write_only_its_requests():
|
|
unit = (UNIT_DIR / "steward-agent.service").read_text()
|
|
for needle in (
|
|
"User=steward-agent",
|
|
"NoNewPrivileges=true",
|
|
"ProtectSystem=strict",
|
|
"ProtectHome=true",
|
|
"PrivateTmp=true",
|
|
"ReadOnlyPaths=/proc /sys",
|
|
):
|
|
assert needle in unit, f"missing {needle}"
|
|
writable = [ln for ln in unit.splitlines() if ln.startswith("ReadWritePaths=")]
|
|
assert writable == ["ReadWritePaths=-/var/lib/steward-agent/requests"]
|
|
|
|
|
|
def test_the_script_installs_the_served_units_not_its_own_copy():
|
|
out = _render()
|
|
assert "[Service]" not in out # no inline unit left to drift
|
|
assert 'UNITS="steward-agent.service steward-maint.service steward-maint.path"' in out
|
|
assert '${FILES_URL}/$u' in out and '${FILES_URL}/maint.py' in out
|
|
assert "systemctl enable --now steward-maint.path" in out
|
|
assert "systemctl restart steward-agent.service" in out
|
|
|
|
|
|
def test_render_has_uninstall_branch():
|
|
out = _render()
|
|
assert "--uninstall" in out
|
|
assert "systemctl disable --now steward-maint.path steward-agent.service" in out
|
|
|
|
|
|
def test_render_uses_agent_py_url():
|
|
out = _render()
|
|
assert "/plugins/host_agent/agent.py" in out
|
|
|
|
|
|
def test_rendered_script_passes_sh_n(tmp_path):
|
|
out = _render()
|
|
script = tmp_path / "install.sh"
|
|
script.write_text(out)
|
|
result = subprocess.run(
|
|
["sh", "-n", str(script)], capture_output=True, text=True
|
|
)
|
|
assert result.returncode == 0, f"sh -n failed: {result.stderr}"
|
|
|
|
|
|
def test_agent_version_parses_on_disk_agent():
|
|
# Version-agnostic: the parsed value must match the agent module's constant,
|
|
# so a future AGENT_VERSION bump doesn't require touching this test.
|
|
from plugins.host_agent import agent
|
|
assert AGENT_SOURCE_PATH.exists()
|
|
v = _agent_version()
|
|
assert v == agent.AGENT_VERSION
|