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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "observation"
4
+
5
+ module CtxChecker
6
+ # 单条检查结果包含原因说明;严重级别不能代替实际结果状态。
7
+ Finding = Data.define(:id, :dimension, :entity, :status, :severity, :reason, :actual, :expected) do
8
+ # 独立校验并冻结检查结果及其说明,不由严重级别推断状态。
9
+ def initialize(id:, dimension:, entity: nil, status:, severity: :info, reason:, actual: nil, expected: nil)
10
+ raise ArgumentError, "Invalid finding dimension" unless Observation::NAMES.include?(dimension) || dimension == :collection
11
+ raise ArgumentError, "Invalid finding status" unless %i[pass fail unknown not_applicable].include?(status)
12
+ raise ArgumentError, "Invalid finding severity" unless %i[info warning critical].include?(severity)
13
+ unless id.is_a?(String) && /\A[a-z][a-z0-9_.]*\z/.match?(id) && reason.is_a?(Symbol) && /\A[a-z][a-z0-9_]*\z/.match?(reason.to_s)
14
+ raise ArgumentError, "Findings require an ID and reason code"
15
+ end
16
+ super(**CtxParser::Immutable.copy(id: id, dimension: dimension, entity: entity, status: status, severity: severity, reason: reason, actual: actual, expected: expected))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 入站、出站查询使用相同包计数表;仅 Err 列表示错误计数。
10
+ module CountersTable
11
+ private
12
+
13
+ # 按来源标签归一化支持的接口或设备计数器。
14
+ def parse_counters(text, complete:, metric:)
15
+ source = clean(text)
16
+ rows, sections, section = [], [], nil
17
+ source.each_line do |raw|
18
+ line = raw.strip
19
+ if /\AInterface\s+Total\s*\(pkts?\)\s+Broadcast\s*\(pkts?\)\s+Multicast\s*\(pkts?\)\s+Err\s*\(pkts?\)\z/i.match?(line)
20
+ sections << (section = { count: 0 })
21
+ elsif (match = /\A([A-Za-z][A-Za-z-]*\d+(?:[\/.]\d+)*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\z/.match(line))
22
+ unless section
23
+ @issues << { code: :missing_counter_header }
24
+ next
25
+ end
26
+ name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :h3c)
27
+ traffic = match.captures[1, 3]
28
+ unless traffic.all? { |value| /\A(?:\d+|Overflow|--)\z/i.match?(value) }
29
+ @issues << { code: :invalid_traffic_counter, interface: name }
30
+ end
31
+ rows << { interface: name, metric: metric, value: error_count(match[5], name, metric), discontinuity: nil }
32
+ section[:count] += 1
33
+ elsif !line.empty? && !/\A(?:-+|Overflow:\s*More than \d+ (?:decimal )?digits.*|--:\s*Not supported\.?)\z/i.match?(line)
34
+ @issues << { code: :unrecognized_counter_line }
35
+ end
36
+ end
37
+ @issues << { code: :empty_counter_table } if sections.any? { |entry| entry[:count].zero? }
38
+ build_result(rows, complete: complete, recognized: sections.any? && rows.any?, identity: %i[interface metric])
39
+ end
40
+
41
+ # 提取错误计数器,不将缺少证据替换为零。
42
+ def error_count(value, interface, metric)
43
+ return value.to_i if /\A\d+\z/.match?(value)
44
+
45
+ code = case value.downcase
46
+ when "overflow" then :counter_overflow
47
+ when "--" then :counter_unavailable
48
+ else :invalid_counter_value
49
+ end
50
+ @issues << { code: code, interface: interface, metric: metric }
51
+ nil
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module H3c
7
+ module Parsers
8
+ # 保留当前告警身份与严重级别,不复制设备自由文本消息。
9
+ class DisplayAlarmActive < CtxChecker::Parsers::Base
10
+ SEVERITIES = { "critical" => :critical, "major" => :critical, "minor" => :warning,
11
+ "error" => :critical, "warning" => :warning, "notice" => :info, "info" => :info }.freeze
12
+
13
+ private
14
+
15
+ # 解析 display alarm active,保留不完整或不支持的证据。
16
+ def parse_output(text, complete:)
17
+ source = clean(text)
18
+ rows, header, empty = [], false, false
19
+ source.each_line do |raw|
20
+ line = raw.strip
21
+ if /\ASeq\s+Level\s+Time\s+Info\z/i.match?(line)
22
+ header = true
23
+ elsif (match = /\A(\d+)\s+(\S+)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+.+\z/.match(line))
24
+ severity = SEVERITIES.fetch(match[2].downcase, :unknown)
25
+ @issues << { code: :unrecognized_alarm_severity } if severity == :unknown
26
+ rows << { component: "system", alarm_id: match[1], severity: severity, active: true, count: 1 }
27
+ elsif /\ANo active alarms\.?\z/i.match?(line)
28
+ empty = true
29
+ elsif !line.empty? && !/\A-+\z/.match?(line)
30
+ @issues << { code: :unrecognized_alarm_line }
31
+ end
32
+ end
33
+ if empty && rows.empty?
34
+ rows << { component: "system", alarm_id: "summary", severity: :info, active: false, count: 0 }
35
+ elsif empty
36
+ @issues << { code: :contradictory_alarm_summary }
37
+ end
38
+ build_result(rows, complete: complete, recognized: (header && rows.any?) || empty, identity: %i[component alarm_id])
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module H3c
7
+ module Parsers
8
+ # 公网 IPv4 单播查询具有明确的地址族作用域。
9
+ class DisplayBgpPeer < CtxChecker::Parsers::Base
10
+ STATES = %w[idle connect active opensent openconfirm established idle(admin)].freeze
11
+
12
+ private
13
+
14
+ # 解析 display bgp peer,保留不完整或不支持的证据。
15
+ def parse_output(text, complete:)
16
+ text = clean(text)
17
+ rows, issues, total, header = [], [], nil, false
18
+ text.each_line do |raw|
19
+ line = raw.strip
20
+ if (match = /\ATotal number of peers:\s*(\d+)\z/i.match(line))
21
+ issues << { code: :duplicate_peer_count } unless total.nil?
22
+ total = Integer(match[1], 10)
23
+ elsif /\APeer\s+AS\s+MsgRcvd\s+MsgSent\s+OutQ\s+PrefRcv\s+Up\/Down\s+State\z/i.match?(line)
24
+ header = true
25
+ elsif (match = /\A\*?\s*([\d.]+)\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+\S+\s+(.+)\z/.match(line))
26
+ state = match[4].downcase.delete(" ")
27
+ if CtxParser::Values.valid_host?(match[1]) && STATES.include?(state) && (1..4_294_967_295).cover?(match[2].to_i)
28
+ rows << { vrf: "default", afi_safi: "ipv4_unicast", peer: match[1], state: state == "idle(admin)" ? :shutdown : state.to_sym,
29
+ received_prefixes: match[3].to_i, remote_as: match[2].to_i }
30
+ else
31
+ issues << { code: :invalid_bgp_peer }
32
+ end
33
+ elsif !line.empty? && !line.match?(/\A(?:BGP local router ID:|Local AS number:|\* - Dynamically created peer)/i)
34
+ issues << { code: :unrecognized_bgp_line }
35
+ end
36
+ end
37
+ issues << { code: :count_mismatch, expected: total, observed: rows.size } if total && total != rows.size
38
+ build_result(rows, complete: complete, recognized: header && (rows.any? || total == 0), issues: issues,
39
+ identity: %i[vrf afi_safi peer], covered_scopes: [ { vrf: "default", afi_safi: "ipv4_unicast" } ])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+ require_relative "../../parsers/base"
6
+
7
+ module CtxChecker
8
+ module H3c
9
+ module Parsers
10
+ # 转换 UTC 需要明确的数字时区偏移,不猜测时区名称。
11
+ class DisplayClock < CtxChecker::Parsers::Base
12
+ private
13
+
14
+ # 解析 display clock,保留不完整或不支持的证据。
15
+ def parse_output(text, complete:)
16
+ source = clean(text)
17
+ timestamps, zones, summer = [], [], false
18
+ source.each_line do |raw|
19
+ line = raw.strip
20
+ if (match = /\A(\d{1,2}:\d{2}:\d{2}(?:\.\d+)?)\s+(\S+)\s+[A-Za-z]{3}\s+(\d{2})\/(\d{2})\/(\d{4})\z/.match(line))
21
+ timestamps << match.captures
22
+ elsif (match = /\ATime Zone\s*:\s*(\S+)\s+(add|minus)\s+(\d{2}):(\d{2}):(\d{2})\z/i.match(line))
23
+ zones << match.captures
24
+ elsif /\ASummer Time\s*:/i.match?(line)
25
+ summer = true
26
+ elsif !line.empty?
27
+ @issues << { code: :unrecognized_clock_line }
28
+ end
29
+ end
30
+ if timestamps.size == 1
31
+ instant = normalize_time(timestamps.first, zones, summer)
32
+ rows = [ { component: "system", time: instant, source: nil, synchronized: nil } ]
33
+ else
34
+ rows = []
35
+ @issues << { code: :ambiguous_clock_time, count: timestamps.size }
36
+ end
37
+ build_result(rows, complete: complete, recognized: timestamps.any?, identity: [ :component ])
38
+ end
39
+
40
+ # 归一化时间戳,同时保留显式声明的时区语义。
41
+ def normalize_time(parts, zones, summer)
42
+ clock, zone, month, day, year = parts
43
+ if summer || zones.size > 1
44
+ @issues << { code: summer ? :ambiguous_daylight_saving_time : :duplicate_clock_timezone }
45
+ return
46
+ end
47
+ offset = zone.casecmp?("UTC") && zones.empty? ? 0 : zone_offset(zone, zones.first)
48
+ if offset.nil?
49
+ @issues << { code: :unknown_clock_timezone }
50
+ return
51
+ end
52
+ hours, minutes, fraction = clock.split(":")
53
+ hours, minutes, seconds = hours.to_i, minutes.to_i, Rational(fraction)
54
+ unless Date.valid_date?(year.to_i, month.to_i, day.to_i) && (0...24).cover?(hours) && (0...60).cover?(minutes) && (0...60).cover?(seconds)
55
+ @issues << { code: :invalid_clock_time }
56
+ return
57
+ end
58
+ (Time.utc(year.to_i, month.to_i, day.to_i, hours, minutes, seconds) - offset).iso8601(3)
59
+ end
60
+
61
+ # 解析支持的时区偏移,不猜测有歧义的时区标签。
62
+ def zone_offset(zone, settings)
63
+ return unless settings && settings.first == zone
64
+ _name, sign, hours, minutes, seconds = settings
65
+ return unless (0...24).cover?(hours.to_i) && (0...60).cover?(minutes.to_i) && (0...60).cover?(seconds.to_i)
66
+
67
+ (hours.to_i * 3600 + minutes.to_i * 60 + seconds.to_i) * (sign.casecmp?("minus") ? -1 : 1)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "counters_table"
4
+
5
+ module CtxChecker
6
+ module H3c
7
+ module Parsers
8
+ # 解析接收错误包数量,避免把正常流量计入错误。
9
+ class DisplayCountersInboundInterface < CtxChecker::Parsers::Base
10
+ include CountersTable
11
+
12
+ private
13
+
14
+ # 解析 display counters inbound interface,保留不完整或不支持的证据。
15
+ def parse_output(text, complete:)
16
+ parse_counters(text, complete: complete, metric: :in_errors)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "counters_table"
4
+
5
+ module CtxChecker
6
+ module H3c
7
+ module Parsers
8
+ # 解析发送错误包数量;CRC 与丢弃量需要独立证据。
9
+ class DisplayCountersOutboundInterface < CtxChecker::Parsers::Base
10
+ include CountersTable
11
+
12
+ private
13
+
14
+ # 解析 display counters outbound interface,保留不完整或不支持的证据。
15
+ def parse_output(text, complete:)
16
+ parse_counters(text, complete: complete, metric: :out_errors)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 保留每个 CPU 及其采样周期,不对机框成员取平均。
10
+ class DisplayCpuUsage < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+
13
+ private
14
+
15
+ # 解析 display cpu usage,保留不完整或不支持的证据。
16
+ def parse_output(text, complete:)
17
+ source = clean(text)
18
+ rows, components, current, table = [], [], nil, nil
19
+ source.each_line do |raw|
20
+ line = raw.strip
21
+ if (match = /\A((?:Chassis \d+ )?Slot \d+(?: CPU \d+(?: Core \d+)?)?) CPU usage:\z/i.match(line))
22
+ current = scope_name(match[1])
23
+ components << current
24
+ elsif /\A(?:Chassis\s+)?Slot\s+CPU\s+Last 5 sec\s+Last 1 min\s+Last 5 min\z/i.match?(line)
25
+ table = line.start_with?("Chassis") ? 3 : 2
26
+ elsif table && (match = /\A(\d+(?:\s+\d+){1,2})\s+(#{NUMBER})%?\s+(#{NUMBER})%?\s+(#{NUMBER})%?\z/i.match(line))
27
+ identifiers = match[1].split
28
+ if identifiers.size == table
29
+ current = identifiers.zip(table == 3 ? %w[chassis slot cpu] : %w[slot cpu]).map { |id, kind| "#{kind} #{id}" }.join(" ")
30
+ components << current
31
+ rows.concat([ 5, 60, 300 ].zip(match.captures.drop(1)).map { |seconds, value| sample(current, seconds, value) })
32
+ else
33
+ @issues << { code: :missing_cpu_scope }
34
+ end
35
+ elsif (match = /\A(#{NUMBER})%? in last (5 seconds|1 minute|5 minutes)\z/i.match(line))
36
+ if current
37
+ seconds = { "5 seconds" => 5, "1 minute" => 60, "5 minutes" => 300 }.fetch(match[2].downcase)
38
+ rows << sample(current, seconds, match[1])
39
+ else
40
+ @issues << { code: :missing_cpu_scope }
41
+ end
42
+ elsif line.empty?
43
+ next
44
+ else
45
+ @issues << { code: :unrecognized_cpu_line }
46
+ end
47
+ end
48
+ components.each do |component|
49
+ missing = [ 5, 60, 300 ] - rows.select { |row| row[:component] == component }.map { |row| row[:window_seconds] }
50
+ @issues << { code: :missing_cpu_windows, component: component, count: missing.size } if missing.any?
51
+ end
52
+ build_result(rows, complete: complete, recognized: components.any?, identity: %i[component scope window_seconds])
53
+ end
54
+
55
+ # 构建包含采样上下文的归一化测量记录。
56
+ def sample(component, seconds, value)
57
+ utilization = measurement(value, field: :utilization, component: component, percent: true)
58
+ @issues << { code: :missing_cpu_measurement, component: component, window_seconds: seconds } if utilization.nil?
59
+ { component: component, scope: "system", window_seconds: seconds, utilization: utilization }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 传感器阈值取自各板卡,不使用统一猜测的温度限制。
10
+ class DisplayEnvironment < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+ SENSOR_ROW = /\A(?:(\d+)\s+)?(\d+|Vent)\s+((?:hotspot|inflow|outflow)\s+\d+(?:\(sub\d+\))?)\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})(?:\s+#{NUMBER})?\z/i
13
+ HOTSPOT_ROW = /\A(\d+)\s+hotspot(\d+)\s+[A-Za-z][A-Za-z0-9_-]*(?:[ \t]+\d+-\d+)?\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})\s+#{NUMBER}\z/i
14
+
15
+ private
16
+
17
+ # 解析 display environment,保留不完整或不支持的证据。
18
+ def parse_output(text, complete:)
19
+ source = clean(text)
20
+ rows, chassis, header, auxiliary, contexts, context = [], nil, nil, nil, [], nil
21
+ source.each_line do |raw|
22
+ line = raw.strip
23
+ if (match = /\AChassis (\d+):\z/i.match(line))
24
+ chassis = "chassis #{match[1]} "
25
+ contexts << (context = { component: chassis.strip, count: 0 })
26
+ header, auxiliary = nil, nil
27
+ elsif (match = /\A(Chassis\s+)?Slot\s+Sensor\s+Temperature\s+Lower(?:Limit)?\s+Warning(?:Limit)?\s+Alarm(?:Limit)?(?:\s+Shutdown(?:Limit)?)?\z/i.match(line))
28
+ header, auxiliary = match[1] ? :chassis : :slot, nil
29
+ elsif /\ASlotNo\s+Temperature\s+Lower limit\s+Upper limit\z/i.match?(line)
30
+ header, auxiliary = :legacy, nil
31
+ elsif /\ASlot\s+Hotspot\s+(?:Sensor|Name)\s+Temperature\s+Lower\s+Warning\s+Alarm\s+Shutdown\z/i.match?(line)
32
+ header, auxiliary = :hotspot, nil
33
+ elsif header == :hotspot && (match = HOTSPOT_ROW.match(line))
34
+ component = "#{chassis}slot #{match[1]}/hotspot #{match[2]}"
35
+ values = match.captures.drop(2).map { |value| measurement(value, field: :temperature, component: component) }
36
+ rows << temperature_record(component, values[0], low_warning: values[1], high_warning: values[2], high_alarm: values[3])
37
+ context[:count] += 1 if context
38
+ elsif %i[chassis slot].include?(header) && (match = SENSOR_ROW.match(line)) && (header == :chassis) == !match[1].nil?
39
+ prefix = match[1] ? "chassis #{match[1]} " : chassis
40
+ component = "#{prefix}#{match[2].casecmp?("vent") ? 'vent' : "slot #{match[2]}"}/#{scope_name(match[3])}"
41
+ values = match.captures.drop(3).map { |value| measurement(value, field: :temperature, component: component) }
42
+ rows << temperature_record(component, values[0], low_warning: values[1], high_warning: values[2], high_alarm: values[3])
43
+ context[:count] += 1 if context
44
+ elsif header == :legacy && (match = /\A(\d+)\s+(#{NUMBER})\s+#{NUMBER}\s+#{NUMBER}\z/i.match(line))
45
+ component = "#{chassis}slot #{match[1]}"
46
+ rows << temperature_record(component, measurement(match[2], field: :temperature, component: component))
47
+ # 旧版表列不能区分警告与告警级别。
48
+ @issues << { code: :temperature_threshold_semantics_unknown, component: component }
49
+ context[:count] += 1 if context
50
+ elsif (match = /\A(Fan|Power) temperature information:\z/i.match(line))
51
+ header, auxiliary = nil, match[1].downcase
52
+ elsif chassis && auxiliary && (match = /\A(Fan|Power) (\d+):\s*(#{NUMBER})\z/i.match(line)) && match[1].casecmp?(auxiliary)
53
+ component = "#{chassis.strip}/#{auxiliary} #{match[2]}"
54
+ rows << temperature_record(component, measurement(match[3], field: :temperature, component: component))
55
+ context[:count] += 1 if context
56
+ elsif !line.empty? && !/\A(?:-+|System temperature information \(degree centigrade\):)\z/i.match?(line)
57
+ @issues << { code: :unrecognized_temperature_line }
58
+ end
59
+ end
60
+ contexts.each do |entry|
61
+ @issues << { code: :missing_temperature_scope_records, component: entry[:component] } if entry[:count].zero?
62
+ end
63
+ build_result(rows, complete: complete, recognized: rows.any?, identity: [ :component ])
64
+ end
65
+
66
+ # 能读取但无设备阈值的传感器仅提供测量值,不据此判定健康。
67
+ def temperature_record(component, value, **limits)
68
+ @issues << { code: :missing_temperature_measurement, component: component } if value.nil?
69
+ { component: component, value: value, high_comparison: :gte, status: :unknown, **limits }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 风扇托盘保留机框与槽位身份;空槽位不直接判为故障。
10
+ class DisplayFan < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+
13
+ private
14
+
15
+ # 解析 display fan,保留不完整或不支持的证据。
16
+ def parse_output(text, complete:)
17
+ source = clean(text)
18
+ rows, location, current, table, seen, contexts, context, fanless = [], "system", nil, false, {}, [], nil, {}
19
+ source.each_line do |raw|
20
+ line = raw.strip
21
+ if (match = /\A((?:Chassis \d+ )?Slot \d+|Chassis \d+)\s*:\z/i.match(line))
22
+ location, current, table = scope_name(match[1]), nil, false
23
+ contexts << (context = { component: location, count: 0 })
24
+ elsif context && line.casecmp?("Fan-less")
25
+ row = { component: "#{location}/fan", kind: :fan, status: :absent }
26
+ rows << row
27
+ fanless[location] = row
28
+ context[:count] += 1
29
+ current, table = nil, false
30
+ elsif (match = /\AFan(?: Frame)?\s+(\d+)(?:\s*:\s*|\s+)(?:State\s*:\s*(\S+))?\z/i.match(line))
31
+ current = { component: "#{location}/fan #{match[1]}", kind: :fan, status: :unknown }
32
+ rows << current
33
+ context[:count] += 1 if context
34
+ if match[2]
35
+ current[:status] = component_state(match[2], component: current[:component])
36
+ seen[current.object_id] = true
37
+ end
38
+ elsif (match = /\AState\s*:\s*(\S+)\z/i.match(line)) && current
39
+ if seen[current.object_id]
40
+ current[:status] = :unknown
41
+ @issues << { code: :duplicate_component_state, component: current[:component] }
42
+ else
43
+ current[:status] = component_state(match[1], component: current[:component])
44
+ end
45
+ seen[current.object_id] = true
46
+ elsif /\AFan No\.\s+State\s+Speed(?:\s+Description)?\z/i.match?(line)
47
+ table = true
48
+ elsif table && (match = /\A(\d+)\s+(\S+)\s+(?:\d+|--)(?:\s+\S+)?\z/.match(line))
49
+ component = "#{location}/fan #{match[1]}"
50
+ rows << { component: component, kind: :fan, status: component_state(match[2], component: component) }
51
+ context[:count] += 1 if context
52
+ elsif !line.empty? && !/\A(?:-+|(?:Prefer )?Airflow Direction:.*)\z/i.match?(line)
53
+ @issues << { code: :unrecognized_fan_line }
54
+ end
55
+ end
56
+ contexts.each do |entry|
57
+ @issues << { code: :missing_fan_scope_records, component: entry[:component] } if entry[:count].zero?
58
+ end
59
+ # 同一作用域已有实际风扇托盘时,fanless 声明不能覆盖它。
60
+ fanless.each do |scope, row|
61
+ next unless rows.any? { |entry| entry[:component].start_with?("#{scope}/fan ") }
62
+
63
+ row[:status] = :unknown
64
+ @issues << { code: :conflicting_fan_presence, component: row[:component] }
65
+ end
66
+ @issues << { code: :missing_component_state } if rows.any? { |row| row[:status] == :unknown }
67
+ build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component kind])
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # Comware 接口块分别产生状态与计数器观测。
10
+ class DisplayInterface < CtxChecker::Parsers::Base
11
+ INTERFACE = /(?:[A-Za-z][A-Za-z-]*[\d\/.:]+|NULL\d+)/
12
+ COUNTERS = {
13
+ crc_errors: /(?<![\w.+-])(\d+) CRC\b/i,
14
+ in_discards: /^[ \t]*(?:Input:[ \t]*\d+ packets?|\d+ packets? input),[ \t]*\d+ bytes?,[ \t]*(\d+) drops?[ \t]*$/i,
15
+ out_discards: /^[ \t]*(?:Output:[ \t]*\d+ packets?|\d+ packets? output),[ \t]*\d+ bytes?,[ \t]*(\d+) drops?[ \t]*$/i
16
+ }.freeze
17
+ DISCARD_LABELS = { in_discards: /^[ \t]*(?:Input:|[^,\n]+ packets? input,).*\bdrops?\b/i, out_discards: /^[ \t]*(?:Output:|[^,\n]+ packets? output,).*\bdrops?\b/i }.freeze
18
+ DETAILS = /\A(?:Current state:|Line protocol (?:current )?state:|Description:|Bandwidth:|Maximum |Allow jumbo|Broadcast |Multicast |Unicast |Known[ -]unicast |Internet |IP(?:v[46])? |Loopback |Media type |Port |[\w.-]+-speed mode|Link speed |Flow-control |The Maximum |Last |Current system time:|Peak |Input\b|Output\b|PVID:|MDI type:|Tagged VLANs:|UnTagged VLANs:|(?:\d+|-) (?:CRC|ignored|unicasts|aborts|lost carrier))/i
19
+ NON_METRIC_DETAILS = /\A(?:Trunk port encapsulation:\s*IEEE 802\.1q|Physical:\s*(?:InLoopBack|Loopback|Unknown)|Physical(?:\s+is|:)\s*NULL DEV(?:, baudrate:\s*\d+ kbps)?)\z/i
20
+ COUNTER_DETAILS = /\A(?:(?:\d+|-) (?:broadcasts,[ \t]*(?:\d+|-) multicasts(?:,[ \t]*(?:\d+|-) pauses)?|carrier,[ \t]*(?:\d+|-) no carrier|frame,[ \t]*(?:\d+|-) overruns,[ \t]*(?:\d+|-) aborts,[ \t]*(?:\d+|-) ignored,[ \t]*(?:\d+|-) parity errors)|\d+ packets? (?:input|output),[ \t]*\d+ bytes?,[ \t]*\d+ drops?)\z/i
21
+
22
+ private
23
+
24
+ # 接口状态独立于各配置块提供的计数器进行投影。
25
+ def parse_output(text, complete:)
26
+ text = clean(text)
27
+ interfaces, counters, interface_issues, counter_issues = [], [], [], []
28
+ sections = text.split(/(?=^[ \t]*#{INTERFACE}(?:[ \t]+current state[ \t]*:.*)?[ \t]*$)/i)
29
+ sections.each do |section|
30
+ header = /\A[ \t]*(#{INTERFACE})(?:[ \t]+current state[ \t]*:[ \t]*([^\n]+))?[ \t]*\n/i.match(section)
31
+ next if section.strip.empty?
32
+ unless header
33
+ interface_issues << { code: :unrecognized_interface_block }
34
+ next
35
+ end
36
+ name = CtxChecker::Parsers::InterfaceNames.normalize(header[1], platform: :h3c)
37
+ unless known_details?(section)
38
+ interface_issues << { code: :unrecognized_interface_field, interface: name }
39
+ end
40
+ states = [ header[2], *section.scan(/^[ \t]*Current state\s*:\s*(.+)$/i).flatten ].compact
41
+ protocols = section.scan(/^[ \t]*Line protocol (?:current )?state\s*:\s*(.+)$/i).flatten
42
+ if states.size > 1 || protocols.size > 1
43
+ interface_issues << { code: :duplicate_interface_state, interface: name }
44
+ end
45
+ state = states.one? ? states.first : nil
46
+ protocol = protocols.one? ? protocols.first : nil
47
+ admin, oper = state_values(state)
48
+ protocol = status(protocol)
49
+ interface_issues << { code: :interface_state_missing, interface: name } if oper == :unknown || protocol == :unknown
50
+ interfaces << { name: name, kind: CtxChecker::Parsers::InterfaceNames.kind(name), admin_status: admin,
51
+ oper_status: oper, protocol_status: protocol, parent: name.include?(".") ? name.split(".").first : nil }
52
+ # Never 或相对时间都不能证明跨重启的计数连续性。
53
+ discontinuity = nil
54
+ section_counters = []
55
+ counter_source = section.lines.reject { |line| /\A\s*Description:/i.match?(line) }.join
56
+ COUNTERS.each do |metric, pattern|
57
+ values = counter_source.scan(pattern).flatten
58
+ if DISCARD_LABELS.key?(metric) && values.size != counter_source.scan(DISCARD_LABELS.fetch(metric)).size
59
+ counter_issues << { code: :invalid_counter_value, interface: name, metric: metric }
60
+ section_counters << { interface: name, metric: metric, value: nil, discontinuity: discontinuity }
61
+ next
62
+ end
63
+ if values.one?
64
+ section_counters << { interface: name, metric: metric, value: Integer(values.first, 10), discontinuity: discontinuity }
65
+ elsif values.any?
66
+ counter_issues << { code: :duplicate_counter, interface: name, metric: metric }
67
+ end
68
+ end
69
+ if interfaces.last[:kind] == :physical
70
+ missing = %i[in_discards out_discards crc_errors] - section_counters.map { |row| row[:metric] }
71
+ missing.each { |metric| section_counters << { interface: name, metric: metric, value: nil, discontinuity: nil } }
72
+ counter_issues << { code: :missing_counter_metrics, interface: name, metrics: missing } if missing.any?
73
+ end
74
+ counters.concat(section_counters)
75
+ end
76
+ CtxParser::ResultSet.new(results: {
77
+ interfaces: build_result(interfaces, complete: complete, recognized: interfaces.any?, issues: interface_issues, identity: [ :name ]),
78
+ counters: build_result(counters, complete: complete, recognized: counters.any?, issues: counter_issues + interface_issues, identity: %i[interface metric])
79
+ })
80
+ end
81
+
82
+ # 换行 VLAN 列表仅在紧接前一列表时作为元数据处理。
83
+ def known_details?(section)
84
+ vlan_list = false
85
+ section.lines.drop(1).all? do |raw|
86
+ line = raw.strip
87
+ if (match = /\A(?:VLAN (?:Passing|permitted)|(?:Tagged|Untagged)\s+VLAN ID)\s*:\s*(.+)\z/i.match(line))
88
+ vlan_list = valid_vlan_list?(match[1])
89
+ elsif vlan_list && /\A\s+\d/.match?(raw)
90
+ valid_vlan_list?(line)
91
+ else
92
+ vlan_list = false
93
+ line.empty? || DETAILS.match?(line) || NON_METRIC_DETAILS.match?(line) || COUNTER_DETAILS.match?(line)
94
+ end
95
+ end
96
+ end
97
+
98
+ # 校验 VLAN 元数据,避免任意数字尾行掩盖截断。
99
+ def valid_vlan_list?(value)
100
+ return true if value.casecmp?("none")
101
+
102
+ value.split(/,\s*/, -1).all? do |token|
103
+ match = /\A(\d+)(?:-(\d+))?(?:\(default vlan\))?\z/i.match(token)
104
+ next false unless match
105
+ first = match[1].to_i
106
+ last = (match[2] || match[1]).to_i
107
+ first.between?(1, 4094) && last.between?(first, 4094)
108
+ end
109
+ end
110
+
111
+ # 区分主动管理关闭与已启用链路故障。
112
+ def state_values(value)
113
+ return [ :down, :down ] if value&.match?(/\AAdministratively\s+DOWN\b/i)
114
+ observed = status(value)
115
+ [ observed == :unknown ? :unknown : :up, observed ]
116
+ end
117
+
118
+ # 新出现或格式错误的状态文本明确保持未解析。
119
+ def status(value)
120
+ return :up if value&.match?(/\AUP(?:\s|\(|$)/i)
121
+ return :down if value&.match?(/\A(?:DOWN|Administratively\s+DOWN|Link-Flap DOWN|mac-address moving down|MAD ShutDown|OFP DOWN|Storm-Constrain|STP DOWN|M-LAG MAD Shutdown)(?:\s|\(|$)/i)
122
+ :unknown
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end