master
go 253 lines 7.6 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package panos
4
5 import (
6 "context"
7 "encoding/xml"
8 "errors"
9 "fmt"
10 "io"
11 "strings"
12 )
13
14 type environmentResult struct {
15 Thermal rawXMLSection `xml:"thermal"`
16 Fan rawXMLSection `xml:"fan"`
17 Fans rawXMLSection `xml:"fans"`
18 Power rawXMLSection `xml:"power"`
19 PowerSupply rawXMLSection `xml:"power-supply"`
20 }
21
22 type rawXMLSection struct {
23 InnerXML string `xml:",innerxml"`
24 }
25
26 type environmentEntry struct {
27 Slot string `xml:"slot"`
28 Name string `xml:"name"`
29 Description string `xml:"description"`
30 Alarm string `xml:"alarm"`
31 Inserted string `xml:"Inserted"`
32 Min string `xml:"min"`
33 Max string `xml:"max"`
34 DegreesC string `xml:"DegreesC"`
35 RPMs string `xml:"RPMs"`
36 Volts string `xml:"Volts"`
37 }
38
39 type environmentSensor struct {
40 kind string
41 entry environmentEntry
42 }
43
44 func (c *Collector) collectEnvironmentMetrics(ctx context.Context) (bool, error) {
45 body, err := c.apiClient.op(ctx, environmentCommand)
46 if err != nil {
47 return false, fmt.Errorf("environment metricset: %s API call: %w", panosCommandName(environmentCommand), err)
48 }
49
50 env, err := parseEnvironment(body)
51 if err != nil {
52 return false, fmt.Errorf("environment metricset: %s response: %w", panosCommandName(environmentCommand), err)
53 }
54
55 sensors := env.sensors()
56
57 var hasMetrics bool
58 var errs []error
59 for _, sensor := range sensors {
60 entry := sensor.entry
61 labels := environmentLabelValues(sensor.kind, entry)
62 switch sensor.kind {
63 case "temperature":
64 alarm, err := parsePANOSAlarmField("environment temperature "+environmentSensorName(entry)+" alarm", entry.Alarm)
65 if err != nil {
66 errs = append(errs, err)
67 } else {
68 hasMetrics = true
69 observeStateSetVec(c.metrics.env.sensorAlarm, alarmState(alarm), labels...)
70 }
71 value, err := parseRequiredPANOSDecimalField("environment temperature "+environmentSensorName(entry), entry.DegreesC, 1000)
72 if err != nil {
73 errs = append(errs, err)
74 continue
75 }
76 hasMetrics = true
77 c.metrics.env.temperature.WithLabelValues(labels...).Observe(float64(value))
78 case "fan":
79 alarm, err := parsePANOSAlarmField("environment fan "+environmentSensorName(entry)+" alarm", entry.Alarm)
80 if err != nil {
81 errs = append(errs, err)
82 } else {
83 hasMetrics = true
84 observeStateSetVec(c.metrics.env.sensorAlarm, alarmState(alarm), labels...)
85 }
86 value, err := parseRequiredPANOSIntField("environment fan "+environmentSensorName(entry)+" RPMs", entry.RPMs)
87 if err != nil {
88 errs = append(errs, err)
89 continue
90 }
91 hasMetrics = true
92 c.metrics.env.fanSpeed.WithLabelValues(labels...).Observe(float64(value))
93 case "voltage":
94 alarm, err := parsePANOSAlarmField("environment voltage "+environmentSensorName(entry)+" alarm", entry.Alarm)
95 if err != nil {
96 errs = append(errs, err)
97 } else {
98 hasMetrics = true
99 observeStateSetVec(c.metrics.env.sensorAlarm, alarmState(alarm), labels...)
100 }
101 value, err := parseRequiredPANOSDecimalField("environment voltage "+environmentSensorName(entry), entry.Volts, 1000)
102 if err != nil {
103 errs = append(errs, err)
104 continue
105 }
106 hasMetrics = true
107 c.metrics.env.voltage.WithLabelValues(labels...).Observe(float64(value))
108 case "power_supply":
109 inserted, insertedErr := parsePANOSAffirmativeField("environment power supply "+environmentSensorName(entry)+" inserted", entry.Inserted)
110 if insertedErr != nil {
111 errs = append(errs, insertedErr)
112 }
113 alarm, alarmErr := parsePANOSAlarmField("environment power supply "+environmentSensorName(entry)+" alarm", entry.Alarm)
114 if alarmErr != nil {
115 errs = append(errs, alarmErr)
116 }
117 if insertedErr == nil {
118 hasMetrics = true
119 observeStateSetVec(c.metrics.env.powerSupplyPresence, boolState(inserted, "present", "absent"), labels...)
120 }
121 if alarmErr == nil {
122 hasMetrics = true
123 observeStateSetVec(c.metrics.env.powerSupplyAlarm, alarmState(alarm), labels...)
124 }
125 }
126 }
127
128 return hasMetrics, errors.Join(errs...)
129 }
130
131 type environmentMetrics struct {
132 ThermalEntries []environmentEntry
133 FanEntries []environmentEntry
134 VoltageEntries []environmentEntry
135 PowerSupplyEntries []environmentEntry
136 }
137
138 func (m environmentMetrics) sensors() []environmentSensor {
139 total := len(m.ThermalEntries) + len(m.FanEntries) + len(m.VoltageEntries) + len(m.PowerSupplyEntries)
140 sensors := make([]environmentSensor, 0, total)
141 for _, entry := range m.ThermalEntries {
142 sensors = append(sensors, environmentSensor{kind: "temperature", entry: entry})
143 }
144 for _, entry := range m.FanEntries {
145 sensors = append(sensors, environmentSensor{kind: "fan", entry: entry})
146 }
147 for _, entry := range m.VoltageEntries {
148 sensors = append(sensors, environmentSensor{kind: "voltage", entry: entry})
149 }
150 for _, entry := range m.PowerSupplyEntries {
151 sensors = append(sensors, environmentSensor{kind: "power_supply", entry: entry})
152 }
153 return sensors
154 }
155
156 func parseEnvironment(body []byte) (environmentMetrics, error) {
157 var result environmentResult
158 if err := decodePANOSResult(body, "PAN-OS environment response", &result); err != nil {
159 return environmentMetrics{}, err
160 }
161 if !result.hasAnySection() {
162 return environmentMetrics{}, missingPANOSResultError{expected: "<thermal>, <fan>, <fans>, <power>, or <power-supply>"}
163 }
164
165 thermal, err := decodeEnvironmentEntries(result.Thermal.InnerXML)
166 if err != nil {
167 return environmentMetrics{}, fmt.Errorf("thermal entries: %w", err)
168 }
169 fan, err := decodeEnvironmentFanEntries(result.Fan.InnerXML, result.Fans.InnerXML)
170 if err != nil {
171 return environmentMetrics{}, fmt.Errorf("fan entries: %w", err)
172 }
173 voltage, err := decodeEnvironmentEntries(result.Power.InnerXML)
174 if err != nil {
175 return environmentMetrics{}, fmt.Errorf("voltage entries: %w", err)
176 }
177 psu, err := decodeEnvironmentEntries(result.PowerSupply.InnerXML)
178 if err != nil {
179 return environmentMetrics{}, fmt.Errorf("power supply entries: %w", err)
180 }
181
182 return environmentMetrics{
183 ThermalEntries: thermal,
184 FanEntries: fan,
185 VoltageEntries: voltage,
186 PowerSupplyEntries: psu,
187 }, nil
188 }
189
190 func (r environmentResult) hasAnySection() bool {
191 return firstNonEmpty(r.Thermal.InnerXML, r.Fan.InnerXML, r.Fans.InnerXML, r.Power.InnerXML, r.PowerSupply.InnerXML) != ""
192 }
193
194 func decodeEnvironmentFanEntries(sections ...string) ([]environmentEntry, error) {
195 var entries []environmentEntry
196 seen := make(map[string]bool)
197 for _, section := range sections {
198 decoded, err := decodeEnvironmentEntries(section)
199 if err != nil {
200 return nil, err
201 }
202 for _, entry := range decoded {
203 key := environmentEntryIdentity(entry)
204 if seen[key] {
205 continue
206 }
207 seen[key] = true
208 entries = append(entries, entry)
209 }
210 }
211 return entries, nil
212 }
213
214 func environmentEntryIdentity(entry environmentEntry) string {
215 return firstNonEmpty(entry.Slot, "unknown") + "\x00" + environmentSensorName(entry)
216 }
217
218 func decodeEnvironmentEntries(innerXML string) ([]environmentEntry, error) {
219 if strings.TrimSpace(innerXML) == "" {
220 return nil, nil
221 }
222
223 decoder := xml.NewDecoder(strings.NewReader(innerXML))
224 var entries []environmentEntry
225
226 for {
227 tok, err := decoder.Token()
228 if err != nil {
229 if err == io.EOF {
230 return entries, nil
231 }
232 return nil, err
233 }
234
235 start, ok := tok.(xml.StartElement)
236 if !ok || start.Name.Local != "entry" {
237 continue
238 }
239
240 var entry environmentEntry
241 if err := decoder.DecodeElement(&entry, &start); err != nil {
242 return nil, err
243 }
244 if firstNonEmpty(entry.Description, entry.Name, entry.Slot) == "" {
245 continue
246 }
247 entries = append(entries, entry)
248 }
249 }
250
251 func environmentSensorName(entry environmentEntry) string {
252 return firstNonEmpty(entry.Description, entry.Name, "unknown")
253 }