@cryptotaxi247 / netdata / commits / 994bdde38

feat(go.d/ddsnmp): sysobjectid-based metadata override support for SNMP profiles (#20803)

Ilya Mashchenko committed Aug 12, 2025 at 07:42 UTC 994bdde38770154a2be7d28334718f7eb55a5707
9 files changed +460 -38
src/go/plugin/go.d/collector/snmp/collect.go
+1 -1
@@ -34,7 +34,7 @@ func (c *Collector) collect() (map[string]int64, error) {
34 }
35
36 if c.ddSnmpColl == nil {
37 - c.ddSnmpColl = ddsnmpcollector.New(c.snmpClient, c.snmpProfiles, c.Logger)
37 + c.ddSnmpColl = ddsnmpcollector.New(c.snmpClient, c.snmpProfiles, c.Logger, si.SysObjectID)
38 c.ddSnmpColl.DoTableMetrics = c.EnableProfilesTableMetrics
39 }
40
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/metadata.go
+12
@@ -73,3 +73,15 @@ func NewMetadataResourceConfig() MetadataResourceConfig {
73 func IsMetadataResourceWithScalarOids(resource string) bool {
74 return resource == MetadataDeviceResource
75 }
76 +
77 +type SysobjectIDMetadataEntryConfig struct {
78 + SysobjectID string `yaml:"sysobjectid"`
79 + Metadata ListMap[MetadataField] `yaml:"metadata"`
80 +}
81 +
82 +func (e SysobjectIDMetadataEntryConfig) Clone() SysobjectIDMetadataEntryConfig {
83 + return SysobjectIDMetadataEntryConfig{
84 + SysobjectID: e.SysobjectID,
85 + Metadata: CloneMap(e.Metadata),
86 + }
87 +}
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/profile_definition.go
+19 -17
@@ -20,14 +20,15 @@ type DeviceMeta struct {
20 // 2/ Datadog backend: the profiles are in json format, they are used to store profiles created via UI.
21 // The serialisation of json profiles are defined by the json annotation.
22 type ProfileDefinition struct {
23 - Name string `yaml:"name,omitempty" json:"name,omitempty"`
24 - Description string `yaml:"description,omitempty" json:"description,omitempty"`
25 - SysObjectIDs StringArray `yaml:"sysobjectid,omitempty" json:"sysobjectid,omitempty"`
26 - Extends []string `yaml:"extends,omitempty" json:"extends,omitempty"`
27 - Metadata MetadataConfig `yaml:"metadata,omitempty" json:"metadata,omitempty"`
28 - MetricTags []MetricTagConfig `yaml:"metric_tags,omitempty" json:"metric_tags,omitempty"`
29 - StaticTags []string `yaml:"static_tags,omitempty" json:"static_tags,omitempty"`
30 - Metrics []MetricsConfig `yaml:"metrics,omitempty" json:"metrics,omitempty"`
23 + Name string `yaml:"name,omitempty" json:"name,omitempty"`
24 + Description string `yaml:"description,omitempty" json:"description,omitempty"`
25 + SysObjectIDs StringArray `yaml:"sysobjectid,omitempty" json:"sysobjectid,omitempty"`
26 + Extends []string `yaml:"extends,omitempty" json:"extends,omitempty"`
27 + Metadata MetadataConfig `yaml:"metadata,omitempty" json:"metadata,omitempty"`
28 + SysobjectIDMetadata []SysobjectIDMetadataEntryConfig `yaml:"sysobjectid_metadata,omitempty"`
29 + MetricTags []MetricTagConfig `yaml:"metric_tags,omitempty" json:"metric_tags,omitempty"`
30 + StaticTags []string `yaml:"static_tags,omitempty" json:"static_tags,omitempty"`
31 + Metrics []MetricsConfig `yaml:"metrics,omitempty" json:"metrics,omitempty"`
32
33 VirtualMetrics []VirtualMetricConfig `yaml:"virtual_metrics,omitempty" json:"virtual_metrics,omitempty"`
34
@@ -65,15 +66,16 @@ func (p *ProfileDefinition) Clone() *ProfileDefinition {
66 return nil
67 }
68 return &ProfileDefinition{
68 - Name: p.Name,
69 - Description: p.Description,
70 - SysObjectIDs: slices.Clone(p.SysObjectIDs),
71 - Extends: slices.Clone(p.Extends),
72 - Metadata: CloneMap(p.Metadata),
73 - MetricTags: CloneSlice(p.MetricTags),
74 - StaticTags: slices.Clone(p.StaticTags),
75 - Metrics: CloneSlice(p.Metrics),
76 - VirtualMetrics: CloneSlice(p.VirtualMetrics),
69 + Name: p.Name,
70 + Description: p.Description,
71 + SysObjectIDs: slices.Clone(p.SysObjectIDs),
72 + Extends: slices.Clone(p.Extends),
73 + Metadata: CloneMap(p.Metadata),
74 + SysobjectIDMetadata: CloneSlice(p.SysobjectIDMetadata),
75 + MetricTags: CloneSlice(p.MetricTags),
76 + StaticTags: slices.Clone(p.StaticTags),
77 + Metrics: CloneSlice(p.Metrics),
78 + VirtualMetrics: CloneSlice(p.VirtualMetrics),
79 Device: DeviceMeta{
80 Vendor: p.Device.Vendor,
81 },
src/go/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition/validation.go
+56
@@ -160,6 +160,62 @@ func ValidateEnrichMetadata(metadata MetadataConfig) []string {
160 return errors
161 }
162
163 +func ValidateEnrichSysobjectIDMetadata(entries []SysobjectIDMetadataEntryConfig) []string {
164 + var errors []string
165 +
166 + // Track seen sysobjectids to detect duplicates
167 + seenOIDs := make(map[string]int) // OID -> first occurrence index
168 +
169 + for i, entry := range entries {
170 + // Validate sysobjectid is not empty
171 + if entry.SysobjectID == "" {
172 + errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d]: missing sysobjectid", i))
173 + continue
174 + }
175 +
176 + // Check for duplicate sysobjectids
177 + if firstIdx, exists := seenOIDs[entry.SysobjectID]; exists {
178 + errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d]: duplicate sysobjectid %s (first occurrence at index %d)",
179 + i, entry.SysobjectID, firstIdx))
180 + } else {
181 + seenOIDs[entry.SysobjectID] = i
182 + }
183 +
184 + // Validate metadata fields
185 + for fieldName, field := range entry.Metadata {
186 + // Validate the field must have either value or symbol(s)
187 + if field.Value == "" && field.Symbol.OID == "" && len(field.Symbols) == 0 {
188 + errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s: must have either value or symbol(s)", i, fieldName))
189 + }
190 +
191 + // Can't have both value and symbols
192 + if field.Value != "" && (field.Symbol.OID != "" || len(field.Symbols) > 0) {
193 + errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s: cannot have both value and symbol(s)", i, fieldName))
194 + }
195 +
196 + // Validate symbols if present
197 + for j := range field.Symbols {
198 + symbolErrors := validateEnrichSymbol(&field.Symbols[j], MetadataSymbol)
199 + for _, err := range symbolErrors {
200 + errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s.symbols[%d]: %s", i, fieldName, j, err))
201 + }
202 + }
203 +
204 + // Validate single symbol if present
205 + if field.Symbol.OID != "" {
206 + symbolErrors := validateEnrichSymbol(&field.Symbol, MetadataSymbol)
207 + for _, err := range symbolErrors {
208 + errors = append(errors, fmt.Sprintf("sysobjectid_metadata[%d].%s.symbol: %s", i, fieldName, err))
209 + }
210 + }
211 +
212 + entry.Metadata[fieldName] = field
213 + }
214 + }
215 +
216 + return errors
217 +}
218 +
219 func validateEnrichSymbol(symbol *SymbolConfig, symbolContext SymbolContext) []string {
220 var errors []string
221 if symbol.Name == "" {
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector.go
+4 -4
@@ -18,7 +18,7 @@ import (
18 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
19 )
20
21 -func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger) *Collector {
21 +func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logger, sysobjectid string) *Collector {
22 coll := &Collector{
23 log: log.With(slog.String("ddsnmp", "collector")),
24 snmpClient: snmpClient,
@@ -34,7 +34,7 @@ func New(snmpClient gosnmp.Handler, profiles []*ddsnmp.Profile, log *logger.Logg
34 }
35
36 coll.globalTagsCollector = newGlobalTagsCollector(snmpClient, coll.missingOIDs, coll.log)
37 - coll.deviceMetadataCollector = newDeviceMetadataCollector(snmpClient, coll.missingOIDs, coll.log)
37 + coll.deviceMetadataCollector = newDeviceMetadataCollector(snmpClient, coll.missingOIDs, coll.log, sysobjectid)
38 coll.scalarCollector = newScalarCollector(snmpClient, coll.missingOIDs, coll.log)
39 coll.tableCollector = newTableCollector(snmpClient, coll.missingOIDs, coll.tableCache, coll.log)
40 coll.vmetricsCollector = newVirtualMetricsCollector(coll.log)
@@ -70,11 +70,11 @@ func (c *Collector) CollectDeviceMetadata() (map[string]map[string]string, error
70 meta := make(map[string]map[string]string)
71
72 for _, prof := range c.profiles {
73 - dm, err := c.deviceMetadataCollector.Collect(prof.profile)
73 + deviceMeta, err := c.deviceMetadataCollector.Collect(prof.profile)
74 if err != nil {
75 return nil, err
76 }
77 - meta[prof.profile.SourceFile] = dm
77 + meta[prof.profile.SourceFile] = deviceMeta
78 }
79
80 return meta, nil
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collector_table_test.go
+1 -1
@@ -4422,7 +4422,7 @@ func TestCollector_Collect_TableCaching(t *testing.T) {
4422 mockHandler := snmpmock.NewMockHandler(ctrl)
4423 tc.setupMock(mockHandler)
4424
4425 - collector := New(mockHandler, tc.profiles, logger.New())
4425 + collector := New(mockHandler, tc.profiles, logger.New(), "")
4426 collector.DoTableMetrics = true
4427
4428 // Configure cache based on test requirements
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta.go
+39 -14
@@ -10,6 +10,7 @@ import (
10 "github.com/gosnmp/gosnmp"
11
12 "github.com/netdata/netdata/go/plugins/logger"
13 + "github.com/netdata/netdata/go/plugins/pkg/matcher"
14 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
15 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
16 )
@@ -145,13 +146,15 @@ type deviceMetadataCollector struct {
146 snmpClient gosnmp.Handler
147 missingOIDs map[string]bool
148 log *logger.Logger
149 + sysobjectid string
150 }
151
150 -func newDeviceMetadataCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger) *deviceMetadataCollector {
152 +func newDeviceMetadataCollector(snmpClient gosnmp.Handler, missingOIDs map[string]bool, log *logger.Logger, sysobjectid string) *deviceMetadataCollector {
153 return &deviceMetadataCollector{
154 snmpClient: snmpClient,
155 missingOIDs: missingOIDs,
156 log: log,
157 + sysobjectid: sysobjectid,
158 }
159 }
160
@@ -160,25 +163,47 @@ func (dc *deviceMetadataCollector) Collect(prof *ddsnmp.Profile) (map[string]str
163 return nil, nil
164 }
165
166 + resName := ddprofiledefinition.MetadataDeviceResource
167 + cfg, ok := prof.Definition.Metadata[resName]
168 + if !ok {
169 + return nil, nil
170 + }
171 +
172 meta := make(map[string]string)
173
165 - for resName, cfg := range prof.Definition.Metadata {
166 - if !ddprofiledefinition.IsMetadataResourceWithScalarOids(resName) {
174 + if err := dc.processMetadataFields(cfg.Fields, meta); err != nil {
175 + return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
176 + }
177 +
178 + if dc.sysobjectid == "" || len(prof.Definition.SysobjectIDMetadata) == 0 {
179 + return meta, nil
180 + }
181 +
182 + for i, entry := range prof.Definition.SysobjectIDMetadata {
183 + m, err := matcher.NewRegExpMatcher(entry.SysobjectID)
184 + if err != nil {
185 + dc.log.Warningf("sysobjectid_metadata[%d]: failed to compile regular expression from sysobjectid '%s': %v",
186 + i, entry.SysobjectID, err)
187 continue
188 }
169 -
170 - if err := dc.processResource(cfg, meta); err != nil {
171 - return ternary(len(meta) > 0, meta, nil), fmt.Errorf("failed to process metadata resource '%s': %w", resName, err)
189 + if m.MatchString(dc.sysobjectid) {
190 + if err := dc.processMetadataFields(entry.Metadata, meta); err != nil {
191 + dc.log.Warningf("sysobjectid_metadata[%d]: failed to process metadata fields for sysobjectid '%s': %v",
192 + i, entry.SysobjectID, err)
193 + continue
194 + }
195 + dc.log.Debugf("sysobjectid_metadata[%d]: matched sysobjectid '%s' with device OID '%s', applying metadata overrides",
196 + i, entry.SysobjectID, dc.sysobjectid)
197 }
198 }
199
200 return meta, nil
201 }
202
178 -// processResource processes a single metadata resource
179 -func (dc *deviceMetadataCollector) processResource(cfg ddprofiledefinition.MetadataResourceConfig, metadata map[string]string) error {
203 +// processMetadataFields processes a single metadata resource
204 +func (dc *deviceMetadataCollector) processMetadataFields(fields map[string]ddprofiledefinition.MetadataField, metadata map[string]string) error {
205 staticValues := make(map[string]string)
181 - oids := dc.collectStaticAndIdentifyOIDs(cfg, staticValues)
206 + oids := dc.collectStaticAndIdentifyOIDs(fields, staticValues)
207
208 for k, v := range staticValues {
209 metadata[k] = v
@@ -193,14 +218,14 @@ func (dc *deviceMetadataCollector) processResource(cfg ddprofiledefinition.Metad
218 return fmt.Errorf("failed to fetch metadata values: %w", err)
219 }
220
196 - return dc.processDynamicFields(cfg, pdus, metadata)
221 + return dc.processDynamicFields(fields, pdus, metadata)
222 }
223
224 // collectStaticAndIdentifyOIDs collects static values and returns OIDs to fetch
200 -func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(cfg ddprofiledefinition.MetadataResourceConfig, staticValues map[string]string) []string {
225 +func (dc *deviceMetadataCollector) collectStaticAndIdentifyOIDs(fields map[string]ddprofiledefinition.MetadataField, staticValues map[string]string) []string {
226 var oids []string
227
203 - for name, field := range cfg.Fields {
228 + for name, field := range fields {
229 switch {
230 case field.Value != "":
231 staticValues[name] = field.Value
@@ -246,10 +271,10 @@ func (dc *deviceMetadataCollector) fetchMetadataValues(oids []string) (map[strin
271 return pdus, nil
272 }
273
249 -func (dc *deviceMetadataCollector) processDynamicFields(cfg ddprofiledefinition.MetadataResourceConfig, pdus map[string]gosnmp.SnmpPDU, metadata map[string]string) error {
274 +func (dc *deviceMetadataCollector) processDynamicFields(fields map[string]ddprofiledefinition.MetadataField, pdus map[string]gosnmp.SnmpPDU, metadata map[string]string) error {
275 var errs []error
276
252 - for name, field := range cfg.Fields {
277 + for name, field := range fields {
278 switch {
279 case field.Symbol.OID != "":
280 // Single symbol
src/go/plugin/go.d/collector/snmp/ddsnmp/ddsnmpcollector/collectors_meta_test.go
+325 -1
@@ -409,6 +409,7 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
409 tests := map[string]struct {
410 profile *ddsnmp.Profile
411 setupMock func(m *snmpmock.MockHandler)
412 + sysobjectid string
413 expectedResult map[string]string
414 expectedError bool
415 errorContains string
@@ -889,6 +890,329 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
890 },
891 expectedError: false,
892 },
893 + "sysobjectid metadata override - single match": {
894 + profile: &ddsnmp.Profile{
895 + Definition: &ddprofiledefinition.ProfileDefinition{
896 + Metadata: ddprofiledefinition.MetadataConfig{
897 + "device": ddprofiledefinition.MetadataResourceConfig{
898 + Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
899 + "vendor": {
900 + Value: "Cisco",
901 + },
902 + "type": {
903 + Value: "Firewall",
904 + },
905 + },
906 + },
907 + },
908 + SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
909 + {
910 + SysobjectID: "1.3.6.1.4.1.9.1.669",
911 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
912 + "model": {
913 + Value: "ASA5510",
914 + },
915 + "series": {
916 + Value: "ASA5500",
917 + },
918 + },
919 + },
920 + {
921 + SysobjectID: "1.3.6.1.4.1.9.1.670",
922 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
923 + "model": {
924 + Value: "ASA5520",
925 + },
926 + },
927 + },
928 + },
929 + },
930 + },
931 + setupMock: func(m *snmpmock.MockHandler) {},
932 + sysobjectid: "1.3.6.1.4.1.9.1.669",
933 + expectedResult: map[string]string{
934 + "vendor": "Cisco",
935 + "type": "Firewall",
936 + "model": "ASA5510",
937 + "series": "ASA5500",
938 + },
939 + expectedError: false,
940 + },
941 + "sysobjectid metadata override - multiple matches cascade": {
942 + profile: &ddsnmp.Profile{
943 + Definition: &ddprofiledefinition.ProfileDefinition{
944 + Metadata: ddprofiledefinition.MetadataConfig{
945 + "device": ddprofiledefinition.MetadataResourceConfig{
946 + Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
947 + "vendor": {
948 + Value: "Cisco",
949 + },
950 + },
951 + },
952 + },
953 + SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
954 + {
955 + SysobjectID: "1.3.6.1.4.1.9.*", // All Cisco devices
956 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
957 + "platform": {
958 + Value: "Enterprise",
959 + },
960 + "support": {
961 + Value: "Premium",
962 + },
963 + },
964 + },
965 + {
966 + SysobjectID: "1.3.6.1.4.1.9.1.*", // Cisco ASA family
967 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
968 + "type": {
969 + Value: "Firewall",
970 + },
971 + "series": {
972 + Value: "ASA",
973 + },
974 + },
975 + },
976 + {
977 + SysobjectID: "1.3.6.1.4.1.9.1.669", // Specific model
978 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
979 + "model": {
980 + Value: "ASA5510",
981 + },
982 + "series": {
983 + Value: "ASA5500", // Override the previous series value
984 + },
985 + },
986 + },
987 + },
988 + },
989 + },
990 + setupMock: func(m *snmpmock.MockHandler) {},
991 + sysobjectid: "1.3.6.1.4.1.9.1.669",
992 + expectedResult: map[string]string{
993 + "vendor": "Cisco",
994 + "platform": "Enterprise",
995 + "support": "Premium",
996 + "type": "Firewall",
997 + "series": "ASA5500", // Last match wins
998 + "model": "ASA5510",
999 + },
1000 + expectedError: false,
1001 + },
1002 + "sysobjectid metadata with dynamic SNMP values": {
1003 + profile: &ddsnmp.Profile{
1004 + Definition: &ddprofiledefinition.ProfileDefinition{
1005 + Metadata: ddprofiledefinition.MetadataConfig{
1006 + "device": ddprofiledefinition.MetadataResourceConfig{
1007 + Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1008 + "vendor": {
1009 + Value: "Cisco",
1010 + },
1011 + },
1012 + },
1013 + },
1014 + SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1015 + {
1016 + SysobjectID: "1.3.6.1.4.1.9.1.669",
1017 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1018 + "model": {
1019 + Value: "ASA5510",
1020 + },
1021 + "firmware": {
1022 + Symbol: ddprofiledefinition.SymbolConfig{
1023 + OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1024 + Name: "ciscoImageVersion",
1025 + },
1026 + },
1027 + },
1028 + },
1029 + },
1030 + },
1031 + },
1032 + setupMock: func(m *snmpmock.MockHandler) {
1033 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1034 + m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(
1035 + &gosnmp.SnmpPacket{
1036 + Variables: []gosnmp.SnmpPDU{
1037 + {
1038 + Name: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1039 + Type: gosnmp.OctetString,
1040 + Value: []byte("9.2(4)"),
1041 + },
1042 + },
1043 + }, nil,
1044 + )
1045 + },
1046 + sysobjectid: "1.3.6.1.4.1.9.1.669",
1047 + expectedResult: map[string]string{
1048 + "vendor": "Cisco",
1049 + "model": "ASA5510",
1050 + "firmware": "9.2(4)",
1051 + },
1052 + expectedError: false,
1053 + },
1054 + "sysobjectid metadata no match": {
1055 + profile: &ddsnmp.Profile{
1056 + Definition: &ddprofiledefinition.ProfileDefinition{
1057 + Metadata: ddprofiledefinition.MetadataConfig{
1058 + "device": ddprofiledefinition.MetadataResourceConfig{
1059 + Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1060 + "vendor": {
1061 + Value: "Cisco",
1062 + },
1063 + "type": {
1064 + Value: "Firewall",
1065 + },
1066 + },
1067 + },
1068 + },
1069 + SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1070 + {
1071 + SysobjectID: "1.3.6.1.4.1.9.1.669",
1072 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1073 + "model": {
1074 + Value: "ASA5510",
1075 + },
1076 + },
1077 + },
1078 + },
1079 + },
1080 + },
1081 + setupMock: func(m *snmpmock.MockHandler) {},
1082 + sysobjectid: "1.3.6.1.4.1.9.1.700",
1083 + expectedResult: map[string]string{
1084 + "vendor": "Cisco",
1085 + "type": "Firewall",
1086 + // No model since sysobjectid doesn't match
1087 + },
1088 + expectedError: false,
1089 + },
1090 + "sysobjectid metadata with invalid regex pattern": {
1091 + profile: &ddsnmp.Profile{
1092 + Definition: &ddprofiledefinition.ProfileDefinition{
1093 + Metadata: ddprofiledefinition.MetadataConfig{
1094 + "device": ddprofiledefinition.MetadataResourceConfig{
1095 + Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1096 + "vendor": {
1097 + Value: "Cisco",
1098 + },
1099 + },
1100 + },
1101 + },
1102 + SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1103 + {
1104 + SysobjectID: "1.3.6.1.4.1.9.1.[invalid", // Invalid regex
1105 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1106 + "model": {
1107 + Value: "ASA5510",
1108 + },
1109 + },
1110 + },
1111 + {
1112 + SysobjectID: "1.3.6.1.4.1.9.1.669", // Valid entry
1113 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1114 + "series": {
1115 + Value: "ASA5500",
1116 + },
1117 + },
1118 + },
1119 + },
1120 + },
1121 + },
1122 + setupMock: func(m *snmpmock.MockHandler) {},
1123 + sysobjectid: "1.3.6.1.4.1.9.1.669",
1124 + expectedResult: map[string]string{
1125 + "vendor": "Cisco",
1126 + "series": "ASA5500", // Valid entry still processes
1127 + },
1128 + expectedError: false,
1129 + },
1130 + "sysobjectid metadata overrides base metadata field": {
1131 + profile: &ddsnmp.Profile{
1132 + Definition: &ddprofiledefinition.ProfileDefinition{
1133 + Metadata: ddprofiledefinition.MetadataConfig{
1134 + "device": ddprofiledefinition.MetadataResourceConfig{
1135 + Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1136 + "vendor": {
1137 + Value: "Generic",
1138 + },
1139 + "type": {
1140 + Value: "Unknown",
1141 + },
1142 + "model": {
1143 + Value: "Generic Model",
1144 + },
1145 + },
1146 + },
1147 + },
1148 + SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1149 + {
1150 + SysobjectID: "1.3.6.1.4.1.9.1.669",
1151 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1152 + "vendor": {
1153 + Value: "Cisco Systems", // Override generic vendor
1154 + },
1155 + "model": {
1156 + Value: "ASA5510", // Override generic model
1157 + },
1158 + },
1159 + },
1160 + },
1161 + },
1162 + },
1163 + setupMock: func(m *snmpmock.MockHandler) {},
1164 + sysobjectid: "1.3.6.1.4.1.9.1.669",
1165 + expectedResult: map[string]string{
1166 + "vendor": "Cisco Systems", // Overridden
1167 + "type": "Unknown", // Not overridden
1168 + "model": "ASA5510", // Overridden
1169 + },
1170 + expectedError: false,
1171 + },
1172 + "sysobjectid metadata with SNMP fetch error continues": {
1173 + profile: &ddsnmp.Profile{
1174 + Definition: &ddprofiledefinition.ProfileDefinition{
1175 + Metadata: ddprofiledefinition.MetadataConfig{
1176 + "device": ddprofiledefinition.MetadataResourceConfig{
1177 + Fields: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1178 + "vendor": {
1179 + Value: "Cisco",
1180 + },
1181 + },
1182 + },
1183 + },
1184 + SysobjectIDMetadata: []ddprofiledefinition.SysobjectIDMetadataEntryConfig{
1185 + {
1186 + SysobjectID: "1.3.6.1.4.1.9.1.669",
1187 + Metadata: ddprofiledefinition.ListMap[ddprofiledefinition.MetadataField]{
1188 + "model": {
1189 + Value: "ASA5510",
1190 + },
1191 + "firmware": {
1192 + Symbol: ddprofiledefinition.SymbolConfig{
1193 + OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.3.1",
1194 + Name: "ciscoImageVersion",
1195 + },
1196 + },
1197 + },
1198 + },
1199 + },
1200 + },
1201 + },
1202 + setupMock: func(m *snmpmock.MockHandler) {
1203 + m.EXPECT().MaxOids().Return(10).AnyTimes()
1204 + m.EXPECT().Get([]string{"1.3.6.1.4.1.9.9.109.1.1.1.1.3.1"}).Return(
1205 + nil,
1206 + errors.New("SNMP timeout"),
1207 + )
1208 + },
1209 + sysobjectid: "1.3.6.1.4.1.9.1.669",
1210 + expectedResult: map[string]string{
1211 + "vendor": "Cisco",
1212 + "model": "ASA5510", // Static value still applied despite SNMP error
1213 + },
1214 + expectedError: false, // Should continue with partial data
1215 + },
1216 }
1217
1218 for name, tc := range tests {
@@ -900,7 +1224,7 @@ func TestDeviceMetadataCollector_Collect(t *testing.T) {
1224 tc.setupMock(mockHandler)
1225
1226 missingOIDs := make(map[string]bool)
903 - collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New())
1227 + collector := newDeviceMetadataCollector(mockHandler, missingOIDs, logger.New(), tc.sysobjectid)
1228
1229 result, err := collector.Collect(tc.profile)
1230
src/go/plugin/go.d/collector/snmp/ddsnmp/profile.go
+3
@@ -203,6 +203,9 @@ func (p *Profile) validate() error {
203 for _, err := range ddprofiledefinition.ValidateEnrichMetadata(p.Definition.Metadata) {
204 errs = append(errs, errors.New(err))
205 }
206 + for _, err := range ddprofiledefinition.ValidateEnrichSysobjectIDMetadata(p.Definition.SysobjectIDMetadata) {
207 + errs = append(errs, errors.New(err))
208 + }
209 for _, err := range ddprofiledefinition.ValidateEnrichMetrics(p.Definition.Metrics) {
210 errs = append(errs, errors.New(err))
211 }