Files
BanGUI/backend/app/tasks/blocklist_import.py
Lukas 1cdc97a729 Stage 11: polish, cross-cutting concerns & hardening
- 11.1 MainLayout health indicator: warning MessageBar when fail2ban offline
- 11.2 formatDate utility + TimezoneProvider + GET /api/setup/timezone
- 11.3 Responsive sidebar: auto-collapse <640px, media query listener
- 11.4 PageFeedback (PageLoading/PageError/PageEmpty), BanTable updated
- 11.5 prefers-reduced-motion: disable sidebar transition
- 11.6 WorldMap ARIA: role/tabIndex/aria-label/onKeyDown for countries
- 11.7 Health transition logging (fail2ban_came_online/went_offline)
- 11.8 Global handlers: Fail2BanConnectionError/ProtocolError -> 502
- 11.9 379 tests pass, 82% coverage, ruff+mypy+tsc+eslint clean
- Timezone endpoint: setup_service.get_timezone, 5 new tests
2026-03-01 15:59:06 +01:00

154 lines
4.9 KiB
Python

"""External blocklist import background task.
Registers an APScheduler job that downloads all enabled blocklist sources,
validates their entries, and applies bans via fail2ban on a configurable
schedule. The default schedule is daily at 03:00 UTC; it is stored in the
application :class:`~app.models.blocklist.ScheduleConfig` settings and can
be updated at runtime through the blocklist router.
The scheduler job ID is ``"blocklist_import"`` — using a stable id means
re-registering the job (e.g. after a schedule update) safely replaces the
existing entry without creating duplicates.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import structlog
from app.models.blocklist import ScheduleFrequency
from app.services import blocklist_service
if TYPE_CHECKING:
from fastapi import FastAPI
log: structlog.stdlib.BoundLogger = structlog.get_logger()
#: Stable APScheduler job id so the job can be replaced without duplicates.
JOB_ID: str = "blocklist_import"
async def _run_import(app: Any) -> None:
"""APScheduler callback that imports all enabled blocklist sources.
Reads shared resources from ``app.state`` and delegates to
:func:`~app.services.blocklist_service.import_all`.
Args:
app: The :class:`fastapi.FastAPI` application instance passed via
APScheduler ``kwargs``.
"""
db = app.state.db
http_session = app.state.http_session
socket_path: str = app.state.settings.fail2ban_socket
log.info("blocklist_import_starting")
try:
result = await blocklist_service.import_all(db, http_session, socket_path)
log.info(
"blocklist_import_finished",
total_imported=result.total_imported,
total_skipped=result.total_skipped,
errors=result.errors_count,
)
except Exception:
log.exception("blocklist_import_unexpected_error")
def register(app: FastAPI) -> None:
"""Add (or replace) the blocklist import job in the application scheduler.
Reads the persisted :class:`~app.models.blocklist.ScheduleConfig` from
the database and translates it into the appropriate APScheduler trigger.
Should be called inside the lifespan handler after the scheduler and
database have been initialised.
Args:
app: The :class:`fastapi.FastAPI` application instance whose
``app.state.scheduler`` will receive the job.
"""
import asyncio # noqa: PLC0415
async def _do_register() -> None:
config = await blocklist_service.get_schedule(app.state.db)
_apply_schedule(app, config)
# APScheduler is synchronous at registration time; use asyncio to read
# the stored schedule from the DB before registering.
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(_do_register())
except RuntimeError:
# If the current thread already has a running loop (uvicorn), schedule
# the registration as a coroutine.
asyncio.ensure_future(_do_register())
def reschedule(app: FastAPI) -> None:
"""Re-register the blocklist import job with the latest schedule config.
Called by the blocklist router after a schedule update so changes take
effect immediately without a server restart.
Args:
app: The :class:`fastapi.FastAPI` application instance.
"""
import asyncio # noqa: PLC0415
async def _do_reschedule() -> None:
config = await blocklist_service.get_schedule(app.state.db)
_apply_schedule(app, config)
asyncio.ensure_future(_do_reschedule())
def _apply_schedule(app: FastAPI, config: Any) -> None:
"""Add or replace the APScheduler cron/interval job for the given config.
Args:
app: FastAPI application instance.
config: :class:`~app.models.blocklist.ScheduleConfig` to apply.
"""
scheduler = app.state.scheduler
kwargs: dict[str, Any] = {"app": app}
trigger_type: str
trigger_kwargs: dict[str, Any]
if config.frequency == ScheduleFrequency.hourly:
trigger_type = "interval"
trigger_kwargs = {"hours": config.interval_hours}
elif config.frequency == ScheduleFrequency.weekly:
trigger_type = "cron"
trigger_kwargs = {
"day_of_week": config.day_of_week,
"hour": config.hour,
"minute": config.minute,
}
else: # daily (default)
trigger_type = "cron"
trigger_kwargs = {
"hour": config.hour,
"minute": config.minute,
}
# Remove existing job if it exists, then add new one.
if scheduler.get_job(JOB_ID):
scheduler.remove_job(JOB_ID)
scheduler.add_job(
_run_import,
trigger=trigger_type,
id=JOB_ID,
kwargs=kwargs,
**trigger_kwargs,
)
log.info(
"blocklist_import_scheduled",
frequency=config.frequency,
trigger=trigger_type,
trigger_kwargs=trigger_kwargs,
)