Consolidate domain exceptions into app.exceptions

Move all shared domain exception classes to backend/app/exceptions.py and update services/routers to import the canonical exceptions. Update docs to reflect the shared exceptions source.
This commit is contained in:
2026-04-13 19:35:12 +02:00
parent 4b2e86edbb
commit a5674f9e4c
10 changed files with 199 additions and 381 deletions

View File

@@ -9,7 +9,6 @@ overrides in jail.d/*.local files.
from __future__ import annotations
import asyncio
from app.utils.async_utils import run_blocking
import configparser
import contextlib
import io
@@ -21,7 +20,14 @@ from typing import TYPE_CHECKING, cast
import structlog
from app.exceptions import JailNotFoundError
from app.exceptions import (
ConfigWriteError,
JailAlreadyActiveError,
JailAlreadyInactiveError,
JailNameError,
JailNotFoundError,
JailNotFoundInConfigError,
)
from app.helpers.config_file_helpers import (
_build_inactive_jail,
_get_active_jail_names,
@@ -38,6 +44,7 @@ from app.models.config import (
RollbackResponse,
)
from app.tasks.health_check import run_probe
from app.utils.async_utils import run_blocking
from app.utils.fail2ban_client import Fail2BanClient
from app.utils.runtime_state import (
clear_activation_record,
@@ -76,58 +83,6 @@ _POST_RELOAD_PROBE_INTERVAL: float = 2.0
_POST_RELOAD_MAX_ATTEMPTS: int = 4
# ---------------------------------------------------------------------------
# Custom exceptions
# ---------------------------------------------------------------------------
class JailNotFoundInConfigError(Exception):
"""Raised when the requested jail name is not defined in any config file."""
def __init__(self, name: str) -> None:
"""Initialise with the jail name that was not found.
Args:
name: The jail name that could not be located.
"""
self.name: str = name
super().__init__(f"Jail not found in config files: {name!r}")
class JailAlreadyActiveError(Exception):
"""Raised when trying to activate a jail that is already active."""
def __init__(self, name: str) -> None:
"""Initialise with the jail name.
Args:
name: The jail that is already active.
"""
self.name: str = name
super().__init__(f"Jail is already active: {name!r}")
class JailAlreadyInactiveError(Exception):
"""Raised when trying to deactivate a jail that is already inactive."""
def __init__(self, name: str) -> None:
"""Initialise with the jail name.
Args:
name: The jail that is already inactive.
"""
self.name: str = name
super().__init__(f"Jail is already inactive: {name!r}")
class JailNameError(Exception):
"""Raised when a jail name contains invalid characters."""
class ConfigWriteError(Exception):
"""Raised when writing a ``.local`` override file fails."""
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
@@ -502,8 +457,9 @@ async def list_inactive_jails(
:class:`~app.models.config.InactiveJailListResponse` with all
inactive jails.
"""
loop = asyncio.get_event_loop()
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = await run_blocking(_parse_jails_sync, Path(config_dir)
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = await run_blocking(
_parse_jails_sync,
Path(config_dir),
)
all_jails, source_files = parsed_result
active_names: set[str] = await _get_active_jail_names(socket_path)
@@ -587,7 +543,6 @@ async def _activate_jail(
"""
_safe_jail_name(name)
loop = asyncio.get_event_loop()
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
if name not in all_jails:
@@ -639,7 +594,8 @@ async def _activate_jail(
# we can restore it if activation fails. #
# ---------------------------------------------------------------------- #
local_path = Path(config_dir) / "jail.d" / f"{name}.local"
original_content: bytes | None = await run_blocking(lambda: local_path.read_bytes() if local_path.exists() else None,
original_content: bytes | None = await run_blocking(
lambda: local_path.read_bytes() if local_path.exists() else None,
)
await run_blocking(_write_local_override_sync,
@@ -783,7 +739,6 @@ async def _rollback_activation_async(
``True`` if fail2ban is responsive again after the rollback, ``False``
if recovery also failed.
"""
loop = asyncio.get_event_loop()
local_path = Path(config_dir) / "jail.d" / f"{name}.local"
# Step 1 — restore original file (or delete it).
@@ -860,7 +815,6 @@ async def _deactivate_jail(
"""
_safe_jail_name(name)
loop = asyncio.get_event_loop()
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
if name not in all_jails:
@@ -916,7 +870,6 @@ async def delete_jail_local_override(
"""
_safe_jail_name(name)
loop = asyncio.get_event_loop()
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
if name not in all_jails:
@@ -956,7 +909,6 @@ async def validate_jail_config(
JailNameError: If *name* contains invalid characters.
"""
_safe_jail_name(name)
loop = asyncio.get_event_loop()
return await run_blocking(_validate_jail_config_sync,
Path(config_dir),
name,
@@ -1008,7 +960,6 @@ async def _rollback_jail(
"""
_safe_jail_name(name)
loop = asyncio.get_event_loop()
# Write enabled=false — this must succeed even when fail2ban is down.
await run_blocking(_write_local_override_sync,