diff --git a/orchestrator/orchestrator/runner.py b/orchestrator/orchestrator/runner.py index 42ca802..b4b0857 100644 --- a/orchestrator/orchestrator/runner.py +++ b/orchestrator/orchestrator/runner.py @@ -204,7 +204,9 @@ def _job_cmd(host: str, job: dict, cfg: "Config") -> list[str]: matching lightweight CLI subcommand — a single TC quarantineWorker call, using this runner's existing TC creds. "tart-host-update" runs the tart VM host maintenance flow (orchestrator/tart_host.py); "builder-upgrade" the Intel builder macOS 15 upgrade - (orchestrator/builder_upgrade.py). Anything else is the full reprovision workflow. + (orchestrator/builder_upgrade.py). "reprovision" (or no action: old rows) is the full EACS + workflow. Anything else raises ValueError: a newer Hangar can queue an action this runner + predates, and falling through to `run` would turn it into a wipe. """ exe = _reprovision_exe() action = job.get("action") or "reprovision" @@ -234,6 +236,8 @@ def _job_cmd(host: str, job: dict, cfg: "Config") -> list[str]: if params.get("puppet_only"): cmd.append("--puppet-only") return cmd + if action != "reprovision": + raise ValueError(f"unknown action {action!r} -- this runner predates it; update relops-bootstrap") cmd = [exe, "run", host] if cfg.unquarantine: cmd.append("--unquarantine") @@ -250,7 +254,11 @@ def _run_job(client: httpx.Client, cfg: Config, job: dict) -> None: except ValueError as e: _complete(client, cfg, job_id, False, f"rejected: {e}") return - cmd = _job_cmd(host, job, cfg) + try: + cmd = _job_cmd(host, job, cfg) + except ValueError as e: + _complete(client, cfg, job_id, False, f"rejected: {e}") + return _event(client, cfg, job_id, f"runner {cfg.runner_id} starting: {' '.join(cmd[1:])}") try: proc = subprocess.Popen( diff --git a/orchestrator/tests/test_runner.py b/orchestrator/tests/test_runner.py index 82219fa..2a910a1 100644 --- a/orchestrator/tests/test_runner.py +++ b/orchestrator/tests/test_runner.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch import httpx +import pytest from orchestrator import runner @@ -95,6 +96,27 @@ def test_job_cmd_unquarantine(): assert runner._job_cmd("macmini-m4-80", job, _Cfg()) == ["reprovision", "unquarantine", "macmini-m4-80"] +def test_job_cmd_explicit_reprovision_action_runs_the_workflow(): + job = {"action": "reprovision", "params": {}} + assert runner._job_cmd("macmini-m4-80", job, _Cfg())[1:] == ["run", "macmini-m4-80"] + + +def test_job_cmd_unknown_action_is_rejected_not_run(): + # A Hangar newer than this runner can queue an action it doesn't know. Falling through + # to `run` would turn that into an EACS wipe. + with pytest.raises(ValueError, match="unknown action"): + runner._job_cmd("macmini-r8-124", {"action": "some-future-action", "params": {}}, _Cfg()) + + +def test_run_job_fails_unknown_action_without_starting_anything(): + client = MagicMock() + with patch("orchestrator.runner.subprocess.Popen") as popen: + runner._run_job(client, _Cfg(), {"id": 11, "short": "macmini-r8-124", "action": "some-future-action"}) + popen.assert_not_called() + body = client.post.call_args.kwargs["json"] + assert body["success"] is False and "unknown action" in body["detail"] + + def test_complete_retries_until_success(): client = MagicMock() ok = MagicMock()