Commit graph

10 commits

Author SHA1 Message Date
ca143a616b Білінг, SLA, вбудовані правила, пісочниця установника, тести сторінок
Some checks failed
CI / hygiene (push) Successful in 8s
CI / web (push) Successful in 59s
CI / server (push) Failing after 3m27s
CI / agent (push) Successful in 3m3s
П'ять паралельних задач. Найцінніше в них — не можливості, а знайдене.

0069 БІЛІНГ. Аудит 0009 показав, що перевірка ліміту не спрацювала б
жодного разу: isPlanLimit шукала слово «ліміт», а тригер писав
"device limit reached" англійською. Перше ж досягнення стелі дало б
клієнту 500 замість пояснення. Плюс три діри: тригер лише на INSERT
(стеля в 15 обходилась за чотири дії через архів), max_maps/max_agents/
max_users не перевіряло ніщо — тобто рівно те, чим відрізняються плани,
і license_keys була закрита політикою tenant_isolation з 0011, хоча
tenant_id там NULLABLE навмисно: головний сценарій self-hosted був
недосяжний.

Після закінчення ліцензії не вимикається нічого — замерзає лише ріст.
Моніторинг, що перестав моніторити через несплачений рахунок, це
аварія в мережі клієнта, спричинена нами.

0070 SLA. Джерелом обрано ts.icmp_1h, а не device_status_history:
остання не вміє сказати «ми не знали» — перехід пишеться лише при
зміні стану, тож доба мовчання зонда виглядає як доба роботи. Час
розкладено на чотири частини, і «немає даних» не додається ні до чого;
замість вибору між двома брехнями звіт каже, яку частку періоду він
бачив. Закритий період тримає тригер, а не домовленість у Go.

0071 ВІДПОВІДНІСТЬ. 20 правил, кожне прив'язане до родини: об'єднаний
вираз, що покриває Cisco й не покриває MikroTik, дав би «0 порушень» і
сховав сліпу пляму. Вендор не входить у перелік, доки для нього немає
зразка конфігу в тесті. TestBuiltinRulesAreNotAlwaysGreen вимагає, щоб
у кожного правила був конфіг, де воно спрацювало, І де ні.

ПІСОЧНИЦЯ УСТАНОВНИКА — та сама установка в ізольованому проєкті
compose. Знайшла дві справжні вади з трьох спроб:
  * healthcheck бази ходив unix-сокетом, а споживачі по TCP. При
    первинній ініціалізації Postgres слухає лише сокет — compose
    вважав базу здоровою, migrate отримував connection refused. На
    створеній базі цієї фази немає, тож вада чекала на першого клієнта;
  * у білому переліку модулів API не було traps і filecfg — зонд із
    приймачем трапів неможливо було зареєструвати взагалі.

ТЕСТИ СТОРІНОК: 137 → 252. Мережевий шар, права доступу, незворотні
дії, фільтри з адресного рядка. Підмінюється лише fetch і WebSocket —
api/client.ts працює справжній.
2026-08-27 21:17:23 +03:00
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
29f440d436 Автопризначення шаблонів за sysObjectID
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run
Пристрій сам каже, що він таке, і шаблон чіпляється без натискань.
Системна група знімається тією ж SNMP-сесією, що й обхід топології:
три зайві PDU дешевші за окремий чек із власним розкладом.

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

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

DiscoveredDevice отримав device_id: зіставляти за адресою не можна —
за одним NAT кілька хостів мають ту саму адресу опитування.

Заразом дубль перевірки перестав давати «внутрішню помилку».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:29:39 +03:00
c28852df66 Syslog: транспорт до сервера і тригер позачергового бекапу
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run
Приймач на зонді лежав без діла — тепер під'єднаний. Окремий стрім
StreamLogs, а не контрольний канал: сплеск логів під час аварії не має
заважати heartbeat і командам.

Хост зіставляється за адресою джерела на зонді: у сервера немає
контексту мережі клієнта, а один приватний діапазон трапляється в
десятках кабінетів. Невідома адреса не привід викинути подію.

