Netpulse_SasS/proto/README.md
zotac 15d22a8d8a Етап 1-2: схема БД та protobuf-контракт агент-сервер
Схема PostgreSQL 16+/TimescaleDB: 11 міграцій, 7 схем, топологія
(neighbors -> links -> maps -> nodes/edges), time-series з CAGG,
NCM, alerting, білінг з entitlements, RLS.

Контракт agent<->server: 6 proto-файлів, gRPC, інтернування серій,
at-least-once з ack, чанкування конфігів.

Перевірено на стенді Debian 13 / PG 17.11 / TimescaleDB 2.29.1:
міграції + 8 функціональних перевірок схеми, buf lint + 5 наскрізних
gRPC-тестів контракту.

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

12 KiB
Raw Permalink Blame History

NetPulse — контракт агент↔сервер (Етап 2)

gRPC / protobuf, пакет netpulse.v1. Джерело істини — файли в netpulse/v1/; gen/go згенерований і комітиться, щоб збірка агента не залежала від наявності buf.

Головне обмеження, з якого випливає вся форма контракту

Усі з'єднання ініціює агент. У мережі клієнта немає ані відкритих портів, ані прокидання NAT — зонд стоїть за фаєрволом і має лише вихідний доступ. Тому «команда з сервера» фізично є повідомленням у зустрічному напрямку вже відкритого агентом bidi-стріму Control. Це не деталь реалізації, а причина, чому контракт побудований навколо довгоживучих потоків, а не навколо RPC-викликів у бік агента.

Файли

Файл Зміст
common.proto Спільні типи: Status, Transport, DiscoveryProto, Error, DeviceTarget, Credential, AgentBuild, AgentHealth
agent.proto EnrollmentService, AgentService, ControlUp/ControlDown і все, що всередині них
telemetry.proto SeriesDescriptor, MetricSample, IcmpResult, InterfaceCounters, TelemetryBatch, TelemetryAck
discovery.proto NeighborRecord, InterfaceRecord, DiscoveredDevice, DiscoveryReport
ncm.proto ConfigJob, ConfigUpload (header/chunk/trailer), ConfigApplyJob
logs.proto SyslogEntry, SnmpTrap, LogBatch

Сервіси

EnrollmentService
  Enroll(EnrollRequest) → EnrollResponse        одноразово, без клієнтського сертифіката

AgentService                                    усе далі — тільки mTLS
  Control(stream ControlUp) → stream ControlDown   керування, довгоживучий
  StreamTelemetry(stream TelemetryBatch) → stream TelemetryAck
  StreamLogs(stream LogBatch) → stream LogAck
  ReportDiscovery(DiscoveryReport) → DiscoveryAck
  UploadConfig(stream ConfigUpload) → ConfigReceipt

Чотири окремі стріми, а не один — навмисно. Пачка на 10 000 семплів не має блокувати heartbeat і затримувати команду з сервера, а сплеск syslog під час аварії не має топити телеметрію, за якою цю аварію й видно.

Життєвий цикл сесії

sequenceDiagram
    participant A as Агент (мережа клієнта)
    participant S as Сервер

    Note over A,S: одноразово
    A->>S: Enroll(enrollment_token, CSR)
    S-->>A: сертифікат + CA + control_endpoint

    Note over A,S: кожна сесія, тільки вихідне з'єднання
    A->>S: Control: Hello(build, task_plan_hash, last_acked_batch_id)
    S-->>A: Welcome(session_id, server_time, ліміти батчингу)
    S-->>A: TaskPlan(tasks, devices) або TaskDelta
    S-->>A: ModuleControl(які модулі активувати)
    S-->>A: CredentialBundle(розшифровані креди, з TTL)

    loop опитування
        A->>S: StreamTelemetry: TelemetryBatch(new_series?, samples, icmp, interfaces)
        S-->>A: TelemetryAck(acked_through, max_in_flight)
        A->>S: Control: Heartbeat(AgentHealth)
        S-->>A: Control: Ping
        A->>S: Control: Pong
    end

    Note over A,S: за подією або розкладом
    S-->>A: Control: ConfigJob(commands, prompt_regex)
    A->>S: UploadConfig: header → chunk* → trailer(sha256)
    S-->>A: ConfigReceipt(commit_sha | unchanged)

Рішення, які варто розуміти перед реалізацією агента

Інтернування серій

Повторювати device_id (36 байт) + metric_key + labels у кожному семплі задорого: при 50 000 пристроїв це десятки байт службових даних на одне число float64. Тому агент реєструє серію один раз під локальним номером series_ref, далі шле лише номер і значення. Сервер тримає мапу series_ref → ts.series.id на час сесії.

Заміряно в test/contract: 66 байт → 25 байт на семпл.

series_ref дійсний рівно в межах сесії. Після реконекту агент нумерує з 1 і реєструє все заново. Якщо сервер втратив стан — відповідає reset_series_table = true, а не мовчки викидає дані.

Хто рахує швидкості

