Netpulse_SasS/agent
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
..
cmd/netpulse-agent Дві сесії роботи: 0058–0068, розгортання однією командою, тести 2026-08-27 17:32:49 +03:00
internal Дві сесії роботи: 0058–0068, розгортання однією командою, тести 2026-08-27 17:32:49 +03:00
go.mod Етап 7: збір конфігів запрацював наскрізно 2026-08-24 12:02:50 +03:00
go.sum Етап 7: збір конфігів запрацював наскрізно 2026-08-24 12:02:50 +03:00
README.md Етап 2: модуль topology — LLDP/CDP/ARP/FDB та інвентар портів 2026-08-14 14:42:01 +03:00

NetPulse Agent

Легкий зонд збору телеметрії. Єдиний статичний бінарник, усі з'єднання вихідні — у мережі клієнта не треба відкривати жодного порту.

go build -trimpath -ldflags "-s -w" -o netpulse-agent ./cmd/netpulse-agent
./netpulse-agent -server monitor.example.com:443 -agent-id <uuid> -cert agent.pem -key agent.key

Будова

cmd/netpulse-agent/       точка входу, збирання компонентів
internal/
  config/                 прапорці + NETPULSE_*, mTLS
  module/                 контракт модуля, реєстр, маршрутизація check_type → модуль
  telemetry/
    interner.go           series_ref: серія реєструється раз за сесію
    buffer.go             обмежений буфер із бюджетом пам'яті
  scheduler/              min-heap за часом запуску, семафор паралельності
  session/                gRPC-клієнт, рукостискання, реконект, ack
  snmpx/                  спільний SNMP-транспорт + розбір індексів OID
  modules/
    icmp/                 ping, RTT, jitter, втрати
    snmp/                 лічильники інтерфейсів (v2c/v3), довільні OID
    topology/             LLDP, CDP, ARP, FDB + інвентар портів

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

Модулі вкомпільовані, а не завантажуються динамічно. Плагінність на агенті навмисно простіша, ніж на сервері: жодних .so. Причина — той самий бінарник має працювати на Alpine, Windows і роутері з musl, а бюджет RSS не дозволяє тягнути рантайм плагінів. «Активація» означає, що сервер дозволив модуль цьому зонду (ControlDown.ModuleControl).

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

Розклад вирівняний по сітці інтервалу, а не «зараз + інтервал». Після рестарту агента задача повертається у свій слот. schedule_offset рахує сервер детерміновано від check_id, тому 5000 чеків з інтервалом 60 с розведені й переживають перезапуск без збою фази.

Буфер викидає найстаріші дані, а не найновіші. Коли зв'язок відновиться, оператору потрібен передусім поточний стан мережі. Зміни статусу пристрою викидаються останніми: без них мапа показуватиме пристрій живим, поки він лежить. Кількість викинутого їде в AgentHealth.dropped_samples, щоб діра була видимою.

Швидкості інтерфейсів рахує агент. Лише він знає фактичний інтервал між двома опитуваннями; серверний розрахунок за номінальним інтервалом помиляється рівно на мережеву затримку й джитер планувальника. При перевороті лічильника виставляється counter_reset, і швидкості не заповнюються — краще діра в графіку, ніж стрибок на терабіт і хибний алерт.

Один писар у контрольний стрім. gRPC не допускає паралельних Send, а слати треба і heartbeat, і статуси задач. Тому окрема горутина-писар і канал. Читання підтверджень телеметрії — на іншій горутині: Send + Recv на різних горутинах це єдине, що gRPC дозволяє робити паралельно на одному стрімі.

GOMEMLIMIT виставляється в коді (48 МБ). Зонд часто живе на роутері або в контейнері з 64 МБ. Хай збирач працює агресивніше, ніж OOM killer вб'є процес і осліпить моніторинг саме тоді, коли він потрібен.

Параметри

Прапорець Змінна Призначення
-server NETPULSE_SERVER адреса сервера host:port
-agent-id NETPULSE_AGENT_ID ідентифікатор зонда з Enroll
-cert / -key / -ca NETPULSE_CERT / _KEY / _CA mTLS
-insecure NETPULSE_INSECURE=1 без TLS, лише локальний стенд
-modules NETPULSE_MODULES активні до першої відповіді сервера
-max-concurrency NETPULSE_MAX_CONCURRENCY скільки чеків паралельно
-buffer-items / -buffer-bytes NETPULSE_BUFFER_* ліміти буфера

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

Параметри чеків

icmp.ping

{"count": 3, "packet_size": 56, "interval_ms": 100}

