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:
2026-04-23 15:11:21 +02:00
parent 6d21a53620
commit b634ce876a
11 changed files with 832 additions and 444 deletions

View File

@@ -301,7 +301,67 @@ async def test_list_jails_returns_200(client: AsyncClient) -> None:
---
## 11. Configuration & Secrets
## 11. fail2ban Response Utilities
All services that interact with the fail2ban daemon must use the canonical response parsing utilities from `app.utils.fail2ban_response`. This ensures consistent error handling, type safety, and makes it easy to fix bugs in response handling across the entire codebase.
### Available Functions
**`ok(response: object) -> object`**
Extracts the payload from a fail2ban ``(return_code, data)`` response tuple.
- Raises `ValueError` if return code ≠ 0 or response shape is invalid.
- Use this on every response from `Fail2BanClient.send()`.
**`to_dict(pairs: object) -> dict[str, object]`**
Converts a list of ``(key, value)`` pairs (fail2ban's native response format) to a Python dict.
- Silently ignores malformed entries and non-list/tuple inputs.
- Always returns a dict (empty if input is invalid).
**`ensure_list(value: object | None) -> list[str]`**
Coerces fail2ban response values (which may be `None`, a single string, or a list) to a normalized list of strings.
- Handles all three cases consistently.
- Returns empty list for `None` or empty strings.
**`is_not_found_error(exc: Exception) -> bool`**
Checks if an exception indicates a jail does not exist.
- Checks for multiple error message patterns (case-insensitive).
- Use this to distinguish "jail not found" errors from other failures.
### Example Usage
```python
from app.utils.fail2ban_response import ok, to_dict, ensure_list, is_not_found_error
from app.utils.fail2ban_client import Fail2BanClient
client = Fail2BanClient(socket_path="/var/run/fail2ban/fail2ban.sock")
try:
# Get jail status
response = await client.send(["status", "sshd", "short"])
status_dict = to_dict(ok(response)) # Extract payload and convert to dict
# Get list of banned IPs
ban_response = await client.send(["get", "sshd", "banip"])
banned_ips = ensure_list(ok(ban_response)) # Normalize to list of strings
except ValueError as exc:
if is_not_found_error(exc):
raise JailNotFoundError("sshd") from exc
raise
```
### Why This Matters
Before this utility module, every service implemented its own copy of these functions, leading to:
- Code duplication across 7+ service files.
- Subtle inconsistencies in error handling.
- Difficult maintenance — every bug fix required touching multiple files.
Now, all services import from a single authoritative source, making response handling consistent, maintainable, and type-safe.
---
## 12. Configuration & Secrets
- All configuration lives in **environment variables** loaded through **pydantic-settings**.
- Secrets (master password hash, session key) are **never** committed to the repository.