ctx_checker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/COUNTER_CONTRACT.md +33 -0
  4. data/LICENSE +22 -0
  5. data/README-zh.md +93 -0
  6. data/README.md +93 -0
  7. data/docs/architecture-zh.md +487 -0
  8. data/docs/release.md +13 -0
  9. data/examples/offline.rb +17 -0
  10. data/lib/ctx_checker/applicability.rb +187 -0
  11. data/lib/ctx_checker/checks/aggregation.rb +73 -0
  12. data/lib/ctx_checker/checks/alarms.rb +25 -0
  13. data/lib/ctx_checker/checks/base.rb +66 -0
  14. data/lib/ctx_checker/checks/clock.rb +30 -0
  15. data/lib/ctx_checker/checks/counters.rb +77 -0
  16. data/lib/ctx_checker/checks/device_counters.rb +32 -0
  17. data/lib/ctx_checker/checks/hardware.rb +45 -0
  18. data/lib/ctx_checker/checks/interfaces.rb +35 -0
  19. data/lib/ctx_checker/checks/ntp.rb +47 -0
  20. data/lib/ctx_checker/checks/optics.rb +107 -0
  21. data/lib/ctx_checker/checks/resources.rb +63 -0
  22. data/lib/ctx_checker/checks/routing.rb +86 -0
  23. data/lib/ctx_checker/checks/temperature.rb +53 -0
  24. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +56 -0
  25. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +153 -0
  26. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +110 -0
  27. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +41 -0
  28. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +140 -0
  29. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +107 -0
  30. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +164 -0
  31. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +96 -0
  32. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +65 -0
  33. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +36 -0
  34. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +44 -0
  35. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +45 -0
  36. data/lib/ctx_checker/cisco_ios/parsers/system_values.rb +62 -0
  37. data/lib/ctx_checker/cisco_ios/profile.rb +39 -0
  38. data/lib/ctx_checker/cisco_nxos/parsers/show_bgp_ipv4_unicast_summary.rb +69 -0
  39. data/lib/ctx_checker/cisco_nxos/parsers/show_clock.rb +13 -0
  40. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +157 -0
  41. data/lib/ctx_checker/cisco_nxos/parsers/show_interface.rb +78 -0
  42. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +141 -0
  43. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +14 -0
  44. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +151 -0
  45. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +116 -0
  46. data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +21 -0
  47. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +89 -0
  48. data/lib/ctx_checker/cisco_nxos/profile.rb +35 -0
  49. data/lib/ctx_checker/collector.rb +175 -0
  50. data/lib/ctx_checker/evaluator.rb +49 -0
  51. data/lib/ctx_checker/failure.rb +24 -0
  52. data/lib/ctx_checker/finding.rb +19 -0
  53. data/lib/ctx_checker/h3c/parsers/counters_table.rb +56 -0
  54. data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +43 -0
  55. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +44 -0
  56. data/lib/ctx_checker/h3c/parsers/display_clock.rb +72 -0
  57. data/lib/ctx_checker/h3c/parsers/display_counters_inbound_interface.rb +21 -0
  58. data/lib/ctx_checker/h3c/parsers/display_counters_outbound_interface.rb +21 -0
  59. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +64 -0
  60. data/lib/ctx_checker/h3c/parsers/display_environment.rb +74 -0
  61. data/lib/ctx_checker/h3c/parsers/display_fan.rb +72 -0
  62. data/lib/ctx_checker/h3c/parsers/display_interface.rb +127 -0
  63. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +72 -0
  64. data/lib/ctx_checker/h3c/parsers/display_memory.rb +132 -0
  65. data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +84 -0
  66. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +47 -0
  67. data/lib/ctx_checker/h3c/parsers/display_power.rb +63 -0
  68. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +109 -0
  69. data/lib/ctx_checker/h3c/parsers/system_values.rb +46 -0
  70. data/lib/ctx_checker/h3c/profile.rb +49 -0
  71. data/lib/ctx_checker/hillstone/parsers/show_clock.rb +68 -0
  72. data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +44 -0
  73. data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +114 -0
  74. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +90 -0
  75. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +82 -0
  76. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +53 -0
  77. data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +22 -0
  78. data/lib/ctx_checker/hillstone/parsers/show_ip_ospf_neighbor.rb +21 -0
  79. data/lib/ctx_checker/hillstone/parsers/show_memory.rb +85 -0
  80. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +110 -0
  81. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +101 -0
  82. data/lib/ctx_checker/hillstone/profile.rb +40 -0
  83. data/lib/ctx_checker/observation.rb +165 -0
  84. data/lib/ctx_checker/observation_merger.rb +80 -0
  85. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +50 -0
  86. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +47 -0
  87. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +68 -0
  88. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +70 -0
  89. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +52 -0
  90. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +78 -0
  91. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +66 -0
  92. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +54 -0
  93. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +142 -0
  94. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +102 -0
  95. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +54 -0
  96. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +44 -0
  97. data/lib/ctx_checker/palo_alto/profile.rb +39 -0
  98. data/lib/ctx_checker/parser_registry.rb +43 -0
  99. data/lib/ctx_checker/parsers/base.rb +68 -0
  100. data/lib/ctx_checker/parsers/interface_names.rb +65 -0
  101. data/lib/ctx_checker/parsers/scoped_result.rb +35 -0
  102. data/lib/ctx_checker/policy.rb +104 -0
  103. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +48 -0
  104. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +46 -0
  105. data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +22 -0
  106. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +71 -0
  107. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +22 -0
  108. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +44 -0
  109. data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +47 -0
  110. data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +48 -0
  111. data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +34 -0
  112. data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +53 -0
  113. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +68 -0
  114. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +39 -0
  115. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +58 -0
  116. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +67 -0
  117. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +74 -0
  118. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +99 -0
  119. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +86 -0
  120. data/lib/ctx_checker/radware_alteon/port_counter_parser.rb +34 -0
  121. data/lib/ctx_checker/radware_alteon/profile.rb +145 -0
  122. data/lib/ctx_checker/radware_alteon.rb +8 -0
  123. data/lib/ctx_checker/report.rb +112 -0
  124. data/lib/ctx_checker/runtime_parser.rb +234 -0
  125. data/lib/ctx_checker/snapshot.rb +44 -0
  126. data/lib/ctx_checker/system_policy.rb +70 -0
  127. data/lib/ctx_checker/system_record.rb +81 -0
  128. data/lib/ctx_checker/version.rb +6 -0
  129. data/lib/ctx_checker.rb +17 -0
  130. metadata +271 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module PaloAlto