snmp.if — перелік інтерфейсів приходить від сервера, бо він уже є в inv.interfaces. Агент не ходить по ifTable, щоб з'ясувати, що існує: виявлення нових інтерфейсів — робота модуля topology, і вона окрема саме тому, що впирається в ліміт тарифу.

{"use_hc_counters": true,
 "interfaces": [{"if_index": 1, "interface_id": "<uuid>", "speed_bps": 1000000000}]}

snmp.get

{"oids": [{"oid": "1.3.6.1.4.1.9.9.109.1.1.1.1.8.1",
           "metric_key": "cpu.util", "unit": "pct", "scale": 1}]}

topology.discover — сусіди й інвентар портів беруться з одного SNMP-обходу, тому окремий чек для інтерфейсів був би зайвим трафіком.

{"protos": ["lldp", "cdp", "arp", "fdb"], "collect_interfaces": true}

Префікс типу чека зобов'язаний дорівнювати ключу модуля: саме за ним агент обирає виконавця. Реєстр відхиляє модуль, який оголошує чужий тип, а в БД те саме закріплено обмеженням check_types_prefix_matches_plugin.

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

Стенд Debian 13, Go 1.25.13. go vet чисто, тести з -race:

Пакет Результат
internal/scheduler ok
internal/telemetry ok
internal/session ok
internal/snmpx ok
internal/modules/icmp ok

Заміряно на релізному бінарнику (CGO_ENABLED=0, -trimpath -s -w):

  • розмір — 12 МБ
  • базовий RSS у циклі реконекту — 11.6 МБ (бюджет 30 МБ)

Що саме доводять тести:

Тест Що перевіряє
TestNextRunSurvivesRestart слот задачі не з'їжджає після перезапуску агента
TestOffsetsSpreadLoad 60 різних offset дають 60 різних моментів запуску
TestSchedulerSkipsWhenPreviousStillRunning повільний чек не множить сам себе; пропуск явний
TestSchedulerRejectsUnknownModule задача на неактивний модуль відхиляється зі слідом
TestInternerLabelOrderDoesNotMatter недетермінований порядок map не плодить серій
TestBufferDropsOldestOnOverflow при переповненні лишається найновіше, викинуте пораховане
TestBufferPrioritisesStatusChanges зміна стану пролазить у батч поперед метрик
TestBufferRequeuePreservesData розрив між Send і Ack не втрачає ні семпли, ні дескриптори
TestSessionFullCycle Hello → план у зустрічному напрямку → виконання з креденшелами → телеметрія на сервері
TestSessionReconnectsAfterDrop агент повертається сам після розриву
TestSplitIndex розбір індексу OID, включно з пасткою «схожий префікс» (.1.2.20 не є нащадком .1.2)
TestFormatMAC сирі байти, 00:11:..., 0011.2233.4455 і дефіси зводяться до одного вигляду
TestPingLoopback реальний ICMP-сокет, не мок

Перевірка проти справжнього SNMP-агента

На стенді піднято snmpd + lldpd (LLDP-MIB через AgentX). Живий прогін topology.discover і snmp.get кожні 10 с:

Що перевірено Результат
Інвентар портів зі справжнього ifTable lo (softwareLoopback) і eth0 (ethernetCsmacd, MAC bc:24:11:07:68:67)
ifHighSpeed замість 32-бітного ifSpeed 10 Гбіт/с — 32-бітне поле впиралося б у 4.29 Гбіт/с
Сусіди зі справжньої ARP-таблиці 2 записи, обидва з локальним портом eth0
Зіставлення за MAC шлюз 192.168.1.1 розпізнано, впевненість 90
Зведення лінка snmp-host:eth0 → gateway, джерело arp, capacity_bps 10 Гбіт/с
Три проходи поспіль лінк один, дублікатів немає
Метрики snmp.get sys.uptime збирається, помилок чеків немає

Обмеження перевірки: TestPingLoopback пропускається під звичайним користувачем в unprivileged LXC — ядро не дає ані unprivileged-, ані raw-сокета, і sysctl net.ipv4.ping_group_range там недоступний. Під root на тому ж стенді тест проходить. У проді агенту треба CAP_NET_RAW або дозволений ping_group_range.

LLDP і CDP не перевірені на живих сусідах: lldpd на стенді зареєстрував LLDP-MIB, але сусідів у нього немає — поруч немає другого пристрою, який шле LLDP. Розбір lldpRemTable і cdpCacheTable покритий лише тестами на індекси OID; ARP-гілка того самого коду перевірена на живих даних. CDP-гілка чекає на Cisco.

Так само не перевірено snmp.if: сервер поки не генерує для нього перелік інтерфейсів, тому чек нікуди не призначається. Логіка перевороту лічильників і util_pct лишається без живої перевірки.