ctx_checker 0.2.1 → 0.2.3

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README-zh.md +7 -1
  4. data/README.md +7 -1
  5. data/lib/ctx_checker/checks/aggregation.rb +7 -3
  6. data/lib/ctx_checker/checks/base.rb +8 -2
  7. data/lib/ctx_checker/checks/routing.rb +10 -1
  8. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +1 -1
  9. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +1 -1
  10. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +1 -1
  11. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +1 -1
  12. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +1 -1
  13. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +1 -1
  14. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +1 -1
  15. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +1 -1
  16. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +1 -1
  17. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +1 -1
  18. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +1 -1
  19. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +1 -1
  20. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +1 -1
  21. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +1 -1
  22. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +1 -1
  23. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +1 -1
  24. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +1 -1
  25. data/lib/ctx_checker/collector.rb +10 -3
  26. data/lib/ctx_checker/h3c/parsers/counters_table.rb +1 -1
  27. data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +1 -1
  28. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +1 -1
  29. data/lib/ctx_checker/h3c/parsers/display_clock.rb +1 -1
  30. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +1 -1
  31. data/lib/ctx_checker/h3c/parsers/display_environment.rb +1 -1
  32. data/lib/ctx_checker/h3c/parsers/display_fan.rb +1 -1
  33. data/lib/ctx_checker/h3c/parsers/display_interface.rb +2 -2
  34. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +1 -1
  35. data/lib/ctx_checker/h3c/parsers/display_memory.rb +2 -2
  36. data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +1 -1
  37. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +1 -1
  38. data/lib/ctx_checker/h3c/parsers/display_power.rb +1 -1
  39. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +1 -1
  40. data/lib/ctx_checker/hillstone/parsers/show_clock.rb +1 -1
  41. data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +1 -1
  42. data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +1 -1
  43. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +1 -1
  44. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +1 -1
  45. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +1 -1
  46. data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +1 -1
  47. data/lib/ctx_checker/hillstone/parsers/show_memory.rb +1 -1
  48. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +1 -1
  49. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +1 -1
  50. data/lib/ctx_checker/observation.rb +1 -2
  51. data/lib/ctx_checker/observation_merger.rb +26 -5
  52. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +1 -1
  53. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +1 -1
  54. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +2 -2
  55. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +1 -1
  56. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +1 -1
  57. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +1 -1
  58. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +1 -1
  59. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +2 -2
  60. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +1 -1
  61. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +2 -2
  62. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +1 -1
  63. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +1 -1
  64. data/lib/ctx_checker/parsers/base.rb +1 -1
  65. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +1 -1
  66. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +1 -1
  67. data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +1 -1
  68. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +1 -1
  69. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +1 -1
  70. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +1 -1
  71. data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +1 -1
  72. data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +1 -1
  73. data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +1 -1
  74. data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +1 -1
  75. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +1 -1
  76. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +1 -1
  77. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +1 -1
  78. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +1 -1
  79. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +1 -1
  80. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +1 -1
  81. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +1 -1
  82. data/lib/ctx_checker/report.rb +6 -1
  83. data/lib/ctx_checker/snapshot.rb +3 -1
  84. data/lib/ctx_checker/version.rb +1 -1
  85. metadata +1 -1
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  # 识别无地址链路时,仍保留 IP 协议 down 状态。
19
19
  def parse_output(text, complete:)
20
20
  rows, issues, header = [], diagnostics, false
21
- clean(text).each_line do |raw|
21
+ clean(text).each_line("\n") do |raw|
22
22
  line = raw.strip
23
23
  next if line.empty? || /\A[-=]+\z/.match?(line) || LEGEND.match?(line)
24
24
 
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  # 解释计数器前先校验列顺序;缺少采样时保留 nil。
16
16
  def parse_output(text, complete:)
17
17
  rows, issues, header, count = [], diagnostics, false, 0
18
- clean(text).each_line do |raw|
18
+ clean(text).each_line("\n") do |raw|
19
19
  line = raw.strip
20
20
  next if line.empty? || /\A[-=]+\z/.match?(line)
