Netpulse_SasS/server
byrsapty 3124fe3163
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run
Відповідність конфігів вимогам: правила, прогін і знахідки
Чотири види правил над зібраними конфігами — має містити, не має
містити, збіг за виразом, немає збігу. jsonpath зі схеми свідомо не
реалізовано: він для конфігів у JSON, а писати його без жодного такого
пристрою під рукою означало б писати навмання.

Перевірка читає вже зібране й не створює сесій до заліза, тому прогін
синхронний і безкоштовний для мережі. Хост без конфігу пропускається,
а не рахується проваленим: «ще не збирали» і «не відповідає» — різні
речі, і плутати їх означає ховати справжні знахідки.

Знахідка показує рядок і його номер. Для правил «має бути» рядка немає,
і таблиця так і пише: нічого — саме це й проблема.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:05:07 +03:00
..
cmd Режим NOC TV: дашборд на екрані в диспетчерській без входу 2026-08-25 15:41:22 +03:00
internal Відповідність конфігів вимогам: правила, прогін і знахідки 2026-08-25 16:05:07 +03:00
migrations Режим NOC TV: дашборд на екрані в диспетчерській без входу 2026-08-25 15:41:22 +03:00
webui Пакування: runner міграцій і вшитий у бінарник фронтенд 2026-08-25 01:01:55 +03:00
API.md Етап 8: реєстрація зонда одноразовим запрошенням 2026-08-25 00:50:57 +03:00
go.mod Git-двигун NCM: версіювання конфігів, переливання історії, довільне порівняння 2026-08-25 13:15:14 +03:00
go.sum Git-двигун NCM: версіювання конфігів, переливання історії, довільне порівняння 2026-08-25 13:15:14 +03:00
README.md Етап 2: сервер сам заводить snmp.if-чеки з виявлених інтерфейсів 2026-08-14 15:10:02 +03:00

NetPulse Server — AgentService

Приймальна сторона: зонди підключаються сюди самі, сервер до них не ходить.

go build -trimpath -ldflags "-s -w" -o netpulse-server ./cmd/netpulse-server
./netpulse-server -listen :9443 -dsn "postgres://..." -dek "k1=<hex32>" -cert srv.pem -key srv.key -client-ca agents-ca.pem

Будова

cmd/netpulse-server/    точка входу, TLS, keepalive, m'яка зупинка
internal/
  crypto/               AES-GCM-256 для core.secrets, keyring із ротацією
  store/
    store.go            пул pgx, InTenantTx із SET LOCAL app.tenant_id
    agents.go           автентифікація зонда за токеном, heartbeat, статуси задач
    plan.go             побудова TaskPlan, детермінований schedule_offset, хеш плану
    credentials.go      розшифровка секретів і видача комплекту з TTL
    telemetry.go        резолвер серій + запис у гіпертаблиці
    discovery.go        сусіди → зіставлення з інвентарем → topo.links
    autochecks.go       snmp.if-чеки з виявлених інтерфейсів
    ncm.go              прийом конфігів, дедуплікація, шифроване тіло
  grpcapi/              AgentService: Control, StreamTelemetry, StreamLogs,
                        ReportDiscovery, UploadConfig + перехоплювачі автентифікації

Рішення, які варто розуміти

Ізоляція тенантів робиться двічі. RLS у БД (через SET LOCAL app.tenant_id) плюс явний предикат tenant_id у кожному запиті. Дублювання не зайве: RLS не працює на гіпертаблях — TimescaleDB не поєднує row level security зі стисненням, а саме туди йде вся телеметрія. Для неї другий механізм єдиний, тому він не «про всяк випадок».

SET LOCAL, а не SET. Значення живе до кінця транзакції й не протікає на наступний запит, який візьме те саме з'єднання з пулу. Протікання тут означало б показ чужих даних.

