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