fix: retry, semaphore, reload lock, activation verify, bans_by_jail diagnostics

Stage 1.1-1.3: reload_all include/exclude_jails params already implemented;
  added keyword-arg assertions in router and service tests.

Stage 2.1/6.1: _send_command_sync retry loop (3 attempts, 150ms exp backoff)
  retrying on EAGAIN/ECONNREFUSED/ENOBUFS; immediate raise on all other errors.

Stage 2.2: asyncio.Lock at module level in jail_service.reload_all to
  serialize concurrent reload--all commands.

Stage 3.1: activate_jail re-queries _get_active_jail_names after reload;
  returns active=False with descriptive message if jail did not start.

Stage 4.1/6.2: asyncio.Semaphore (max 10) in Fail2BanClient.send, lazy-
  initialized; logs fail2ban_command_waiting_semaphore at debug when waiting.

Stage 5.1/5.2: unit tests asserting reload_all is called with include_jails
  and exclude_jails; activation verification happy/sad path tests.

Stage 6.3: TestSendCommandSyncRetry (5 cases) + TestFail2BanClientSemaphore
  concurrency test.

Stage 7.1-7.3: _since_unix uses time.time(); bans_by_jail debug logging with
  since_iso; diagnostic warning when total==0 despite table rows; unit test
  verifying the warning fires for stale data.
This commit is contained in:
2026-03-14 11:09:55 +01:00
parent 2274e20123
commit 2f2e5a7419
9 changed files with 880 additions and 115 deletions

View File

@@ -292,7 +292,7 @@ class TestJailControls:
with _patch_client(
{
"status": _make_global_status("sshd, nginx"),
"reload|--all|[]|[['start', 'sshd'], ['start', 'nginx']]": (0, "OK"),
"reload|--all|[]|[['start', 'nginx'], ['start', 'sshd']]": (0, "OK"),
}
):
await jail_service.reload_all(_SOCKET) # should not raise
@@ -307,6 +307,38 @@ class TestJailControls:
):
await jail_service.reload_all(_SOCKET) # should not raise
async def test_reload_all_include_jails(self) -> None:
"""reload_all with include_jails adds the new jail to the stream."""
with _patch_client(
{
"status": _make_global_status("sshd, nginx"),
"reload|--all|[]|[['start', 'apache-auth'], ['start', 'nginx'], ['start', 'sshd']]": (0, "OK"),
}
):
await jail_service.reload_all(_SOCKET, include_jails=["apache-auth"])
async def test_reload_all_exclude_jails(self) -> None:
"""reload_all with exclude_jails removes the jail from the stream."""
with _patch_client(
{
"status": _make_global_status("sshd, nginx"),
"reload|--all|[]|[['start', 'nginx']]": (0, "OK"),
}
):
await jail_service.reload_all(_SOCKET, exclude_jails=["sshd"])
async def test_reload_all_include_and_exclude(self) -> None:
"""reload_all with both include and exclude applies both correctly."""
with _patch_client(
{
"status": _make_global_status("old, nginx"),
"reload|--all|[]|[['start', 'new'], ['start', 'nginx']]": (0, "OK"),
}
):
await jail_service.reload_all(
_SOCKET, include_jails=["new"], exclude_jails=["old"]
)
async def test_start_not_found_raises(self) -> None:
"""start_jail raises JailNotFoundError for unknown jail."""
with _patch_client({"start|ghost": (1, Exception("Unknown jail: 'ghost'"))}), pytest.raises(JailNotFoundError):