Пристрій сам каже, що він таке, і шаблон чіпляється без натискань. Системна група знімається тією ж SNMP-сесією, що й обхід топології: три зайві PDU дешевші за окремий чек із власним розкладом. Збіг за префіксом на межі компонента: моделей у виробника тисячі, і повний збіг означав би рядок на кожну коробку. Довший префікс перемагає. Дванадцять вбудованих правил на основних виробників. Шаблони тільки додаються, ніколи не знімаються: автоматика знає модель пристрою, але не знає, чому цьому хосту дали ще один шаблон руками. DiscoveredDevice отримав device_id: зіставляти за адресою не можна — за одним NAT кілька хостів мають ту саму адресу опитування. Заразом дубль перевірки перестав давати «внутрішню помилку». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
268 lines
9 KiB
Go
268 lines
9 KiB
Go
// Package module визначає контракт модуля опитування на боці агента.
|
||
//
|
||
// Плагінність на агенті влаштована простіше, ніж на сервері: динамічного
|
||
// завантаження .so немає навмисно. Причина — єдиний статичний бінарник,
|
||
// який має працювати на Alpine, Windows і роутері з musl, і бюджет
|
||
// RSS < 30 МБ. Тому модулі вкомпільовані, а "активація" означає, що
|
||
// сервер дозволив використання модуля цим зондом.
|
||
package module
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1"
|
||
)
|
||
|
||
// Target — пристрій, який треба опитати.
|
||
type Target struct {
|
||
DeviceID string
|
||
Name string
|
||
Address string
|
||
}
|
||
|
||
// Task — одиниця роботи. Дзеркалить npv1.Task, але вже з розібраними
|
||
// полями: модулю не потрібно знати про protobuf.
|
||
type Task struct {
|
||
CheckID string
|
||
DeviceID string
|
||
InterfaceID string
|
||
// "<plugin>.<check>", напр. icmp.ping
|
||
CheckType string
|
||
// Непрозорий JSON: кожен модуль розбирає свою частину сам.
|
||
Params json.RawMessage
|
||
|
||
Target Target
|
||
Credentials []*npv1.Credential
|
||
Timeout time.Duration
|
||
}
|
||
|
||
// CheckName повертає частину після крапки: "icmp.ping" → "ping".
|
||
func (t Task) CheckName() string {
|
||
_, name, _ := strings.Cut(t.CheckType, ".")
|
||
return name
|
||
}
|
||
|
||
// ModuleKey повертає частину до крапки: "icmp.ping" → "icmp".
|
||
func ModuleKey(checkType string) string {
|
||
key, _, _ := strings.Cut(checkType, ".")
|
||
return key
|
||
}
|
||
|
||
// Metric — узагальнений вимір. Модуль не знає про series_ref: інтернування
|
||
// живе в межах сесії, а модуль може пережити кілька сесій.
|
||
type Metric struct {
|
||
MetricKey string
|
||
Unit string
|
||
Labels map[string]string
|
||
InterfaceID string
|
||
Value float64
|
||
Ts time.Time
|
||
}
|
||
|
||
// SeriesKey — канонічний ключ серії. Порядок labels зафіксовано сортуванням,
|
||
// інакше та сама серія отримувала б різні ref між запусками.
|
||
func (m Metric) SeriesKey(deviceID string) string {
|
||
var sb strings.Builder
|
||
sb.WriteString(deviceID)
|
||
sb.WriteByte(0)
|
||
sb.WriteString(m.InterfaceID)
|
||
sb.WriteByte(0)
|
||
sb.WriteString(m.MetricKey)
|
||
if len(m.Labels) > 0 {
|
||
keys := make([]string, 0, len(m.Labels))
|
||
for k := range m.Labels {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
for _, k := range keys {
|
||
sb.WriteByte(0)
|
||
sb.WriteString(k)
|
||
sb.WriteByte('=')
|
||
sb.WriteString(m.Labels[k])
|
||
}
|
||
}
|
||
return sb.String()
|
||
}
|
||
|
||
// Result — те, що модуль повертає після виконання задачі.
|
||
type Result struct {
|
||
Metrics []Metric
|
||
Icmp *npv1.IcmpResult
|
||
Interfaces []*npv1.InterfaceCounters
|
||
// Сусіди й інвентар портів ідуть на сервер не телеметричним
|
||
// стрімом, а окремим ReportDiscovery: вони рідкі, великі й не
|
||
// прив'язані до моменту часу так, як метрики.
|
||
Neighbors []*npv1.NeighborRecord
|
||
InterfaceRecords []*npv1.InterfaceRecord
|
||
// Системна інформація про вже відомі хости: sysObjectID, sysDescr,
|
||
// sysName. Їде тим самим звітом, бо збирається тією ж сесією.
|
||
Devices []*npv1.DiscoveredDevice
|
||
// Непрозоре навантаження, яке не є метрикою (напр. http.status → тіло відповіді).
|
||
Payload []byte
|
||
}
|
||
|
||
// Module — виконавець одного або кількох типів чеків.
|
||
type Module interface {
|
||
// Key — ідентифікатор модуля: icmp, snmp, topology, ncm.
|
||
Key() string
|
||
// CheckTypes — які типи чеків модуль уміє виконувати.
|
||
CheckTypes() []string
|
||
// Run виконує задачу. Дедлайн передається через ctx; модуль
|
||
// зобов'язаний його поважати, інакше планувальник заб'ється.
|
||
Run(ctx context.Context, task Task) (Result, error)
|
||
// Close звільняє ресурси (пули з'єднань, сокети).
|
||
Close() error
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Реєстр
|
||
// ---------------------------------------------------------------------
|
||
|
||
// Registry — набір вкомпільованих модулів і їхній стан активації.
|
||
//
|
||
// Активацію диктує сервер (ControlDown.ModuleControl). Незареєстрований
|
||
// або неактивний модуль означає, що задача буде відхилена з
|
||
// STATE_REJECTED — це краще, ніж мовчки її пропустити: у сервера
|
||
// лишається слід у core.checks.last_error.
|
||
type Registry struct {
|
||
mu sync.RWMutex
|
||
modules map[string]Module
|
||
active map[string]bool
|
||
// checkType → module key
|
||
routes map[string]string
|
||
}
|
||
|
||
func NewRegistry() *Registry {
|
||
return &Registry{
|
||
modules: make(map[string]Module),
|
||
active: make(map[string]bool),
|
||
routes: make(map[string]string),
|
||
}
|
||
}
|
||
|
||
// Register додає вкомпільований модуль. Модуль неактивний, доки сервер
|
||
// його не увімкне — крім випадку, коли активацію ще не отримано взагалі
|
||
// (див. SetActive/EnsureDefaults).
|
||
func (r *Registry) Register(m Module) error {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
key := m.Key()
|
||
if _, dup := r.modules[key]; dup {
|
||
return fmt.Errorf("модуль %q вже зареєстровано", key)
|
||
}
|
||
for _, ct := range m.CheckTypes() {
|
||
if ModuleKey(ct) != key {
|
||
return fmt.Errorf("модуль %q оголошує чужий тип чека %q", key, ct)
|
||
}
|
||
if owner, dup := r.routes[ct]; dup {
|
||
return fmt.Errorf("тип чека %q вже належить модулю %q", ct, owner)
|
||
}
|
||
}
|
||
|
||
r.modules[key] = m
|
||
for _, ct := range m.CheckTypes() {
|
||
r.routes[ct] = key
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Compiled повертає ключі всіх вкомпільованих модулів — рівно те, що
|
||
// агент оголошує в Hello.build.compiled_modules.
|
||
func (r *Registry) Compiled() []string {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
|
||
keys := make([]string, 0, len(r.modules))
|
||
for k := range r.modules {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
return keys
|
||
}
|
||
|
||
// SetActive застосовує рішення сервера. exclusive = вимкнути все,
|
||
// чого немає в списку.
|
||
func (r *Registry) SetActive(enabled map[string]bool, exclusive bool) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
if exclusive {
|
||
for k := range r.active {
|
||
r.active[k] = false
|
||
}
|
||
}
|
||
for k, v := range enabled {
|
||
if _, known := r.modules[k]; known {
|
||
r.active[k] = v
|
||
}
|
||
}
|
||
}
|
||
|
||
// EnsureDefaults вмикає перелічені модулі, якщо сервер ще нічого не
|
||
// сказав. Потрібно для першого запуску до отримання ModuleControl.
|
||
func (r *Registry) EnsureDefaults(keys ...string) {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
if len(r.active) > 0 {
|
||
return
|
||
}
|
||
for _, k := range keys {
|
||
if _, known := r.modules[k]; known {
|
||
r.active[k] = true
|
||
}
|
||
}
|
||
}
|
||
|
||
func (r *Registry) IsActive(key string) bool {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
return r.active[key]
|
||
}
|
||
|
||
// ErrNoModule — задача адресована модулю, якого немає або він не активний.
|
||
type ErrNoModule struct {
|
||
CheckType string
|
||
Reason string
|
||
}
|
||
|
||
func (e *ErrNoModule) Error() string {
|
||
return fmt.Sprintf("чек %q: %s", e.CheckType, e.Reason)
|
||
}
|
||
|
||
// Resolve знаходить модуль для типу чека.
|
||
func (r *Registry) Resolve(checkType string) (Module, error) {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
|
||
key, ok := r.routes[checkType]
|
||
if !ok {
|
||
return nil, &ErrNoModule{CheckType: checkType, Reason: "невідомий тип чека"}
|
||
}
|
||
if !r.active[key] {
|
||
return nil, &ErrNoModule{CheckType: checkType, Reason: "модуль " + key + " не активований сервером"}
|
||
}
|
||
return r.modules[key], nil
|
||
}
|
||
|
||
// CloseAll закриває всі модулі. Помилки збираються, а не глушаться:
|
||
// зависле з'єднання при завершенні — теж діагностика.
|
||
func (r *Registry) CloseAll() []error {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
var errs []error
|
||
for key, m := range r.modules {
|
||
if err := m.Close(); err != nil {
|
||
errs = append(errs, fmt.Errorf("модуль %s: %w", key, err))
|
||
}
|
||
}
|
||
return errs
|
||
}
|