Fix restart/reload endpoint correctness and safety
- jail_service.restart(): replace invalid ["restart"] socket command with ["stop"], matching fail2ban transmitter protocol. The daemon is now stopped via socket; the caller starts it via subprocess. - config_file_service: expose _start_daemon and _wait_for_fail2ban as public start_daemon / wait_for_fail2ban functions. - restart_fail2ban router: orchestrate stop (socket) → start (subprocess) → probe (socket). Returns 204 on success, 503 when fail2ban does not come back within 10 s. Catches JailOperationError → 409. - reload_fail2ban router: add JailOperationError catch → 409 Conflict, consistent with other jail control endpoints. - Tests: add TestJailControls.test_restart_* (3 cases), TestReloadFail2ban 502/409 cases, TestRestartFail2ban (5 cases), TestRollbackJail (6 integration tests verifying file-write, subprocess invocation, socket- probe truthiness, active_jails count, and offline-at-call-time).
This commit is contained in:
@@ -21,6 +21,7 @@ from app.services.config_file_service import (
|
||||
activate_jail,
|
||||
deactivate_jail,
|
||||
list_inactive_jails,
|
||||
rollback_jail,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -3174,4 +3175,150 @@ class TestActivateJailRollback:
|
||||
assert "logpath" in result.message.lower() or "check that all logpath" in result.message.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# rollback_jail
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestRollbackJail:
|
||||
"""Integration tests for :func:`~app.services.config_file_service.rollback_jail`."""
|
||||
|
||||
async def test_local_file_written_enabled_false(self, tmp_path: Path) -> None:
|
||||
"""rollback_jail writes enabled=false to jail.d/{name}.local before any socket call."""
|
||||
(tmp_path / "jail.d").mkdir()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service.start_daemon",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
AsyncMock(return_value={"sshd"}),
|
||||
),
|
||||
):
|
||||
await rollback_jail(str(tmp_path), "/fake.sock", "sshd", ["fail2ban-client", "start"])
|
||||
|
||||
local = tmp_path / "jail.d" / "sshd.local"
|
||||
assert local.is_file(), "jail.d/sshd.local must be written"
|
||||
content = local.read_text()
|
||||
assert "enabled = false" in content
|
||||
|
||||
async def test_start_command_invoked_via_subprocess(self, tmp_path: Path) -> None:
|
||||
"""rollback_jail invokes the daemon start command via start_daemon, not via socket."""
|
||||
mock_start = AsyncMock(return_value=True)
|
||||
|
||||
with (
|
||||
patch("app.services.config_file_service.start_daemon", mock_start),
|
||||
patch(
|
||||
"app.services.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
AsyncMock(return_value={"other"}),
|
||||
),
|
||||
):
|
||||
await rollback_jail(
|
||||
str(tmp_path), "/fake.sock", "sshd", ["fail2ban-client", "start"]
|
||||
)
|
||||
|
||||
mock_start.assert_awaited_once_with(["fail2ban-client", "start"])
|
||||
|
||||
async def test_fail2ban_running_reflects_socket_probe_not_subprocess_exit(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""fail2ban_running in the response reflects the socket probe result.
|
||||
|
||||
Even when start_daemon returns True (subprocess exit 0), if the socket
|
||||
probe returns False the response must report fail2ban_running=False.
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service.start_daemon",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=False), # socket still unresponsive
|
||||
),
|
||||
):
|
||||
result = await rollback_jail(
|
||||
str(tmp_path), "/fake.sock", "sshd", ["fail2ban-client", "start"]
|
||||
)
|
||||
|
||||
assert result.fail2ban_running is False
|
||||
|
||||
async def test_active_jails_zero_when_fail2ban_not_running(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""active_jails is 0 in the response when fail2ban_running is False."""
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service.start_daemon",
|
||||
AsyncMock(return_value=False),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=False),
|
||||
),
|
||||
):
|
||||
result = await rollback_jail(
|
||||
str(tmp_path), "/fake.sock", "sshd", ["fail2ban-client", "start"]
|
||||
)
|
||||
|
||||
assert result.active_jails == 0
|
||||
|
||||
async def test_active_jails_count_from_socket_when_running(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""active_jails reflects the actual jail count from the socket when fail2ban is up."""
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service.start_daemon",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
AsyncMock(return_value={"sshd", "nginx", "apache-auth"}),
|
||||
),
|
||||
):
|
||||
result = await rollback_jail(
|
||||
str(tmp_path), "/fake.sock", "sshd", ["fail2ban-client", "start"]
|
||||
)
|
||||
|
||||
assert result.active_jails == 3
|
||||
|
||||
async def test_fail2ban_down_at_start_still_succeeds_file_write(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""rollback_jail writes the local file even when fail2ban is down at call time."""
|
||||
# fail2ban is down: start_daemon fails and wait_for_fail2ban returns False.
|
||||
with (
|
||||
patch(
|
||||
"app.services.config_file_service.start_daemon",
|
||||
AsyncMock(return_value=False),
|
||||
),
|
||||
patch(
|
||||
"app.services.config_file_service.wait_for_fail2ban",
|
||||
AsyncMock(return_value=False),
|
||||
),
|
||||
):
|
||||
result = await rollback_jail(
|
||||
str(tmp_path), "/fake.sock", "sshd", ["fail2ban-client", "start"]
|
||||
)
|
||||
|
||||
local = tmp_path / "jail.d" / "sshd.local"
|
||||
assert local.is_file(), "local file must be written even when fail2ban is down"
|
||||
assert result.disabled is True
|
||||
assert result.fail2ban_running is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user