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