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:
@@ -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}",
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user