21
21
 
@@ -11,7 +11,7 @@ module CtxChecker
11
11
 
12
12
  # 仅支持已观察到的 StoneOS 格式,不假设使用 Cisco 表格语法。
13
13
  def parse_output(text, complete:)
14
- lines = clean(text).lines.map(&:strip).reject(&:empty?)
14
+ lines = clean(text).lines("\n").map(&:strip).reject(&:empty?)
15
15
  recognized = lines == [ "No IPv4 neighbor is configured" ]
16
16
  issues = [ { code: recognized ? :protocol_not_configured : :unrecognized_bgp_output } ]
17
17
  build_result([], complete: complete, recognized: recognized, issues: issues)
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  def parse_output(text, complete:)
16
16
  percentages, totals, histories = [], [], []
17
17
  header_count, history = 0, nil
18
- clean(text).each_line.with_index(1) do |raw, number|
18
+ clean(text).each_line("\n").with_index(1) do |raw, number|
19
19
  line = raw.strip
20
20
  if (match = /\AThe percentage of memory utilization:\s*(\S+)\s*%\z/i.match(line))
21
21
  percentages << percentage(match[1])
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  def parse_output(text, complete:)
16
16
  source = clean(text)
17
17
  rows, clients, versions, settings = [], [], [], []
18
- source.each_line.with_index(1) do |raw, number|
18
+ source.each_line("\n").with_index(1) do |raw, number|
19
19
  line = raw.strip
20
20
  next if line.empty?
21
21
 
@@ -17,7 +17,7 @@ module CtxChecker
17
17
  # 逐模块隔离,避免未知块将测量值错误关联到前一模块。
18
18
  def parse_output(text, complete:)
19
19
  rows, issues, blocks, current, header = [], diagnostics, [], nil, false
20
- clean(text).each_line do |raw|
20
+ clean(text).each_line("\n") do |raw|
21
21
  line = raw.strip
22
22
  next if line.empty? || /\A=+\z/.match?(line)
23
23
 
@@ -68,8 +68,7 @@ module CtxChecker
68
68
  raise ArgumentError, "Invalid evidence byte count" if entry.key?(:bytes) && !(entry[:bytes].is_a?(Integer) && entry[:bytes] >= 0)
69
69
  raise ArgumentError, "Invalid evidence reference" if entry.key?(:reference) && !text?(entry[:reference])
70
70
  if entry.key?(:observed_at)
71
- raise ArgumentError, "Invalid evidence time" unless entry[:observed_at].is_a?(String)
72
- Time.iso8601(entry[:observed_at])
71
+ raise ArgumentError, "Invalid evidence time" unless SystemFields.timestamp?(entry[:observed_at])
73
72
  end
74
73
  end
75
74
  @records, @issues, @evidence = [ normalized, issues, evidence ].map { |value| CtxParser::Immutable.copy(value) }
@@ -8,8 +8,10 @@ module CtxChecker
8
8
  class ObservationMerger
9
9
  # 归并已声明的命令证据,不丢失来源失败或身份歧义。
10
10
  def self.call(name, observations, commands:, required_metrics: [])
11
- unless observations.is_a?(Array) && observations.all? { |observation| observation.is_a?(Observation) && observation.name == name }
12
- raise ArgumentError, "Command observations must share a dimension"
11
+ unless observations.is_a?(Array) && observations.all? do |observation|
12
+ observation.is_a?(Observation) && observation.name == name && observation.status != :not_requested
13
+ end
14
+ raise ArgumentError, "Command observations must share a requested dimension"
13
15
  end
14
16
  sources = observations.map do |observation|
15
17
  source = observation.evidence.map { |entry| entry[:command] }.compact.uniq
@@ -46,9 +48,9 @@ module CtxChecker
46
48
  issues << { code: :duplicate_identity, count: grouped.duplicate_identity_count }
47
49
  records = grouped.unambiguous.dup
48
50
  # 隔离的对等体实际已被观察到,因此移除后不能再据其作用域证明缺失。
