master
go 244 lines 6.56 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "slices"
7 "strings"
8 "sync"
9 "time"
10
11 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
12 )
13
14 // Licensing pipeline contract — profiles emit typed licensing rows through
15 // ddsnmp.ProfileMetrics.LicenseRows. Hidden metrics remain a generic ddsnmp
16 // transport but are not part of the licensing consumer contract.
17
18 const (
19 metricIDLicenseRemainingTime = "snmp_device_license_remaining_time"
20 metricIDLicenseAuthorizationRemainingTime = "snmp_device_license_authorization_remaining_time"
21 metricIDLicenseCertificateRemainingTime = "snmp_device_license_certificate_remaining_time"
22 metricIDLicenseGraceRemainingTime = "snmp_device_license_grace_remaining_time"
23 metricIDLicenseUsagePercent = "snmp_device_license_usage_percent"
24 metricIDLicenseStateHealthy = "snmp_device_license_state_healthy"
25 metricIDLicenseStateInformational = "snmp_device_license_state_informational"
26 metricIDLicenseStateDegraded = "snmp_device_license_state_degraded"
27 metricIDLicenseStateBroken = "snmp_device_license_state_broken"
28 metricIDLicenseStateIgnored = "snmp_device_license_state_ignored"
29 )
30
31 type licenseRow struct {
32 ID string
33 StructuralID string
34 Source string
35 Table string
36 Name string
37
38 Feature string
39 Component string
40 Type string
41 Impact string
42
43 StateRaw string
44 StateSeverity int64
45 HasState bool
46 StateBucket licenseStateBucket
47
48 ExpiryTS int64
49 HasExpiry bool
50 AuthorizationExpiry int64
51 HasAuthorizationTime bool
52 CertificateExpiry int64
53 HasCertificateTime bool
54 GraceExpiry int64
55 HasGraceTime bool
56
57 Usage int64
58 HasUsage bool
59 Capacity int64
60 HasCapacity bool
61 Available int64
62 HasAvailable bool
63 UsagePercent float64
64 HasUsagePct bool
65
66 IsUnlimited bool
67 IsPerpetual bool
68
69 ExpirySource string
70 AuthSource string
71 CertSource string
72 GraceSource string
73 }
74
75 type licenseCache struct {
76 mu sync.RWMutex
77 lastUpdate time.Time
78 rows []licenseRow
79 }
80
81 func newLicenseCache() *licenseCache {
82 return &licenseCache{}
83 }
84
85 func (c *licenseCache) store(ts time.Time, rows []licenseRow) {
86 c.mu.Lock()
87 defer c.mu.Unlock()
88
89 c.lastUpdate = ts
90 c.rows = slices.Clone(rows)
91 }
92
93 func (c *licenseCache) snapshot() (time.Time, []licenseRow) {
94 c.mu.RLock()
95 defer c.mu.RUnlock()
96
97 return c.lastUpdate, slices.Clone(c.rows)
98 }
99
100 // extractLicenseRows converts typed ddsnmp licensing rows into the collector's
101 // cached row shape. HiddenMetrics are intentionally ignored here: they remain a
102 // generic ddsnmp transport, not a licensing protocol.
103 func extractLicenseRows(pms []*ddsnmp.ProfileMetrics, now time.Time) []licenseRow {
104 var rows []licenseRow
105
106 for _, pm := range pms {
107 if pm == nil {
108 continue
109 }
110 for _, src := range pm.LicenseRows {
111 row, ok := licenseRowFromTyped(pm, src, now)
112 if !ok {
113 continue
114 }
115 rows = append(rows, row)
116 }
117 }
118
119 rows = dropLicenseRowsWithoutSignals(rows)
120
121 for i := range rows {
122 rows[i].StateBucket = normalizeLicenseStateBucket(rows[i], now)
123 }
124
125 return rows
126 }
127
128 func licenseRowFromTyped(pm *ddsnmp.ProfileMetrics, src ddsnmp.LicenseRow, now time.Time) (licenseRow, bool) {
129 row := licenseRow{
130 Source: licenseRowSource(pm, src),
131 Table: licenseRowTable(src),
132 ID: licenseRowID(src),
133 StructuralID: firstNonBlank(src.StructuralID, src.RowKey, src.ID),
134 Name: src.Name,
135 Feature: src.Feature,
136 Component: src.Component,
137 Type: src.Type,
138 Impact: src.Impact,
139 IsUnlimited: src.IsUnlimited,
140 IsPerpetual: src.IsPerpetual,
141 StateRaw: src.State.Raw,
142 StateSeverity: clampLicenseSeverity(src.State.Severity),
143 HasState: src.State.Has,
144 }
145
146 setLicenseTimer(&row.ExpiryTS, &row.HasExpiry, &row.ExpirySource, src.Expiry, now)
147 setLicenseTimer(&row.AuthorizationExpiry, &row.HasAuthorizationTime, &row.AuthSource, src.Authorization, now)
148 setLicenseTimer(&row.CertificateExpiry, &row.HasCertificateTime, &row.CertSource, src.Certificate, now)
149 setLicenseTimer(&row.GraceExpiry, &row.HasGraceTime, &row.GraceSource, src.Grace, now)
150
151 row.Usage = src.Usage.Used
152 row.HasUsage = src.Usage.HasUsed
153 row.Capacity = src.Usage.Capacity
154 row.HasCapacity = src.Usage.HasCapacity
155 row.Available = src.Usage.Available
156 row.HasAvailable = src.Usage.HasAvailable
157 row.UsagePercent = float64(src.Usage.Percent)
158 row.HasUsagePct = src.Usage.HasPercent
159 deriveLicenseUsage(&row)
160
161 return row, firstNonBlank(row.ID, row.StructuralID) != "" && licenseRowHasAnySignal(row)
162 }
163
164 func licenseRowSource(pm *ddsnmp.ProfileMetrics, src ddsnmp.LicenseRow) string {
165 if strings.TrimSpace(src.OriginProfileID) != "" {
166 return src.OriginProfileID
167 }
168 if pm == nil {
169 return ""
170 }
171 return pm.Source
172 }
173
174 func licenseRowTable(src ddsnmp.LicenseRow) string {
175 return firstNonBlank(src.TableOID, src.Table)
176 }
177
178 func licenseRowID(src ddsnmp.LicenseRow) string {
179 return firstNonBlank(src.ID, src.StructuralID, src.RowKey)
180 }
181
182 func setLicenseTimer(dst *int64, has *bool, source *string, timer ddsnmp.LicenseTimer, now time.Time) {
183 if !timer.Has {
184 return
185 }
186 if timer.Timestamp != 0 {
187 *dst = timer.Timestamp
188 } else {
189 *dst = now.Unix() + timer.RemainingSeconds
190 }
191 *has = true
192 *source = timer.SourceOID
193 }
194
195 func clampLicenseSeverity(value int64) int64 {
196 switch {
197 case value < 0:
198 return 0
199 case value > 2:
200 return 2
201 default:
202 return value
203 }
204 }
205
206 func deriveLicenseUsage(row *licenseRow) {
207 if !row.HasUsage && row.HasCapacity && row.HasAvailable && row.Available >= 0 && row.Available <= row.Capacity {
208 row.Usage = row.Capacity - row.Available
209 row.HasUsage = true
210 }
211 if !row.HasUsagePct && row.HasUsage && row.HasCapacity && row.Capacity > 0 && !row.IsUnlimited {
212 row.UsagePercent = float64(row.Usage) * 100 / float64(row.Capacity)
213 row.HasUsagePct = true
214 }
215 }
216
217 func dropLicenseRowsWithoutSignals(rows []licenseRow) []licenseRow {
218 return slices.DeleteFunc(rows, func(row licenseRow) bool {
219 return !licenseRowHasAnySignal(row)
220 })
221 }
222
223 // licenseRowHasAnySignal returns true when at least one merged signal
224 // (state, expiry, auth/cert/grace timer, usage shape) is set on the row.
225 func licenseRowHasAnySignal(row licenseRow) bool {
226 return row.HasState ||
227 row.HasExpiry ||
228 row.HasAuthorizationTime ||
229 row.HasCertificateTime ||
230 row.HasGraceTime ||
231 row.HasUsage ||
232 row.HasCapacity ||
233 row.HasAvailable ||
234 row.HasUsagePct
235 }
236
237 func firstNonBlank(values ...string) string {
238 for _, value := range values {
239 if strings.TrimSpace(value) != "" {
240 return value
241 }
242 }
243 return ""
244 }