master
go 304 lines 15.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "context"
7 "sort"
8
9 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
10 )
11
12 // Compile-time interface check.
13 var _ funcapi.MethodHandler = (*funcInterfaces)(nil)
14
15 // funcInterfaces handles the "interfaces" function for SNMP devices.
16 // It provides network interface traffic and status metrics from cached SNMP data.
17 type funcInterfaces struct {
18 router *funcRouter
19 }
20
21 func newFuncInterfaces(r *funcRouter) *funcInterfaces {
22 return &funcInterfaces{router: r}
23 }
24
25 const (
26 ifacesMethodID = "interfaces"
27 ifacesParamTypeGroup = "if_type_group"
28 ifacesDefaultTypeGroup = "ethernet"
29 )
30
31 func ifacesMethodConfig() funcapi.MethodConfig {
32 return funcapi.MethodConfig{
33 ID: ifacesMethodID,
34 Name: "Network Interfaces",
35 UpdateEvery: 10,
36 Help: "Network interface traffic and status metrics",
37 RequiredParams: []funcapi.ParamConfig{{
38 ID: ifacesParamTypeGroup,
39 Name: "Type Group",
40 Help: "Filter by interface type group",
41 Selection: funcapi.ParamSelect,
42 Options: []funcapi.ParamOption{
43 {ID: "ethernet", Name: "Ethernet", Default: true},
44 {ID: "aggregation", Name: "Aggregation"},
45 {ID: "virtual", Name: "Virtual"},
46 {ID: "other", Name: "Other"},
47 },
48 }},
49 }
50 }
51
52 // MethodParams implements funcapi.MethodHandler.
53 func (f *funcInterfaces) MethodParams(_ context.Context, method string) ([]funcapi.ParamConfig, error) {
54 if method != ifacesMethodID {
55 return nil, nil
56 }
57 return []funcapi.ParamConfig{{
58 ID: ifacesParamTypeGroup,
59 Name: "Type Group",
60 Help: "Filter by interface type group",
61 Selection: funcapi.ParamSelect,
62 Options: []funcapi.ParamOption{
63 {ID: "ethernet", Name: "Ethernet", Default: true},
64 {ID: "aggregation", Name: "Aggregation"},
65 {ID: "virtual", Name: "Virtual"},
66 {ID: "other", Name: "Other"},
67 },
68 }}, nil
69 }
70
71 // Cleanup implements funcapi.MethodHandler.
72 func (f *funcInterfaces) Cleanup(_ context.Context) {}
73
74 // Handle implements funcapi.MethodHandler.
75 func (f *funcInterfaces) Handle(_ context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
76 if method != ifacesMethodID {
77 return funcapi.NotFoundResponse(method)
78 }
79
80 if f.router.ifaceCache == nil {
81 return funcapi.UnavailableResponse("interface data not available yet, please retry after data collection")
82 }
83
84 f.router.ifaceCache.mu.RLock()
85 defer f.router.ifaceCache.mu.RUnlock()
86
87 typeGroupFilter := params.GetOne(ifacesParamTypeGroup)
88 if typeGroupFilter == "" {
89 typeGroupFilter = ifacesDefaultTypeGroup
90 }
91
92 // Build data rows from cache
93 data := make([][]any, 0, len(f.router.ifaceCache.interfaces))
94 for _, entry := range f.router.ifaceCache.interfaces {
95 if !matchesTypeGroup(entry.ifTypeGroup, typeGroupFilter) {
96 continue
97 }
98 data = append(data, f.buildRow(entry))
99 }
100
101 f.sortData(data, f.defaultSortColumn())
102
103 cs := snmpColumnSet(snmpAllColumns)
104
105 return &funcapi.FunctionResponse{
106 Status: 200,
107 Help: "Network interface traffic and status metrics",
108 Columns: f.buildColumns(cs),
109 Data: data,
110 DefaultSortColumn: f.defaultSortColumn(),
111 ChartingConfig: cs.BuildCharting(),
112 }
113 }
114
115 // buildColumns builds column definitions for the response.
116 func (f *funcInterfaces) buildColumns(cs funcapi.ColumnSet[snmpColumn]) map[string]any {
117 columns := cs.BuildColumns()
118
119 // Add rowOptions column (not part of the regular column set)
120 rowOptions := funcapi.Column{
121 Index: cs.Len(),
122 Name: "rowOptions",
123 Type: funcapi.FieldTypeNone,
124 Visualization: funcapi.FieldVisualRowOptions,
125 Sort: funcapi.FieldSortAscending,
126 Sortable: false,
127 Sticky: false,
128 Summary: funcapi.FieldSummaryCount,
129 Filter: funcapi.FieldFilterNone,
130 Visible: false,
131 Dummy: true,
132 ValueOptions: funcapi.ValueOptions{
133 Transform: funcapi.FieldTransformNone,
134 DecimalPoints: 0,
135 DefaultValue: nil,
136 },
137 }
138 columns["rowOptions"] = rowOptions.BuildColumn()
139
140 return columns
141 }
142
143 // buildRow builds a data row from an interface entry.
144 func (f *funcInterfaces) buildRow(entry *ifaceEntry) []any {
145 row := make([]any, len(snmpAllColumns)+1)
146 for i, col := range snmpAllColumns {
147 row[i] = col.Value(entry)
148 }
149 if isIfaceDown(entry) {
150 for i, col := range snmpAllColumns {
151 if col.Type == funcapi.FieldTypeFloat {
152 row[i] = nil
153 }
154 }
155 }
156 row[len(snmpAllColumns)] = rowOptionsForIface(entry)
157 return row
158 }
159
160 // sortData sorts the data rows by the specified column.
161 func (f *funcInterfaces) sortData(data [][]any, sortColumn string) {
162 if len(data) == 0 {
163 return
164 }
165
166 colIdx := 0
167 sortDir := funcapi.FieldSortAscending
168
169 for i, col := range snmpAllColumns {
170 if col.Name == sortColumn {
171 colIdx = i
172 sortDir = col.Sort
173 break
174 }
175 }
176
177 sort.Slice(data, func(i, j int) bool {
178 vi := data[i][colIdx]
179 vj := data[j][colIdx]
180
181 if vi == nil && vj == nil {
182 return false
183 }
184 if vi == nil {
185 return false
186 }
187 if vj == nil {
188 return true
189 }
190
191 switch a := vi.(type) {
192 case string:
193 b := vj.(string)
194 if sortDir == funcapi.FieldSortAscending {
195 return a < b
196 }
197 return a > b
198 case float64:
199 b := vj.(float64)
200 if sortDir == funcapi.FieldSortAscending {
201 return a < b
202 }
203 return a > b
204 default:
205 return false
206 }
207 })
208 }
209
210 // defaultSortColumn returns the default sort column key.
211 func (f *funcInterfaces) defaultSortColumn() string {
212 for _, col := range snmpAllColumns {
213 if col.DefaultSort {
214 return col.Name
215 }
216 }
217 return "Interface"
218 }
219
220 func rowOptionsForIface(entry *ifaceEntry) any {
221 // TODO: Re-enable row coloring once the UI supports more severity options.
222 return nil
223 }
224
225 func isIfaceDown(entry *ifaceEntry) bool {
226 if entry == nil {
227 return false
228 }
229 return entry.adminStatus != "up" || entry.operStatus != "up"
230 }
231
232 func matchesTypeGroup(group, filter string) bool {
233 knownGroups := map[string]bool{
234 "ethernet": true,
235 "aggregation": true,
236 "virtual": true,
237 }
238
239 if filter == "other" {
240 return !knownGroups[group]
241 }
242
243 return group == filter
244 }
245
246 func ptrToAny(p *float64) any {
247 if p == nil {
248 return nil
249 }
250 return *p
251 }
252
253 func sumRates(vals ...*float64) *float64 {
254 var sum float64
255 hasValue := false
256 for _, v := range vals {
257 if v == nil {
258 continue
259 }
260 sum += *v
261 hasValue = true
262 }
263 if !hasValue {
264 return nil
265 }
266 return &sum
267 }
268
269 // snmpColumn defines a column for SNMP interfaces function.
270 type snmpColumn struct {
271 funcapi.ColumnMeta
272 Value func(*ifaceEntry) any // Extracts value from entry
273 DefaultSort bool // Is default sort column
274 }
275
276 func snmpColumnSet(cols []snmpColumn) funcapi.ColumnSet[snmpColumn] {
277 return funcapi.Columns(cols, func(c snmpColumn) funcapi.ColumnMeta { return c.ColumnMeta })
278 }
279
280 var snmpAllColumns = []snmpColumn{
281 {ColumnMeta: funcapi.ColumnMeta{Name: "Interface", Tooltip: "Interface", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sticky: true, Sortable: true}, Value: func(e *ifaceEntry) any { return e.name }, DefaultSort: true},
282 {ColumnMeta: funcapi.ColumnMeta{Name: "Type", Tooltip: "IANA ifType (IF-MIB)", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true, GroupBy: &funcapi.GroupByOptions{IsDefault: true}}, Value: func(e *ifaceEntry) any { return e.ifType }},
283 {ColumnMeta: funcapi.ColumnMeta{Name: "Type Group", Tooltip: "Custom mapping of IANA ifType into groups", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, Value: func(e *ifaceEntry) any { return e.ifTypeGroup }},
284 {ColumnMeta: funcapi.ColumnMeta{Name: "Admin Status", Tooltip: "Administrative status: up, down, testing", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, Value: func(e *ifaceEntry) any { return e.adminStatus }},
285 {ColumnMeta: funcapi.ColumnMeta{Name: "Oper Status", Tooltip: "Operational status: up, down, testing, unknown, dormant, notPresent, lowerLayerDown", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true, Chart: &funcapi.ChartOptions{Group: "OperationalStatus", Title: "Operational Status", IsDefault: true, DefaultGroupBy: "Oper Status"}, GroupBy: &funcapi.GroupByOptions{}}, Value: func(e *ifaceEntry) any { return e.operStatus }},
286 {ColumnMeta: funcapi.ColumnMeta{Name: "Traffic In", Tooltip: "Traffic In", Type: funcapi.FieldTypeFloat, Units: "bit/s", Visualization: funcapi.FieldVisualBar, Visible: true, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "Traffic", IsDefault: true, DefaultGroupBy: "Type"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.trafficIn) }},
287 {ColumnMeta: funcapi.ColumnMeta{Name: "Traffic Out", Tooltip: "Traffic Out", Type: funcapi.FieldTypeFloat, Units: "bit/s", Visualization: funcapi.FieldVisualBar, Visible: true, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "Traffic"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.trafficOut) }},
288 {ColumnMeta: funcapi.ColumnMeta{Name: "Unicast In", Tooltip: "Unicast In", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "UnicastPackets", Title: "Unicast Packets"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.ucastPktsIn) }},
289 {ColumnMeta: funcapi.ColumnMeta{Name: "Unicast Out", Tooltip: "Unicast Out", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "UnicastPackets"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.ucastPktsOut) }},
290 {ColumnMeta: funcapi.ColumnMeta{Name: "Broadcast In", Tooltip: "Broadcast In", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "BroadcastPackets", Title: "Broadcast Packets"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.bcastPktsIn) }},
291 {ColumnMeta: funcapi.ColumnMeta{Name: "Broadcast Out", Tooltip: "Broadcast Out", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "BroadcastPackets"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.bcastPktsOut) }},
292 {ColumnMeta: funcapi.ColumnMeta{Name: "Packets In", Tooltip: "Packets In", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: true, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(e *ifaceEntry) any {
293 return ptrToAny(sumRates(e.rates.ucastPktsIn, e.rates.bcastPktsIn, e.rates.mcastPktsIn))
294 }},
295 {ColumnMeta: funcapi.ColumnMeta{Name: "Packets Out", Tooltip: "Packets Out", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: true, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(e *ifaceEntry) any {
296 return ptrToAny(sumRates(e.rates.ucastPktsOut, e.rates.bcastPktsOut, e.rates.mcastPktsOut))
297 }},
298 {ColumnMeta: funcapi.ColumnMeta{Name: "Errors In", Tooltip: "Errors In", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.errorsIn) }},
299 {ColumnMeta: funcapi.ColumnMeta{Name: "Errors Out", Tooltip: "Errors Out", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.errorsOut) }},
300 {ColumnMeta: funcapi.ColumnMeta{Name: "Discards In", Tooltip: "Discards In", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.discardsIn) }},
301 {ColumnMeta: funcapi.ColumnMeta{Name: "Discards Out", Tooltip: "Discards Out", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.discardsOut) }},
302 {ColumnMeta: funcapi.ColumnMeta{Name: "Multicast In", Tooltip: "Multicast In", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "MulticastPackets", Title: "Multicast Packets"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.mcastPktsIn) }},
303 {ColumnMeta: funcapi.ColumnMeta{Name: "Multicast Out", Tooltip: "Multicast Out", Type: funcapi.FieldTypeFloat, Units: "packets/s", Visualization: funcapi.FieldVisualBar, Visible: false, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true, Chart: &funcapi.ChartOptions{Group: "MulticastPackets"}}, Value: func(e *ifaceEntry) any { return ptrToAny(e.rates.mcastPktsOut) }},
304 }