Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (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 серпня.
|
||
|---|---|---|
| .. | ||
| profiles | ||
| tests | ||
| README-migrations.md | ||
| README.md | ||
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 |