Files
awg-dash/backend/awg_api/store
..

store — хранилище состояния (issue #20)

SQLite (WAL) в /data/awg-dash.db: правила маршрутизации, исключения, режим, расписание GeoIP, метаданные конфигов, устройства, журнал аудита и снимки состояния. Путь переопределяется аргументом или переменной AWG_DASH_DB.

Модуль Назначение
db.py открытие соединения, PRAGMA (WAL, synchronous=NORMAL, foreign_keys, busy_timeout), транзакции BEGIN IMMEDIATE
migrations.py версионированные миграции, schema_migrations, проверка схемы при старте (verify_schema)
models.py dataclass-модели сущностей и перечисления (Mode, Entity, Rule, …)
validation.py чистые функции валидации (domain/CIDR/ipset, MAC, cron, режим, политика)
repository.py Store: чтения + аудируемые мутации «кто/когда/что/old→new»
audit.py запись и чтение audit_log (мутация и её аудит — одна транзакция)
state.py снимки состояния (state_snapshots), capture_state, restore_state, rollback_to_snapshot
apply.py ApplyEngine: validate → stage → apply → rollback и протокол Applier (awg/iptables)
service.py open_storage() — сборка Database + Store + ApplyEngine для старта приложения
errors.py StorageError, SchemaError, ValidationError, ApplyError(restored=…)

Гарантии

  • Пустой /data — каталог создаётся, миграции применяются, схема v1 появляется без ручных действий.
  • Версия схемы — при старте схема проверяется: новее ожидаемой или не мигрированная → SchemaError (без «автоотката»).
  • Аудит — каждая мутация пишет actor/ts/action/entity/old_value/new_value в audit_log в той же транзакции.
  • Атомарность — применяемое состояние проходит validate → stage → apply; падение внешнего применителя откатывает транзакцию БД и вызывает Applier.revert(previous), ApplyError.restored=True.
  • Откат — ApplyEngine.rollback(plan_id) возвращает состояние из снимка.

Пример

from awg_api.store import DesiredState, Rule, open_storage

with open_storage() as storage:  # /data/awg-dash.db
    storage.store.set_geoip_schedule(cron="0 3 * * *", actor="ui:admin")
    plan = storage.engine.stage(
        DesiredState(
            mode=storage.store.get_mode(), rules=(Rule("yt", "domain", "youtube.com", "tun0"),)
        ),
        reason="ui: split-tunnel для YouTube",
    )
    result = storage.engine.apply(plan)

Тесты

backend/tests/test_store_*.py — миграции, аудит, репозиторий, apply/rollback, персистентность после рестарта. Порог покрытия — 70 % (pyproject.toml).