| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package panos |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | type haResult struct { |
| 12 | Enabled string `xml:"enabled"` |
| 13 | Group haGroup `xml:"group"` |
| 14 | } |
| 15 | |
| 16 | type haGroup struct { |
| 17 | Mode string `xml:"mode"` |
| 18 | RunningSync string `xml:"running-sync"` |
| 19 | LocalInfo haInfo `xml:"local-info"` |
| 20 | PeerInfo haInfo `xml:"peer-info"` |
| 21 | } |
| 22 | |
| 23 | type haInfo struct { |
| 24 | State string `xml:"state"` |
| 25 | ConnStatus string `xml:"conn-status"` |
| 26 | StateSync string `xml:"state-sync"` |
| 27 | ConnHA1 haConn `xml:"conn-ha1"` |
| 28 | ConnHA1B haConn `xml:"conn-ha1-backup"` |
| 29 | ConnHA2 haConn `xml:"conn-ha2"` |
| 30 | ConnHA2B haConn `xml:"conn-ha2-backup"` |
| 31 | } |
| 32 | |
| 33 | type haConn struct { |
| 34 | Status string `xml:"conn-status"` |
| 35 | } |
| 36 | |
| 37 | func (c *Collector) collectHAMetrics(ctx context.Context) (bool, error) { |
| 38 | body, err := c.apiClient.op(ctx, haStateCommand) |
| 39 | if err != nil { |
| 40 | return false, fmt.Errorf("ha metricset: %s API call: %w", panosCommandName(haStateCommand), err) |
| 41 | } |
| 42 | |
| 43 | ha, err := parseHAState(body) |
| 44 | if err != nil { |
| 45 | return false, fmt.Errorf("ha metricset: %s response: %w", panosCommandName(haStateCommand), err) |
| 46 | } |
| 47 | if firstNonEmpty(ha.Enabled, ha.Group.LocalInfo.State, ha.Group.PeerInfo.State) == "" { |
| 48 | return false, fmt.Errorf("ha metricset: %s response: %w", panosCommandName(haStateCommand), missingPANOSResultError{expected: "<enabled> or <group>"}) |
| 49 | } |
| 50 | |
| 51 | enabled, err := c.haEnabledStatus(ha) |
| 52 | if err != nil { |
| 53 | return false, fmt.Errorf("ha metricset: %s response: %w", panosCommandName(haStateCommand), err) |
| 54 | } |
| 55 | if !enabled && firstNonEmpty(ha.Group.LocalInfo.State, ha.Group.PeerInfo.State, ha.Group.RunningSync) == "" { |
| 56 | observeStateSet(c.metrics.ha.status, "disabled") |
| 57 | return true, nil |
| 58 | } |
| 59 | |
| 60 | localState := normalizeHAState(ha.Group.LocalInfo.State) |
| 61 | peerState := normalizeHAState(ha.Group.PeerInfo.State) |
| 62 | stateSync := firstNonEmpty(ha.Group.RunningSync, ha.Group.LocalInfo.StateSync) |
| 63 | |
| 64 | observeStateSet(c.metrics.ha.status, boolState(enabled, "enabled", "disabled")) |
| 65 | observeStateSet(c.metrics.ha.localState, localState) |
| 66 | observeStateSet(c.metrics.ha.peerState, peerState) |
| 67 | if ha.Group.PeerInfo.ConnStatus != "" { |
| 68 | observeStateSet(c.metrics.ha.peerConnectionStatus, normalizeUpDownState(ha.Group.PeerInfo.ConnStatus)) |
| 69 | } |
| 70 | if stateSync != "" { |
| 71 | observeStateSet(c.metrics.ha.stateSync, normalizeHASyncState(stateSync)) |
| 72 | } |
| 73 | c.observeHALinkStatus("ha1", ha.Group.PeerInfo.ConnHA1.Status) |
| 74 | c.observeHALinkStatus("ha1_backup", ha.Group.PeerInfo.ConnHA1B.Status) |
| 75 | c.observeHALinkStatus("ha2", ha.Group.PeerInfo.ConnHA2.Status) |
| 76 | c.observeHALinkStatus("ha2_backup", ha.Group.PeerInfo.ConnHA2B.Status) |
| 77 | return true, nil |
| 78 | } |
| 79 | |
| 80 | func (c *Collector) observeHALinkStatus(link, status string) { |
| 81 | state := normalizeUpDownState(status) |
| 82 | if state == "" { |
| 83 | return |
| 84 | } |
| 85 | |
| 86 | observeStateSetVec(c.metrics.ha.linkStatus, state, link) |
| 87 | } |
| 88 | |
| 89 | func parseHAState(body []byte) (haResult, error) { |
| 90 | var result haResult |
| 91 | if err := decodePANOSResult(body, "PAN-OS HA response", &result); err != nil { |
| 92 | return haResult{}, err |
| 93 | } |
| 94 | return result, nil |
| 95 | } |
| 96 | |
| 97 | func normalizeHAState(state string) string { |
| 98 | state = strings.ToLower(strings.TrimSpace(state)) |
| 99 | state = strings.ReplaceAll(state, "-", "_") |
| 100 | state = strings.ReplaceAll(state, " ", "_") |
| 101 | switch state { |
| 102 | case "": |
| 103 | return "" |
| 104 | case "active", "passive", "suspended", "unknown": |
| 105 | return state |
| 106 | case "nonfunctional", "non_functional", "non_function": |
| 107 | return "non_functional" |
| 108 | default: |
| 109 | return "unknown" |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | func (c *Collector) haEnabledStatus(ha haResult) (bool, error) { |
| 114 | if strings.TrimSpace(ha.Enabled) != "" { |
| 115 | return parsePANOSAffirmativeField("HA enabled", ha.Enabled) |
| 116 | } |
| 117 | return firstNonEmpty(ha.Group.LocalInfo.State, ha.Group.PeerInfo.State, ha.Group.RunningSync) != "", nil |
| 118 | } |
| 119 | |
| 120 | func normalizeHASyncState(v string) string { |
| 121 | switch strings.ToLower(strings.TrimSpace(v)) { |
| 122 | case "synchronized", "complete": |
| 123 | return "synchronized" |
| 124 | case "not synchronized", "not-synchronized", "not_synchronized", "unsynchronized", "out of sync", "out-of-sync", "out_of_sync", "incomplete", "syncing", "synchronizing": |
| 125 | return "not_synchronized" |
| 126 | case "": |
| 127 | return "" |
| 128 | default: |
| 129 | return "unknown" |
| 130 | } |
| 131 | } |