Netpulse_SasS/agent/internal/config/config.go
zotac 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

164 lines
5.8 KiB
Go
Raw 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 config — налаштування агента з прапорців і змінних оточення.
//
// Конфіг-файлу навмисно немає: у зонда рівно жменя параметрів, а
// відсутність YAML економить залежність і кілька сотень кілобайт
// бінарника. Усе інше агент отримує від сервера в Welcome і ModuleControl.
package config
import (
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
// Куди підключатись: host:port.
Endpoint string
AgentID string
Hostname string
// Токен зонда, виданий при Enroll. Визначає, ЯКИЙ це зонд;
// сертифікат mTLS — що він узагалі має право говорити з сервером.
Token string
// mTLS. Порожні шляхи + Insecure = локальний стенд без шифрування.
CertFile string
KeyFile string
CAFile string
Insecure bool
MaxConcurrency int
BufferMaxItems int
BufferMaxBytes int
MinBackoff time.Duration
MaxBackoff time.Duration
LogLevel string
LogJSON bool
// Модулі, увімкнені до першої відповіді сервера.
DefaultModules []string
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
// Parse читає прапорці поверх змінних оточення (NETPULSE_*).
func Parse(args []string) (*Config, error) {
host, _ := os.Hostname()
c := &Config{}
fs := flag.NewFlagSet("netpulse-agent", flag.ContinueOnError)
fs.StringVar(&c.Endpoint, "server", envOr("NETPULSE_SERVER", ""), "адреса сервера, host:port")
fs.StringVar(&c.AgentID, "agent-id", envOr("NETPULSE_AGENT_ID", ""), "ідентифікатор зонда")
fs.StringVar(&c.Hostname, "hostname", envOr("NETPULSE_HOSTNAME", host), "ім'я хоста для журналу сервера")
fs.StringVar(&c.Token, "token", envOr("NETPULSE_TOKEN", ""), "токен зонда з Enroll")
fs.StringVar(&c.CertFile, "cert", envOr("NETPULSE_CERT", ""), "клієнтський сертифікат (mTLS)")
fs.StringVar(&c.KeyFile, "key", envOr("NETPULSE_KEY", ""), "приватний ключ")
fs.StringVar(&c.CAFile, "ca", envOr("NETPULSE_CA", ""), "CA сервера")
fs.BoolVar(&c.Insecure, "insecure", os.Getenv("NETPULSE_INSECURE") == "1",
"без TLS — лише для локального стенду")
fs.IntVar(&c.MaxConcurrency, "max-concurrency", envInt("NETPULSE_MAX_CONCURRENCY", 64),
"скільки чеків виконувати паралельно")
fs.IntVar(&c.BufferMaxItems, "buffer-items", envInt("NETPULSE_BUFFER_ITEMS", 50_000),
"ліміт записів у буфері телеметрії")
fs.IntVar(&c.BufferMaxBytes, "buffer-bytes", envInt("NETPULSE_BUFFER_BYTES", 8<<20),
"оціночний ліміт пам'яті буфера в байтах")
fs.DurationVar(&c.MinBackoff, "min-backoff", time.Second, "мінімальна пауза перед реконектом")
fs.DurationVar(&c.MaxBackoff, "max-backoff", 2*time.Minute, "максимальна пауза перед реконектом")
fs.StringVar(&c.LogLevel, "log-level", envOr("NETPULSE_LOG_LEVEL", "info"), "debug|info|warn|error")
fs.BoolVar(&c.LogJSON, "log-json", os.Getenv("NETPULSE_LOG_JSON") == "1", "журнал у JSON")
mods := fs.String("modules", envOr("NETPULSE_MODULES", "icmp"),
"модулі, увімкнені до відповіді сервера (через кому)")
if err := fs.Parse(args); err != nil {
return nil, err
}
for _, m := range strings.Split(*mods, ",") {
if m = strings.TrimSpace(m); m != "" {
c.DefaultModules = append(c.DefaultModules, m)
}
}
return c, c.validate()
}
func (c *Config) validate() error {
if c.Endpoint == "" {
return errors.New("не вказано адресу сервера (-server або NETPULSE_SERVER)")
}
if c.AgentID == "" {
return errors.New("не вказано -agent-id: зонд має бути зареєстрований через Enroll")
}
if c.Token == "" {
return errors.New("не вказано -token (або NETPULSE_TOKEN)")
}
if !c.Insecure && (c.CertFile == "" || c.KeyFile == "") {
return errors.New("потрібні -cert і -key для mTLS (або -insecure для локального стенду)")
}
if c.MaxConcurrency <= 0 {
return errors.New("-max-concurrency має бути додатним")
}
return nil
}
// TLSConfig збирає mTLS-конфігурацію.
//
// Перевірка сертифіката сервера обов'язкова й не вимикається прапорцем:
// зонд ходить через інтернет, і «тимчасово без перевірки» — це той
// самий канал, яким їдуть паролі від усіх комутаторів клієнта.
func (c *Config) TLSConfig() (*tls.Config, error) {
if c.Insecure {
return nil, nil
}
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
if err != nil {
return nil, fmt.Errorf("клієнтський сертифікат: %w", err)
}
tc := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
}
if c.CAFile != "" {
pem, err := os.ReadFile(c.CAFile)
if err != nil {
return nil, fmt.Errorf("CA: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pem) {
return nil, errors.New("CA: не вдалося розібрати PEM")
}
tc.RootCAs = pool
}
return tc, nil
}