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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README-zh.md +7 -1
- data/README.md +7 -1
- data/lib/ctx_checker/checks/aggregation.rb +7 -3
- data/lib/ctx_checker/checks/base.rb +8 -2
- data/lib/ctx_checker/checks/routing.rb +10 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +1 -1
- data/lib/ctx_checker/collector.rb +10 -3
- data/lib/ctx_checker/h3c/parsers/counters_table.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_clock.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_environment.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_fan.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_interface.rb +2 -2
- data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_memory.rb +2 -2
- data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_power.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_clock.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_environment.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_interface.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_memory.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +1 -1
- data/lib/ctx_checker/observation.rb +1 -2
- data/lib/ctx_checker/observation_merger.rb +26 -5
- data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +2 -2
- data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +2 -2
- data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +2 -2
- data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +1 -1
- data/lib/ctx_checker/parsers/base.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +1 -1
- data/lib/ctx_checker/report.rb +6 -1
- data/lib/ctx_checker/snapshot.rb +3 -1
- data/lib/ctx_checker/version.rb +1 -1
- 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]
|
|
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?
|
|
12
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
data/lib/ctx_checker/report.rb
CHANGED
|
@@ -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 =
|
|
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)
|
data/lib/ctx_checker/snapshot.rb
CHANGED
|
@@ -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
|
|
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)
|
data/lib/ctx_checker/version.rb
CHANGED