refactor: Extract fail2ban response utilities into shared module
Consolidate duplicate _ok(), _to_dict(), ensure_list(), and is_not_found_error() functions from 6 service modules into a single canonical implementation at backend/app/utils/fail2ban_response.py. Changes: - Create fail2ban_response.py with canonical implementations - Remove local duplicates from: ban_service, jail_service, config_service, health_service, server_service, config_file_utils - Update all imports to use shared module - Add comprehensive docstrings and examples - Update Architecture.md and Backend-Development.md documentation Benefits: - Single source of truth for response parsing logic - Eliminates code duplication across service layer - Improves maintainability and consistency - Enables centralized bug fixes and improvements Tests: All 228 service tests passing, no regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -23,7 +23,10 @@ from app.utils.fail2ban_client import (
|
||||
Fail2BanCommand,
|
||||
Fail2BanConnectionError,
|
||||
Fail2BanProtocolError,
|
||||
Fail2BanResponse,
|
||||
)
|
||||
from app.utils.fail2ban_response import (
|
||||
ok,
|
||||
to_dict,
|
||||
)
|
||||
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
@@ -35,59 +38,6 @@ log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
_SOCKET_TIMEOUT: float = 5.0
|
||||
|
||||
|
||||
def _ok(response: object) -> object:
|
||||
"""Extract the payload from a fail2ban ``(return_code, data)`` response.
|
||||
|
||||
fail2ban wraps every response in a ``(0, data)`` success tuple or
|
||||
a ``(1, exception)`` error tuple. This helper returns ``data`` for
|
||||
successful responses or raises :class:`ValueError` for error responses.
|
||||
|
||||
Args:
|
||||
response: Raw value returned by :meth:`~Fail2BanClient.send`.
|
||||
|
||||
Returns:
|
||||
The payload ``data`` portion of the response.
|
||||
|
||||
Raises:
|
||||
ValueError: If the response indicates an error (return code ≠ 0).
|
||||
"""
|
||||
try:
|
||||
code, data = cast("Fail2BanResponse", response)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(
|
||||
f"Unexpected fail2ban response shape: {response!r}"
|
||||
) from exc
|
||||
|
||||
if code != 0:
|
||||
raise ValueError(f"fail2ban returned error code {code}: {data!r}")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _to_dict(pairs: object) -> dict[str, object]:
|
||||
"""Convert a list of ``(key, value)`` pairs to a plain dict.
|
||||
|
||||
fail2ban returns structured data as lists of 2-tuples rather than dicts.
|
||||
This helper converts them safely, ignoring non-pair items.
|
||||
|
||||
Args:
|
||||
pairs: A list of ``(key, value)`` pairs (or any iterable thereof).
|
||||
|
||||
Returns:
|
||||
A :class:`dict` with the keys and values from *pairs*.
|
||||
"""
|
||||
if not isinstance(pairs, (list, tuple)):
|
||||
return {}
|
||||
result: dict[str, object] = {}
|
||||
for item in pairs:
|
||||
try:
|
||||
k, v = item
|
||||
result[str(k)] = v
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -98,7 +48,7 @@ async def _safe_get(
|
||||
) -> object | None:
|
||||
"""Send a command and return *default* if it fails."""
|
||||
try:
|
||||
return _ok(await client.send(command))
|
||||
return ok(await client.send(command))
|
||||
except (
|
||||
Fail2BanConnectionError,
|
||||
Fail2BanProtocolError,
|
||||
@@ -202,7 +152,7 @@ async def probe(
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. Connectivity check #
|
||||
# ------------------------------------------------------------------ #
|
||||
ping_data = _ok(await client.send(["ping"]))
|
||||
ping_data = ok(await client.send(["ping"]))
|
||||
if ping_data != "pong":
|
||||
log.warning(
|
||||
"fail2ban_unexpected_ping_response",
|
||||
@@ -214,14 +164,14 @@ async def probe(
|
||||
# 2. Version
|
||||
# ------------------------------------------------------------------ #
|
||||
try:
|
||||
version: str | None = str(_ok(await client.send(["version"])))
|
||||
version: str | None = str(ok(await client.send(["version"])))
|
||||
except (ValueError, TypeError):
|
||||
version = None
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. Global status — jail count and names #
|
||||
# ------------------------------------------------------------------ #
|
||||
status_data = _to_dict(_ok(await client.send(["status"])))
|
||||
status_data = to_dict(ok(await client.send(["status"])))
|
||||
active_jails: int = int(str(status_data.get("Number of jail", 0) or 0))
|
||||
jail_list_raw: str = str(
|
||||
status_data.get("Jail list", "") or ""
|
||||
@@ -240,11 +190,11 @@ async def probe(
|
||||
|
||||
for jail_name in jail_names:
|
||||
try:
|
||||
jail_resp = _to_dict(
|
||||
_ok(await client.send(["status", jail_name]))
|
||||
jail_resp = to_dict(
|
||||
ok(await client.send(["status", jail_name]))
|
||||
)
|
||||
filter_stats = _to_dict(jail_resp.get("Filter") or [])
|
||||
action_stats = _to_dict(jail_resp.get("Actions") or [])
|
||||
filter_stats = to_dict(jail_resp.get("Filter") or [])
|
||||
action_stats = to_dict(jail_resp.get("Actions") or [])
|
||||
total_failures += int(
|
||||
str(filter_stats.get("Currently failed", 0) or 0)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user