| 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 | } |