Netpulse_SasS/db/README.md
zotac 15d22a8d8a Етап 1-2: схема БД та protobuf-контракт агент-сервер
Схема PostgreSQL 16+/TimescaleDB: 11 міграцій, 7 схем, топологія
(neighbors -> links -> maps -> nodes/edges), time-series з CAGG,
NCM, alerting, білінг з entitlements, RLS.

Контракт agent<->server: 6 proto-файлів, gRPC, інтернування серій,
at-least-once з ack, чанкування конфігів.

Перевірено на стенді Debian 13 / PG 17.11 / TimescaleDB 2.29.1:
міграції + 8 функціональних перевірок схеми, buf lint + 5 наскрізних
gRPC-тестів контракту.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 03:28:40 +03:00

141 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NetPulse — схема бази даних (Етап 1)
PostgreSQL 16+ / TimescaleDB 2.14+. Міграції в `migrations/`, накочуються по порядку номерів.
```bash
docker compose up -d db
```
```bash
pwsh ./db/migrate.ps1
```
## Схеми (namespaces)
| Схема | Призначення |
|--------|-------------|
| `core` | Ядро: тенанти, users, RBAC, секрети, агенти, плагіни, чеки, дашборди, аудит |
| `inv` | Інвентар: сайти, групи, пристрої, інтерфейси, IP, підмережі, креденшели |
| `topo` | Топологія: сирі сусіди (LLDP/CDP/ARP), зведені лінки, мапи, вузли, ребра, підкладки |
| `ts` | Time-series: hypertables + continuous aggregates |
| `ncm` | Config management: репо, профілі, задачі, версії конфігів, diff, compliance |
| `alr` | Alerting: правила, алерти, канали, ескалації, вікна обслуговування |
| `bill` | Тарифи, підписки, entitlements, usage, інвойси, ліцензійні ключі |
## Порядок міграцій
| # | Файл | Що містить |
|---|------|-----------|
| 0001 | `0001_core.sql` | Розширення, домени, tenants, users, RBAC, секрети (AES-GCM), audit_log |
| 0002 | `0002_inventory.sql` | Sites, groups, devices, interfaces, addresses, subnets, credentials |
| 0003 | `0003_agents_plugins.sql` | Реєстр плагінів, агенти, типи чеків, checks, event outbox |
| 0004 | `0004_topology.sql` | **Neighbors → links → maps → nodes/edges → backgrounds → revisions** |
| 0005 | `0005_telemetry_timescale.sql` | Hypertables, CAGG, compression, retention, view'и для мапи |
| 0006 | `0006_ncm.sql` | Git-репо, профілі збору, jobs, configs, diffs, compliance, rollback |
| 0007 | `0007_alerting.sql` | Rules, alerts, channels, push, routes, maintenance, mutes |
| 0008 | `0008_dashboards.sql` | Дашборди, віджети (в т.ч. `map`), saved views, SLA |
| 0009 | `0009_billing_licensing.sql` | Plans, subscriptions, entitlements, usage, invoices, license keys |
| 0010 | `0010_seed.sql` | Довідники: права, ролі, плагіни, чеки, тарифи, NCM-профілі |
| 0011 | `0011_rls.sql` | Row Level Security + ролі БД |
> 0010 навмисно йде **перед** 0011: після `FORCE ROW LEVEL SECURITY` навіть власник схеми не зможе вставити довідникові рядки з `tenant_id IS NULL`.
## ERD: ядро топології
```mermaid
erDiagram
TENANTS ||--o{ DEVICES : ""
TENANTS ||--o{ MAPS : ""
SITES ||--o{ DEVICES : ""
DEVICES ||--o{ INTERFACES : ""
DEVICES ||--o{ NEIGHBORS : "виявлено з"
NEIGHBORS }o--|| LINKS : "резолвиться в"
INTERFACES ||--o{ LINKS : "a_if / b_if"
MAPS ||--o{ MAP_NODES : ""
MAPS ||--o{ MAP_EDGES : ""
MAPS ||--o{ MAP_BACKGROUNDS : "floor plan / OSM / rack"
MAPS ||--o{ MAP_REVISIONS : "undo/redo"
DEVICES ||--o{ MAP_NODES : "kind='device'"
MAP_NODES ||--o{ MAP_EDGES : "source/target"
LINKS ||--o{ MAP_EDGES : "статус і трафік"
INTERFACES ||--o{ IF_COUNTERS : "трафік → анімація"
DEVICES ||--o{ ICMP_SAMPLES : "колір вузла"
```
## Ключові рішення
**1. Фізична топологія відокремлена від візуальної.**
`topo.links` — те, що існує в мережі (виявлено LLDP/CDP або створено вручну).
`topo.map_edges` — як це намальовано на конкретній мапі. Один лінк може бути показаний на N мапах з різними стилями; видалення мапи не чіпає топологію. `map_edges.link_id` — джерело статусу й трафіку для лінії.
**2. Порт-у-порт зв'язки.**
І `topo.links`, і `topo.map_edges` посилаються на `inv.interfaces`, тому `Switch1:Port1 → Router1:eth0` — це FK, а не текст. Унікальний індекс `links_pair_uniq` нормалізує пару через `LEAST/GREATEST`, щоб A→B і B→A не дублювались.
**3. Автовиявлення не затирає ручну роботу.**
`topo.neighbors` зберігає сире, як його віддав агент, плюс `resolved_*` і `confidence`. Резолвер зводить це в `topo.links`. Прапорець `is_pinned` захищає підтверджені людиною лінки від перезапису.
**4. Дві моделі метрик.**
- `ts.series` + `ts.samples` — узагальнена (Prometheus-подібна): будь-який плагін реєструє свій `metric_key` без зміни DDL.
- `ts.icmp_samples` і `ts.if_counters` — широкі таблиці для гарячих шляхів. Це те, що читає мапа на кожному тику WebSocket, і денормалізація тут окупається.
**5. Анімація трафіку має конкретне джерело.**
`if_counters.util_out_pct` (% від `interfaces.speed_bps`) → `topo.link_live` (гірша з двох сторін) → `map_edges.animation.speed_source='utilization'`. Кольори порогів — у `map_edges.thresholds`.
**6. Продуктивність мапи на 1000+ вузлів.**
`map_nodes` має GiST-індекс по `point(x,y)` для viewport-culling, `map_edges` — індекси по source/target. Повний стан полотна тягнеться одним запитом (nodes + edges + backgrounds + останні статуси через `ts.device_last_icmp` і `topo.link_live`), далі дельти йдуть по WebSocket.
**7. Ліміти тарифу перевіряються двічі.**
`bill.entitlements` — матеріалізовані права тенанта (читає API на кожному запиті, кешується в Redis). Плюс тригери `bill.assert_device_limit()` і `bill.assert_map_node_limit()` як друга лінія оборони на рівні БД.
**8. Секрети ніколи не лежать відкрито.**
`core.secrets` зберігає лише `ciphertext` + `nonce` + `auth_tag` + `key_id` (AES-GCM-256, шифрування на боці застосунку, DEK у KMS/Vault). Паролі SSH/SNMPv3, приватні ключі, токени каналів і тіла конфігів — усе через цю таблицю.
**9. Тіло конфігів — у Git, метадані — у БД.**
`ncm.configs` тримає `commit_sha`/`blob_sha`/`path` та `content_hash` для швидкої відповіді «змінилось?». Diff-и кешуються в `ncm.diffs`, щоб Telegram Mini App не рахував їх щоразу.
## Ізоляція тенантів
API виставляє на кожній транзакції:
```sql
SET LOCAL app.tenant_id = '<uuid>';
```
Політика `tenant_isolation` (USING + WITH CHECK) вмикається автоматично на кожній таблиці з колонкою `tenant_id`. Якщо змінна не виставлена — `core.current_tenant()` повертає NULL і запит дає порожній результат: це навмисно безпечніше за «усі тенанти».
**Свідомі винятки, які треба тримати в голові:**
- **Усі 12 hypertables** — RLS вимкнено. TimescaleDB відмовляє: `operation not supported on hypertables that have columnstore enabled`, тобто RLS несумісний зі стисненням, а стиснення увімкнене на всіх гарячих time-series таблицях. Виключено гіпертаблі цілком, а не вибірково — інакше набір захищених таблиць мовчки залежав би від того, на які з них уже накотили compression policy. Ізоляція для них: доступ лише через JOIN з `inv.devices` / `inv.interfaces` / `ts.series` (усі під RLS) плюс обов'язковий предикат `tenant_id` у репозиторному шарі API.
- Зв'язкові таблиці без власного `tenant_id` (`core.role_permissions`, `inv.device_group_members`, `inv.device_tags`, `inv.device_credentials`, `bill.invoice_lines`, `topo.map_shares`) покладаються на FK-каскад від захищеного батька.
- `netpulse_worker` має `BYPASSRLS` — воркери агрегації, білінгу й retention працюють поверх тенантів.
## Стан перевірки
Схема прогнана на живому стенді: **Debian 13 / PostgreSQL 17.11 / TimescaleDB 2.29.1**.
Усі 11 міграцій накотились на чисту БД без помилок.
| Об'єкт | Кількість |
|--------|-----------|
| Таблиць | 81 |
| Hypertables | 12 |
| Continuous aggregates | 6 |
| Фонових job-ів (compression/retention/refresh) | 25 |
| RLS-політик | 63 |
| Індексів | 220 |
`tests/smoke.sql` перевіряє не лише синтаксис, а й поведінку моделі:
```bash
psql -d netpulse -f db/tests/smoke.sql
```
| Перевірка | Результат |
|-----------|-----------|
| Дзеркальний лінк B→A відхилено (`links_pair_uniq`) | PASS |
| `kind='device'` без `device_id` відхилено (`map_nodes_kind_ref_chk`) | PASS |
| Дубль активного алерту відхилено (`alerts_active_dedup_uniq`) | PASS |
| 16-й пристрій на тарифі Free відхилено тригером | PASS |
| Стан полотна мапи одним запитом (вузли + статуси + RTT) | PASS |
| Ребро з портами `Gi0/1 → ether1` і живим `util_pct` = 78% | PASS |
| CAGG `if_counters_5m` рахує роллапи | PASS |
| RLS: ACME бачить 15 пристроїв, Globex — 0, без `app.tenant_id` — 0 | PASS |