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

@@ -7,7 +7,6 @@ for fail2ban action configurations.
from __future__ import annotations
import asyncio
from app.utils.async_utils import run_blocking
import configparser
import contextlib
import io
@@ -18,7 +17,15 @@ from pathlib import Path
import structlog
from app.exceptions import ConfigWriteError, JailNotFoundInConfigError
from app.exceptions import (
ActionAlreadyExistsError,
ActionNameError,
ActionNotFoundError,
ActionReadonlyError,
ConfigWriteError,
JailNameError,
JailNotFoundInConfigError,
)
from app.helpers.config_file_helpers import (
_get_active_jail_names,
_parse_jails_sync,
@@ -33,59 +40,10 @@ from app.models.config import (
AssignActionRequest,
)
from app.utils import conffile_parser
from app.utils.async_utils import run_blocking
log: structlog.stdlib.BoundLogger = structlog.get_logger()
# ---------------------------------------------------------------------------
# Custom exceptions
# ---------------------------------------------------------------------------
class ActionNotFoundError(Exception):
"""Raised when the requested action name is not found in ``action.d/``."""
def __init__(self, name: str) -> None:
"""Initialise with the action name that was not found.
Args:
name: The action name that could not be located.
"""
self.name: str = name
super().__init__(f"Action not found: {name!r}")
class ActionAlreadyExistsError(Exception):
"""Raised when trying to create an action whose ``.conf`` or ``.local`` already exists."""
def __init__(self, name: str) -> None:
"""Initialise with the action name that already exists.
Args:
name: The action name that already exists.
"""
self.name: str = name
super().__init__(f"Action already exists: {name!r}")
class ActionReadonlyError(Exception):
"""Raised when trying to delete a shipped ``.conf`` action with no ``.local`` override."""
def __init__(self, name: str) -> None:
"""Initialise with the action name that cannot be deleted.
Args:
name: The action name that is read-only (shipped ``.conf`` only).
"""
self.name: str = name
super().__init__(
f"Action {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
)
class ActionNameError(Exception):
"""Raised when an action name contains invalid characters."""
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
@@ -108,15 +66,6 @@ _TRUE_VALUES: frozenset[str] = frozenset({"true", "yes", "1"})
_FALSE_VALUES: frozenset[str] = frozenset({"false", "no", "0"})
# ---------------------------------------------------------------------------
# Helper exceptions
# ---------------------------------------------------------------------------
class JailNameError(Exception):
"""Raised when a jail name contains invalid characters."""
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
@@ -601,7 +550,6 @@ async def list_actions(
``used_by_jails`` lists.
"""
action_d = Path(config_dir) / "action.d"
loop = asyncio.get_event_loop()
raw_actions: list[tuple[str, str, str, bool, str]] = await run_blocking( _parse_actions_sync, action_d)
@@ -676,7 +624,6 @@ async def get_action(
action_d = Path(config_dir) / "action.d"
conf_path = action_d / f"{base_name}.conf"
local_path = action_d / f"{base_name}.local"
loop = asyncio.get_event_loop()
def _read() -> tuple[str, bool, str]:
"""Read action content and return (content, has_local_override, source_path)."""
@@ -787,7 +734,6 @@ async def update_action(
content = conffile_parser.serialize_action_config(merged)
action_d = Path(config_dir) / "action.d"
loop = asyncio.get_event_loop()
await run_blocking( _write_action_local_sync, action_d, base_name, content)
if do_reload:
@@ -840,7 +786,6 @@ async def create_action(
if conf_path.is_file() or local_path.is_file():
raise ActionAlreadyExistsError(req.name)
loop = asyncio.get_event_loop()
await run_blocking( _check_not_exists)
cfg = ActionConfig(
@@ -903,7 +848,6 @@ async def delete_action(
conf_path = action_d / f"{base_name}.conf"
local_path = action_d / f"{base_name}.local"
loop = asyncio.get_event_loop()
def _delete() -> None:
has_conf = conf_path.is_file()
@@ -957,7 +901,6 @@ async def assign_action_to_jail(
_safe_jail_name(jail_name)
_safe_action_name(req.action_name)
loop = asyncio.get_event_loop()
all_jails, _src = await run_blocking( _parse_jails_sync, Path(config_dir))
if jail_name not in all_jails:
@@ -1035,7 +978,6 @@ async def remove_action_from_jail(
_safe_jail_name(jail_name)
_safe_action_name(action_name)
loop = asyncio.get_event_loop()
all_jails, _src = await run_blocking( _parse_jails_sync, Path(config_dir))
if jail_name not in all_jails: