master
go 193 lines 4.71 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmputils
4
5 import (
6 "errors"
7 "fmt"
8 "io/fs"
9 "os"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "sync"
14
15 "gopkg.in/yaml.v2"
16
17 "github.com/netdata/netdata/go/plugins/pkg/executable"
18 "github.com/netdata/netdata/go/plugins/pkg/pluginconfig"
19 )
20
21 var (
22 overridesData *overrides
23 loadOverridesOnce sync.Once
24 )
25
26 type (
27 overrides struct {
28 EnterpriseNumbers enterpriseNumbersOverrides `yaml:"enterprisenumbers"`
29 SysObjectIDs sysObjectIDOverrides `yaml:"sysobjectids"`
30 }
31 enterpriseNumbersOverrides struct {
32 // Map PEN org names -> canonical vendor names
33 OrgToVendor map[string]string `yaml:"org_to_vendor"`
34 }
35 sysObjectIDOverrides struct {
36 // Exact OID overrides
37 OIDOverrides map[string]sysObjectIDOverride `yaml:"oid_overrides"`
38 // Category normalization
39 CategoryMap map[string]string `yaml:"category_map"`
40 }
41 sysObjectIDOverride struct {
42 // If non-empty, overrides the value; empty string => no change
43 Category string `yaml:"category,omitempty"`
44 Model string `yaml:"model,omitempty"`
45 }
46 )
47
48 // loadOverrides reads ALL *.yaml/*.yml files under getSnmpMetadataDir(),
49 // merges them in deterministic (alphabetical) order, and stores the result
50 // in overridesData. Later files win on conflicts.
51 func loadOverrides() {
52 loadOverridesOnce.Do(func() {
53 dir := getSnmpMetadataDir()
54 if dir == "" {
55 log.Warningf("snmp metadata overrides dir not found: %q (running without overrides)", dir)
56 return
57 }
58 agg, err := loadOverridesFromDir(dir)
59 if err != nil {
60 log.Errorf("failed to load one or more metadata overrides from %s: %v", dir, err)
61 return
62 }
63
64 overridesData = agg
65 })
66 }
67
68 // loadOverridesFromDir scans dir recursively, parses all *.yaml|*.yml strictly,
69 // merges them into a single overrides.
70 func loadOverridesFromDir(dir string) (*overrides, error) {
71 var agg overrides
72 var loaded int
73
74 err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
75 if err != nil {
76 return err
77 }
78 if d.IsDir() {
79 return nil
80 }
81 ext := strings.ToLower(filepath.Ext(d.Name()))
82 if ext != ".yaml" && ext != ".yml" {
83 return nil
84 }
85
86 bs, err := os.ReadFile(path)
87 if err != nil {
88 return err
89 }
90
91 var cur overrides
92 if err := yaml.UnmarshalStrict(bs, &cur); err != nil {
93 return fmt.Errorf("unmarshalling %s: %v", path, err)
94 }
95
96 loaded++
97 mergeOverrides(&agg, &cur)
98 return nil
99 })
100 if err != nil {
101 return nil, err
102 }
103 if loaded == 0 {
104 return nil, errors.New("no snmp metadata overrides loaded")
105 }
106
107 return &agg, nil
108 }
109
110 func mergeOverrides(dst, src *overrides) {
111 if src == nil {
112 return
113 }
114
115 if len(src.EnterpriseNumbers.OrgToVendor) > 0 {
116 if dst.EnterpriseNumbers.OrgToVendor == nil {
117 dst.EnterpriseNumbers.OrgToVendor = make(map[string]string, len(src.EnterpriseNumbers.OrgToVendor))
118 }
119 for k, v := range src.EnterpriseNumbers.OrgToVendor {
120 if v != "" {
121 dst.EnterpriseNumbers.OrgToVendor[k] = v
122 }
123 }
124 }
125
126 if len(src.SysObjectIDs.CategoryMap) > 0 {
127 if dst.SysObjectIDs.CategoryMap == nil {
128 dst.SysObjectIDs.CategoryMap = make(map[string]string, len(src.SysObjectIDs.CategoryMap))
129 }
130 for k, v := range src.SysObjectIDs.CategoryMap {
131 if v != "" {
132 dst.SysObjectIDs.CategoryMap[k] = v
133 }
134 }
135 }
136
137 if len(src.SysObjectIDs.OIDOverrides) > 0 {
138 if dst.SysObjectIDs.OIDOverrides == nil {
139 dst.SysObjectIDs.OIDOverrides = make(map[string]sysObjectIDOverride, len(src.SysObjectIDs.OIDOverrides))
140 }
141 for oid, o := range src.SysObjectIDs.OIDOverrides {
142 existing := dst.SysObjectIDs.OIDOverrides[oid]
143 if o.Category != "" {
144 existing.Category = o.Category
145 }
146 if o.Model != "" {
147 existing.Model = o.Model
148 }
149 dst.SysObjectIDs.OIDOverrides[oid] = existing
150 }
151 }
152 }
153
154 func getSnmpMetadataDir() string {
155 if executable.Name == "test" {
156 return snmpMetadataDirFromThisFile()
157 }
158 if path := filepath.Join(executable.Directory, "..", "config", executable.Name, "snmp.profiles", "metadata"); isDirExists(path) {
159 return path
160 }
161 return filepath.Join(pluginconfig.CollectorsStockDir(), "snmp.profiles", "metadata")
162 }
163
164 func snmpMetadataDirFromThisFile() string {
165 // runtime.Caller(0) returns the absolute path to THIS .go file at build time.
166 _, thisFile, _, ok := runtime.Caller(0)
167 if !ok {
168 return ""
169 }
170 base := filepath.Dir(thisFile)
171
172 // Try a couple of common repo layouts
173 candidates := []string{
174 filepath.Join(base, "..", "config", "go.d", "snmp.profiles", "metadata"),
175 filepath.Join(base, "..", "..", "config", "go.d", "snmp.profiles", "metadata"),
176 }
177
178 for _, p := range candidates {
179 if isDirExists(p) {
180 abs, _ := filepath.Abs(p)
181 return abs
182 }
183 }
184 return ""
185 }
186
187 func isDirExists(path string) bool {
188 fi, err := os.Stat(path)
189 if err != nil {
190 return !errors.Is(err, fs.ErrNotExist)
191 }
192 return fi.Mode().IsDir()
193 }