Netpulse_SasS/db
byrsapty 387980d655 Картка хоста на вкладках, графіки в шаблонах, пошукові списки
Форма хоста стала вкладками: Хост, Шаблони, Доступи, Збір конфігів,
Ручні перевірки. Інтервалів опитування тут більше немає — вони живуть
у шаблонах, і два джерела правди про одне число розійшлися б саме тоді,
коли треба швидко зрозуміти, чому хост опитується не так, як написано.
Новий хост отримує шаблон «Доступність (ICMP)», а не ручний чек.

Групи, шаблони й доступи обираються пошуком із чипами: список галочок
працює на пʼяти позиціях і перестає працювати на пʼятдесяти.

Графіки описуються в шаблоні (tpl.graphs): шість видів, жорсткі межі
осей, перелік ключів метрик замість посилань на елементи — графік має
право показувати й те, що прийшло з іншого шаблону. Графік без жодного
знайденого ряду не показується: порожня рамка з підписом — це обіцянка
даних, яких немає.

Знайдено живим прогоном і виправлено: обробник виділення на полотні
створював новий масив щорендеру й зациклював оновлення. Цикл зʼїдав
головний потік, і мертвими ставали всі сторінки, не лише мапа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 19:11:24 +03:00
..
migrations Картка хоста на вкладках, графіки в шаблонах, пошукові списки 2026-08-24 19:11:24 +03:00
profiles Каталог команд збору конфігу як власні дані 2026-08-24 02:46:23 +03:00
tests Етап 1-2: схема БД та protobuf-контракт агент-сервер 2026-08-14 03:28:40 +03:00
migrate.ps1 Етап 1-2: схема БД та protobuf-контракт агент-сервер 2026-08-14 03:28:40 +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