# tests/plugins/host_agent/test_maint_helper.py """#5766: the root helper the agent can start, and the agent's side of it. The helper runs as root and reads files an unprivileged agent wrote, so what it refuses matters as much as what it does: a link, a big file, an action not on its list. The agent leaves requests and reports results; Steward serves the helper and the units from one copy. """ import importlib.util import json import os from pathlib import Path import pytest from plugins.host_agent import agent as a FILES = Path(__file__).resolve().parents[3] / "plugins" / "host_agent" / "files" @pytest.fixture def maint(): spec = importlib.util.spec_from_file_location("steward_maint", FILES / "maint.py") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def _write(path, req): path.write_text(json.dumps(req)) return str(path) def test_the_helper_reads_a_request_naming_one_of_its_actions(maint, tmp_path): for action in ("update", "reboot", "drain", "undrain"): assert maint.read_request(_write(tmp_path / "r.json", {"id": "job-1", "action": action})) node = "abcdefghijklmnopqrstuvwxy" assert maint.read_request(_write(tmp_path / "n.json", {"id": "job-2", "action": "drain", "node": node}))["node"] == node @pytest.mark.parametrize("req", [ {"id": "job-1", "action": "shell"}, # not on the list {"id": "job-1", "action": "update; rm -rf /"}, {"id": "../../etc/x", "action": "update"}, # an id that walks paths {"id": "", "action": "update"}, {"id": "job-1", "action": "drain", "node": "--force"}, # a flag, not a node id ["update"], ]) def test_the_helper_refuses_anything_else(maint, tmp_path, req): assert maint.read_request(_write(tmp_path / "r.json", req)) is None def test_the_helper_never_follows_a_link_the_agent_planted(maint, tmp_path): target = tmp_path / "secret.json" _write(target, {"id": "job-1", "action": "update"}) link = tmp_path / "r.json" link.symlink_to(target) assert maint.read_request(str(link)) is None def test_the_helper_refuses_a_big_file_and_a_non_file(maint, tmp_path): big = tmp_path / "big.json" big.write_text(json.dumps({"id": "job-1", "action": "update", "pad": "x" * 5000})) assert maint.read_request(str(big)) is None (tmp_path / "dir.json").mkdir() assert maint.read_request(str(tmp_path / "dir.json")) is None assert maint.read_request(str(tmp_path / "missing.json")) is None def test_the_helper_counts_what_an_upgrade_changed(maint): assert maint.upgraded_count("12 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.") == 14 dnf = "Transaction Summary\n====\nInstall 1 Package\nUpgrade 23 Packages\n" assert maint.upgraded_count(dnf) == 24 assert maint.upgraded_count("nothing to do") is None def test_a_request_is_deleted_whatever_it_held_and_a_repeat_is_not_run_twice(maint, tmp_path, monkeypatch): requests, results = tmp_path / "req", tmp_path / "res" requests.mkdir() monkeypatch.setattr(maint, "REQUESTS", str(requests)) monkeypatch.setattr(maint, "RESULTS", str(results)) ran = [] monkeypatch.setattr(maint, "do_node", lambda avail, node: ran.append(avail) or {"ok": True}) _write(requests / "bad.json", {"id": "x", "action": "shell"}) _write(requests / "job-1.json", {"id": "job-1", "action": "drain"}) maint.main() assert list(requests.iterdir()) == [] assert json.loads((results / "job-1.json").read_text())["state"] == "done" _write(requests / "job-1.json", {"id": "job-1", "action": "drain"}) maint.main() assert ran == ["drain"] def test_a_reboot_says_so_before_it_goes(maint, tmp_path, monkeypatch): monkeypatch.setattr(maint, "RESULTS", str(tmp_path)) called = [] monkeypatch.setattr(maint, "run", lambda cmd, deadline=0, env=None: called.append(cmd) or (True, "")) monkeypatch.setattr(maint.os, "sync", lambda: None) out = maint.handle({"id": "job-9", "action": "reboot"}) assert out["state"] == "rebooting" assert json.loads((tmp_path / "job-9.json").read_text())["state"] == "rebooting" assert called == [["systemctl", "reboot"]] # ── the agent's side ───────────────────────────────────────────────────────── def test_the_agent_leaves_a_whole_request_the_helper_accepts(maint, tmp_path): a.request_maintenance("job-3", "drain", node="abcdefghijklmnopqrstuvwxy", requests_dir=str(tmp_path)) assert sorted(os.listdir(tmp_path)) == ["job-3.json"] # no temp file left behind assert maint.read_request(str(tmp_path / "job-3.json"))["action"] == "drain" def test_the_agent_refuses_to_ask_for_anything_else(tmp_path): with pytest.raises(ValueError): a.request_maintenance("job-3", "shell", requests_dir=str(tmp_path)) with pytest.raises(ValueError): a.request_maintenance("../x", "update", requests_dir=str(tmp_path)) assert os.listdir(tmp_path) == [] def test_each_result_state_is_reported_once(tmp_path): res = tmp_path / "job-4.json" res.write_text(json.dumps({"id": "job-4", "action": "update", "state": "running"})) seen: set = set() events, keys = a.maintenance_results(seen, results_dir=str(tmp_path)) assert [e["state"] for e in events] == ["running"] and events[0]["kind"] == "maintenance" assert a.maintenance_results(seen, results_dir=str(tmp_path))[0] # not posted yet: said again seen.update(keys) assert a.maintenance_results(seen, results_dir=str(tmp_path)) == ([], []) res.write_text(json.dumps({"id": "job-4", "action": "update", "state": "done", "ok": True, "packages": 12, "reboot_required": True})) events, _ = a.maintenance_results(seen, results_dir=str(tmp_path)) assert events == [{"kind": "maintenance", "id": "job-4", "action": "update", "state": "done", "ok": True, "packages": 12, "reboot_required": True}] def test_an_old_or_broken_result_is_not_reported(tmp_path): old = tmp_path / "job-5.json" old.write_text(json.dumps({"id": "job-5", "action": "update", "state": "done"})) week_ago = os.stat(old).st_mtime - 7 * 86400 os.utime(old, (week_ago, week_ago)) (tmp_path / "job-6.json").write_text("{not json") (tmp_path / "job-7.json").write_text(json.dumps({"id": "../x", "state": "done"})) assert a.maintenance_results(set(), results_dir=str(tmp_path)) == ([], []) assert a.maintenance_results(set(), results_dir=str(tmp_path / "missing")) == ([], []) def test_the_agent_says_whether_the_helper_is_installed(tmp_path): unit, requests = tmp_path / "steward-maint.path", tmp_path / "requests" assert a.maint_helper_installed(str(unit), str(requests)) is False unit.write_text("[Path]\n") requests.mkdir() assert a.maint_helper_installed(str(unit), str(requests)) is True # ── what Steward serves ────────────────────────────────────────────────────── def test_the_helper_and_the_agent_agree_on_paths_and_actions(maint): assert a.MAINT_REQUESTS == maint.REQUESTS and a.MAINT_RESULTS == maint.RESULTS assert a.MAINT_ACTIONS == maint.ACTIONS path_unit = (FILES / "steward-maint.path").read_text() assert f"PathExistsGlob={maint.REQUESTS}/*.json" in path_unit async def test_steward_serves_only_the_listed_files(app): from plugins.host_agent.routes import AGENT_FILES, agent_file assert sorted(AGENT_FILES) == sorted(p.name for p in FILES.iterdir() if p.is_file()) async with app.test_request_context("/plugins/host_agent/files/maint.py"): ok = await agent_file("maint.py") assert ok.status_code == 200 assert "ACTIONS = (" in await ok.get_data(as_text=True) for name in ("../agent.py", "agent.py", "routes.py", "../../steward/config.py"): _, status = await agent_file(name) assert status == 404, name