8
+ module Parsers
9
+ # PAN-OS 分别展示物理状态与逻辑接口清单。
10
+ class ShowInterfaceAll < CtxChecker::Parsers::Base
11
+ private
12
+
13
+ # 解析 show interface all,保留不完整或不支持的证据。
14
+ def parse_output(text, complete:)
15
+ text = clean(text)
16
+ rows, issues, section, totals, counts, duplicates = {}, [], nil, {}, Hash.new(0), []
17
+ logical_names = {}
18
+ text.each_line do |raw|
19
+ line = raw.strip
20
+ if (match = /\Atotal configured (hardware|logical) interfaces:\s*(\d+)\z/i.match(line))
21
+ section = match[1].downcase.to_sym
22
+ totals[section] = match[2].to_i
23
+ elsif (match = /\Aaggregation groups:\s*(\d+)\z/i.match(line))
24
+ section = :aggregation
25
+ totals[section] = match[1].to_i
26
+ elsif line.empty? || line.match?(/\A[- ]+\z/) || line.match?(/\Aname\s+id\s+/i)
27
+ next
28
+ elsif section == :hardware && (match = /\A(\S+)\s+\d+\s+\S+\/\S+\/(up|down)(?:\([^\s]*\))?\s+[\da-f:]+\z/i.match(line))
29
+ name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :palo_alto)
30
+ if rows.key?(name)
31
+ issues << { code: :duplicate_interface, interface: name }
32
+ duplicates << name
33
+ end
34
+ rows[name] = record(name).merge(oper_status: match[2].downcase.to_sym)
35
+ counts[section] += 1
36
+ elsif section == :aggregation && line.match?(/\Aae\d+ members:\z/i)
37
+ counts[section] += 1
38
+ elsif section == :aggregation && line.match?(/\Aethernet\d+\/\d+(?:\s+ethernet\d+\/\d+)*\z/i)
39
+ next # 清单中的成员关系不表示 LACP 选中状态或运行状态。
40
+ elsif section == :logical && (match = /\A(\S+)\s+\d+\s+\d+\s+.+\z/.match(line))
41
+ name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :palo_alto)
42
+ if logical_names.key?(name)
43
+ issues << { code: :duplicate_interface, interface: name }
44
+ duplicates << name
45
+ end
46
+ logical_names[name] = true
47
+ rows[name] ||= record(name)
48
+ counts[section] += 1
49
+ elsif section == :logical && line.match?(%r{\A[\da-f:.]+/\d+\z}i)
50
+ next # 地址续行不携带接口状态。
51
+ else
52
+ issues << { code: :unrecognized_interface_line }
53
+ end
54
+ end
55
+ totals.each do |kind, total|
56
+ issues << { code: :count_mismatch, section: kind, expected: total, observed: counts[kind] } if total != counts[kind]
57
+ end
58
+ duplicates.each { |name| rows.delete(name) }
59
+ build_result(rows.values, complete: complete, recognized: totals.key?(:hardware), issues: issues, identity: [ :name ])
60
+ end
61
+
62
+ # 先创建状态未知的接口身份,再由所属表补充明确证据。
63
+ def record(name)
64
+ { name: name, kind: CtxChecker::Parsers::InterfaceNames.kind(name), admin_status: :unknown, oper_status: :unknown,
65
+ protocol_status: :unknown, parent: name.match?(/\A(?:ethernet|ae).*\./) ? name.split(".").first : nil }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module PaloAlto
8
+ module Parsers
9
+ # PAN-OS LACP 回显中的成组关系与选中状态是独立事实。
10
+ class ShowLacpAggregateEthernet < CtxChecker::Parsers::Base
11
+ private
12
+
13
+ # 解析 show lacp aggregate ethernet,保留不完整或不支持的证据。
14
+ def parse_output(text, complete:)
15
+ text = clean(text)
16
+ rows, issues, group, header, groups = [], [], nil, false, {}
17
+ text.each_line do |raw|
18
+ line = raw.strip
19
+ if (match = /\AAE group:\s*(ae\d+)\z/i.match(line))
20
+ group = match[1].downcase
21
+ groups[group] ||= 0
22
+ elsif line.match?(/\AMembers:\s+Bndl\s+Rx state\s+Mux state\s+Sel state\z/i)
23
+ header = true
24
+ elsif (match = /\A(ethernet\d+\/\d+)\s+(yes|no)\s+(\S+)\s+(\S+)\s+(Selected|Unselected|Standby)(?:\(.*\))?\z/i.match(line))
25
+ if group
26
+ state = if match[5].casecmp?("Standby")
27
+ :standby
28
+ elsif match[5].casecmp?("Unselected")
29
+ :unselected
30
+ elsif match[2].casecmp?("yes") && match[4].casecmp?("Tx_Rx")
31
+ :active
32
+ else
33
+ :unknown
34
+ end
35
+ rows << { group: group, member: match[1].downcase, member_status: state, oper_status: :unknown }
36
+ groups[group] += 1
37
+ else
38
+ issues << { code: :aggregation_group_missing }
39
+ end
40
+ elsif line.match?(/\Aethernet\d+\//i)
41
+ issues << { code: :invalid_aggregation_member }
42
+ elsif !line.empty? && !line.match?(/\A(?:\*+|-+|LACP:|Status:|Mode:|Rate:|Max-port:|Fast-failover:|Pre-negotiation:|Local:|Partner:|System Priority:|System MAC:|Key:)/i)
43
+ issues << { code: :unrecognized_aggregation_line }
44
+ end
45
+ end
46
+ groups.each { |name, count| issues << { code: :aggregation_members_missing, group: name } if count.zero? }
47
+ build_result(rows, complete: complete, recognized: header && rows.any?, issues: issues, identity: %i[group member])
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module PaloAlto
7
+ module Parsers
8
+ # 对等体可达与 LOCAL 回退不能证明 NTP 已同步。
9
+ class ShowNtp < CtxChecker::Parsers::Base
10
+ private
11
+
12
+ # 解析 show ntp,保留不完整或不支持的证据。
13
+ def parse_output(text, complete:)
14
+ source = clean(text)
15
+ rows, globals, issues = [], [], []
16
+ current, fields, header, peer_fields = nil, {}, false, {}
17
+ source.each_line.with_index(1) do |raw, number|
18
+ line = raw.strip
19
+ next if line.empty?
20
+ if line == "NTP state:"
21
+ header = true
22
+ elsif (match = /\ANTP synched to (\S+)\z/i.match(line))
23
+ globals << { component: "system", synchronized: !match[1].casecmp?("LOCAL"), reference: match[1] }
24
+ current = nil
25
+ elsif line.match?(/\ANTP not synched, using local clock\z/i)
26
+ globals << { component: "system", synchronized: false, reference: "LOCAL" }
27
+ current = nil
28
+ elsif (match = /\ANTP server (\S+) connected: (True|False)\z/i.match(line))
29
+ row = { component: "peer/#{match[1]}", peer: match[1], synchronized: nil }
30
+ rows << row
31
+ peer_fields[row.object_id] = { "reachable" => match[2].casecmp?("true") ? "yes" : "no" }
32
+ current = nil
33
+ elsif (match = /\ANTP server:\s*(\S+)\z/.match(line))
34
+ current = { component: "peer/#{match[1]}", peer: match[1], synchronized: nil }
35
+ rows << current
36
+ fields = {}
37
+ peer_fields[current.object_id] = fields
38
+ elsif current && (match = /\A(status|reachable|authentication-type):\s*(\S+)\z/.match(line))
39
+ key, value = match[1], match[2].downcase
40
+ if fields.key?(key)
41
+ issues << { code: :duplicate_ntp_field, line: number }
42
+ current[:synchronized] = nil if key == "status"
43
+ elsif key == "status"
44
+ current[:synchronized] = { "synched" => true, "rejected" => false, "unsynched" => false }[value]
45
+ # 可用备份对等体可以参与同步,但不是当前选中的同步源。
46
+ issues << { code: :unknown_ntp_status, line: number } if current[:synchronized].nil? && value != "available"
47
+ elsif key == "reachable" && !%w[yes no].include?(value)
48
+ issues << { code: :unknown_ntp_reachability, line: number }
49
+ end
50
+ fields[key] = value
51
+ else
52
+ issues << { code: :unrecognized_ntp_row, line: number }
53
+ end
54
+ end
55
+ rows.each do |row|
56
+ if row[:synchronized] == true && peer_fields.fetch(row.object_id, {})["reachable"] == "no"
57
+ row[:synchronized] = nil
58
+ issues << { code: :conflicting_ntp_state, component: row[:component] }
59
+ end
60
+ end
61
+ globals.each do |global|
62
+ next unless global[:synchronized] == true
63
+ peers = rows.select { |row| row[:peer] == global[:reference] }
64
+ if peers.any? { |row| row[:synchronized] == false || peer_fields.fetch(row.object_id, {})["reachable"] == "no" }
65
+ global[:synchronized] = nil
66
+ issues << { code: :conflicting_ntp_state, component: "system" }
67
+ end
68
+ end
69
+ if globals.empty? && header
70
+ globals << { component: "system", synchronized: nil }
71
+ issues << { code: :ntp_system_status_missing }
72
+ end
73
+ build_result(globals + rows, complete: complete, recognized: header && globals.any?, issues: issues, identity: [ :component ])
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module PaloAlto
7
+ module Parsers
8
+ # 同一对等体可能报告多个地址族。
9
+ class ShowRoutingProtocolBgpPeer < CtxChecker::Parsers::Base
10
+ STATES = %w[established idle active connect opensent openconfirm shutdown].freeze
11
+ FIELDS = /\A(?:Peer:|virtual router:|Peer router id:|Remote AS:|Peer group:|Peer status:|Password set:|Passive:|Multi-hop TTL:|Remote Address:|Local Address:|\(R\) reflector client:|same confederation:|send aggr confed as-path:|peering type:|Connect-Retry interval:|Open Delay:|Idle Hold:|Prefix limit:|Holdtime:|Keep-Alive interval:|Update messages:|Total messages:|Last update age:|Last error:|Flap counts:|\(R\) ORF entries:|Nexthop set to self:|use 3rd party as next-hop:|override nexthop to peer:|remove private AS number:|Capability:|Prefix counter for:|Incoming Prefix:|Outgoing Prefix:|Advertised Prefix:|[-=]+\z)/i
12
+
13
+ private
14
+
15
+ # 解析 show routing protocol bgp peer,保留不完整或不支持的证据。
16
+ def parse_output(text, complete:)
17
+ text = clean(text)
18
+ rows, issues = [], []
19
+ text.split(/(?=^\s*Peer:\s)/i).each do |block|
20
+ next if block.strip.empty? || block.strip.match?(/\A=+\z/)
21
+ unless block.match?(/\A\s*Peer:\s/i)
22
+ issues << { code: :unrecognized_bgp_block }
23
+ next
24
+ end
25
+ if block.lines.any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
26
+ issues << { code: :unrecognized_bgp_field }
27
+ end
28
+ if [ "Remote Address", "Peer status", "Remote AS", "virtual router" ].any? { |field| block.scan(/^\s*#{Regexp.escape(field)}:/i).size > 1 }
29
+ issues << { code: :duplicate_bgp_field }
30
+ next
31
+ end
32
+ address = block[/^\s*Remote Address:\s*(\S+)/i, 1]
33
+ peer = address&.sub(/\A\[([^\]]+)\]:\d+\z/, '\1')
34
+ peer = peer.sub(/:\d+\z/, "") if peer&.match?(/\A\d+(?:\.\d+){3}:\d+\z/)
35
+ unless CtxParser::Values.valid_host?(peer)
36
+ issues << { code: :bgp_peer_address_missing }
37
+ next
38
+ end
39
+ peer = CtxParser::Values.host(peer)
40
+ state = block[/^\s*Peer status:\s*([^,\n]+)/i, 1]&.strip&.downcase
41
+ state = STATES.include?(state) ? state.to_sym : :unknown
42
+ remote_as = block[/^\s*Remote AS:\s*(\d+)\s*$/i, 1]&.to_i
43
+ if remote_as && !(1..4_294_967_295).cover?(remote_as)
44
+ issues << { code: :invalid_bgp_asn }
45
+ remote_as = nil
46
+ end
47
+ issues << { code: :bgp_peer_fields_missing } if state == :unknown || remote_as.nil?
48
+ base = { vrf: block[/^\s*virtual router:\s*(\S+)/i, 1], peer: peer, state: state, remote_as: remote_as }
49
+ families = block.split(/(?=^\s*Prefix counter for:)/i).drop(1)
50
+ if families.empty?
51
+ rows << base.merge(afi_safi: nil, received_prefixes: nil)
52
+ else
53
+ families.each do |family|
54
+ scope = /^[ \t]*Prefix counter for:[ \t]*bgpAfiIpv([46])[ \t]*\/[ \t]*(unicast|multicast)[ \t]*$/i.match(family)
55
+ prefixes = family[/^\s*Incoming Prefix:\s*Accepted \d+, Rejected \d+, Policy Rej \d+, Total (\d+)\s*$/i, 1]
56
+ issues << { code: :bgp_address_family_unknown } unless scope
57
+ rows << base.merge(afi_safi: scope ? "ipv#{scope[1]}_#{scope[2].downcase}" : nil, received_prefixes: prefixes&.to_i)
58
+ end
59
+ end
60
+ end
61
+ build_result(rows, complete: complete, recognized: rows.any?, issues: issues, identity: %i[vrf afi_safi peer])
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module PaloAlto
7
+ module Parsers
8
+ # 旧版 PAN-OS 邻居回显不标明本地接口或进程。
9
+ class ShowRoutingProtocolOspfNeighbor < CtxChecker::Parsers::Base
10
+ STATES = { "full" => :full, "down" => :down, "init" => :init, "attempt" => :attempt,
11
+ "2-way" => :two_way, "two way" => :two_way, "exchange start" => :exstart, "exstart" => :exstart,
12
+ "exchange" => :exchange, "loading" => :loading }.freeze
13
+ FIELDS = /\A(?:virtual router:|neighbor address:|local address binding:|type:|status:|neighbor router ID:|area id:|neighbor priority:|lifetime remain:|messages pending:|LSA request pending:|options:|hello suppressed:|restart helper status:|restart helper time remaining:|restart helper exit reason:|=+\z)/i
14
+
15
+ private
16
+
17
+ # 解析 show routing protocol ospf neighbor,保留不完整或不支持的证据。
18
+ def parse_output(text, complete:)
19
+ text = clean(text)
20
+ rows, issues = [], []
21
+ text.split(/(?=^\s*virtual router:)/i).each do |block|
22
+ unless block.match?(/\A\s*virtual router:/i)
23
+ # 文档规定的图例位于首个邻居前,任意前导文本不构成证据。
24
+ lines = block.lines.map(&:strip).reject(&:empty?)
25
+ unless lines.all? { |line| line.match?(/\A(?:=+|Options: 0x80:reserved,.*|N\/P:NSSA option,.*)\z/i) }
26
+ issues << { code: :unrecognized_ospf_block }
27
+ end
28
+ next
29
+ end
30
+ issues << { code: :unrecognized_ospf_field } if block.lines.any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
31
+ if [ "neighbor address", "neighbor router ID", "status" ].any? { |field| block.scan(/^\s*#{Regexp.escape(field)}:/i).size > 1 }
32
+ issues << { code: :duplicate_ospf_field }
33
+ next
34
+ end
35
+ neighbor = block[/^\s*neighbor address:\s*(\S+)/i, 1]
36
+ router_id = block[/^\s*neighbor router ID:\s*(\S+)/i, 1]
37
+ unless CtxParser::Values.valid_host?(neighbor) && CtxParser::Values.valid_host?(router_id)
38
+ issues << { code: :ospf_neighbor_identity_missing }
39
+ next
40
+ end
41
+ neighbor = CtxParser::Values.host(neighbor)
42
+ router_id = CtxParser::Values.host(router_id)
43
+ state = STATES.fetch(block[/^\s*status:\s*(.+)$/i, 1]&.strip&.downcase, :unknown)
44
+ issues << { code: :ospf_state_unknown } if state == :unknown
45
+ rows << { vrf: block[/^\s*virtual router:\s*(\S+)/i, 1], process_id: nil,
46
+ area: block[/^\s*area id:\s*(\S+)/i, 1], interface: nil, router_id: router_id, neighbor_address: neighbor,
47
+ state: state, network_type: :unknown, local_role: :unknown, neighbor_role: :unknown }
48
+ end
49
+ build_result(rows, complete: complete, recognized: rows.any?, issues: issues, identity: %i[vrf process_id interface router_id neighbor_address])
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require "ctx_parser/result_set"
5
+
6
+ module CtxChecker
7
+ module PaloAlto
8
+ module Parsers
9
+ # 特定章节的硬件传感器不能证明未出现的电源或风扇已被覆盖。
10
+ class ShowSystemEnvironmentals < CtxChecker::Parsers::Base
11
+ SECTIONS = { "thermal" => :temperature, "fans" => :environment, "fan tray" => :environment,
12
+ "power" => :power, "power supplies" => :power }.freeze
13
+
14
+ private
15
+
16
+ # 解析 show system environmentals,保留不完整或不支持的证据。
17
+ def parse_output(text, complete:)
18
+ source = clean(text)
19
+ rows = { temperature: [], environment: [], power: [] }
20
+ issues = rows.transform_values { [] }
21
+ section, header, sections, section_rows = nil, nil, [], 0
22
+ source.each_line.with_index(1) do |raw, number|
23
+ line = raw.strip
24
+ next if line.empty?
25
+ if (match = /\A-+\s*([^-]+?)\s*-+\z/.match(line))
26
+ empty_section(section, section_rows, issues)
27
+ section, header = match[1].downcase, nil
28
+ section_rows = 0
29
+ sections << section
30
+ issues.each_value { |items| items << { code: :unrecognized_environment_section, line: number } } unless SECTIONS.key?(section)
31
+ elsif line.match?(/\ASlot\s+Description\s+Alarm\s+/)
32
+ header = line
33
+ elsif SECTIONS.key?(section) && header
34
+ dimension = SECTIONS.fetch(section)
35
+ if (match = /\A(\S+)\s+(.+?)\s+(True|False)\s+(.+)\z/i.match(line))
36
+ section_rows += 1
37
+ component = "#{match[1]}/#{match[2].strip}"
38
+ alarm = match[3].casecmp?("true")
39
+ status = alarm ? :critical : :ok
40
+ values = match[4].split
41
+ case section
42
+ when "thermal"
43
+ measurements = values.map { |value| numeric(value) }
44
+ expected = header.include?("Min C") && header.include?("Max C") ? 3 : 1
45
+ valid = measurements.size == expected && measurements.none?(&:nil?)
46
+ issues[:temperature] << { code: :invalid_temperature_measurement, component: component } unless valid
47
+ rows[:temperature] << { component: component, value: valid ? measurements.first : nil,
48
+ low_alarm: valid && expected == 3 ? measurements[1] : nil,
49
+ high_alarm: valid && expected == 3 ? measurements[2] : nil, status: status }
50
+ when "power supplies", "fan tray"
51
+ present = values.one? && %w[true false].include?(values.first.downcase) ? values.first.casecmp?("true") : nil
52
+ status = :absent if !alarm && present == false
53
+ issues[dimension] << { code: :invalid_presence, component: component } if present.nil?
54
+ rows[dimension] << if section == "power supplies"
55
+ { component: component, kind: :power_supply, status: status, present: present }
56
+ else
57
+ { component: component, kind: :fan, status: status }
58
+ end
59
+ when "fans", "power"
60
+ valid = sensor_values?(section, header, values)
61
+ issues[dimension] << { code: :invalid_environment_measurement, component: component } unless valid
62
+ status = :unknown if !valid && !alarm
63
+ if valid && limits_invalid?(section, header, values)
64
+ issues[dimension] << { code: :invalid_environment_thresholds, component: component }
65
+ status = :unknown unless alarm
66
+ elsif valid && outside_limits?(section, header, values)
67
+ issues[dimension] << { code: :conflicting_environment_state, component: component } unless alarm
68
+ status = :failed unless alarm
69
+ end
70
+ if section == "fans" && header.match?(/Operational\s*\z/) && values == [ "False" ]
71
+ issues[dimension] << { code: :conflicting_environment_state, component: component } unless alarm
72
+ status = :failed
73
+ end
74
+ if section == "power"
75
+ rows[:power] << { component: component, kind: :power_rail, status: status }
76
+ else
77
+ rows[:environment] << { component: component, kind: :fan, status: status }
78
+ end
79
+ end
80
+ else
81
+ issues[dimension] << { code: :unrecognized_environment_row, line: number }
82
+ end
83
+ else
84
+ issues.each_value { |items| items << { code: :unrecognized_environment_row, line: number } }
85
+ end
86
+ end
87
+ empty_section(section, section_rows, issues)
88
+ if rows[:power].any? && !sections.include?("power supplies")
89
+ issues[:power] << { code: :power_supply_status_missing }
90
+ end
91
+ CtxParser::ResultSet.new(results: rows.to_h do |dimension, values|
92
+ identity = dimension == :temperature ? [ :component ] : %i[component kind]
93
+ [ dimension, build_result(values, complete: complete, recognized: values.any?, issues: issues.fetch(dimension), identity: identity) ]
94
+ end)
95
+ end
96
+
97
+ # 解析支持的数值字段,并拒绝格式错误的测量值。
98
+ def numeric(value)
99
+ return unless /\A-?\d+(?:\.\d+)?\z/.match?(value)
100
+ number = Float(value)
101
+ number if number.finite?
102
+ end
103
+
104
+ # 表达明确的空章节,不虚构组件记录。
105
+ def empty_section(section, count, issues)
106
+ return unless SECTIONS.key?(section) && count.zero?
107
+ issues.fetch(SECTIONS.fetch(section)) << { code: :empty_environment_section, section: section }
108
+ end
109
+
110
+ # 检查传感器行是否包含可用的数值测量。
111
+ def sensor_values?(section, header, values)
112
+ if section == "fans" && header.match?(/Operational\s*\z/)
113
+ values.one? && %w[True False].include?(values.first)
114
+ else
115
+ expected = if section == "fans"
116
+ header.include?("Min RPM") ? 2 : 1
117
+ else
118
+ header.include?("Min V") && header.include?("Max V") ? 3 : 1
119
+ end
120
+ values.size == expected && values.all? { |value| (number = numeric(value)) && (section != "fans" || number >= 0) }
121
+ end
122
+ end
123
+
124
+ # 识别矛盾或格式错误的传感器阈值边界。
125
+ def limits_invalid?(section, header, values)
126
+ section == "power" && header.include?("Min V") && header.include?("Max V") && numeric(values[1]) > numeric(values[2])
127
+ end
128
+
129
+ # 过期的 False 告警标志不能抹除实际观察到的设备阈值违规。
130
+ def outside_limits?(section, header, values)
131
+ if section == "fans" && header.include?("Min RPM")
132
+ numeric(values[0]) < numeric(values[1])
133
+ elsif section == "power" && header.include?("Min V") && header.include?("Max V")
134
+ !numeric(values[0]).between?(numeric(values[1]), numeric(values[2]))
135
+ else
136
+ false
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require "ctx_parser/result_set"
5
+
6
+ module CtxChecker
7
+ module PaloAlto
8
+ module Parsers
9
+ # PAN top 描述管理平面,并包含可回收内存。
10
+ class ShowSystemResources < CtxChecker::Parsers::Base
11
+ UNITS = { "KiB" => 1024, "MiB" => 1024**2, "GiB" => 1024**3, "TiB" => 1024**4 }.freeze
12
+ NUMBER = /\d+(?:\.\d+)?/
13
+
14
+ private
15
+
16
+ # 解析 show system resources,保留不完整或不支持的证据。
17
+ def parse_output(text, complete:)
18
+ source = clean(text)
19
+ cpus, memories, cpu_issues, memory_issues, shared_issues = [], [], [], [], []
20
+ swap_lines = source.lines.grep(/\A\s*(?:(?:KiB|MiB|GiB|TiB)\s+)?Swap\s*:/)
21
+ process_table = false
22
+ source.each_line.with_index(1) do |raw, number|
23
+ line = raw.strip
24
+ if (match = /\A%?Cpu(?:\(s\)|(\d+))\s*:\s*(.*)\z/i.match(line))
25
+ cpus << cpu(match, cpu_issues)
26
+ elsif line.match?(/\A(?:(?:KiB|MiB|GiB|TiB)\s+)?Mem\s*:/)
27
+ memories << memory(line, swap_lines, memory_issues)
28
+ elsif line.match?(/\APID\s+USER\s+/)
29
+ process_table = true
30
+ elsif line.empty? || line.match?(/\A(?:top - |Tasks:|(?:(?:KiB|MiB|GiB|TiB)\s+)?Swap\s*:)/) || process_table && line.match?(/\A\d+\s+/)
31
+ next
32
+ else
33
+ shared_issues << { code: :unrecognized_resource_row, line: number }
34
+ end
35
+ end
36
+ CtxParser::ResultSet.new(results: {
37
+ cpu: build_result(cpus, complete: complete, recognized: cpus.any?, issues: shared_issues + cpu_issues,
38
+ identity: %i[component scope window_seconds]),
39
+ memory: build_result(memories, complete: complete, recognized: memories.any?, issues: shared_issues + memory_issues,
40
+ identity: %i[component scope])
41
+ })
42
+ end
43
+
44
+ # 归一化 CPU 利用率,并保留其采样周期。
45
+ def cpu(match, issues)
46
+ parts = match[2].split(",").map { |part| /\A\s*(#{NUMBER})\s*%?\s*(us|sy|ni|id|wa|hi|si|st)\s*\z/.match(part) }
47
+ values = parts.compact.to_h { |part| [ part[2], Float(part[1]) ] }
48
+ valid = parts.size == 8 && parts.all? && values.size == 8 && values.values.all? { |value| value.finite? && value.between?(0, 100) } &&
49
+ (values.values.sum - 100).abs <= 1
50
+ issues << { code: :invalid_cpu_measurement } unless valid
51
+ { component: match[1] ? "management/cpu#{match[1]}" : "management", scope: "management_plane", window_seconds: nil,
52
+ utilization: valid ? (100 - values.fetch("id")).round(6) : nil, interrupt_utilization: valid ? (values.fetch("hi") + values.fetch("si")).round(6) : nil }
53
+ end
54
+
55
+ # 仅根据可用内存,或文档明确的旧版缓存调整方式计算利用率。
56
+ def memory(line, swap_lines, issues)
57
+ header = /\A(?:(KiB|MiB|GiB|TiB)\s+)?Mem\s*:\s*(.*)\z/.match(line)
58
+ unit = header[1] ? UNITS.fetch(header[1]) : 1024
59
+ pairs = header[2].split(",").map { |part| /\A\s*(#{NUMBER})(k?)\s+(total|used|free|buffers|buff\/cache)\s*\z/.match(part) }
60
+ values = pairs.compact.to_h { |pair| [ pair[3], bytes(pair[1], unit) ] }
61
+ available = swap_lines.flat_map { |swap| swap.scan(/(?:\A|[\s,.])(#{NUMBER})\s+avail Mem\s*\z/).flatten }
62
+ cached = swap_lines.flat_map { |swap| swap.scan(/(?:\A|[\s,])(#{NUMBER})k\s+cached\s*\z/).flatten }
63
+ row = { component: "management", scope: "management_plane", total_bytes: values["total"], free_bytes: values["free"],
64
+ used_bytes: values["used"], available_bytes: nil, utilization: nil, basis: :unknown }
65
+ valid_units = pairs.all? { |pair| pair && (header[1] ? pair[2].empty? : pair[2] == "k") }
66
+ valid = valid_units && pairs.size == values.size && values.values.none?(&:nil?) && row[:total_bytes]&.positive? &&
67
+ values.values.all? { |value| value <= row[:total_bytes] }
68
+ if valid && available.one? && swap_lines.one?
69
+ swap_unit = /\A\s*(KiB|MiB|GiB|TiB)\s+Swap/.match(swap_lines.first)&.[](1)
70
+ row[:available_bytes] = bytes(available.first, swap_unit ? UNITS.fetch(swap_unit) : 1024)
71
+ valid &&= row[:available_bytes] && row[:available_bytes] <= row[:total_bytes]
72
+ row[:used_bytes] = row[:total_bytes] - row[:available_bytes] if valid
73
+ row[:basis] = :available if valid
74
+ elsif valid && header[1].nil? && cached.one? && swap_lines.one? && values["buffers"] && values["used"]
75
+ cache_bytes = bytes(cached.first, 1024)
76
+ adjusted = cache_bytes && values["used"] - values["buffers"] - cache_bytes
77
+ valid &&= adjusted && adjusted >= 0
78
+ row[:used_bytes], row[:basis] = adjusted, :used_minus_cache if valid
79
+ row[:available_bytes] = row[:total_bytes] - adjusted if valid
80
+ else
81
+ valid = false
82
+ end
83
+ if valid
84
+ row[:utilization] = (100.0 * row[:used_bytes] / row[:total_bytes]).round(6)
85
+ else
86
+ issues << { code: :memory_available_unknown, component: "management" }
87
+ row[:basis] = :unknown
88
+ row[:available_bytes] = nil
89
+ row[:used_bytes] = nil if row[:used_bytes] && row[:total_bytes] && row[:used_bytes] > row[:total_bytes]
90
+ end
91
+ row
92
+ end
93
+
94
+ # 将支持的内存单位转换为字节。
95
+ def bytes(value, multiplier)
96
+ number = Float(value) * multiplier
97
+ number.round if number.finite? && number >= 0
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module PaloAlto
7
+ module Parsers
8
+ # 读取当前告警布尔值,不执行 PAN 类 Python 状态文本。
9
+ class ShowSystemStateAlarms < CtxChecker::Parsers::Base
10
+ private
11
+
12
+ # 解析 show system state alarms,保留不完整或不支持的证据。
13
+ def parse_output(text, complete:)
14
+ source = clean(text)
15
+ rows, issues = [], []
16
+ source.each_line.with_index(1) do |raw, number|
17
+ line = raw.strip
18
+ # 配置阈值和复制的 HA 对端状态不属于本地当前告警。
19
+ next if line.empty? || line.match?(/\A(?:cfg\.|peer\.)/)
20
+ if line.match?(/\Achassis\.alarm:\s*\{\s*\}\z/)
21
+ rows << { component: "chassis", alarm_id: "summary", active: false, count: 0, severity: :unknown }
22
+ elsif (match = /\A(ha\.(?:runtime\.device|app\.local)\.alarm):\s*(True|False)\z/i.match(line))
23
+ rows << alarm(match[1], match[2])
24
+ elsif (match = /\A(env\.[a-z0-9_.-]+):\s*\{(.+)\}\z/i.match(line))
25
+ states = match[2].scan(/(?:\A|,)\s*'alarm':\s*(True|False)\s*(?=,|\z)/i).flatten
26
+ if states.one?
27
+ rows << alarm(match[1], states.first)
28
+ else
29
+ issues << { code: :ambiguous_alarm_state, line: number }
30
+ end
31
+ elsif (match = /\A((?:chassis(?:\.s\d+)?|hw\.(?:slot\d+|s\d+\.mp))\.leds):\s*\{(.+)\}\z/i.match(line))
32
+ states = match[2].scan(/(?:\A|,)\s*'alarm':\s*(Off|Red|Green|Yellow|Amber)\s*(?=,|\z)/i).flatten
33
+ if states.one?
34
+ value = states.first.downcase
35
+ rows << { component: match[1], alarm_id: "alarm_led", active: !%w[off green].include?(value),
36
+ severity: :unknown, count: nil }
37
+ else
38
+ issues << { code: :ambiguous_alarm_state, line: number }
39
+ end
40
+ else
41
+ issues << { code: :unrecognized_alarm_state, line: number }
42
+ end
43
+ end
44
+ build_result(rows, complete: complete, recognized: rows.any?, issues: issues, identity: %i[component alarm_id])
45
+ end
46
+
47
+ # 归一化单条当前告警及其严重级别。
48
+ def alarm(component, value)
49
+ { component: component, alarm_id: "alarm", active: value.casecmp?("true"), severity: :unknown, count: nil }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end