feat(store): SQLite WAL, миграции, валидация, аудит, снимки состояния (#20)

This commit is contained in:
2026-10-21 23:43:14 +03:00
parent 9163265782
commit c5b06a7ea4
6 changed files with 663 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# 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)` возвращает состояние из снимка.
## Пример
```python
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`).