master
go 816 lines 22.3 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 "net"
12 "net/netip"
13 "regexp"
14 "strconv"
15 "strings"
16 "time"
17 )
18
19 const legacyBGPPeerCommand = "<show><routing><protocol><bgp><peer></peer></bgp></protocol></routing></show>"
20
21 const (
22 noBGPReprobeInterval = 5 * time.Minute
23 maxBGPPeerEntryDepth = 8
24
25 logKeyBGPEmptyQuery = "panos:bgp:empty_query"
26 logKeyBGPQueryFailed = "panos:bgp:query_failed"
27 logKeyBGPProbeErrors = "panos:bgp:probe_errors"
28 logKeyBGPNoPeers = "panos:bgp:no_peers"
29 logKeyBGPLegacy = "panos:bgp:legacy"
30 logKeyBGPAdvanced = "panos:bgp:advanced"
31 )
32
33 var advancedBGPPeerCommands = []string{
34 "<show><advanced-routing><bgp><peer><details></details></peer></bgp></advanced-routing></show>",
35 "<show><advanced-routing><bgp><peer><status></status></peer></bgp></advanced-routing></show>",
36 "<show><advanced-routing><bgp><peer></peer></bgp></advanced-routing></show>",
37 }
38
39 type routingEngine string
40
41 const (
42 routingEngineUnknown routingEngine = ""
43 routingEngineLegacy routingEngine = "legacy"
44 routingEngineAdvanced routingEngine = "advanced"
45 routingEngineNone routingEngine = "none"
46 )
47
48 type bgpPeer struct {
49 VR string
50 PeerAddress string
51 LocalAddress string
52 RemoteAS string
53 PeerGroup string
54 State string
55 Uptime int64
56 MessagesIn int64
57 MessagesOut int64
58 UpdatesIn int64
59 UpdatesOut int64
60 Flaps int64
61 Established int64
62 PrefixCounters []bgpPrefixCounter
63 }
64
65 type bgpPrefixCounter struct {
66 AFI string
67 SAFI string
68 IncomingTotal int64
69 IncomingAccepted int64
70 IncomingRejected int64
71 OutgoingAdvertised int64
72 }
73
74 type panosResponseMessage struct {
75 Text string `xml:",chardata"`
76 Lines []string `xml:"line"`
77 }
78
79 type panosBGPPeerEntry struct {
80 PeerAttr string `xml:"peer,attr"`
81 NameAttr string `xml:"name,attr"`
82 PeerNameAttr string `xml:"peer-name,attr"`
83 PeerAddressAttr string `xml:"peer-address,attr"`
84 VRAttr string `xml:"vr,attr"`
85 VirtualRouterAttr string `xml:"virtual-router,attr"`
86 LogicalRouterAttr string `xml:"logical-router,attr"`
87 PeerGroupAttr string `xml:"peer-group,attr"`
88 RemoteASAttr string `xml:"remote-as,attr"`
89
90 Peer string `xml:"peer"`
91 PeerName string `xml:"peer-name"`
92 PeerAddress string `xml:"peer-address"`
93 VR string `xml:"vr"`
94 VirtualRouter string `xml:"virtual-router"`
95 LogicalRouter string `xml:"logical-router"`
96 PeerGroup string `xml:"peer-group"`
97 LocalAddress string `xml:"local-address"`
98 RemoteAS string `xml:"remote-as"`
99 PeerAS string `xml:"peer-as"`
100 PeerASNumber string `xml:"peer-as-number"`
101 Status string `xml:"status"`
102 State string `xml:"state"`
103 BGPState string `xml:"bgp-state"`
104 PeerState string `xml:"peer-state"`
105 SessionState string `xml:"sess-state"`
106 StatusDuration string `xml:"status-duration"`
107 Uptime string `xml:"uptime"`
108 UptimeSeconds string `xml:"uptime-seconds"`
109 MsgTotalIn string `xml:"msg-total-in"`
110 MsgTotalOut string `xml:"msg-total-out"`
111 MsgUpdateIn string `xml:"msg-update-in"`
112 MsgUpdateOut string `xml:"msg-update-out"`
113 StatusFlapCounts string `xml:"status-flap-counts"`
114 FlapCount string `xml:"flap-count"`
115 EstablishedCount string `xml:"established-counts"`
116
117 PrefixCounter struct {
118 Entries []panosBGPPrefixEntry `xml:"entry"`
119 } `xml:"prefix-counter"`
120 Entries []panosBGPPeerEntry `xml:"entry"`
121 }
122
123 type panosBGPPrefixEntry struct {
124 AFISAFIAttr string `xml:"afi-safi,attr"`
125 NameAttr string `xml:"name,attr"`
126 AFIAttr string `xml:"afi,attr"`
127 SAFIAttr string `xml:"safi,attr"`
128
129 AFI string `xml:"afi"`
130 SAFI string `xml:"safi"`
131 IncomingTotal string `xml:"incoming-total"`
132 IncomingAccepted string `xml:"incoming-accepted"`
133 IncomingRejected string `xml:"incoming-rejected"`
134 OutgoingAdvertised string `xml:"outgoing-advertised"`
135 }
136
137 func (c *Collector) collectBGPPeers(ctx context.Context) ([]bgpPeer, error) {
138 if c.apiClient == nil {
139 return nil, errors.New("PAN-OS API client not initialized")
140 }
141 defer c.logSystemInfo()
142 if err := contextError(ctx); err != nil {
143 return nil, err
144 }
145
146 if c.routingEngine == routingEngineNone && c.now().Sub(c.noBGPProbedAt) < noBGPReprobeInterval {
147 c.Debugf("PAN-OS BGP peers not found on previous probe; skipping routing-engine probe until %s", c.noBGPProbedAt.Add(noBGPReprobeInterval).Format(time.RFC3339))
148 return nil, nil
149 }
150
151 if c.bgpCommand != "" {
152 peers, err := c.queryBGPPeers(ctx, c.bgpCommand)
153 if len(peers) > 0 {
154 return peers, err
155 }
156 if err == nil {
157 staleCommand := c.bgpCommand
158 c.Limit(logKeyBGPEmptyQuery, 1, recurringLogEvery).
159 Warningf("PAN-OS BGP query returned no peers (routing_engine=%s, command=%s), probing routing engine again", c.routingEngine, bgpCommandName(staleCommand))
160 c.routingEngine = routingEngineUnknown
161 c.bgpCommand = ""
162 return c.probeAndCollectBGPPeersExcept(ctx, staleCommand, true)
163 }
164 c.Limit(logKeyBGPQueryFailed, 1, recurringLogEvery).
165 Warningf("PAN-OS BGP query failed (routing_engine=%s, command=%s), probing routing engine again: %v", c.routingEngine, bgpCommandName(c.bgpCommand), err)
166 c.routingEngine = routingEngineUnknown
167 c.bgpCommand = ""
168 }
169
170 return c.probeAndCollectBGPPeers(ctx)
171 }
172
173 func (c *Collector) probeAndCollectBGPPeers(ctx context.Context) ([]bgpPeer, error) {
174 return c.probeAndCollectBGPPeersExcept(ctx, "", false)
175 }
176
177 func (c *Collector) probeAndCollectBGPPeersExcept(ctx context.Context, skipCommand string, emptySuccess bool) ([]bgpPeer, error) {
178 var errs []error
179
180 if legacyBGPPeerCommand != skipCommand {
181 if err := contextError(ctx); err != nil {
182 return nil, err
183 }
184 peers, err := c.queryBGPPeers(ctx, legacyBGPPeerCommand)
185 if len(peers) > 0 {
186 c.routingEngine = routingEngineLegacy
187 c.bgpCommand = legacyBGPPeerCommand
188 c.Limit(logKeyBGPLegacy, 1, recurringLogEvery).
189 Infof("detected PAN-OS legacy routing engine for BGP collection (command=%s)", bgpCommandName(c.bgpCommand))
190 return peers, err
191 }
192 if err == nil {
193 emptySuccess = true
194 } else {
195 errs = append(errs, fmt.Errorf("%s: %w", bgpCommandName(legacyBGPPeerCommand), err))
196 }
197 }
198
199 for _, cmd := range c.advancedBGPCommands {
200 if cmd == skipCommand {
201 continue
202 }
203 if err := contextError(ctx); err != nil {
204 return nil, err
205 }
206 peers, err := c.queryBGPPeers(ctx, cmd)
207 if len(peers) > 0 {
208 c.routingEngine = routingEngineAdvanced
209 c.bgpCommand = cmd
210 c.Limit(logKeyBGPAdvanced, 1, recurringLogEvery).
211 Infof("detected PAN-OS Advanced Routing Engine for BGP collection (command=%s)", bgpCommandName(c.bgpCommand))
212 return peers, err
213 }
214 if err == nil {
215 emptySuccess = true
216 } else {
217 errs = append(errs, fmt.Errorf("%s: %w", bgpCommandName(cmd), err))
218 }
219 }
220
221 if emptySuccess {
222 if len(errs) > 0 {
223 err := errors.Join(errs...)
224 c.Limit(logKeyBGPProbeErrors, 1, recurringLogEvery).
225 Warningf("PAN-OS BGP probes returned no peers, but at least one probe failed; will retry all BGP probes next cycle: %v", err)
226 return nil, err
227 }
228 c.routingEngine = routingEngineNone
229 c.noBGPProbedAt = c.now()
230 c.Limit(logKeyBGPNoPeers, 1, recurringLogEvery).
231 Info("connected to PAN-OS XML API, but no BGP peers were found by legacy or Advanced Routing Engine probes")
232 return nil, nil
233 }
234 if len(errs) > 0 {
235 return nil, errors.Join(errs...)
236 }
237 return nil, nil
238 }
239
240 func (c *Collector) queryBGPPeers(ctx context.Context, cmd string) ([]bgpPeer, error) {
241 body, err := c.apiClient.op(ctx, cmd)
242 if err != nil {
243 return nil, fmt.Errorf("%s API call: %w", bgpCommandName(cmd), err)
244 }
245 peers, err := parseBGPPeers(body)
246 if err != nil {
247 return peers, fmt.Errorf("%s response: %w", bgpCommandName(cmd), err)
248 }
249 return peers, nil
250 }
251
252 func parseBGPPeers(body []byte) ([]bgpPeer, error) {
253 innerXML, err := decodePANOSResultInner(body, "PAN-OS BGP response")
254 if err != nil {
255 return nil, err
256 }
257 if strings.TrimSpace(innerXML) == "" {
258 return nil, nil
259 }
260
261 entries, err := decodeBGPPeerEntries(innerXML)
262 if err != nil {
263 return nil, err
264 }
265
266 var peers []bgpPeer
267 var errs []error
268 seen := make(map[string]bool)
269 for _, entry := range entries {
270 peer, ok, err := entry.toBGPPeer()
271 if err != nil {
272 errs = append(errs, fmt.Errorf("BGP peer entry %s: %w", bgpPeerEntryName(entry), err))
273 }
274 if !ok {
275 continue
276 }
277 key := peer.VR + "\x00" + peer.PeerAddress
278 if seen[key] {
279 continue
280 }
281 seen[key] = true
282 peers = append(peers, peer)
283 }
284
285 return peers, errors.Join(errs...)
286 }
287
288 func bgpPeerEntryName(entry panosBGPPeerEntry) string {
289 return firstNonEmpty(normalizeAddress(entry.peerAddress()), normalizeAddress(entry.peerName()), "unknown")
290 }
291
292 func decodeBGPPeerEntries(innerXML string) ([]panosBGPPeerEntry, error) {
293 decoder := xml.NewDecoder(strings.NewReader(innerXML))
294
295 var entries []panosBGPPeerEntry
296 for {
297 tok, err := decoder.Token()
298 if err != nil {
299 if errors.Is(err, io.EOF) {
300 break
301 }
302 return nil, fmt.Errorf("parse PAN-OS BGP result: %w", err)
303 }
304
305 start, ok := tok.(xml.StartElement)
306 if !ok || start.Name.Local != "entry" {
307 continue
308 }
309
310 var entry panosBGPPeerEntry
311 if err := decoder.DecodeElement(&entry, &start); err != nil {
312 return nil, fmt.Errorf("parse PAN-OS BGP peer entry: %w", err)
313 }
314 entries = appendFlattenedBGPPeerEntries(entries, entry, inheritedBGPPeerFields{}, 0)
315 }
316
317 return entries, nil
318 }
319
320 type inheritedBGPPeerFields struct {
321 vr string
322 peerGroup string
323 localAddr string
324 remoteAS string
325 peerAddress string
326 }
327
328 func appendFlattenedBGPPeerEntries(entries []panosBGPPeerEntry, entry panosBGPPeerEntry, parent inheritedBGPPeerFields, depth int) []panosBGPPeerEntry {
329 if depth > maxBGPPeerEntryDepth {
330 return entries
331 }
332 entry.inherit(parent)
333 entries = append(entries, entry)
334
335 vr := entry.vr()
336 if vr == "" && len(entry.Entries) > 0 {
337 vr = entry.NameAttr
338 }
339 next := inheritedBGPPeerFields{
340 vr: vr,
341 peerGroup: entry.peerGroup(),
342 localAddr: entry.localAddress(),
343 remoteAS: entry.remoteAS(),
344 peerAddress: entry.peerAddress(),
345 }
346 for _, child := range entry.Entries {
347 entries = appendFlattenedBGPPeerEntries(entries, child, next, depth+1)
348 }
349 return entries
350 }
351
352 func (e *panosBGPPeerEntry) inherit(parent inheritedBGPPeerFields) {
353 if e.vr() == "" {
354 e.VR = parent.vr
355 }
356 if e.peerGroup() == "" {
357 e.PeerGroup = parent.peerGroup
358 }
359 if e.localAddress() == "" {
360 e.LocalAddress = parent.localAddr
361 }
362 if e.remoteAS() == "" {
363 e.RemoteAS = parent.remoteAS
364 }
365 if e.peerAddress() == "" {
366 e.PeerAddress = parent.peerAddress
367 }
368 }
369
370 func (e panosBGPPeerEntry) toBGPPeer() (bgpPeer, bool, error) {
371 peerAddr := normalizeAddress(e.peerAddress())
372 if peerAddr == "" && e.hasPeerData() {
373 peerAddr = normalizeAddress(e.peerName())
374 }
375 if peerAddr == "" {
376 return bgpPeer{}, false, nil
377 }
378
379 uptime, err := parseRequiredPANOSDurationField("BGP peer "+peerAddr+" uptime", firstNonEmpty(e.StatusDuration, e.UptimeSeconds, e.Uptime))
380 if err != nil {
381 return bgpPeer{}, false, err
382 }
383 messagesIn, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" msg-total-in", e.MsgTotalIn)
384 if err != nil {
385 return bgpPeer{}, false, err
386 }
387 messagesOut, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" msg-total-out", e.MsgTotalOut)
388 if err != nil {
389 return bgpPeer{}, false, err
390 }
391 updatesIn, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" msg-update-in", e.MsgUpdateIn)
392 if err != nil {
393 return bgpPeer{}, false, err
394 }
395 updatesOut, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" msg-update-out", e.MsgUpdateOut)
396 if err != nil {
397 return bgpPeer{}, false, err
398 }
399 flaps, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" flap-count", firstNonEmpty(e.StatusFlapCounts, e.FlapCount))
400 if err != nil {
401 return bgpPeer{}, false, err
402 }
403 established, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" established-counts", e.EstablishedCount)
404 if err != nil {
405 return bgpPeer{}, false, err
406 }
407 prefixCounters, err := e.prefixCounters(peerAddr)
408 peer := bgpPeer{
409 VR: firstNonEmpty(e.vr(), "default"),
410 PeerAddress: peerAddr,
411 LocalAddress: normalizeAddress(e.localAddress()),
412 RemoteAS: e.remoteAS(),
413 PeerGroup: e.peerGroup(),
414 State: normalizeBGPState(firstNonEmpty(e.Status, e.State, e.BGPState, e.PeerState, e.SessionState)),
415 Uptime: uptime,
416 MessagesIn: messagesIn,
417 MessagesOut: messagesOut,
418 UpdatesIn: updatesIn,
419 UpdatesOut: updatesOut,
420 Flaps: flaps,
421 Established: established,
422 PrefixCounters: prefixCounters,
423 }
424
425 return peer, true, err
426 }
427
428 func (e panosBGPPeerEntry) hasPeerData() bool {
429 return firstNonEmpty(
430 e.Status,
431 e.State,
432 e.BGPState,
433 e.PeerState,
434 e.SessionState,
435 e.StatusDuration,
436 e.Uptime,
437 e.UptimeSeconds,
438 e.MsgTotalIn,
439 e.MsgTotalOut,
440 e.MsgUpdateIn,
441 e.MsgUpdateOut,
442 e.StatusFlapCounts,
443 e.FlapCount,
444 e.EstablishedCount,
445 ) != "" || len(e.PrefixCounter.Entries) > 0
446 }
447
448 func (e panosBGPPeerEntry) vr() string {
449 return firstNonEmpty(e.VR, e.VRAttr, e.VirtualRouter, e.VirtualRouterAttr, e.LogicalRouter, e.LogicalRouterAttr)
450 }
451
452 func (e panosBGPPeerEntry) peerName() string {
453 return firstNonEmpty(e.PeerAddress, e.PeerAddressAttr, e.Peer, e.PeerAttr, e.PeerName, e.PeerNameAttr, e.NameAttr)
454 }
455
456 func (e panosBGPPeerEntry) peerAddress() string {
457 return firstNonEmpty(e.PeerAddress, e.PeerAddressAttr, e.Peer, e.PeerAttr)
458 }
459
460 func (e panosBGPPeerEntry) localAddress() string {
461 return e.LocalAddress
462 }
463
464 func (e panosBGPPeerEntry) remoteAS() string {
465 return firstNonEmpty(e.RemoteAS, e.RemoteASAttr, e.PeerAS, e.PeerASNumber)
466 }
467
468 func (e panosBGPPeerEntry) peerGroup() string {
469 return firstNonEmpty(e.PeerGroup, e.PeerGroupAttr)
470 }
471
472 func (e panosBGPPeerEntry) prefixCounters(peerAddr string) ([]bgpPrefixCounter, error) {
473 counters := make([]bgpPrefixCounter, 0, len(e.PrefixCounter.Entries))
474 var errs []error
475 for _, entry := range e.PrefixCounter.Entries {
476 counter, err := entry.toBGPPrefixCounter(peerAddr)
477 if err != nil {
478 errs = append(errs, err)
479 continue
480 }
481 if counter.AFI == "" {
482 counter.AFI = "unknown"
483 }
484 if counter.SAFI == "" {
485 counter.SAFI = "unknown"
486 }
487 counters = append(counters, counter)
488 }
489 return counters, errors.Join(errs...)
490 }
491
492 func (e panosBGPPrefixEntry) toBGPPrefixCounter(peerAddr string) (bgpPrefixCounter, error) {
493 afi, safi := normalizeAFISAFI(firstNonEmpty(e.AFISAFIAttr, e.NameAttr))
494 if afi == "" {
495 afi = normalizeAFI(firstNonEmpty(e.AFI, e.AFIAttr))
496 }
497 if safi == "" {
498 safi = normalizeSAFI(firstNonEmpty(e.SAFI, e.SAFIAttr))
499 }
500 family := "unknown"
501 if afi != "" && safi != "" {
502 family = afi + "-" + safi
503 } else if afi != "" {
504 family = afi
505 } else if safi != "" {
506 family = safi
507 }
508
509 incomingTotal, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" "+family+" incoming-total", e.IncomingTotal)
510 if err != nil {
511 return bgpPrefixCounter{}, err
512 }
513 incomingAccepted, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" "+family+" incoming-accepted", e.IncomingAccepted)
514 if err != nil {
515 return bgpPrefixCounter{}, err
516 }
517 incomingRejected, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" "+family+" incoming-rejected", e.IncomingRejected)
518 if err != nil {
519 return bgpPrefixCounter{}, err
520 }
521 outgoingAdvertised, err := parseRequiredPANOSIntField("BGP peer "+peerAddr+" "+family+" outgoing-advertised", e.OutgoingAdvertised)
522 if err != nil {
523 return bgpPrefixCounter{}, err
524 }
525
526 return bgpPrefixCounter{
527 AFI: afi,
528 SAFI: safi,
529 IncomingTotal: incomingTotal,
530 IncomingAccepted: incomingAccepted,
531 IncomingRejected: incomingRejected,
532 OutgoingAdvertised: outgoingAdvertised,
533 }, nil
534 }
535
536 func normalizeBGPState(state string) string {
537 v := strings.ToLower(strings.TrimSpace(state))
538 if v == "" {
539 return ""
540 }
541 v = stateNameReplacer.Replace(v)
542
543 switch {
544 case strings.Contains(v, "established"):
545 return "established"
546 case strings.Contains(v, "openconfirm"):
547 return "openconfirm"
548 case strings.Contains(v, "opensent"):
549 return "opensent"
550 case strings.Contains(v, "active"):
551 return "active"
552 case strings.Contains(v, "connect"):
553 return "connect"
554 case strings.Contains(v, "idle"):
555 return "idle"
556 default:
557 return "unknown"
558 }
559 }
560
561 func normalizeAFISAFI(v string) (string, string) {
562 v = strings.ToLower(strings.TrimSpace(v))
563 v = strings.TrimPrefix(v, "bgpafi")
564 v = strings.TrimPrefix(v, "afi-")
565 v = strings.TrimPrefix(v, "afi_")
566 v = strings.ReplaceAll(v, "_", "-")
567
568 parts := strings.Split(v, "-")
569 if len(parts) < 2 {
570 return normalizeAFI(v), ""
571 }
572 return normalizeAFI(parts[0]), normalizeSAFI(strings.Join(parts[1:], "-"))
573 }
574
575 func normalizeAFI(v string) string {
576 v = strings.ToLower(strings.TrimSpace(v))
577 v = strings.TrimPrefix(v, "bgpafi")
578 switch v {
579 case "ipv4", "ip":
580 return "ipv4"
581 case "ipv6":
582 return "ipv6"
583 default:
584 return strings.ReplaceAll(v, " ", "_")
585 }
586 }
587
588 func normalizeSAFI(v string) string {
589 v = strings.ToLower(strings.TrimSpace(v))
590 v = strings.ReplaceAll(v, " ", "_")
591 return strings.ReplaceAll(v, "-", "_")
592 }
593
594 func normalizeAddress(addr string) string {
595 addr = strings.TrimSpace(addr)
596 if addr == "" {
597 return ""
598 }
599
600 if host, _, err := net.SplitHostPort(addr); err == nil {
601 return strings.Trim(host, "[]")
602 }
603 if _, err := netip.ParseAddr(addr); err == nil {
604 return addr
605 }
606
607 if strings.Count(addr, ":") == 1 {
608 host, port, ok := strings.Cut(addr, ":")
609 if ok && isDigits(port) {
610 return host
611 }
612 }
613
614 return strings.Trim(addr, "[]")
615 }
616
617 var digitsOnly = regexp.MustCompile(`^\d+$`)
618
619 func isDigits(v string) bool {
620 return digitsOnly.MatchString(v)
621 }
622
623 func parsePANOSIntField(field, v string) (int64, error) {
624 raw := strings.TrimSpace(v)
625 v = strings.ReplaceAll(raw, ",", "")
626 if v == "" {
627 return 0, nil
628 }
629 n, err := strconv.ParseInt(v, 10, 64)
630 if err != nil {
631 return 0, fmt.Errorf("%s: invalid integer %q", field, raw)
632 }
633 return n, nil
634 }
635
636 func parseRequiredPANOSIntField(field, v string) (int64, error) {
637 if strings.TrimSpace(v) == "" {
638 return 0, fmt.Errorf("%s: missing integer", field)
639 }
640 return parsePANOSIntField(field, v)
641 }
642
643 func parsePANOSDurationField(field, v string) (int64, error) {
644 v = strings.TrimSpace(strings.ToLower(v))
645 if v == "" {
646 return 0, nil
647 }
648 if isDigits(v) {
649 return parsePANOSIntField(field, v)
650 }
651
652 var total int64
653 matched := false
654 for _, match := range durationPartRe.FindAllStringSubmatch(v, -1) {
655 matched = true
656 n, err := parsePANOSIntField(field, match[1])
657 if err != nil {
658 return 0, err
659 }
660 switch match[2] {
661 case "day", "days", "d":
662 total += n * int64((24 * time.Hour).Seconds())
663 case "hour", "hours", "h":
664 total += n * int64(time.Hour.Seconds())
665 case "minute", "minutes", "min", "mins", "m":
666 total += n * int64(time.Minute.Seconds())
667 case "second", "seconds", "sec", "secs", "s":
668 total += n
669 }
670 }
671
672 timeMatch := durationClockRe.FindStringSubmatch(v)
673 if len(timeMatch) == 4 {
674 matched = true
675 hour, err := parsePANOSIntField(field, timeMatch[1])
676 if err != nil {
677 return 0, err
678 }
679 minute, err := parsePANOSIntField(field, timeMatch[2])
680 if err != nil {
681 return 0, err
682 }
683 second, err := parsePANOSIntField(field, timeMatch[3])
684 if err != nil {
685 return 0, err
686 }
687 if minute >= 60 || second >= 60 {
688 return 0, fmt.Errorf("%s: invalid duration %q", field, v)
689 }
690 total += hour*int64(time.Hour.Seconds()) +
691 minute*int64(time.Minute.Seconds()) +
692 second
693 }
694
695 remainder := durationPartRe.ReplaceAllString(v, "")
696 remainder = durationClockRe.ReplaceAllString(remainder, "")
697 remainder = strings.TrimSpace(strings.Trim(remainder, ","))
698 if !matched || remainder != "" {
699 return 0, fmt.Errorf("%s: invalid duration %q", field, v)
700 }
701
702 return total, nil
703 }
704
705 func parseRequiredPANOSDurationField(field, v string) (int64, error) {
706 if strings.TrimSpace(v) == "" {
707 return 0, fmt.Errorf("%s: missing duration", field)
708 }
709 return parsePANOSDurationField(field, v)
710 }
711
712 func (m panosResponseMessage) String() string {
713 var lines []string
714 if text := strings.TrimSpace(m.Text); text != "" {
715 lines = append(lines, text)
716 }
717 for _, line := range m.Lines {
718 if line = strings.TrimSpace(line); line != "" {
719 lines = append(lines, line)
720 }
721 }
722 return strings.Join(lines, "; ")
723 }
724
725 type panosResponseError struct {
726 code string
727 message string
728 }
729
730 func (e panosResponseError) Error() string {
731 code := strings.TrimSpace(e.code)
732 codeName := panosResponseCodeName(code)
733 if e.message == "" {
734 if code == "" {
735 return "PAN-OS XML API response error"
736 }
737 if codeName != "" {
738 return fmt.Sprintf("PAN-OS XML API response error code %s (%s)", code, codeName)
739 }
740 return fmt.Sprintf("PAN-OS XML API response error code %s", code)
741 }
742 if code == "" {
743 return fmt.Sprintf("PAN-OS XML API response error: %s", e.message)
744 }
745 if codeName != "" && !strings.Contains(strings.ToLower(e.message), strings.ToLower(codeName)) {
746 return fmt.Sprintf("PAN-OS XML API response error code %s (%s): %s", code, codeName, e.message)
747 }
748 return fmt.Sprintf("PAN-OS XML API response error code %s: %s", code, e.message)
749 }
750
751 func panosResponseCodeName(code string) string {
752 switch strings.TrimSpace(code) {
753 case "1":
754 return "Unknown command"
755 case "2", "3", "4", "5", "11":
756 return "Internal error"
757 case "6":
758 return "Bad XPath"
759 case "7":
760 return "Object not found"
761 case "8":
762 return "Object not unique"
763 case "10":
764 return "Reference count not zero"
765 case "12":
766 return "Invalid object"
767 case "14":
768 return "Operation not possible"
769 case "15":
770 return "Operation denied"
771 case "16":
772 return "Unauthorized"
773 case "17":
774 return "Invalid command"
775 case "18":
776 return "Malformed command"
777 case "22":
778 return "Session timed out"
779 case "400":
780 return "Bad request"
781 case "403":
782 return "Forbidden"
783 default:
784 return ""
785 }
786 }
787
788 func bgpCommandName(cmd string) string {
789 switch cmd {
790 case legacyBGPPeerCommand:
791 return "legacy routing BGP peer query"
792 case advancedBGPPeerCommands[0]:
793 return "advanced routing BGP peer details query"
794 case advancedBGPPeerCommands[1]:
795 return "advanced routing BGP peer status query"
796 case advancedBGPPeerCommands[2]:
797 return "advanced routing BGP peer query"
798 default:
799 return "PAN-OS BGP query"
800 }
801 }
802
803 var (
804 stateNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "")
805 durationPartRe = regexp.MustCompile(`\b(\d+)\s*(days?|d|hours?|h|minutes?|mins?|min|m|seconds?|secs?|sec|s)\b`)
806 durationClockRe = regexp.MustCompile(`\b(\d{1,2}):(\d{2}):(\d{2})\b`)
807 )
808
809 func firstNonEmpty(values ...string) string {
810 for _, value := range values {
811 if strings.TrimSpace(value) != "" {
812 return strings.TrimSpace(value)
813 }
814 }
815 return ""
816 }