49
- ambiguous = grouped.first_occurrences - grouped.unambiguous
50
- covered_scopes = covered_scopes.reject do |scope|
51
- ambiguous.any? { |row| scope.all? { |key, value| row[key].nil? || row[key] == value } }
51
+ if covered_scopes.any?
52
+ ambiguous = grouped.first_occurrences - grouped.unambiguous
53
+ covered_scopes = unambiguous_scopes(covered_scopes, ambiguous)
52
54
  end
53
55
  end
54
56
  if name == :counters && required_metrics.any?
@@ -76,5 +78,24 @@ module CtxChecker
76
78
  Observation.new(name: name, status: status, records: records, issues: issues.uniq,
77
79
  covered_scopes: covered_scopes, evidence: observations.flat_map(&:evidence))
78
80
  end
81
+
82
+ # 各覆盖字段组合只建立一次冲突索引,保留原有范围顺序。
83
+ def self.unambiguous_scopes(scopes, records)
84
+ indexes = scopes.map { |scope| scope.keys.sort }.uniq.to_h do |fields|
85
+ [ fields, ambiguity_index(records, fields) ]
86
+ end
87
+ scopes.reject do |scope|
88
+ indexes.fetch(scope.keys.sort).any? { |fields, values| values.key?(scope.values_at(*fields)) }
89
+ end
90
+ end
91
+
92
+ # 冲突记录中缺失的字段仍匹配任意值,不能据此证明某个范围为空。
93
+ def self.ambiguity_index(records, fields)
94
+ records.each_with_object({}) do |record, index|
95
+ present = fields.reject { |field| record[field].nil? }
96
+ (index[present] ||= {})[record.values_at(*present)] = true
97
+ end
98
+ end
99
+ private_class_method :unambiguous_scopes, :ambiguity_index
79
100
  end
80
101
  end
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  def parse_output(text, complete:)
16
16
  source = clean(text)
17
17
  rows, issues = [], diagnostics
18
- source.each_line.with_index(1) do |raw, number|
18
+ source.each_line("\n").with_index(1) do |raw, number|
19
19
  next if raw.strip.empty?
20
20
  match = /\A(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+([A-Z][a-z]{2})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\d{4})\z/.match(raw.strip)
21
21
  unless match
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  source = clean(text)
19
19
  rows, totals, issues = [], [], diagnostics
20
20
  title, header = false, false
21
- source.each_line.with_index(1) do |raw, number|
21
+ source.each_line("\n").with_index(1) do |raw, number|
22
22
  line = raw.strip
23
23
  if line == "Global counters:"
24
24
  title = true
@@ -24,7 +24,7 @@ module CtxChecker
24
24
  next if block.strip.empty?
25
25
  match = /\A\s*Interface:\s*([^\n]+)\n/i.match(block)
26
26
  unless match
27
- lines = block.lines.map(&:strip).reject { |line| line.empty? || line.match?(/\A-+\z/) }
27
+ lines = block.lines("\n").map(&:strip).reject { |line| line.empty? || line.match?(/\A-+\z/) }
28
28
  issues << { code: :unrecognized_counter_block } unless lines == [ "Hardware interface counters:" ]
29
29
  next
30
30
  end
@@ -33,7 +33,7 @@ module CtxChecker
33
33
  kind = CtxChecker::Parsers::InterfaceNames.kind(name)
34
34
  wanted = kind == :physical ? :hardware : :logical
35
35
  section, values = nil, []
36
- block.lines.drop(1).each do |raw|
36
+ block.lines("\n").drop(1).each do |raw|
37
37
  line = raw.strip
38
38
  if (heading = /\A(Hardware|Logical) interface counters(?: read from CPU)?:\z/i.match(line))
39
39
  section = heading[1].downcase.to_sym
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  text = clean(text)
16
16
  rows, issues, section, totals, counts, duplicates = {}, diagnostics, nil, {}, Hash.new(0), []
17
17
  logical_names = {}
18
- text.each_line do |raw|
18
+ text.each_line("\n") do |raw|
19
19
  line = raw.strip
