GRM-17: fix grm list status — use docker inspect for docker mode, add host/user context

- Docker mode runners now check container status via docker inspect
  instead of systemctl is-active, avoiding false unknown when systemd
  service is missing or stderr output is discarded
- Binary mode still uses systemctl is-active with stderr suppressed
- Both modes now show a translated context message before the check so
  users know which host/user each BECOME password prompt belongs to
- Better ansible output filtering: strip CHANGED/FAILED/UNREACHABLE
  header lines and separator noise
- Map Docker container states (running/exited/dead) to systemd vocabulary
- All new user-facing messages fully translated (en/bg/de/ru/zh)
- 125 tests, 100% coverage, pyright clean, ruff clean
This commit is contained in:
Emil Simeonov
2026-06-19 02:35:53 +02:00
parent 51f204f90a
commit 12e6ce1601
3 changed files with 86 additions and 16 deletions
+7
View File
@@ -205,6 +205,13 @@ TRANSLATIONS: dict[str, dict[str, str]] = {
"ru": "Проверка состояния Gitea Runner {name} на {host}", "ru": "Проверка состояния Gitea Runner {name} на {host}",
"zh": "正在检查 {host} 上 Gitea Runner {name} 的状态", "zh": "正在检查 {host} 上 Gitea Runner {name} 的状态",
}, },
"Checking status of Gitea Runner {name} on {host} as {user} (sudo required)": {
"en": "Checking status of Gitea Runner {name} on {host} as {user} (sudo required)",
"bg": "Проверка на състоянието на Gitea Runner {name} на {host} като {user} (необходим е sudo)",
"de": "Prüfe Status von Gitea Runner {name} auf {host} als {user} (sudo erforderlich)",
"ru": "Проверка состояния Gitea Runner {name} на {host} как {user} (требуется sudo)",
"zh": "正在检查 {host} 上 Gitea Runner {name} 的状态(用户 {user},需要 sudo",
},
"Removing Gitea Runner {name} from {host}": { "Removing Gitea Runner {name} from {host}": {
"en": "Removing Gitea Runner {name} from {host}", "en": "Removing Gitea Runner {name} from {host}",
"bg": "Премахване на Gitea Runner {name} от {host}", "bg": "Премахване на Gitea Runner {name} от {host}",
+39 -13
View File
@@ -9,6 +9,7 @@ from .executor import AnsibleExecutor
from .i18n import _ from .i18n import _
from .registry import RunnerRegistry from .registry import RunnerRegistry
from .report import track_steps from .report import track_steps
from .ui import say
class RunnerManager: class RunnerManager:
@@ -280,23 +281,48 @@ class RunnerManager:
user = info["user"] user = info["user"]
key = info.get("key") key = info.get("key")
mode = info.get("mode", "docker") mode = info.get("mode", "docker")
try: say(
stdout = self._executor.run_ad_hoc( _(
host, "Checking status of Gitea Runner {name} on {host} as {user} (sudo required)",
user, name=name,
key, host=host,
"shell", user=user,
f"systemctl is-active gitea-runner@{name}",
become=True,
ask_become_pass=True,
check=False,
) )
# Filter out ansible noise (SSH warnings etc) )
lines = [ln for ln in stdout.splitlines() if ln.strip() and not ln.startswith(("[WARNING]", "ssh:"))] try:
if mode == "docker":
stdout = self._executor.run_ad_hoc(
host,
user,
key,
"shell",
f"docker inspect -f '{{{{.State.Status}}}}' gitea-runner-{name} 2>/dev/null",
become=True,
ask_become_pass=True,
check=False,
)
else:
stdout = self._executor.run_ad_hoc(
host,
user,
key,
"shell",
f"systemctl is-active gitea-runner@{name} 2>/dev/null",
become=True,
ask_become_pass=True,
check=False,
)
# Filter out ansible noise (headers, warnings, SSH messages)
ansible_noise = (" | CHANGED | ", " | FAILED | ", " | UNREACHABLE | ", "[WARNING]", "ssh:", ">>")
lines = [ln for ln in stdout.splitlines() if ln.strip() and not any(p in ln for p in ansible_noise)]
service_status = lines[-1].strip() if lines else "unknown" service_status = lines[-1].strip() if lines else "unknown"
# Docker statuses need mapping to systemd vocabulary
if mode == "docker":
docker_map = {"running": "active", "exited": "inactive", "dead": "failed"}
service_status = docker_map.get(service_status, "unknown")
except Exception: except Exception:
service_status = "unknown" service_status = "unknown"
# Translate known systemd status values # Translate known status values
translated_status = _( translated_status = _(
service_status if service_status in {"active", "inactive", "failed", "unknown"} else "unknown" service_status if service_status in {"active", "inactive", "failed", "unknown"} else "unknown"
) )
+40 -3
View File
@@ -392,14 +392,14 @@ class TestRunnerManager:
mock_executor.run.assert_not_called() mock_executor.run.assert_not_called()
mock_registry.remove.assert_called_once_with("r1") mock_registry.remove.assert_called_once_with("r1")
def test_list_runners(self) -> None: def test_list_runners_docker(self) -> None:
mock_registry = MagicMock() mock_registry = MagicMock()
mock_registry.list.return_value = { mock_registry.list.return_value = {
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "docker"}, "r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "docker"},
} }
manager = RunnerManager(registry=mock_registry) manager = RunnerManager(registry=mock_registry)
mock_executor = MagicMock() mock_executor = MagicMock()
mock_executor.run_ad_hoc.return_value = "active" mock_executor.run_ad_hoc.return_value = "running"
manager._executor = mock_executor manager._executor = mock_executor
runners = manager.list_runners() runners = manager.list_runners()
@@ -412,7 +412,31 @@ class TestRunnerManager:
"ubuntu", "ubuntu",
"/key", "/key",
"shell", "shell",
"systemctl is-active gitea-runner@r1", "docker inspect -f '{{.State.Status}}' gitea-runner-r1 2>/dev/null",
become=True,
ask_become_pass=True,
check=False,
)
def test_list_runners_binary(self) -> None:
mock_registry = MagicMock()
mock_registry.list.return_value = {
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "binary"},
}
manager = RunnerManager(registry=mock_registry)
mock_executor = MagicMock()
mock_executor.run_ad_hoc.return_value = "active"
manager._executor = mock_executor
runners = manager.list_runners()
assert len(runners) == 1
assert runners[0]["status"] == "active"
mock_executor.run_ad_hoc.assert_called_once_with(
"10.0.0.1",
"ubuntu",
"/key",
"shell",
"systemctl is-active gitea-runner@r1 2>/dev/null",
become=True, become=True,
ask_become_pass=True, ask_become_pass=True,
check=False, check=False,
@@ -424,6 +448,19 @@ class TestRunnerManager:
manager = RunnerManager(registry=mock_registry) manager = RunnerManager(registry=mock_registry)
assert manager.list_runners() == [] assert manager.list_runners() == []
def test_list_runners_docker_exited(self) -> None:
mock_registry = MagicMock()
mock_registry.list.return_value = {
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
}
manager = RunnerManager(registry=mock_registry)
mock_executor = MagicMock()
mock_executor.run_ad_hoc.return_value = "exited"
manager._executor = mock_executor
runners = manager.list_runners()
assert runners[0]["status"] == "inactive"
def test_list_runners_unknown_status(self) -> None: def test_list_runners_unknown_status(self) -> None:
mock_registry = MagicMock() mock_registry = MagicMock()
mock_registry.list.return_value = { mock_registry.list.return_value = {