Netpulse_SasS/agent/internal/modules/topology/topology.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

719 lines
22 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 topology — автовиявлення сусідів і інвентаризація портів.
//
// Модуль НЕ вирішує, хто з ким з'єднаний. Він доповідає сире:
// «на порту X бачу chassis Y, port Z». Зведення в лінки — на сервері
// (topo.neighbors → topo.links), і саме тому правила зіставлення можна
// міняти без оновлення зондів у полі.
package topology
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/gosnmp/gosnmp"
"github.com/netpulse/netpulse/agent/internal/module"
"github.com/netpulse/netpulse/agent/internal/snmpx"
npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1"
"google.golang.org/protobuf/types/known/timestamppb"
)
// LLDP-MIB (1.0.8802.1.1.2)
const (
oidLldpLocPortIDSubtype = ".1.0.8802.1.1.2.1.3.7.1.2"
oidLldpLocPortID = ".1.0.8802.1.1.2.1.3.7.1.3"
oidLldpRemChassisIDSubtype = ".1.0.8802.1.1.2.1.4.1.1.4"
oidLldpRemChassisID = ".1.0.8802.1.1.2.1.4.1.1.5"
oidLldpRemPortIDSubtype = ".1.0.8802.1.1.2.1.4.1.1.6"
oidLldpRemPortID = ".1.0.8802.1.1.2.1.4.1.1.7"
oidLldpRemPortDesc = ".1.0.8802.1.1.2.1.4.1.1.8"
oidLldpRemSysName = ".1.0.8802.1.1.2.1.4.1.1.9"
oidLldpRemSysCapEnabled = ".1.0.8802.1.1.2.1.4.1.1.12"
)
// CISCO-CDP-MIB (cdpCacheTable). Індекс: ifIndex.deviceIndex —
// перший компонент одразу дає локальний порт, на відміну від LLDP.
const (
oidCdpCacheAddress = ".1.3.6.1.4.1.9.9.23.1.2.1.1.4"
oidCdpCacheVersion = ".1.3.6.1.4.1.9.9.23.1.2.1.1.5"
oidCdpCacheDeviceID = ".1.3.6.1.4.1.9.9.23.1.2.1.1.6"
oidCdpCacheDevicePort = ".1.3.6.1.4.1.9.9.23.1.2.1.1.7"
oidCdpCachePlatform = ".1.3.6.1.4.1.9.9.23.1.2.1.1.8"
oidCdpCacheCapabilities = ".1.3.6.1.4.1.9.9.23.1.2.1.1.9"
)
// IP-MIB / BRIDGE-MIB
const (
oidIPNetToMediaPhysAddress = ".1.3.6.1.2.1.4.22.1.2"
oidDot1dTpFdbPort = ".1.3.6.1.2.1.17.4.3.1.2"
oidDot1dBasePortIfIndex = ".1.3.6.1.2.1.17.1.4.1.2"
)
// IF-MIB
const (
oidIfDescr = ".1.3.6.1.2.1.2.2.1.2"
oidIfType = ".1.3.6.1.2.1.2.2.1.3"
oidIfMtu = ".1.3.6.1.2.1.2.2.1.4"
oidIfSpeed = ".1.3.6.1.2.1.2.2.1.5"
oidIfPhysAddress = ".1.3.6.1.2.1.2.2.1.6"
oidIfAdminStatus = ".1.3.6.1.2.1.2.2.1.7"
oidIfOperStatus = ".1.3.6.1.2.1.2.2.1.8"
oidIfName = ".1.3.6.1.2.1.31.1.1.1.1"
oidIfHighSpeed = ".1.3.6.1.2.1.31.1.1.1.15"
oidIfAlias = ".1.3.6.1.2.1.31.1.1.1.18"
)
// Params — вміст params_json для topology.discover.
type Params struct {
// lldp | cdp | arp | fdb. Порожньо — lldp + cdp.
Protos []string `json:"protos"`
// Збирати інвентар портів разом із сусідами. Обидва беруться з
// одного SNMP-обходу, тому окремий чек був би зайвим трафіком.
CollectInterfaces *bool `json:"collect_interfaces"`
}
type Module struct{}
func New() *Module { return &Module{} }
func (m *Module) Key() string { return "topology" }
func (m *Module) CheckTypes() []string { return []string{"topology.discover"} }
func (m *Module) Close() error { return nil }
func (m *Module) Run(ctx context.Context, task module.Task) (module.Result, error) {
var p Params
if len(task.Params) > 0 {
if err := json.Unmarshal(task.Params, &p); err != nil {
return module.Result{}, fmt.Errorf("невалідні params для topology.discover: %w", err)
}
}
if len(p.Protos) == 0 {
p.Protos = []string{"lldp", "cdp"}
}
collectIfaces := p.CollectInterfaces == nil || *p.CollectInterfaces
client, err := snmpx.Dial(ctx, task.Target.Address, task.Credentials, task.Timeout)
if err != nil {
return module.Result{}, err
}
defer client.Conn.Close()
var (
res module.Result
errs []string
)
// Системна група — найдешевше, що можна взяти з цієї сесії, і
// єдине джерело для автопризначення шаблонів за sysObjectID.
if d := collectSystem(ctx, client, task.DeviceID, task.Target.Address); d != nil {
res.Devices = append(res.Devices, d)
}
// Інвентар портів потрібен першим: LLDP оперує власною нумерацією
// портів, і без ifName/ifDescr її нема на що відобразити.
ifaces, err := collectInterfaces(ctx, client, task.DeviceID)
if err != nil {
errs = append(errs, "інтерфейси: "+err.Error())
} else if collectIfaces {
res.InterfaceRecords = ifaces
}
byIndex := make(map[int64]*npv1.InterfaceRecord, len(ifaces))
for _, r := range ifaces {
byIndex[r.IfIndex] = r
}
for _, proto := range p.Protos {
var (
found []*npv1.NeighborRecord
perr error
)
switch strings.ToLower(proto) {
case "lldp":
found, perr = collectLLDP(ctx, client, task.DeviceID, byIndex)
case "cdp":
found, perr = collectCDP(ctx, client, task.DeviceID, byIndex)
case "arp":
found, perr = collectARP(ctx, client, task.DeviceID, byIndex)
case "fdb":
found, perr = collectFDB(ctx, client, task.DeviceID, byIndex)
default:
continue
}
if perr != nil {
// Відсутність LLDP-MIB — норма для половини обладнання,
// а не привід завалити весь чек: CDP може працювати.
errs = append(errs, proto+": "+perr.Error())
continue
}
res.Neighbors = append(res.Neighbors, found...)
}
if len(res.Neighbors) == 0 && len(res.InterfaceRecords) == 0 && len(errs) > 0 {
return module.Result{}, fmt.Errorf("автовиявлення не дало результату: %s", strings.Join(errs, "; "))
}
if len(errs) > 0 {
res.Payload, _ = json.Marshal(map[string]any{"warnings": errs})
}
return res, nil
}
// ---------------------------------------------------------------------
// Інтерфейси
// ---------------------------------------------------------------------
func collectInterfaces(ctx context.Context, c *gosnmp.GoSNMP, deviceID string) ([]*npv1.InterfaceRecord, error) {
recs := make(map[uint64]*npv1.InterfaceRecord)
get := func(idx uint64) *npv1.InterfaceRecord {
r, ok := recs[idx]
if !ok {
r = &npv1.InterfaceRecord{DeviceId: deviceID, IfIndex: int64(idx)}
recs[idx] = r
}
return r
}
columns := []struct {
oid string
apply func(r *npv1.InterfaceRecord, pdu gosnmp.SnmpPDU)
}{
{oidIfDescr, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if r.Name == "" {
r.Name = snmpx.AsString(p)
}
}},
// ifName точніший за ifDescr і має пріоритет: саме його
// віддає LLDP як port-id, тому саме за ним зійдеться лінк.
{oidIfName, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if s := snmpx.AsString(p); s != "" {
r.Name = s
}
}},
{oidIfAlias, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
r.Alias = snmpx.AsString(p)
}},
{oidIfType, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(p); ok {
r.Type = ifTypeName(v)
}
}},
{oidIfMtu, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(p); ok {
r.Mtu = uint32(v)
}
}},
{oidIfSpeed, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(p); ok && r.SpeedBps == 0 {
r.SpeedBps = v
}
}},
// ifSpeed 32-бітний і на 10G+ упирається в 4294967295.
// ifHighSpeed (у Мбіт/с) — єдине джерело правди для швидких портів.
{oidIfHighSpeed, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(p); ok && v > 0 {
r.SpeedBps = v * 1_000_000
}
}},
{oidIfPhysAddress, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if b, ok := snmpx.AsBytes(p); ok {
r.Mac = snmpx.FormatMAC(b)
}
}},
{oidIfAdminStatus, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(p); ok {
r.AdminStatus = adminStatusName(v)
}
}},
{oidIfOperStatus, func(r *npv1.InterfaceRecord, p gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(p); ok {
r.OperStatus = operStatusName(v)
}
}},
}
for _, col := range columns {
oid := col.oid
apply := col.apply
err := snmpx.Walk(ctx, c, oid, func(pdu gosnmp.SnmpPDU) error {
idx, ok := snmpx.SplitIndex(oid, pdu.Name)
if !ok || len(idx) != 1 {
return nil
}
apply(get(idx[0]), pdu)
return nil
})
if err != nil && oid == oidIfDescr {
// Без ifDescr таблиці інтерфейсів фактично немає.
return nil, err
}
}
out := make([]*npv1.InterfaceRecord, 0, len(recs))
for _, r := range recs {
if r.Name == "" {
r.Name = fmt.Sprintf("if%d", r.IfIndex)
}
out = append(out, r)
}
return out, nil
}
// ---------------------------------------------------------------------
// LLDP
// ---------------------------------------------------------------------
// collectLLDP читає lldpRemTable.
//
// Індекс рядка: timeMark.lldpRemLocalPortNum.lldpRemIndex. Другий
// компонент — НЕ ifIndex, а власна нумерація LLDP. Більшість
// реалізацій робить її рівною ifIndex, але покладатись на це не можна,
// тому спершу пробуємо відобразити через lldpLocPortId (ім'я порту).
func collectLLDP(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
localPorts, err := lldpLocalPortMap(ctx, c)
if err != nil {
return nil, err
}
byRow := make(map[string]*npv1.NeighborRecord)
subtypes := make(map[string]uint64)
portSubtypes := make(map[string]uint64)
row := func(idx snmpx.Index) *npv1.NeighborRecord {
key := idx.Key()
r, ok := byRow[key]
if !ok {
r = &npv1.NeighborRecord{
DeviceId: deviceID,
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_LLDP,
SeenAt: timestamppb.Now(),
}
if portNum, ok := idx.At(1); ok {
resolveLocalPort(r, portNum, localPorts, byIndex)
}
byRow[key] = r
}
return r
}
walkCol := func(oid string, fn func(r *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU)) error {
return snmpx.Walk(ctx, c, oid, func(pdu gosnmp.SnmpPDU) error {
idx, ok := snmpx.SplitIndex(oid, pdu.Name)
if !ok || len(idx) < 3 {
return nil
}
fn(row(idx), idx, pdu)
return nil
})
}
// Підтипи читаємо першими: від них залежить, як тлумачити
// chassis-id і port-id — як MAC чи як текст.
if err := walkCol(oidLldpRemChassisIDSubtype, func(_ *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(pdu); ok {
subtypes[idx.Key()] = v
}
}); err != nil {
return nil, err
}
_ = walkCol(oidLldpRemPortIDSubtype, func(_ *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
if v, ok := snmpx.AsUint(pdu); ok {
portSubtypes[idx.Key()] = v
}
})
if err := walkCol(oidLldpRemChassisID, func(r *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
raw, _ := snmpx.AsBytes(pdu)
// subtype 4 = macAddress
if subtypes[idx.Key()] == 4 {
if mac := snmpx.FormatMAC(raw); mac != "" {
r.RemoteChassisId = mac
r.RemoteMac = mac
return
}
}
r.RemoteChassisId = snmpx.AsString(pdu)
}); err != nil {
return nil, err
}
_ = walkCol(oidLldpRemPortID, func(r *npv1.NeighborRecord, idx snmpx.Index, pdu gosnmp.SnmpPDU) {
raw, _ := snmpx.AsBytes(pdu)
// subtype 3 = macAddress
if portSubtypes[idx.Key()] == 3 {
if mac := snmpx.FormatMAC(raw); mac != "" {
r.RemotePortId = mac
return
}
}
r.RemotePortId = snmpx.AsString(pdu)
})
_ = walkCol(oidLldpRemPortDesc, func(r *npv1.NeighborRecord, _ snmpx.Index, pdu gosnmp.SnmpPDU) {
r.RemotePortDescr = snmpx.AsString(pdu)
})
_ = walkCol(oidLldpRemSysName, func(r *npv1.NeighborRecord, _ snmpx.Index, pdu gosnmp.SnmpPDU) {
r.RemoteSystemName = snmpx.AsString(pdu)
})
_ = walkCol(oidLldpRemSysCapEnabled, func(r *npv1.NeighborRecord, _ snmpx.Index, pdu gosnmp.SnmpPDU) {
if raw, ok := snmpx.AsBytes(pdu); ok {
r.RemoteCapabilities = decodeLldpCaps(raw)
}
})
out := make([]*npv1.NeighborRecord, 0, len(byRow))
for _, r := range byRow {
// Сусід без жодного ідентифікатора не зіставиться ні з чим —
// краще не засмічувати topo.neighbors.
if r.RemoteChassisId == "" && r.RemoteSystemName == "" && r.RemoteMac == "" {
continue
}
out = append(out, r)
}
return out, nil
}
// lldpLocalPortMap: lldpRemLocalPortNum → ім'я локального порту.
func lldpLocalPortMap(ctx context.Context, c *gosnmp.GoSNMP) (map[uint64]string, error) {
subtypes := make(map[uint64]uint64)
_ = snmpx.Walk(ctx, c, oidLldpLocPortIDSubtype, func(pdu gosnmp.SnmpPDU) error {
if idx, ok := snmpx.SplitIndex(oidLldpLocPortIDSubtype, pdu.Name); ok && len(idx) == 1 {
if v, ok := snmpx.AsUint(pdu); ok {
subtypes[idx[0]] = v
}
}
return nil
})
names := make(map[uint64]string)
err := snmpx.Walk(ctx, c, oidLldpLocPortID, func(pdu gosnmp.SnmpPDU) error {
idx, ok := snmpx.SplitIndex(oidLldpLocPortID, pdu.Name)
if !ok || len(idx) != 1 {
return nil
}
// subtype 5 = interfaceName, 7 = local — обидва текстові.
names[idx[0]] = snmpx.AsString(pdu)
return nil
})
if err != nil {
return names, err
}
return names, nil
}
// resolveLocalPort прив'язує запис до локального інтерфейсу.
func resolveLocalPort(r *npv1.NeighborRecord, portNum uint64, localPorts map[uint64]string, byIndex map[int64]*npv1.InterfaceRecord) {
name := localPorts[portNum]
if name != "" {
r.LocalPortName = name
for _, ifc := range byIndex {
if strings.EqualFold(ifc.Name, name) || strings.EqualFold(ifc.Alias, name) {
r.LocalIfIndex = ifc.IfIndex
return
}
}
}
// Запасний варіант: більшість реалізацій робить lldpLocPortNum
// рівним ifIndex. Приймаємо лише якщо такий інтерфейс справді є —
// інакше сервер отримає посилання в нікуди.
if ifc, ok := byIndex[int64(portNum)]; ok {
r.LocalIfIndex = ifc.IfIndex
if r.LocalPortName == "" {
r.LocalPortName = ifc.Name
}
}
}
// decodeLldpCaps розбирає бітову маску можливостей (LLDP-MIB).
func decodeLldpCaps(raw []byte) []string {
if len(raw) == 0 {
return nil
}
names := []string{"other", "repeater", "bridge", "wlan-ap", "router",
"telephone", "docsis", "station"}
var out []string
bits := raw[0]
for i, name := range names {
if i >= 8 {
break
}
if bits&(1<<(7-uint(i))) != 0 {
out = append(out, name)
}
}
return out
}
// ---------------------------------------------------------------------
// CDP
// ---------------------------------------------------------------------
func collectCDP(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
byRow := make(map[string]*npv1.NeighborRecord)
row := func(idx snmpx.Index) *npv1.NeighborRecord {
key := idx.Key()
r, ok := byRow[key]
if !ok {
r = &npv1.NeighborRecord{
DeviceId: deviceID,
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_CDP,
SeenAt: timestamppb.Now(),
}
// У CDP перший компонент індексу — одразу ifIndex.
if ifIdx, ok := idx.At(0); ok {
if ifc, exists := byIndex[int64(ifIdx)]; exists {
r.LocalIfIndex = ifc.IfIndex
r.LocalPortName = ifc.Name
} else {
r.LocalIfIndex = int64(ifIdx)
}
}
byRow[key] = r
}
return r
}
walkCol := func(oid string, fn func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU)) error {
return snmpx.Walk(ctx, c, oid, func(pdu gosnmp.SnmpPDU) error {
idx, ok := snmpx.SplitIndex(oid, pdu.Name)
if !ok || len(idx) < 2 {
return nil
}
fn(row(idx), pdu)
return nil
})
}
if err := walkCol(oidCdpCacheDeviceID, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
s := snmpx.AsString(pdu)
r.RemoteSystemName = s
// CDP device-id часто є серійником або MAC, а не іменем.
// Якщо це схоже на MAC — кладемо і в chassis_id, бо саме за
// ним сервер зіставляє надійніше, ніж за іменем.
if raw, ok := snmpx.AsBytes(pdu); ok {
if mac := snmpx.FormatMAC(raw); mac != "" {
r.RemoteChassisId = mac
r.RemoteMac = mac
}
}
if r.RemoteChassisId == "" {
r.RemoteChassisId = s
}
}); err != nil {
return nil, err
}
_ = walkCol(oidCdpCacheDevicePort, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
r.RemotePortId = snmpx.AsString(pdu)
})
_ = walkCol(oidCdpCachePlatform, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
r.RemotePlatform = snmpx.AsString(pdu)
})
_ = walkCol(oidCdpCacheVersion, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
if r.RemotePortDescr == "" {
r.RemotePortDescr = truncate(snmpx.AsString(pdu), 120)
}
})
_ = walkCol(oidCdpCacheAddress, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
if raw, ok := snmpx.AsBytes(pdu); ok {
r.RemoteMgmtIp = snmpx.FormatIP(raw)
}
})
_ = walkCol(oidCdpCacheCapabilities, func(r *npv1.NeighborRecord, pdu gosnmp.SnmpPDU) {
if raw, ok := snmpx.AsBytes(pdu); ok {
r.RemoteCapabilities = decodeCdpCaps(raw)
}
})
out := make([]*npv1.NeighborRecord, 0, len(byRow))
for _, r := range byRow {
if r.RemoteChassisId == "" && r.RemoteSystemName == "" {
continue
}
out = append(out, r)
}
return out, nil
}
func decodeCdpCaps(raw []byte) []string {
if len(raw) < 4 {
return nil
}
bits := uint32(raw[0])<<24 | uint32(raw[1])<<16 | uint32(raw[2])<<8 | uint32(raw[3])
names := map[uint32]string{
0x01: "router", 0x02: "transparent-bridge", 0x04: "source-route-bridge",
0x08: "switch", 0x10: "host", 0x20: "igmp", 0x40: "repeater",
}
var out []string
for bit, name := range names {
if bits&bit != 0 {
out = append(out, name)
}
}
return out
}
// ---------------------------------------------------------------------
// ARP і FDB
//
// Це не «сусіди» в сенсі LLDP: ARP каже лише, що якийсь MAC живе за
// цим портом. Але для пристроїв без LLDP/CDP (принтери, IP-камери,
// дешеві комутатори) це єдиний шанс потрапити на мапу, тому сервер
// приймає їх з низькою впевненістю.
// ---------------------------------------------------------------------
func collectARP(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
var out []*npv1.NeighborRecord
err := snmpx.Walk(ctx, c, oidIPNetToMediaPhysAddress, func(pdu gosnmp.SnmpPDU) error {
idx, ok := snmpx.SplitIndex(oidIPNetToMediaPhysAddress, pdu.Name)
if !ok || len(idx) < 5 {
return nil
}
ip, ok := idx.IPv4At(1)
if !ok {
return nil
}
raw, _ := snmpx.AsBytes(pdu)
mac := snmpx.FormatMAC(raw)
if mac == "" {
return nil
}
r := &npv1.NeighborRecord{
DeviceId: deviceID,
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_ARP,
RemoteMac: mac,
RemoteMgmtIp: ip,
SeenAt: timestamppb.Now(),
}
if ifc, exists := byIndex[int64(idx[0])]; exists {
r.LocalIfIndex = ifc.IfIndex
r.LocalPortName = ifc.Name
} else {
r.LocalIfIndex = int64(idx[0])
}
out = append(out, r)
return nil
})
return out, err
}
func collectFDB(ctx context.Context, c *gosnmp.GoSNMP, deviceID string, byIndex map[int64]*npv1.InterfaceRecord) ([]*npv1.NeighborRecord, error) {
// dot1dTpFdbPort віддає НЕ ifIndex, а номер мосту (bridge port).
// Без dot1dBasePortIfIndex ці числа ні з чим не зіставити.
bridgeToIf := make(map[uint64]int64)
_ = snmpx.Walk(ctx, c, oidDot1dBasePortIfIndex, func(pdu gosnmp.SnmpPDU) error {
idx, ok := snmpx.SplitIndex(oidDot1dBasePortIfIndex, pdu.Name)
if !ok || len(idx) != 1 {
return nil
}
if v, ok := snmpx.AsUint(pdu); ok {
bridgeToIf[idx[0]] = int64(v)
}
return nil
})
var out []*npv1.NeighborRecord
err := snmpx.Walk(ctx, c, oidDot1dTpFdbPort, func(pdu gosnmp.SnmpPDU) error {
idx, ok := snmpx.SplitIndex(oidDot1dTpFdbPort, pdu.Name)
if !ok || len(idx) < 6 {
return nil
}
mac, ok := idx.MACAt(len(idx) - 6)
if !ok {
return nil
}
port, ok := snmpx.AsUint(pdu)
if !ok || port == 0 {
return nil
}
r := &npv1.NeighborRecord{
DeviceId: deviceID,
Proto: npv1.DiscoveryProto_DISCOVERY_PROTO_FDB,
RemoteMac: mac,
SeenAt: timestamppb.Now(),
}
if ifIdx, exists := bridgeToIf[port]; exists {
r.LocalIfIndex = ifIdx
if ifc, ok := byIndex[ifIdx]; ok {
r.LocalPortName = ifc.Name
}
}
out = append(out, r)
return nil
})
return out, err
}
// ---------------------------------------------------------------------
func adminStatusName(v uint64) string {
switch v {
case 1:
return "up"
case 2:
return "down"
case 3:
return "testing"
default:
return "unknown"
}
}
func operStatusName(v uint64) string {
switch v {
case 1:
return "up"
case 2:
return "down"
case 3:
return "testing"
case 5:
return "dormant"
case 6:
return "notPresent"
case 7:
return "lowerLayerDown"
default:
return "unknown"
}
}
func ifTypeName(v uint64) string {
switch v {
case 6:
return "ethernetCsmacd"
case 24:
return "softwareLoopback"
case 53:
return "propVirtual"
case 131:
return "tunnel"
case 135:
return "l2vlan"
case 136:
return "l3ipvlan"
case 161:
return "ieee8023adLag"
default:
return fmt.Sprintf("type%d", v)
}
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}