20
20
  if (match = /\Atotal configured (hardware|logical) interfaces:\s*(\d+)\z/i.match(line))
21
21
  section = match[1].downcase.to_sym
@@ -14,7 +14,7 @@ module CtxChecker
14
14
  def parse_output(text, complete:)
15
15
  text = clean(text)
16
16
  rows, issues, group, header, groups = [], diagnostics, nil, false, {}
17
- text.each_line do |raw|
17
+ text.each_line("\n") do |raw|
18
18
  line = raw.strip
19
19
  if (match = /\AAE group:\s*(ae\d+)\z/i.match(line))
20
20
  group = match[1].downcase
@@ -14,7 +14,7 @@ module CtxChecker
14
14
  source = clean(text)
15
15
  rows, globals, issues = [], [], diagnostics
16
16
  current, fields, header, peer_fields = nil, {}, false, {}
17
- source.each_line.with_index(1) do |raw, number|
17
+ source.each_line("\n").with_index(1) do |raw, number|
18
18
  line = raw.strip
19
19
  next if line.empty?
20
20
  if line == "NTP state:"
@@ -22,7 +22,7 @@ module CtxChecker
22
22
  issues << { code: :unrecognized_bgp_block }
23
23
  next
24
24
  end
25
- if block.lines.any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
25
+ if block.lines("\n").any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
26
26
  issues << { code: :unrecognized_bgp_field }
27
27
  end
