package snmp import ( "context" "fmt" "strconv" "strings" "sync" "time" "github.com/gosnmp/gosnmp" ) // ============================================ // SNMP 采集器 // ============================================ // Collector SNMP采集器 // 封装 gosnmp 库,提供SNMP设备的OID采集功能 // 支持 SNMP v1/v2c/v3,支持 Get/Walk/BulkWalk 操作 type Collector struct { config *gosnmp.GoSNMP mu sync.Mutex } // CollectorConfig 采集器配置 type CollectorConfig struct { IP string // 目标IP地址 Port uint16 // SNMP端口,默认161 Version gosnmp.SnmpVersion // SNMP版本 Community string // 共同体字符串(v1/v2c) Timeout time.Duration // 超时时间 Retries int // 重试次数 MaxRepetitions uint8 // BulkWalk最大重复数 // SNMP v3 参数 SecurityModel gosnmp.SnmpV3SecurityModel MsgFlags gosnmp.SnmpV3MsgFlags SecurityParameters *gosnmp.UsmSecurityParameters } // WalkResult SNMP Walk结果 type WalkResult struct { OID string `json:"oid"` // OID Type gosnmp.Asn1BER `json:"type"` // SNMP数据类型 TypeName string `json:"type_name"` // 类型名称 Value interface{} `json:"value"` // 值 Index string `json:"index"` // 索引后缀 Timestamp time.Time `json:"timestamp"` // 采集时间 } // CollectResult 采集结果 type CollectResult struct { DeviceID uint `json:"device_id"` DeviceIP string `json:"device_ip"` Timestamp int64 `json:"timestamp"` Success bool `json:"success"` Error string `json:"error,omitempty"` Metrics []SNMPMetricValue `json:"metrics"` Duration float64 `json:"duration_ms"` // 采集耗时(ms) } // SNMPMetricValue SNMP采集指标值 type SNMPMetricValue struct { Name string `json:"name"` OID string `json:"oid"` Index string `json:"index,omitempty"` Value float64 `json:"value"` RawValue string `json:"raw_value,omitempty"` Unit string `json:"unit,omitempty"` DataType string `json:"data_type"` } // NewCollector 创建SNMP采集器 // config: 采集器配置 // 返回配置好的SNMP采集器实例和错误 func NewCollector(config *CollectorConfig) (*Collector, error) { if config.IP == "" { return nil, fmt.Errorf("target IP is required") } g := &gosnmp.GoSNMP{ Target: config.IP, Port: config.Port, Version: config.Version, Timeout: config.Timeout, Retries: config.Retries, MaxRepetitions: config.MaxRepetitions, ExponentialTimeout: true, } // 设置默认值 if g.Port == 0 { g.Port = 161 } if g.Timeout == 0 { g.Timeout = 10 * time.Second } if g.Retries == 0 { g.Retries = 3 } if g.MaxRepetitions == 0 { g.MaxRepetitions = 25 } // 根据版本设置认证参数 switch config.Version { case gosnmp.Version1: g.Community = config.Community if g.Community == "" { g.Community = "public" } case gosnmp.Version2c: g.Community = config.Community if g.Community == "" { g.Community = "public" } case gosnmp.Version3: g.SecurityModel = config.SecurityModel if g.SecurityModel == 0 { g.SecurityModel = gosnmp.UserSecurityModel } g.MsgFlags = config.MsgFlags if config.SecurityParameters != nil { g.SecurityParameters = config.SecurityParameters } default: return nil, fmt.Errorf("unsupported SNMP version: %v", config.Version) } return &Collector{ config: g, }, nil } // NewCollectorV2c 快速创建SNMPv2c采集器 // ip: 目标IP // community: 共同体字符串 func NewCollectorV2c(ip, community string) (*Collector, error) { if community == "" { community = "public" } return NewCollector(&CollectorConfig{ IP: ip, Port: 161, Version: gosnmp.Version2c, Community: community, Timeout: 10 * time.Second, Retries: 3, }) } // NewCollectorV3 快速创建SNMPv3采集器 // ip: 目标IP // username: 用户名 // authProtocol: 认证协议 // authPassword: 认证密码 // privProtocol: 加密协议 // privPassword: 加密密码 // securityLevel: 安全级别 func NewCollectorV3(ip, username string, authProtocol gosnmp.SnmpV3AuthProtocol, authPassword string, privProtocol gosnmp.SnmpV3PrivProtocol, privPassword string, securityLevel gosnmp.SnmpV3MsgFlags) (*Collector, error) { securityParams := &gosnmp.UsmSecurityParameters{ UserName: username, AuthenticationProtocol: authProtocol, AuthenticationPassphrase: authPassword, PrivacyProtocol: privProtocol, PrivacyPassphrase: privPassword, } return NewCollector(&CollectorConfig{ IP: ip, Port: 161, Version: gosnmp.Version3, Timeout: 10 * time.Second, Retries: 3, MsgFlags: securityLevel, SecurityParameters: securityParams, }) } // Connect 建立SNMP连接 func (c *Collector) Connect() error { c.mu.Lock() defer c.mu.Unlock() if err := c.config.Connect(); err != nil { return fmt.Errorf("SNMP connect to %s failed: %w", c.config.Target, err) } return nil } // Close 关闭SNMP连接 func (c *Collector) Close() error { c.mu.Lock() defer c.mu.Unlock() return c.config.Conn.Close() } // ============================================ // SNMP Get 操作 // ============================================ // Get 执行SNMP Get操作,获取指定OID的值 // oids: OID字符串列表 // 返回OID到值的映射和错误 func (c *Collector) Get(ctx context.Context, oids []string) (map[string]*WalkResult, error) { if len(oids) == 0 { return nil, fmt.Errorf("oids are required") } // 解析OID parsedOIDs := make([]string, 0, len(oids)) for _, oid := range oids { parsedOIDs = append(parsedOIDs, oid) } c.mu.Lock() defer c.mu.Unlock() // 确保连接 if c.config.Conn == nil { if err := c.config.Connect(); err != nil { return nil, fmt.Errorf("connect failed: %w", err) } } result, err := c.config.Get(parsedOIDs) if err != nil { return nil, fmt.Errorf("SNMP Get failed: %w", err) } results := make(map[string]*WalkResult) for _, v := range result.Variables { r := &WalkResult{ OID: v.Name, Type: v.Type, TypeName: getSNMPTypeName(v.Type), Value: convertSNMPValue(v), Timestamp: time.Now(), } results[v.Name] = r } return results, nil } // GetSingle 获取单个OID的值 func (c *Collector) GetSingle(ctx context.Context, oid string) (*WalkResult, error) { results, err := c.Get(ctx, []string{oid}) if err != nil { return nil, err } result, ok := results[oid] if !ok { return nil, fmt.Errorf("OID not found in response: %s", oid) } return result, nil } // ============================================ // SNMP Walk 操作 // ============================================ // Walk 执行SNMP Walk操作,遍历指定OID下的所有子节点 // rootOID: 根OID // 返回遍历结果列表和错误 func (c *Collector) Walk(ctx context.Context, rootOID string) ([]*WalkResult, error) { if rootOID == "" { return nil, fmt.Errorf("root OID is required") } c.mu.Lock() defer c.mu.Unlock() // 确保连接 if c.config.Conn == nil { if err := c.config.Connect(); err != nil { return nil, fmt.Errorf("connect failed: %w", err) } } var results []*WalkResult walkFn := func(data gosnmp.SnmpPDU) error { index := "" if strings.HasPrefix(data.Name, rootOID) && len(data.Name) > len(rootOID) { suffix := strings.TrimPrefix(data.Name, rootOID) index = strings.TrimPrefix(suffix, ".") } r := &WalkResult{ OID: data.Name, Type: data.Type, TypeName: getSNMPTypeName(data.Type), Value: convertSNMPValue(data), Index: index, Timestamp: time.Now(), } results = append(results, r) return nil } if err := c.config.Walk(rootOID, walkFn); err != nil { return nil, fmt.Errorf("SNMP Walk failed for OID %s: %w", rootOID, err) } return results, nil } // ============================================ // SNMP BulkWalk 操作 // ============================================ // BulkWalk 执行SNMP BulkWalk操作,高效遍历指定OID下的所有子节点 // 相比Walk使用GetBulk请求,效率更高,适合遍历大量数据 // rootOID: 根OID // 返回遍历结果列表和错误 func (c *Collector) BulkWalk(ctx context.Context, rootOID string) ([]*WalkResult, error) { if rootOID == "" { return nil, fmt.Errorf("root OID is required") } c.mu.Lock() defer c.mu.Unlock() // 确保连接 if c.config.Conn == nil { if err := c.config.Connect(); err != nil { return nil, fmt.Errorf("connect failed: %w", err) } } var results []*WalkResult walkFn := func(data gosnmp.SnmpPDU) error { index := "" if strings.HasPrefix(data.Name, rootOID) && len(data.Name) > len(rootOID) { suffix := strings.TrimPrefix(data.Name, rootOID) index = strings.TrimPrefix(suffix, ".") } r := &WalkResult{ OID: data.Name, Type: data.Type, TypeName: getSNMPTypeName(data.Type), Value: convertSNMPValue(data), Index: index, Timestamp: time.Now(), } results = append(results, r) return nil } if err := c.config.BulkWalk(rootOID, walkFn); err != nil { return nil, fmt.Errorf("SNMP BulkWalk failed for OID %s: %w", rootOID, err) } return results, nil } // ============================================ // 批量采集 // ============================================ // OIDDefinition OID定义 type OIDDefinition struct { Name string `json:"name"` // 指标名称 OID string `json:"oid"` // OID Unit string `json:"unit"` // 单位 DataType string `json:"data_type"` // 数据类型 Conversion string `json:"conversion"` // 转换公式 IsTable bool `json:"is_table"` // 是否表OID(使用Walk) } // Collect 执行批量采集 // oids: OID定义列表 // 返回采集结果 func (c *Collector) Collect(ctx context.Context, oids []OIDDefinition) *CollectResult { start := time.Now() result := &CollectResult{ DeviceIP: c.config.Target, Timestamp: time.Now().Unix(), Metrics: make([]SNMPMetricValue, 0), } // 建立连接 if err := c.Connect(); err != nil { result.Success = false result.Error = err.Error() result.Duration = float64(time.Since(start).Milliseconds()) return result } defer c.Close() for _, def := range oids { if def.IsTable { // 表OID使用Walk/BulkWalk walkResults, err := c.BulkWalk(ctx, def.OID) if err != nil { result.Error += fmt.Sprintf("[%s walk failed: %v]", def.Name, err) continue } for _, wr := range walkResults { mv := SNMPMetricValue{ Name: def.Name, OID: wr.OID, Index: wr.Index, Unit: def.Unit, } if val, ok := wr.Value.(float64); ok { mv.Value = applyConversion(val, def.Conversion) mv.RawValue = fmt.Sprintf("%v", wr.Value) } else { mv.RawValue = fmt.Sprintf("%v", wr.Value) if v, err := strconv.ParseFloat(mv.RawValue, 64); err == nil { mv.Value = applyConversion(v, def.Conversion) } } mv.DataType = def.DataType if mv.DataType == "" { mv.DataType = wr.TypeName } result.Metrics = append(result.Metrics, mv) } } else { // 标量OID使用Get wr, err := c.GetSingle(ctx, def.OID) if err != nil { result.Error += fmt.Sprintf("[%s get failed: %v]", def.Name, err) continue } mv := SNMPMetricValue{ Name: def.Name, OID: wr.OID, Unit: def.Unit, } if val, ok := wr.Value.(float64); ok { mv.Value = applyConversion(val, def.Conversion) mv.RawValue = fmt.Sprintf("%v", wr.Value) } else { mv.RawValue = fmt.Sprintf("%v", wr.Value) if v, err := strconv.ParseFloat(mv.RawValue, 64); err == nil { mv.Value = applyConversion(v, def.Conversion) } } mv.DataType = def.DataType if mv.DataType == "" { mv.DataType = wr.TypeName } result.Metrics = append(result.Metrics, mv) } } result.Success = len(result.Metrics) > 0 result.Duration = float64(time.Since(start).Milliseconds()) return result } // ============================================ // 工具方法 // ============================================ // getSNMPTypeName 获取SNMP数据类型名称 func getSNMPTypeName(t gosnmp.Asn1BER) string { switch t { case gosnmp.Integer: return "integer" case gosnmp.OctetString: return "string" case gosnmp.ObjectIdentifier: return "oid" case gosnmp.IPAddress: return "ipaddress" case gosnmp.Counter32: return "counter32" case gosnmp.Gauge32: return "gauge32" case gosnmp.TimeTicks: return "timeticks" case gosnmp.Opaque: return "opaque" case gosnmp.NsapAddress: return "nsapaddress" case gosnmp.Counter64: return "counter64" case gosnmp.Uinteger32: return "uinteger32" case gosnmp.NoSuchObject: return "nosuchobject" case gosnmp.NoSuchInstance: return "nosuchinstance" default: return fmt.Sprintf("unknown(%d)", t) } } // convertSNMPValue 转换SNMP值为Go类型 func convertSNMPValue(pdu gosnmp.SnmpPDU) interface{} { switch pdu.Type { case gosnmp.Integer, gosnmp.Counter32, gosnmp.Gauge32, gosnmp.TimeTicks, gosnmp.Counter64, gosnmp.Uinteger32: if val, ok := pdu.Value.(int); ok { return float64(val) } if val, ok := pdu.Value.(uint); ok { return float64(val) } if val, ok := pdu.Value.(uint32); ok { return float64(val) } if val, ok := pdu.Value.(uint64); ok { return float64(val) } if val, ok := pdu.Value.(int64); ok { return float64(val) } return pdu.Value case gosnmp.OctetString: if val, ok := pdu.Value.([]byte); ok { return string(val) } return pdu.Value case gosnmp.OpaqueFloat: if val, ok := pdu.Value.(float32); ok { return float64(val) } return pdu.Value case gosnmp.OpaqueDouble: if val, ok := pdu.Value.(float64); ok { return val } return pdu.Value default: return fmt.Sprintf("%v", pdu.Value) } } // applyConversion 应用转换公式 // 支持简单的数学运算: *N, /N, +N, -N func applyConversion(value float64, conversion string) float64 { if conversion == "" { return value } conversion = strings.TrimSpace(conversion) if strings.HasPrefix(conversion, "*") { if f, err := strconv.ParseFloat(strings.TrimPrefix(conversion, "*"), 64); err == nil { return value * f } } if strings.HasPrefix(conversion, "/") { if f, err := strconv.ParseFloat(strings.TrimPrefix(conversion, "/"), 64); err == nil && f != 0 { return value / f } } if strings.HasPrefix(conversion, "+") { if f, err := strconv.ParseFloat(strings.TrimPrefix(conversion, "+"), 64); err == nil { return value + f } } if strings.HasPrefix(conversion, "-") && len(conversion) > 1 { if f, err := strconv.ParseFloat(conversion, 64); err == nil { return value + f // f is negative } } return value } // GetTargetIP 获取目标IP func (c *Collector) GetTargetIP() string { return c.config.Target } // GetVersion 获取SNMP版本 func (c *Collector) GetVersion() gosnmp.SnmpVersion { return c.config.Version } // ============================================ // 预定义OID常量 (RFC1213-MIB, IF-MIB) // ============================================ // 系统信息 OID const ( OIDSysDescr = ".1.3.6.1.2.1.1.1.0" // 系统描述 OIDSysObjectID = ".1.3.6.1.2.1.1.2.0" // 系统对象ID OIDSysUptime = ".1.3.6.1.2.1.1.3.0" // 系统运行时间 OIDSysContact = ".1.3.6.1.2.1.1.4.0" // 系统联系人 OIDSysName = ".1.3.6.1.2.1.1.5.0" // 系统名称 OIDSysLocation = ".1.3.6.1.2.1.1.6.0" // 系统位置 ) // 接口信息 OID (IF-MIB) const ( OIDIfNumber = ".1.3.6.1.2.1.2.1.0" // 接口数量 OIDIfTable = ".1.3.6.1.2.1.2.2" // 接口表 OIDIfDescr = ".1.3.6.1.2.1.2.2.1.2" // 接口描述 OIDIfType = ".1.3.6.1.2.1.2.2.1.3" // 接口类型 OIDIfSpeed = ".1.3.6.1.2.1.2.2.1.5" // 接口速率 OIDIfPhysAddr = ".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" // 操作状态 OIDIfInOctets = ".1.3.6.1.2.1.2.2.1.10" // 接收字节数 OIDIfInUcastPkts = ".1.3.6.1.2.1.2.2.1.11" // 接收单播包数 OIDIfInErrors = ".1.3.6.1.2.1.2.2.1.14" // 接收错误数 OIDIfOutOctets = ".1.3.6.1.2.1.2.2.1.16" // 发送字节数 OIDIfOutUcastPkts = ".1.3.6.1.2.1.2.2.1.17" // 发送单播包数 OIDIfOutErrors = ".1.3.6.1.2.1.2.2.1.20" // 发送错误数 ) // HostResource MIB OID const ( OIDHrSystemUptime = ".1.3.6.1.2.1.25.1.1.0" // 系统运行时间 OIDHrProcessorLoad = ".1.3.6.1.2.1.25.3.3.1.2" // CPU负载 OIDHrStorageTable = ".1.3.6.1.2.1.25.2.3" // 存储表 OIDHrStorageDescr = ".1.3.6.1.2.1.25.2.3.1.3" // 存储描述 OIDHrStorageSize = ".1.3.6.1.2.1.25.2.3.1.5" // 存储大小 OIDHrStorageUsed = ".1.3.6.1.2.1.25.2.3.1.6" // 已用存储 )