schedule_offset рахує сервер, детерміновано від check_id (fnv64 % interval). Функція чиста, тому будь-який вузол сервера дає те саме значення, а зонд після перезапуску повертається у свій слот. Інакше 5000 чеків з інтервалом 60 с раз на хвилину били б сплеском.

Хеш плану рахується з полів, що впливають на поведінку — id, тип, параметри, інтервали. Зміна опису пристрою не змушує переливати 50 000 задач після кожного обриву зв'язку. Агент шле хеш у Hello, сервер відповідає task_plan_follows=false, якщо він збігся.

Токен каже, ЯКИЙ це зонд; сертифікат — що він має право говорити. Обидва потрібні. Зонд, який назвався чужим agent_id при валідному токені, отримує PermissionDenied: інакше він забрав би чужий план і чужі креденшели.

Креденшели розшифровуються на сервері. DEK не покидає сервер; у БД лише key_id, тому дамп бази без доступу до ключів не дає жодного пароля від обладнання клієнта. Комплект їде з TTL в годину — після відкликання доступу зонд перестане ним користуватись сам, навіть якщо зв'язок із ним втрачено.

Один нечитабельний секрет не рушить весь комплект. Решта пристроїв мусить опитуватись далі.

Запис телеметрії — ON CONFLICT DO NOTHING. Семантика доставки at-least-once, тож повторний батч після реконекту не має ані падати, ані дублювати рядки. Первинні ключі (ts, device_id), (ts, interface_id), (ts, series_id) роблять це безпечним.

Статус пристрою змінюється одним запитом із умовним записом в історію. Не read-modify-write: два воркери, що обробляють сусідні батчі, наввипередки писали б неіснуючі переходи up→up.

Виявлені інтерфейси одразу отримують snmp.if-чек. Без цього кроку автовиявлення наповнює inv.interfaces, але ніхто їх не опитує: на мапі є лінки й немає трафіку. Чекати наступного перепідключення агента, щоб він забрав новий план, — це години порожніх графіків після кожного нового комутатора, тому зміна штовхається живій сесії як TaskDelta. Чек не створюється для пристрою без SNMP-креденшела: він лише щохвилини писав би помилку автентифікації.

Склад портів порівнюється як множина, а не як рядок JSON. Postgres не гарантує порядок ключів у jsonb, тому пряме порівняння давало б хибну зміну на кожному обході автовиявлення — і агент отримував би новий план щоразу.

Автовиявлення інтерпретує сервер. Агент доповідає лише «на порту X бачу chassis Y». Впевненість зіставлення спадає за надійністю ознаки: chassis-id (95) → MAC (90) → IP керування (80) → sysName (60). Останнє низьке навмисно: sysName вводить людина, і на двох комутаторах цілком може бути switch. Лінк із is_pinned автовиявлення не чіпає — інакше кожен запуск затирав би ручні правки.

Параметри

Прапорець Змінна Призначення
-listen NETPULSE_LISTEN адреса gRPC, типово :9443
-dsn NETPULSE_DSN PostgreSQL
-dek NETPULSE_DEK ключі шифрування id=<hex|base64>[,...]
-cert / -key NETPULSE_CERT / _KEY сертифікат сервера
-client-ca NETPULSE_CLIENT_CA CA зондів; вмикає mTLS
-insecure NETPULSE_INSECURE=1 без TLS, лише локальний стенд

Стан перевірки

Стенд Debian 13 / PostgreSQL 17.11 / TimescaleDB 2.29.1 / Go 1.25. go vet чисто, go test -raceусі тести проходять.

Інтеграційні тести працюють проти справжньої БД зі схемою Етапу 1 і справжнього gRPC (NETPULSE_TEST_DSN; без змінної пропускаються):