Подія, що збіглася зі зразком у ncm.device_policies.syslog_match,
ставить позачерговий збір конфігу. Типовий зразок покриває Cisco,
HP/Huawei, Juniper і MikroTik — навмисно широкий: зайвий бекап коштує
секунд, пропущений — цілої зміни.

Заразом увесь репозиторій прогнано через gofmt: CI, написаний два
кроки тому, перевіряє це і впав би на 29 файлах із порушеннями,
накопиченими за весь проєкт.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:20:32 +03:00
ddaae60fa0 Етап 8: реєстрація зонда одноразовим запрошенням
Найбільше вузьке місце до запуску: агент заводився INSERT-ом у базу, а
токен вписувався в командний рядок руками. Поставити зонд у клієнта було
неможливо.

core.agent_enrollments тримає sha256 одноразового токена; сам токен
повертається рівно один раз. Видача під FOR UPDATE в одній транзакції:
два агенти з однієї скопійованої команди інакше створили б два зонди з
одного запрошення. Відповідь на «немає», «згоріло» і «використано»
однакова — розрізняти їх означає підказувати тому, хто підбирає токени.

Токен зонда їде окремим полем agent_token, а не в certificate:
сертифікат відповідає на інше питання й живе за іншим циклом.

Агент зберігає посвідчення в /etc/netpulse/agent.json з правами 0600,
через тимчасовий файл і перейменування — обрив живлення посеред запису
інакше лишив би половину токена.

Знайдено живим прогоном: реєстрація не проходила автентифікацію, бо
інтерсептор стоїть на всьому сервері, а не на окремому сервісі — мій же
коментар стверджував протилежне. І запуск із самим посвідченням падав:
validate() вимагав -agent-id, не знаючи про файл.

