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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user