Тест Що доводить
TestControlHandshake план зібрано з core.checks, offset детермінований і в межах інтервалу, креденшели розшифрувались тим самим ключем і AAD, зонд позначений online
TestControlRejectsMismatchedAgentID чужий agent_id при валідному токені відхилено
TestUnauthenticatedRejected без токена сесії немає
TestTelemetryPersisted семпл, ICMP і util_out_pct у гіпертаблицях; статус пристрою піднявся; повтор батчу не продублював ані рядки, ані переходи в історії
TestTelemetryUnknownSeriesRef невідомий ref → reset_series_table, а не тихе відкидання
TestDiscoveryResolvesLink сусід зіставлений за chassis-id, лінк створено з портами й capacity_bps; зустрічний звіт B→A не створив дублікат
TestDiscoveryRespectsPinnedLink ручний лінк не затерто
TestConfigUploadAndDedup чанки склеєні, тіло збережено зашифрованим і читається назад; повторний збір не створює версію
TestConfigUploadRejectsBadChecksum зіпсований конфіг не потрапляє в базу
TestHeartbeatPersisted самометрики в ts.agent_health, dropped_samples видно у зведенні зонда
TestPlanHashSkipsResend збіг хеша → сервер не шле план
TestDiscoveryCreatesInterfaceChecks виявлені порти дають snmp.if-чек із interface_id і speed_bps; loopback відсіяно; зміна складу портів оновлює чек, а не задвоює
TestNoInterfaceCheckWithoutSnmpCredential без SNMP-креденшела чек не створюється, але інтерфейси все одно збережені

Живий наскрізний прогін

Справжній netpulse-agent проти справжнього netpulse-server і живої БД, 40 секунд, чек icmp.ping кожні 5 с проти 127.0.0.1:

Що перевірено Результат
Зонд підключився, отримав план tasks=1, plan_unchanged=false
ICMP-семпли в ts.icmp_samples 5 за 25 с — рівно за розкладом
Узагальнені метрики icmp.rtt_avg 0.108 мс, icmp.jitter, icmp.loss_pct
Статус пристрою unknown → up, причина icmp, рівно один перехід в історії
Heartbeat записано, RSS зонда 11.6 МБ, dropped_samples=0
Версія/ОС/архітектура зонда зафіксовані в core.agents
Розрив сесії зонд позначений offline
Розсинхронізація годинника 0.9 мс

Наскрізний ланцюг проти справжнього SNMP

Стенд із snmpd + lldpd. Агент і сервер запущені як є, без жодного ручного кроку між ними:

Крок Результат
topology.discover знайшов порти lo, eth0 у inv.interfaces
Сервер створив snmp.if-чек 1 порт (loopback відсіяно), 10 Гбіт/с
Дельта доїхала до живої сесії надісланоаживо: true
Агент опитав справжні HC-лічильники in_octets 625 246 266, in_bps 11 938
util_out_pct пораховано 0.000001 % — знаменник 10 Гбіт/с із ifHighSpeed
Помилок чеків немає

Це повний шлях даних для анімації трафіку на мапі: від виявлення порту до ts.if_counters, без жодного ручного налаштування.

Знайдено під час перевірки

Приведення типу на місці ($1::text) не розв'язує конфлікт виведення типів у Postgres, а нав'язує тип обом уживанням параметра. Коли $1 потрібен і як uuid для колонки, і як text для конкатенації, кастувати треба протилежне уживання: VALUES ($1::uuid, …, '/шлях/' || $1 || '.git').

Чого ще немає

  • Git-двигун (libgit2) не підключено. Тіло конфігу зберігається зашифрованим у core.secrets, а ncm.configs.commit_sha тимчасово містить hex контентного хеша. Дедуплікація, підрахунок рядків і ланцюжок prev_config_id працюють уже зараз, тож diff між версіями будується без Git. Коли двигун з'явиться, зміниться лише джерело commit_sha.
  • EnrollmentService не реалізовано — зонди поки заводяться вставкою в core.agents з sha256 токена.
  • TaskDelta сервер не надсилає: при зміні плану поки йде повна синхронізація. Механізм на боці агента вже є.
  • ConfigJob / ConfigApplyJob сервер не ініціює — планувальник бекапів за cron і Syslog-подією ще не написаний.