Сторінка зондів: команда встановлення з токеном, відкликання
запрошень, керування модулями й лімітами, видалення.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:50:57 +03:00
2de1894fd5 Етап 6: шаблони опитування + звірка планів зонда
Схема tpl.* (шаблон → елементи → прив'язка до хоста), реконсиляція в
core.checks, REST, редактор у вебі, чотири вбудовані шаблони SNMP.

Елемент шаблону — одна метрика; у чеки вони групуються за (шаблон, тип,
інтервал) в один snmp.get. Сотня окремих чеків замість однієї пачки —
це сотня SNMP-сесій там, де досить кількох PDU. Позначка template_id у
core.checks дає реконсиляції право власності: без неї відв'язування
шаблону не знало б, що прибирати.

Звірка планів раз на 5 секунд — те, чого бракувало весь час. Чеки міняє
REST-процес, живу сесію зонда тримає AgentService; досі будь-яка зміна
доїжджала до зонда лише при обриві зв'язку, тобто ніколи.

Знайдено живими прогонами й виправлено:
- креденшели не їхали разом із планом, і хост, приписаний зонду після
  його підключення, падав на кожній задачі з «немає SNMP-креденшелів»;
- форма хоста відв'язувала зонд: поле починалося порожнім, підпис
  обіцяв «не змінювати», сервер трактував порожнє буквально;
- hrProcessorLoad.1 у базовому шаблоні — здогадка, а не адреса: на
  net-snmp «No Such Instance».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:07:54 +03:00
436eff343d Етап 7: планувальник бекапів за cron
Збір конфігів перестав залежати від того, чи згадає людина натиснути
кнопку.

Свій парсер cron (internal/cronx) замість залежності: бітові маски
uint64 на поле, пошук наступного запуску покроково по хвилинах із
запобіжником у чотири роки. Правило dom/dow — об'єднання, як у справжнього
cron. Неможливий розклад (30 лютого) чесно відмовляє замість зациклення.

Планувальник тікає раз на хвилину під advisory-блокуванням, тож у
кластері розклад розкручує рівно один екземпляр. Спершу переноситься
next_backup_at, потім ставиться завдання: падіння між кроками коштує
одного пропущеного бекапу, зворотний порядок дав би нескінченну чергу.

Форма розкладу: чотири пресети плюс довільний cron. Виправлено помилку,
через яку пункт «свій розклад…» нічого не робив — обробник select
відсікав порожнє значення, тобто саме той випадок, заради якого пункт
існує.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:54:07 +03:00
3816e2cfcf Етап 7: збір конфігів запрацював наскрізно
Профілі були даними без виконавця. Тепер ланцюг замкнено: черга в БД →
диспетчер у AgentService → зонд по SSH/Telnet → вивантаження стрімом →
звірка sha256 і дедуплікація.

Агент (internal/ncmx):
- усе побудовано навколо пошуку промпту: у консолі немає ані коду
  завершення, ані довжини відповіді
- промпт шукається лише в хвості 512 байтів, інакше "banner motd #"
  обривав би збір на середині конфігу
- дедлайн на паузу між байтами, а не на всю операцію: збір із шасі
  триває хвилини, а тиша означає завислий пристрій або хибний regex
- ключі SSH обладнання не звіряються свідомо: залізо міняє їх з кожною
  прошивкою, і known_hosts на сотні пристроїв означав би не збирати
  конфіги зовсім

Сервер: черга ncm.jobs із FOR UPDATE SKIP LOCKED (два екземпляри не
надішлють одне завдання двічі), диспетчер, прибирання завислих.

Знайдено живим прогоном:
- сервер ігнорував поле encoding: агент стискав gzip і рахував sha256
  від оригіналу, сервер рахував від стиснених байтів і відхиляв кожен
  бекап. Поле в контракті з Етапу 2, реалізації не було — інтеграційний
  тест користувався "none" і повз дірку проходив
- промпт обрізався не по рядку: шаблон [>#]\s*$ ловить лише символ, і
  в конфізі лишалось ім'я пристрою окремим рядком
- у конфіг потрапляли escape-послідовності bash і подвоєні \r\r\n від
  PTY: 295 байтів / 12 рядків замість 286 / 10. Після виправлення —
  285 / 10, різниця лише у фінальному переводі рядка

Живий прогін проти стенду: success, конфіг 285 байтів, повторний збір —
unchanged без другої версії.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:02:50 +03:00
aaf067a46b Етап 2: сервер сам заводить snmp.if-чеки з виявлених інтерфейсів
Автовиявлення наповнювало inv.interfaces, але їх ніхто не опитував: на
мапі були лінки й не було трафіку. Тепер сервер формує snmp.if-чек зі
складу портів і штовхає його живій сесії як TaskDelta — без цього після
кожного нового комутатора були б години порожніх графіків.

Чек оновлюється, а не задвоюється: унікальний індекс core.checks включає
md5(params). Склад портів порівнюється як множина, бо порядок ключів у
jsonb не гарантований. Без SNMP-креденшела чек не створюється.

Живий прогін знайшов помилку: OID у запиті йшов без провідної крапки, а
pdu.Name повертається з нею — пошук у мапі мовчки не знаходив нічого, і
чек виглядав як "жоден інтерфейс не відповів". Канонізація тепер у
snmpx.Normalize, застосована з обох боків.

Перевірено наскрізь: виявлення -> автостворення чека -> справжні
HC-лічильники -> ts.if_counters, без жодного ручного кроку.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 15:10:02 +03:00
8a92ef8a45 Етап 2: серверна сторона AgentService
Автентифікація зондів за токеном, побудова TaskPlan із детермінованим
schedule_offset, видача розшифрованих креденшелів із TTL, запис телеметрії
в гіпертаблиці, резолвер сусідів LLDP/CDP у topo.links, прийом конфігів.

Ізоляція тенантів робиться двічі — RLS плюс явний предикат tenant_id, бо
RLS не працює на гіпертаблях, а саме туди йде вся телеметрія.

Агент: -token і передача його в метаданих; MarkAllPending() перереєстровує
серії на початку сесії замість обнуляти нумерацію й губити буфер.

Перевірено на Debian 13 / PG 17.11 / TimescaleDB 2.29.1: 11 інтеграційних
тестів проти живої БД (-race), плюс живий прогін справжнього агента проти
справжнього сервера — телеметрія, статус пристрою, heartbeat у базі.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 04:13:40 +03:00