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