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

151 lines
4.7 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 snmpx
import (
"fmt"
"net"
"strconv"
"strings"
)
// Normalize зводить OID до канонічного вигляду з провідною крапкою.
//
// Потрібне тому, що gosnmp повертає pdu.Name ЗАВЖДИ з крапкою, а
// запитувати дозволяє і без неї. Без канонізації пошук у мапі
// результатів мовчки не знаходить нічого, і чек виглядає як «пристрій
// не відповів» — саме так це й проявилось на живому стенді.
func Normalize(oid string) string {
if oid == "" || oid[0] == '.' {
return oid
}
return "." + oid
}
// Index — суфікс OID після базового піддерева, розібраний на числа.
//
// Уся робота з SNMP-таблицями зводиться до одного: узяти OID змінної,
// відрізати базу й прочитати індекс. Для ifTable це ifIndex, для
// cdpCacheTable — (ifIndex, deviceIndex), для ARP — (ifIndex, IPv4),
// для LLDP — (timeMark, localPortNum, remIndex).
type Index []uint64
// SplitIndex відрізає базовий OID і повертає індекс.
// Обидва OID можуть бути з провідною крапкою або без неї.
func SplitIndex(base, full string) (Index, bool) {
b := strings.TrimPrefix(base, ".")
f := strings.TrimPrefix(full, ".")
if !strings.HasPrefix(f, b+".") {
return nil, false
}
rest := f[len(b)+1:]
if rest == "" {
return nil, false
}
parts := strings.Split(rest, ".")
idx := make(Index, 0, len(parts))
for _, p := range parts {
n, err := strconv.ParseUint(p, 10, 64)
if err != nil {
return nil, false
}
idx = append(idx, n)
}
return idx, true
}
// At повертає i-й елемент індексу.
func (ix Index) At(i int) (uint64, bool) {
if i < 0 || i >= len(ix) {
return 0, false
}
return ix[i], true
}
// Key склеює індекс назад у рядок — зручно як ключ мапи, коли треба
// зіставити кілька колонок однієї таблиці.
func (ix Index) Key() string {
parts := make([]string, len(ix))
for i, v := range ix {
parts[i] = strconv.FormatUint(v, 10)
}
return strings.Join(parts, ".")
}
// IPv4At читає чотири октети індексу як IPv4-адресу.
// Так влаштований індекс ipNetToMediaTable: ifIndex.a.b.c.d
func (ix Index) IPv4At(offset int) (string, bool) {
if offset < 0 || offset+4 > len(ix) {
return "", false
}
for i := offset; i < offset+4; i++ {
if ix[i] > 255 {
return "", false
}
}
return fmt.Sprintf("%d.%d.%d.%d", ix[offset], ix[offset+1], ix[offset+2], ix[offset+3]), true
}
// MACAt читає шість октетів індексу як MAC.
// Так влаштований індекс dot1dTpFdbTable.
func (ix Index) MACAt(offset int) (string, bool) {
if offset < 0 || offset+6 > len(ix) {
return "", false
}
b := make([]byte, 6)
for i := 0; i < 6; i++ {
if ix[offset+i] > 255 {
return "", false
}
b[i] = byte(ix[offset+i])
}
return net.HardwareAddr(b).String(), true
}
// FormatMAC перетворює сирі байти в канонічний вигляд.
//
// Обладнання віддає MAC по-різному: 6 сирих байтів (норма), текстом
// із дефісами або двокрапками, іноді з великими літерами. Зводимо все
// до одного вигляду, інакше зіставлення сусіда з інвентарем провалиться
// через косметичну різницю.
func FormatMAC(raw []byte) string {
if len(raw) == 6 {
return net.HardwareAddr(raw).String()
}
s := strings.TrimSpace(strings.TrimRight(string(raw), "\x00"))
if s == "" {
return ""
}
if hw, err := net.ParseMAC(s); err == nil {
return hw.String()
}
// Формат Cisco: 0011.2233.4455
cleaned := strings.NewReplacer(".", "", ":", "", "-", "", " ", "").Replace(s)
if len(cleaned) == 12 {
b := make([]byte, 6)
for i := 0; i < 6; i++ {
v, err := strconv.ParseUint(cleaned[i*2:i*2+2], 16, 8)
if err != nil {
return ""
}
b[i] = byte(v)
}
return net.HardwareAddr(b).String()
}
return ""
}
// FormatIP розбирає адресу з OCTET STRING (4 або 16 байтів) чи тексту.
func FormatIP(raw []byte) string {
switch len(raw) {
case 4, 16:
return net.IP(raw).String()
}
s := strings.TrimSpace(strings.TrimRight(string(raw), "\x00"))
if ip := net.ParseIP(s); ip != nil {
return ip.String()
}
return ""
}