master
go 260 lines 14.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "context"
7 "sort"
8 "strconv"
9 "strings"
10 "time"
11
12 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
13 )
14
15 const licensesMethodID = "licenses"
16
17 func licensesMethodConfig() funcapi.MethodConfig {
18 return funcapi.MethodConfig{
19 ID: licensesMethodID,
20 Name: "Licenses",
21 UpdateEvery: 10,
22 Help: "Normalized SNMP licensing rows for the selected device",
23 // Licenses is intentionally parameterless: it returns this job's cached rows.
24 RequiredParams: []funcapi.ParamConfig{},
25 }
26 }
27
28 // Compile-time interface check.
29 var _ funcapi.MethodHandler = (*funcLicenses)(nil)
30
31 type funcLicenses struct {
32 cache *licenseCache
33 }
34
35 func newFuncLicenses(cache *licenseCache) *funcLicenses {
36 return &funcLicenses{cache: cache}
37 }
38
39 func (f *funcLicenses) MethodParams(_ context.Context, method string) ([]funcapi.ParamConfig, error) {
40 if method != licensesMethodID {
41 return nil, nil
42 }
43 return []funcapi.ParamConfig{}, nil
44 }
45
46 func (f *funcLicenses) Cleanup(_ context.Context) {}
47
48 func (f *funcLicenses) Handle(_ context.Context, method string, _ funcapi.ResolvedParams) *funcapi.FunctionResponse {
49 if method != licensesMethodID {
50 return funcapi.NotFoundResponse(method)
51 }
52
53 if f.cache == nil {
54 return funcapi.UnavailableResponse("license data not available yet, please retry after data collection")
55 }
56
57 lastUpdate, rows := f.cache.snapshot()
58 if lastUpdate.IsZero() {
59 return funcapi.UnavailableResponse("license data not available yet, please retry after data collection")
60 }
61 if len(rows) == 0 {
62 return funcapi.UnavailableResponse("license data is not available for this device")
63 }
64
65 sortLicenseRows(rows)
66 now := time.Now().UTC()
67 cs := licenseColumnSet(licenseAllColumns)
68 data := make([][]any, 0, len(rows))
69 for _, row := range rows {
70 data = append(data, buildLicenseFunctionRow(row, now))
71 }
72
73 return &funcapi.FunctionResponse{
74 Status: 200,
75 Help: "Normalized SNMP licensing rows for the selected device",
76 Columns: buildLicenseColumns(cs),
77 Data: data,
78 DefaultSortColumn: defaultLicenseSortColumn(),
79 }
80 }
81
82 type licenseColumn struct {
83 funcapi.ColumnMeta
84 Value func(licenseRow, time.Time) any
85 DefaultSort bool
86 }
87
88 func licenseColumnSet(cols []licenseColumn) funcapi.ColumnSet[licenseColumn] {
89 return funcapi.Columns(cols, func(c licenseColumn) funcapi.ColumnMeta { return c.ColumnMeta })
90 }
91
92 var licenseAllColumns = []licenseColumn{
93 {ColumnMeta: funcapi.ColumnMeta{Name: "License", Tooltip: "License", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sticky: true, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return licenseDisplayValue(r) }, DefaultSort: true},
94 {ColumnMeta: funcapi.ColumnMeta{Name: "Bucket", Tooltip: "Normalized State Bucket", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true, Visualization: funcapi.FieldVisualPill}, Value: func(r licenseRow, _ time.Time) any { return string(r.StateBucket) }},
95 {ColumnMeta: funcapi.ColumnMeta{Name: "State", Tooltip: "Raw vendor state", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return emptyToNil(r.StateRaw) }},
96 {ColumnMeta: funcapi.ColumnMeta{Name: "Source", Tooltip: "Profile source", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return emptyToNil(r.Source) }},
97 {ColumnMeta: funcapi.ColumnMeta{Name: "ID", Tooltip: "Stable row identifier", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true, UniqueKey: true}, Value: func(r licenseRow, _ time.Time) any { return licenseRowUniqueKey(r) }},
98 {ColumnMeta: funcapi.ColumnMeta{Name: "Feature", Tooltip: "Feature name", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return emptyToNil(r.Feature) }},
99 {ColumnMeta: funcapi.ColumnMeta{Name: "Component", Tooltip: "License component", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return emptyToNil(r.Component) }},
100 {ColumnMeta: funcapi.ColumnMeta{Name: "Type", Tooltip: "License type", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return emptyToNil(r.Type) }},
101 {ColumnMeta: funcapi.ColumnMeta{Name: "Remaining", Tooltip: "Time remaining until expiry", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDuration}, Value: func(r licenseRow, ts time.Time) any {
102 return licenseRemainingCell(r.ExpiryTS, r.HasExpiry && !r.IsPerpetual, ts)
103 }},
104 {ColumnMeta: funcapi.ColumnMeta{Name: "Expiry", Tooltip: "Expiry time", Type: funcapi.FieldTypeTimestamp, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDatetime}, Value: func(r licenseRow, _ time.Time) any {
105 return licenseTimestampCell(r.ExpiryTS, r.HasExpiry && !r.IsPerpetual)
106 }},
107 {ColumnMeta: funcapi.ColumnMeta{Name: "Auth Remaining", Tooltip: "Authorization time remaining", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDuration}, Value: func(r licenseRow, ts time.Time) any {
108 return licenseRemainingCell(r.AuthorizationExpiry, r.HasAuthorizationTime, ts)
109 }},
110 {ColumnMeta: funcapi.ColumnMeta{Name: "Auth Expiry", Tooltip: "Authorization expiry time", Type: funcapi.FieldTypeTimestamp, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDatetime}, Value: func(r licenseRow, _ time.Time) any {
111 return licenseTimestampCell(r.AuthorizationExpiry, r.HasAuthorizationTime)
112 }},
113 {ColumnMeta: funcapi.ColumnMeta{Name: "Cert Remaining", Tooltip: "Certificate time remaining", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDuration}, Value: func(r licenseRow, ts time.Time) any {
114 return licenseRemainingCell(r.CertificateExpiry, r.HasCertificateTime, ts)
115 }},
116 {ColumnMeta: funcapi.ColumnMeta{Name: "Cert Expiry", Tooltip: "Certificate expiry time", Type: funcapi.FieldTypeTimestamp, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDatetime}, Value: func(r licenseRow, _ time.Time) any {
117 return licenseTimestampCell(r.CertificateExpiry, r.HasCertificateTime)
118 }},
119 {ColumnMeta: funcapi.ColumnMeta{Name: "Grace Remaining", Tooltip: "Grace/evaluation time remaining", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDuration}, Value: func(r licenseRow, ts time.Time) any { return licenseRemainingCell(r.GraceExpiry, r.HasGraceTime, ts) }},
120 {ColumnMeta: funcapi.ColumnMeta{Name: "Grace Expiry", Tooltip: "Grace/evaluation expiry time", Type: funcapi.FieldTypeTimestamp, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true, Transform: funcapi.FieldTransformDatetime}, Value: func(r licenseRow, _ time.Time) any { return licenseTimestampCell(r.GraceExpiry, r.HasGraceTime) }},
121 {ColumnMeta: funcapi.ColumnMeta{Name: "Usage", Tooltip: "Used license units", Type: funcapi.FieldTypeInteger, Units: "licenses", Visible: true, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return licenseIntCell(r.Usage, r.HasUsage) }},
122 {ColumnMeta: funcapi.ColumnMeta{Name: "Capacity", Tooltip: "Total license capacity", Type: funcapi.FieldTypeInteger, Units: "licenses", Visible: true, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return licenseIntCell(r.Capacity, r.HasCapacity) }},
123 {ColumnMeta: funcapi.ColumnMeta{Name: "Available", Tooltip: "Available license units", Type: funcapi.FieldTypeInteger, Units: "licenses", Visible: false, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, Value: func(r licenseRow, _ time.Time) any { return licenseIntCell(r.Available, r.HasAvailable) }},
124 {ColumnMeta: funcapi.ColumnMeta{Name: "Usage %", Tooltip: "License usage percentage", Type: funcapi.FieldTypeFloat, Units: "percentage", Visible: true, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true, Visualization: funcapi.FieldVisualBar, Transform: funcapi.FieldTransformNumber, DecimalPoints: 2}, Value: func(r licenseRow, _ time.Time) any { return licensePercentCell(r.UsagePercent, r.HasUsagePct) }},
125 {ColumnMeta: funcapi.ColumnMeta{Name: "Unlimited", Tooltip: "Unlimited license pool", Type: funcapi.FieldTypeBoolean, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true, Visualization: funcapi.FieldVisualPill}, Value: func(r licenseRow, _ time.Time) any { return r.IsUnlimited }},
126 {ColumnMeta: funcapi.ColumnMeta{Name: "Perpetual", Tooltip: "Perpetual license", Type: funcapi.FieldTypeBoolean, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true, Visualization: funcapi.FieldVisualPill}, Value: func(r licenseRow, _ time.Time) any { return r.IsPerpetual }},
127 {ColumnMeta: funcapi.ColumnMeta{Name: "Impact", Tooltip: "Operational impact", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true, FullWidth: true, Wrap: true}, Value: func(r licenseRow, _ time.Time) any { return emptyToNil(r.Impact) }},
128 }
129
130 func buildLicenseColumns(cs funcapi.ColumnSet[licenseColumn]) map[string]any {
131 columns := cs.BuildColumns()
132 rowOptions := funcapi.Column{
133 Index: cs.Len(),
134 Name: "rowOptions",
135 Type: funcapi.FieldTypeNone,
136 Visualization: funcapi.FieldVisualRowOptions,
137 Sort: funcapi.FieldSortAscending,
138 Sortable: false,
139 Sticky: false,
140 Summary: funcapi.FieldSummaryCount,
141 Filter: funcapi.FieldFilterNone,
142 Visible: false,
143 Dummy: true,
144 ValueOptions: funcapi.ValueOptions{
145 Transform: funcapi.FieldTransformNone,
146 DecimalPoints: 0,
147 DefaultValue: nil,
148 },
149 }
150 columns["rowOptions"] = rowOptions.BuildColumn()
151 return columns
152 }
153
154 func buildLicenseFunctionRow(row licenseRow, ts time.Time) []any {
155 data := make([]any, len(licenseAllColumns)+1)
156 for i, col := range licenseAllColumns {
157 data[i] = col.Value(row, ts)
158 }
159 data[len(licenseAllColumns)] = nil
160 return data
161 }
162
163 func defaultLicenseSortColumn() string {
164 for _, col := range licenseAllColumns {
165 if col.DefaultSort {
166 return col.Name
167 }
168 }
169 return "License"
170 }
171
172 func sortLicenseRows(rows []licenseRow) {
173 sort.SliceStable(rows, func(i, j int) bool {
174 left := rows[i]
175 right := rows[j]
176
177 // Put actionable rows first; the UI can still default-sort by License.
178 if lp, rp := licenseBucketPriority(left.StateBucket), licenseBucketPriority(right.StateBucket); lp != rp {
179 return lp < rp
180 }
181 if leftDisplay, rightDisplay := licenseDisplayValue(left), licenseDisplayValue(right); leftDisplay != rightDisplay {
182 return leftDisplay < rightDisplay
183 }
184 if left.Source != right.Source {
185 return left.Source < right.Source
186 }
187 if left.Table != right.Table {
188 return left.Table < right.Table
189 }
190 return left.ID < right.ID
191 })
192 }
193
194 func licenseDisplayValue(row licenseRow) string {
195 return firstNonBlank(row.Name, row.ID)
196 }
197
198 func licenseBucketPriority(bucket licenseStateBucket) int {
199 switch bucket {
200 case licenseStateBucketBroken:
201 return 0
202 case licenseStateBucketDegraded:
203 return 1
204 case licenseStateBucketInformational:
205 return 2
206 case licenseStateBucketHealthy:
207 return 3
208 default:
209 return 4
210 }
211 }
212
213 func licenseRowUniqueKey(row licenseRow) string {
214 return licenseKeyParts(row.Source, row.Table, firstNonBlank(row.StructuralID, row.ID))
215 }
216
217 func licenseKeyParts(parts ...string) string {
218 var b strings.Builder
219 for _, part := range parts {
220 b.WriteString(strconv.Itoa(len(part)))
221 b.WriteByte(':')
222 b.WriteString(part)
223 }
224 return b.String()
225 }
226
227 func licenseRemainingCell(expiry int64, ok bool, ts time.Time) any {
228 if !ok {
229 return nil
230 }
231 return (expiry - ts.Unix()) * 1000
232 }
233
234 func licenseTimestampCell(expiry int64, ok bool) any {
235 if !ok {
236 return nil
237 }
238 return time.Unix(expiry, 0).UnixMilli()
239 }
240
241 func licenseIntCell(value int64, ok bool) any {
242 if !ok {
243 return nil
244 }
245 return value
246 }
247
248 func licensePercentCell(value float64, ok bool) any {
249 if !ok {
250 return nil
251 }
252 return value
253 }
254
255 func emptyToNil(v string) any {
256 if v == "" {
257 return nil
258 }
259 return v
260 }