Автовиявлення наповнювало 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>
151 lines
4.7 KiB
Go
151 lines
4.7 KiB
Go
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 ""
|
||
}
|