28
28
  if [ "Remote Address", "Peer status", "Remote AS", "virtual router" ].any? { |field| block.scan(/^\s*#{Regexp.escape(field)}:/i).size > 1 }
@@ -21,13 +21,13 @@ module CtxChecker
21
21
  text.split(/(?=^\s*virtual router:)/i).each do |block|
22
22
  unless block.match?(/\A\s*virtual router:/i)
23
23
  # 文档规定的图例位于首个邻居前,任意前导文本不构成证据。
24
- lines = block.lines.map(&:strip).reject(&:empty?)
24
+ lines = block.lines("\n").map(&:strip).reject(&:empty?)
25
25
  unless lines.all? { |line| line.match?(/\A(?:=+|Options: 0x80:reserved,.*|N\/P:NSSA option,.*)\z/i) }
26
26
  issues << { code: :unrecognized_ospf_block }
27
27
  end
28
28
  next
29
29
  end
30
- issues << { code: :unrecognized_ospf_field } if block.lines.any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
30
+ issues << { code: :unrecognized_ospf_field } if block.lines("\n").any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
31
31
  if [ "neighbor address", "neighbor router ID", "status" ].any? { |field| block.scan(/^\s*#{Regexp.escape(field)}:/i).size > 1 }
32
32
  issues << { code: :duplicate_ospf_field }
33
33
  next
@@ -19,7 +19,7 @@ module CtxChecker
19
19
  rows = { temperature: [], environment: [], power: [] }
20
20
  issues = rows.transform_values { diagnostics }
21
21
  section, header, sections, section_rows = nil, nil, [], 0
22
- source.each_line.with_index(1) do |raw, number|
22
+ source.each_line("\n").with_index(1) do |raw, number|
23
23
  line = raw.strip
24
24
  next if line.empty?
25
25
  if (match = /\A-+\s*([^-]+?)\s*-+\z/.match(line))
@@ -17,9 +17,9 @@ module CtxChecker
17
17
  def parse_output(text, complete:)
18
18
  source = clean(text)
19
19
  cpus, memories, cpu_issues, memory_issues, shared_issues = [], [], diagnostics, diagnostics, diagnostics
20
- swap_lines = source.lines.grep(/\A\s*(?:(?:KiB|MiB|GiB|TiB)\s+)?Swap\s*:/)
20
+ swap_lines = source.lines("\n").grep(/\A\s*(?:(?:KiB|MiB|GiB|TiB)\s+)?Swap\s*:/)
21
21
  process_table = false
22
- source.each_line.with_index(1) do |raw, number|
22
+ source.each_line("\n").with_index(1) do |raw, number|
23
23
  line = raw.strip
24
24
  if (match = /\A%?Cpu(?:\(s\)|(\d+))\s*:\s*(.*)\z/i.match(line))
25
25
  cpus << cpu(match, cpu_issues)
@@ -13,7 +13,7 @@ module CtxChecker
13
13
  def parse_output(text, complete:)
14
14
  source = clean(text)
15
15
  rows, issues = [], diagnostics
16
- source.each_line.with_index(1) do |raw, number|
16
+ source.each_line("\n").with_index(1) do |raw, number|
17
17
  line = raw.strip
18
18
  # 配置阈值和复制的 HA 对端状态不属于本地当前告警。
19
19
  next if line.empty? || line.match?(/\A(?:cfg\.|peer\.)/)
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  def parse_output(text, complete:)
16
16
  text = clean(text)
17
17
  rows, issues, header = [], diagnostics, false
18
- text.each_line do |raw|
18
+ text.each_line("\n") do |raw|
19
19
  line = raw.strip
20
20
  if /\APort\s+Temperature\s+Voltage\s+Current\s+Tx Power\s+Rx Power\z/i.match?(line)
21
21
  header = true
@@ -23,7 +23,7 @@ module CtxChecker
23
23
  @unsupported = false
24
24
  @command_failed = false
25
25
  source = CtxParser::Input.text(text, encoding: :transcode, newlines: :lf, max_bytes: MAX_BYTES)
26
- source.each_line.with_index(1) do |line, number|
26
+ source.each_line("\n").with_index(1) do |line, number|
27
27
  if /\A\s*(?:(?:%\s*)?(?:Error:\s*)?(?:Invalid (?:input|command|syntax)|Unrecognized command|Unknown command|command not (?:found|supported)|not supported on this|syntax error)|\^-+\s*unrecognized keyword\b)/i.match?(line)
28
28
  @unsupported = true
29
29
  @issues << { code: :unsupported_command, line: number }
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  # 活跃模式需要额外运行证据,不能由重复的 trunk 记录替代。
16
16
  def parse_output(text, complete:)
17
17
  issues, ports, header = diagnostics, {}, false
18
- clean(text).each_line do |raw|
18
+ clean(text).each_line("\n") do |raw|
19
19
  line = raw.strip
20
20
  next if line.empty? || /\A-+\z/.match?(line)
21
21
 
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  # 将成员限定在其所属 trunk 中,明确保留不完整聚合组。
16
16
  def parse_output(text, complete:)
17
17
  rows, issues, groups, current = [], diagnostics, {}, nil
18
- clean(text).each_line do |raw|
18
+ clean(text).each_line("\n") do |raw|
19
19
  line = raw.strip
20
20
  next if line.empty? || /\A-+\z/.match?(line)
21
21
 
@@ -11,7 +11,7 @@ module CtxChecker
11
11
 
12
12
  # 协议明确禁用不代表策略要求的对等体健康。
13
13
  def parse_output(text, complete:)
14
- lines = clean(text).lines.map(&:strip).reject(&:empty?)
14
+ lines = clean(text).lines("\n").map(&:strip).reject(&:empty?)
15
15
  disabled = lines == [ "BGP is disabled." ]
16
16
  issues = [ { code: disabled ? :protocol_disabled : :unrecognized_bgp_output } ]
17
17
  build_result([], complete: complete, recognized: disabled, issues: issues)
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  def parse_output(text, complete:)
17
17
  rows, issues, section = [], diagnostics, :preamble
18
18
  header, interfaces, ipv6, closed = false, false, false, false
19
- clean(text).each_line do |raw|
19
+ clean(text).each_line("\n") do |raw|
20
20
  line = raw.strip
21
21
  next if line.empty? || /\A-+\z/.match?(line)
22
22
 
@@ -11,7 +11,7 @@ module CtxChecker
11
11
 
12
12
  # 在验证活跃 Alteon 邻居表前,保留明确禁用状态。
13
13
  def parse_output(text, complete:)
14
- lines = clean(text).lines.map(&:strip).reject(&:empty?)
14
+ lines = clean(text).lines("\n").map(&:strip).reject(&:empty?)
15
15
  disabled = lines == [ "OSPF is disabled." ]
16
16
  issues = [ { code: disabled ? :protocol_disabled : :unrecognized_ospf_output } ]
17
17
  build_result([], complete: complete, recognized: disabled, issues: issues)
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  # 链路行缺失不能证明端口已禁用或健康。
16
16
  def parse_output(text, complete:)
17
17
  rows, issues, header = [], diagnostics, false
18
- clean(text).each_line do |raw|
18
+ clean(text).each_line("\n") do |raw|
19
19
  line = raw.strip
20
20
  next if line.empty? || /\A[-\s]+\z/.match?(line)
21
21
  next if /\A-+\s+-+\s+-+\s+-+\s+--TX---RX--\s+-+\z/.match?(line)
@@ -12,7 +12,7 @@ module CtxChecker
12
12
  # 校验每条风扇记录,并按厂商关键性定义确定故障严重级别。
13
13
  def parse_output(text, complete:)
14
14
  rows, tables, headers = [], 0, 0
15
- clean(text).each_line.with_index(1) do |raw, number|
15
+ clean(text).each_line("\n").with_index(1) do |raw, number|
16
16
  line = raw.strip
17
17
  if line == "Fans table:"
18
18
  tables += 1
@@ -13,7 +13,7 @@ module CtxChecker
13
13
 
14
14
  # 仅投影当前时钟,不使用启动、应用或保存时间戳。
15
15
  def parse_output(text, complete:)
16
- lines = clean(text).lines.map(&:strip)
16
+ lines = clean(text).lines("\n").map(&:strip)
17
17
  clocks = lines.grep(/\ASystem Information at\b/)
18
18
  zones = lines.grep(/\ATime zone:/)
19
19
  @issues << { code: :ambiguous_clock_time, count: clocks.size } unless clocks.one?
@@ -14,7 +14,7 @@ module CtxChecker
14
14
  # 未识别的汇总状态保持不可用,不推定冗余供电正常。
15
15
  def parse_output(text, complete:)
16
16
  rows = []
17
- clean(text).each_line.with_index(1) do |raw, number|
17
+ clean(text).each_line("\n").with_index(1) do |raw, number|
18
18
  line = raw.strip
19
19
  if (match = /\A(Single|Dual) Power Supply Status:\s*(.+)\z/i.match(line))
20
20
  state = STATES.fetch(match[2].downcase, :unknown)
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  # 阈值缺失时保留未知,拒绝矛盾的汇总或范围证据。
16
16
  def parse_output(text, complete:)
17
17
  rows, highs, criticals, summaries = [], [], [], []
18
- clean(text).each_line.with_index(1) do |raw, number|
18
+ clean(text).each_line("\n").with_index(1) do |raw, number|
19
19
  line = raw.strip
20
20
  if (match = /\ASensor (\d+) has (\S+) degree Celsius\z/.match(line))
21
21
  value = /\A#{NUMBER}\z/.match?(match[2]) ? Float(match[2], exception: false) : nil
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  # 指定光学方向或单位前,先校验多行表头。
19
19
  def parse_output(text, complete:)
20
20
  rows, issues, headings = [], diagnostics, []
21
- clean(text).each_line do |raw|
21
+ clean(text).each_line("\n") do |raw|
22
22
  line = raw.strip
23
23
  next if line.empty? || /\A[-\s]+\z/.match?(line)
24
24
 
@@ -14,7 +14,7 @@ module CtxChecker
14
14
  # 要求各文档规定的 MP 时间窗完整,无效读数保持未知。
15
15
  def parse_output(text, complete:)
16
16
  rows, headers = [], 0
17
- clean(text).each_line.with_index(1) do |raw, number|
17
+ clean(text).each_line("\n").with_index(1) do |raw, number|
18
18
  line = raw.strip
19
19
  if line == "CPU utilization:"
20
20
  headers += 1
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  # 扣除明确报告的 Linux 缓存;原始进程数值仅作为来源证据保留。
17
17
  def parse_output(text, complete:)
18
18
  values, headers = Hash.new { |hash, key| hash[key] = [] }, 0
19
- clean(text).each_line.with_index(1) do |raw, number|
19
+ clean(text).each_line("\n").with_index(1) do |raw, number|
20
20
  line = raw.strip
21
21
  if line == "Memory stats:"
22
22
  headers += 1
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  # 将各 IEEE 计数器绑定到其明确的数据端口标题。
19
19
  def parse_output(text, complete:)
20
20
  blocks, issues, current = [], diagnostics, nil
21
- clean(text).each_line do |raw|
21
+ clean(text).each_line("\n") do |raw|
22
22
  line = raw.strip
23
23
  next if line.empty? || /\A-+\z/.match?(line)
24
24
 
@@ -17,7 +17,7 @@ module CtxChecker
17
17
  # 解释方向性测量前,要求存在带标签的累计计数列。
18
18
  def parse_output(text, complete:)
19
19
  blocks, issues, current, section = [], diagnostics, nil, nil
20
- clean(text).each_line do |raw|
20
+ clean(text).each_line("\n") do |raw|
21
21
  line = raw.strip
22
22
  next if line.empty? || /\A-+\z/.match?(line)
23
23
 
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  rows = { cpu: [], memory: [] }
16
16
  issues = rows.transform_values { diagnostics }
17
17
  sections, current = [], nil
18
- clean(text).each_line.with_index(1) do |raw, number|
18
+ clean(text).each_line("\n").with_index(1) do |raw, number|
19
19
  line = raw.strip
20
20
  if (match = /\ASPs CPU utilization percentage for (\d+) sec sampling\s*:\z/.match(line))
21
21
  current = section(:cpu, match[1].to_i)
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  source = clean(text)
19
19
  rows, headers, sections, time_fields = [], 0, [], []
20
20
  counter_issues, ntp_issues, section = diagnostics, diagnostics, nil
21
- source.each_line.with_index(1) do |raw, number|
21
+ source.each_line("\n").with_index(1) do |raw, number|
22
22
  line = raw.strip
23
23
  next if line.empty? || /\A-+\z/.match?(line)
24
24
 
@@ -65,11 +65,16 @@ module CtxChecker
65
65
  grouped = {}
66
66
  snapshot.observations.each_value do |observation|
67
67
  sources = observation.evidence.filter_map { |entry| entry[:command] }.uniq
68
+ # 逐命令索引诊断,避免发现数百个端口时反复扫描整个列表。
69
+ issues_by_command = observation.issues.group_by { |issue| issue[:command] }
70
+ if sources.one?
71
+ issues_by_command[sources.first] = observation.issues.select { |issue| !issue[:command] || issue[:command] == sources.first }
72
+ end
68
73
  observation.evidence.each do |evidence|
69
74
  command = evidence[:command]
70
75
  next unless command
71
76
 
72
- issues = observation.issues.select { |issue| issue[:command] == command || !issue[:command] && sources.one? }
77
+ issues = issues_by_command.fetch(command, [])
73
78
  state = issues.any? { |issue| issue[:skipped] } ? :skipped : evidence.fetch(:status, observation.status)
74
79
  entry = grouped[command] ||= { command: command, dimensions: {}, issues: [] }
75
80
  entry[:dimensions][observation.name] = command_status([ entry[:dimensions][observation.name], state ].compact)
@@ -23,7 +23,9 @@ module CtxChecker
23
23
  end
24
24
  @observed_at = case observed_at
25
25
  when Time then observed_at.getutc.freeze
26
- when String then Time.iso8601(observed_at).getutc.freeze
26
+ when String
27
+ raise ArgumentError, "Observation time requires a valid date and explicit UTC offset" unless SystemFields.timestamp?(observed_at)
28
+ Time.iso8601(observed_at).getutc.freeze
27
29
  else raise ArgumentError, "Observation time must be Time or ISO8601"
28
30
  end
29
31
  @target = CtxParser::Immutable.copy(target)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CtxChecker
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.3"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ctx_checker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors