master
go 142 lines 4.04 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package panos
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "strings"
10 "time"
11 )
12
13 const licenseNeverExpires = int64(-1)
14
15 type licenseInfoResult struct {
16 Licenses *licenseEntries `xml:"licenses"`
17 }
18
19 type licenseEntries struct {
20 Entries []licenseEntry `xml:"entry"`
21 }
22
23 type licenseEntry struct {
24 Feature string `xml:"feature"`
25 Description string `xml:"description"`
26 Expires string `xml:"expires"`
27 Expired string `xml:"expired"`
28 }
29
30 func (c *Collector) collectLicenseMetrics(ctx context.Context) (bool, error) {
31 body, err := c.apiClient.op(ctx, licenseInfoCommand)
32 if err != nil {
33 return false, fmt.Errorf("licenses metricset: %s API call: %w", panosCommandName(licenseInfoCommand), err)
34 }
35
36 licenses, found, err := parseLicenses(body)
37 if err != nil {
38 return false, fmt.Errorf("licenses metricset: %s response: %w", panosCommandName(licenseInfoCommand), err)
39 }
40 if !found {
41 return false, fmt.Errorf("licenses metricset: %s response: %w", panosCommandName(licenseInfoCommand), missingPANOSResultError{expected: "<licenses>"})
42 }
43
44 var expired int64
45 var errs []error
46 for _, entry := range licenses {
47 labels := licenseLabelValues(entry)
48 isExpired, err := c.licenseExpiredStatus(entry)
49 if err != nil {
50 errs = append(errs, fmt.Errorf("license %s expired status: %w", firstNonEmpty(entry.Feature, "unknown"), err))
51 } else {
52 observeStateSetVec(c.metrics.lic.status, boolState(!isExpired, "valid", "expired"), labels...)
53 }
54 if err == nil && isExpired {
55 expired++
56 continue
57 }
58
59 days, ok, err := c.licenseDaysUntilExpiration(entry)
60 if err != nil {
61 errs = append(errs, fmt.Errorf("license %s expiration: %w", firstNonEmpty(entry.Feature, "unknown"), err))
62 continue
63 }
64 if ok {
65 c.metrics.lic.timeUntilExpiration.WithLabelValues(labels...).Observe(float64(days))
66 }
67 }
68
69 c.metrics.lic.countTotal.Observe(float64(len(licenses)))
70 c.metrics.lic.countExpired.Observe(float64(expired))
71 return true, errors.Join(errs...)
72 }
73
74 func parseLicenses(body []byte) ([]licenseEntry, bool, error) {
75 var result licenseInfoResult
76 if err := decodePANOSResult(body, "PAN-OS licenses response", &result); err != nil {
77 return nil, false, err
78 }
79 if result.Licenses == nil {
80 return nil, false, nil
81 }
82 return result.Licenses.Entries, true, nil
83 }
84
85 func (c *Collector) licenseExpiredStatus(entry licenseEntry) (bool, error) {
86 raw := strings.TrimSpace(entry.Expired)
87 switch strings.ToLower(raw) {
88 case "yes", "true", "expired":
89 return true, nil
90 case "no", "false", "valid":
91 return false, nil
92 case "":
93 expires := strings.TrimSpace(entry.Expires)
94 if strings.EqualFold(expires, "never") {
95 return false, nil
96 }
97 if expires == "" {
98 return false, errors.New("missing status")
99 }
100 exp, err := parseLicenseExpirationDate(expires)
101 if err != nil {
102 return false, fmt.Errorf("missing status and %w", err)
103 }
104 now := c.now().UTC()
105 today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
106 expireDay := time.Date(exp.Year(), exp.Month(), exp.Day(), 0, 0, 0, 0, time.UTC)
107 return expireDay.Before(today), nil
108 default:
109 return false, fmt.Errorf("invalid status %q", raw)
110 }
111 }
112
113 func (c *Collector) licenseDaysUntilExpiration(entry licenseEntry) (int64, bool, error) {
114 expires := strings.TrimSpace(entry.Expires)
115 if strings.EqualFold(expires, "never") {
116 return licenseNeverExpires, true, nil
117 }
118 if expires == "" {
119 return 0, false, errors.New("missing expiration date")
120 }
121 exp, err := parseLicenseExpirationDate(expires)
122 if err != nil {
123 return 0, false, err
124 }
125
126 now := c.now().UTC()
127 today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
128 expireDay := time.Date(exp.Year(), exp.Month(), exp.Day(), 0, 0, 0, 0, time.UTC)
129 days := int64(expireDay.Sub(today).Hours() / 24)
130 if days < 0 {
131 return 0, false, nil
132 }
133 return days, true, nil
134 }
135
136 func parseLicenseExpirationDate(expires string) (time.Time, error) {
137 exp, err := time.ParseInLocation("January 02, 2006", expires, time.UTC)
138 if err != nil {
139 return time.Time{}, fmt.Errorf("invalid expiration date %q", expires)
140 }
141 return exp, nil
142 }