Netpulse_SasS/agent/internal/module/module.go
byrsapty 29f440d436
Some checks are pending
CI / web (push) Waiting to run
CI / server (push) Waiting to run
CI / agent (push) Waiting to run
Автопризначення шаблонів за sysObjectID
Пристрій сам каже, що він таке, і шаблон чіпляється без натискань.
Системна група знімається тією ж SNMP-сесією, що й обхід топології:
три зайві PDU дешевші за окремий чек із власним розкладом.

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:29:39 +03:00

268 lines
9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}