Netpulse_SasS/db
byrsapty ed8fc831bf Дві сесії роботи: 0058–0068, розгортання однією командою, тести
Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.

ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ

  0058  подієві алерти: syslog, ncm, compliance спрацьовують у мить
        події; правило з нереалізованим джерелом більше не зберігається
        мовчки
  0059  snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
        описуються шаблоном, а не Go
  0060  відкат конфігу: план як різниця, маскування паролів із підписом
        плану, обов'язковий контрольний збір, verifying при обриві
  0061  кнопки Telegram: довге опитування, авторизація не з callback_data
  0062  аудит і архів хостів; тест на AST, що падає на ключі без назви
  0063  RLS: три ролі, окремий пул для фонових тактів
  0064  строки зберігання даних і сторінка сховища
  0065  приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
        переклад v1→v2 за RFC 3584 дає правильний OID
  0066  ескалації сповіщень
  0067  алерт про вичерпання диска
  0068  поля заливки конфігу переїхали в каталог профілів

Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.

ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ

  netpulse            установник: одна команда замість 18 змінних і
                      593 рядків інструкції
  RLS з першого запуску  нова інсталяція під політиками одразу;
                      RLS-EXISTING-INSTALL.md лишається тільки для
                      старих інсталяцій
  .forgejo + CI       раннер не зареєстрований

Ці три перевірені компіляцією й міркуванням, але не виконанням.

ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ

Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.

Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.

Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
2026-08-27 17:32:49 +03:00
..
profiles Дві сесії роботи: 0058–0068, розгортання однією командою, тести 2026-08-27 17:32:49 +03:00
tests Етап 1-2: схема БД та protobuf-контракт агент-сервер 2026-08-14 03:28:40 +03:00
README-migrations.md Пакування: runner міграцій і вшитий у бінарник фронтенд 2026-08-25 01:01:55 +03:00
README.md Етап 1-2: схема БД та protobuf-контракт агент-сервер 2026-08-14 03:28:40 +03:00

NetPulse — схема бази даних (Етап 1)

PostgreSQL 16+ / TimescaleDB 2.14+. Міграції в migrations/, накочуються по порядку номерів.

docker compose up -d db
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: ядро топології

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 виставляє на кожній транзакції:

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 перевіряє не лише синтаксис, а й поведінку моделі:

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