Агент шле і сирі 64-бітні лічильники, і вже пораховані in_bps/out_bps/ util_*_pct. Швидкості рахує агент, бо лише він знає точний інтервал між двома опитуваннями — мережева затримка робить серверний розрахунок неточним. Сирі лічильники потрібні, щоб сервер міг перерахувати заднім числом. Обробка wrap/reset — на агенті: при перезавантаженні пристрою виставляється counter_reset = true, і швидкості цього разу недійсні.

Хто чистить конфіги

Агент віддає сирий текст. Scrub (прибрати uptime та timestamp'и, щоб не шуміти в diff), redact (замаскувати паролі) і коміт у Git — на сервері. Це навмисно: правила очищення живуть у ncm.profiles і мають змінюватись без оновлення агентів у полі.

Хто вирішує топологію

Агент доповідає сире: «на порту X я бачу chassis-id Y, port-id Z». Він не вирішує, хто з ким з'єднаний. Резолвер на сервері зводить це в topo.links і виставляє confidence. Так автовиявлення лишається відтворюваним і не залежить від версії агента в полі.

Непрозорі параметри задач

Task.params_json — байти, а не типізоване поле. Валідність гарантує params_schema з core.check_types, розбирає їх сам модуль. Завдяки цьому новий плагін не потребує зміни .proto — саме те, заради чого затівалась плагінна архітектура. Модуль-виконавець визначається префіксом check_type до крапки: snmp.if → модуль snmp.

Семантика доставки

At-least-once. Дедуплікацію забезпечує сама схема БД: первинні ключі (ts, device_id), (ts, interface_id), (ts, series_id) роблять повторний запис безпечним. Агент тримає неacknowledged батчі й після реконекту шле їх із is_retransmit = true, продовжуючи від last_acked_batch_id з Hello.

Зворотний тиск

Розмір батчу, інтервал і max_in_flight диктує сервер у Welcome, а коригує в кожному TelemetryAck. Балакучого агента можна пригальмувати на льоту, не оновлюючи бінарник у полі. AgentHealth.dropped_samples показує, коли ліміти затиснуті надто сильно.

Безпека

  • mTLS на всьому, крім Enroll. Агент приходить із одноразовим enrollment-токеном і CSR, іде з власним сертифікатом. Приватний ключ ніколи не залишає агента.
  • Креденшели передаються розшифрованими — сервер дістає їх із core.secrets і розшифровує. Агент тримає їх лише в пам'яті, не пише на диск, не логує, і зобов'язаний занулити після Credential.expires_at.
  • Самооновлення підписане. UpdateInfo несе sha256 бінарника і підпис Ed25519 над ним. Без валідного підпису оновлення не застосовується: інакше компрометація CDN перетворюється на RCE в мережі кожного клієнта.
  • Відкат конфігурації (ConfigApplyJob) агент виконує беззастережно — уся логіка погодження (ncm.rollbacks: draft → awaiting_approval → approved) лишається на сервері. confirm_timeout вмикає confirmed commit там, де пристрій це підтримує: найкращий захист від втрати керування після помилкового правила фаєрвола.

Версіонування

Пакет netpulse.v1. Правила: не змінювати номери полів, не перевикористовувати видалені (позначати reserved), нові поля — тільки додавати. Ламкі зміни — через netpulse.v2 поруч, бо агенти в полі оновлюються не одночасно з сервером.

buf breaking у CI проти головної гілки ловить порушення автоматично.

Кодогенерація

buf generate

Без buf (те, чим це перевірялось):

protoc -I proto --go_out=gen/go --go_opt=paths=source_relative --go-grpc_out=gen/go --go-grpc_opt=paths=source_relative proto/netpulse/v1/*.proto

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

Перевірено на стенді Debian 13: protoc 3.21.12, Go 1.24.4, buf 1.72.0.

Крок Результат
protoc — валідність усіх 6 файлів і залежностей OK
buf lint (STANDARD) без зауважень
Генерація Go + gRPC, go build, go vet OK
go test ./test/contract/... — 5 наскрізних тестів на bufconn усі PASS

Тести в test/contract/contract_test.go перевіряють не компіляцію, а поведінку:

Тест Що доводить
TestControlHandshakeAndTaskPush Hello → Welcome → TaskPlan доїжджає в зустрічному напрямку стріму; params_json розбирається; модуль виводиться з check_type; Ping/Pong міряє clock skew
TestTelemetrySeriesInterning Серія реєструється раз, далі розв'язується з номера; ICMP і util_out_pct доїжджають без спотворень; економія 66 → 25 байт
TestTelemetryUnknownSeriesRefTriggersReset Невідомий series_ref дає reset_series_table + retryable-помилку, а не тихе відкидання даних
TestConfigUploadChunked header → chunk × N → trailer збирається байт-у-байт зі звіркою sha256
TestConfigUploadRejectsBadChecksum Пошкоджений конфіг відхиляється з checksum_mismatch, а не комітиться в Git