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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/COUNTER_CONTRACT.md +33 -0
- data/LICENSE +22 -0
- data/README-zh.md +93 -0
- data/README.md +93 -0
- data/docs/architecture-zh.md +487 -0
- data/docs/release.md +13 -0
- data/examples/offline.rb +17 -0
- data/lib/ctx_checker/applicability.rb +187 -0
- data/lib/ctx_checker/checks/aggregation.rb +73 -0
- data/lib/ctx_checker/checks/alarms.rb +25 -0
- data/lib/ctx_checker/checks/base.rb +66 -0
- data/lib/ctx_checker/checks/clock.rb +30 -0
- data/lib/ctx_checker/checks/counters.rb +77 -0
- data/lib/ctx_checker/checks/device_counters.rb +32 -0
- data/lib/ctx_checker/checks/hardware.rb +45 -0
- data/lib/ctx_checker/checks/interfaces.rb +35 -0
- data/lib/ctx_checker/checks/ntp.rb +47 -0
- data/lib/ctx_checker/checks/optics.rb +107 -0
- data/lib/ctx_checker/checks/resources.rb +63 -0
- data/lib/ctx_checker/checks/routing.rb +86 -0
- data/lib/ctx_checker/checks/temperature.rb +53 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +56 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +153 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +110 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +41 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +140 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +107 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +164 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +96 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +65 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +36 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +44 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +45 -0
- data/lib/ctx_checker/cisco_ios/parsers/system_values.rb +62 -0
- data/lib/ctx_checker/cisco_ios/profile.rb +39 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_bgp_ipv4_unicast_summary.rb +69 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_clock.rb +13 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +157 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_interface.rb +78 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +141 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +14 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +151 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +116 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +21 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +89 -0
- data/lib/ctx_checker/cisco_nxos/profile.rb +35 -0
- data/lib/ctx_checker/collector.rb +175 -0
- data/lib/ctx_checker/evaluator.rb +49 -0
- data/lib/ctx_checker/failure.rb +24 -0
- data/lib/ctx_checker/finding.rb +19 -0
- data/lib/ctx_checker/h3c/parsers/counters_table.rb +56 -0
- data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +43 -0
- data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +44 -0
- data/lib/ctx_checker/h3c/parsers/display_clock.rb +72 -0
- data/lib/ctx_checker/h3c/parsers/display_counters_inbound_interface.rb +21 -0
- data/lib/ctx_checker/h3c/parsers/display_counters_outbound_interface.rb +21 -0
- data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +64 -0
- data/lib/ctx_checker/h3c/parsers/display_environment.rb +74 -0
- data/lib/ctx_checker/h3c/parsers/display_fan.rb +72 -0
- data/lib/ctx_checker/h3c/parsers/display_interface.rb +127 -0
- data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +72 -0
- data/lib/ctx_checker/h3c/parsers/display_memory.rb +132 -0
- data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +84 -0
- data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +47 -0
- data/lib/ctx_checker/h3c/parsers/display_power.rb +63 -0
- data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +109 -0
- data/lib/ctx_checker/h3c/parsers/system_values.rb +46 -0
- data/lib/ctx_checker/h3c/profile.rb +49 -0
- data/lib/ctx_checker/hillstone/parsers/show_clock.rb +68 -0
- data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +44 -0
- data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +114 -0
- data/lib/ctx_checker/hillstone/parsers/show_environment.rb +90 -0
- data/lib/ctx_checker/hillstone/parsers/show_interface.rb +82 -0
- data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +53 -0
- data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +22 -0
- data/lib/ctx_checker/hillstone/parsers/show_ip_ospf_neighbor.rb +21 -0
- data/lib/ctx_checker/hillstone/parsers/show_memory.rb +85 -0
- data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +110 -0
- data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +101 -0
- data/lib/ctx_checker/hillstone/profile.rb +40 -0
- data/lib/ctx_checker/observation.rb +165 -0
- data/lib/ctx_checker/observation_merger.rb +80 -0
- data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +50 -0
- data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +47 -0
- data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +68 -0
- data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +70 -0
- data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +52 -0
- data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +78 -0
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +66 -0
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +54 -0
- data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +142 -0
- data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +102 -0
- data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +54 -0
- data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +44 -0
- data/lib/ctx_checker/palo_alto/profile.rb +39 -0
- data/lib/ctx_checker/parser_registry.rb +43 -0
- data/lib/ctx_checker/parsers/base.rb +68 -0
- data/lib/ctx_checker/parsers/interface_names.rb +65 -0
- data/lib/ctx_checker/parsers/scoped_result.rb +35 -0
- data/lib/ctx_checker/policy.rb +104 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +48 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +46 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +22 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +71 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +22 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +44 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +47 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +48 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +34 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +53 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +68 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +39 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +58 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +67 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +74 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +99 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +86 -0
- data/lib/ctx_checker/radware_alteon/port_counter_parser.rb +34 -0
- data/lib/ctx_checker/radware_alteon/profile.rb +145 -0
- data/lib/ctx_checker/radware_alteon.rb +8 -0
- data/lib/ctx_checker/report.rb +112 -0
- data/lib/ctx_checker/runtime_parser.rb +234 -0
- data/lib/ctx_checker/snapshot.rb +44 -0
- data/lib/ctx_checker/system_policy.rb +70 -0
- data/lib/ctx_checker/system_record.rb +81 -0
- data/lib/ctx_checker/version.rb +6 -0
- data/lib/ctx_checker.rb +17 -0
- metadata +271 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 保留厂商对关键与非关键风扇的区分。
|
|
9
|
+
class InfoSysFan < CtxChecker::Parsers::Base
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# 校验每条风扇记录,并按厂商关键性定义确定故障严重级别。
|
|
13
|
+
def parse_output(text, complete:)
|
|
14
|
+
rows, tables, headers = [], 0, 0
|
|
15
|
+
clean(text).each_line.with_index(1) do |raw, number|
|
|
16
|
+
line = raw.strip
|
|
17
|
+
if line == "Fans table:"
|
|
18
|
+
tables += 1
|
|
19
|
+
elsif /\AFan-Number\s+Type\s+Status\s+Speed\z/.match?(line)
|
|
20
|
+
headers += 1
|
|
21
|
+
elsif (match = /\A(\d+)\s+(Critical|Non-critical)\s+(\S+)\s+(\S+)\z/i.match(line))
|
|
22
|
+
type, state, speed = match[2].downcase, match[3].downcase, match[4]
|
|
23
|
+
value = /\A\d+(?:\.\d+)?%\z/.match?(speed) ? Float(speed.delete_suffix("%"), exception: false) : nil
|
|
24
|
+
valid = value&.finite? && value.between?(0, 100)
|
|
25
|
+
@issues << { code: :invalid_fan_speed, line: number } unless valid
|
|
26
|
+
status = case state
|
|
27
|
+
when "operational" then :ok
|
|
28
|
+
when "failed" then type == "critical" ? :critical : :warning
|
|
29
|
+
else :unknown
|
|
30
|
+
end
|
|
31
|
+
@issues << { code: :unrecognized_fan_state, line: number } if status == :unknown
|
|
32
|
+
if status == :ok && (!valid || value.zero?)
|
|
33
|
+
status = :unknown
|
|
34
|
+
@issues << { code: :fan_measurements_inconsistent, line: number }
|
|
35
|
+
end
|
|
36
|
+
rows << { component: "fan#{match[1]}", kind: :fan, status: status }
|
|
37
|
+
elsif !line.empty? && !/\A[-\s]+\z/.match?(line)
|
|
38
|
+
@issues << { code: :unrecognized_fan_row, line: number }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
@issues << { code: :fan_header_invalid } unless tables == 1 && headers == 1
|
|
42
|
+
build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component kind])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../../parsers/base"
|
|
6
|
+
|
|
7
|
+
module CtxChecker
|
|
8
|
+
module RadwareAlteon
|
|
9
|
+
module Parsers
|
|
10
|
+
# 通用信息提供设备本地时间及其明确的 GMT 偏移。
|
|
11
|
+
class InfoSysGeneral < CtxChecker::Parsers::Base
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 仅投影当前时钟,不使用启动、应用或保存时间戳。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
lines = clean(text).lines.map(&:strip)
|
|
17
|
+
clocks = lines.grep(/\ASystem Information at\b/)
|
|
18
|
+
zones = lines.grep(/\ATime zone:/)
|
|
19
|
+
@issues << { code: :ambiguous_clock_time, count: clocks.size } unless clocks.one?
|
|
20
|
+
@issues << { code: :ambiguous_clock_timezone, count: zones.size } unless zones.one?
|
|
21
|
+
instant = normalize_time(clocks.first, zones.first) if clocks.one? && zones.one?
|
|
22
|
+
rows = clocks.any? ? [ { component: "system", time: instant, source: "system_clock", synchronized: nil } ] : []
|
|
23
|
+
build_result(rows, complete: complete, recognized: clocks.any?, identity: [ :component ])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# 计算偏差前,要求日期、星期、时钟与数字偏移均有效。
|
|
27
|
+
def normalize_time(clock_line, zone_line)
|
|
28
|
+
clock = /\ASystem Information at (\d{2}:\d{2}:\d{2}) (Sun|Mon|Tue|Wed|Thu|Fri|Sat) ([A-Z][a-z]{2}) (\d{1,2}), (\d{4})\z/.match(clock_line)
|
|
29
|
+
offset = /\ATime zone:\s*.+\(GMT offset ([+-])(\d{2}):(\d{2})\)\z/.match(zone_line)
|
|
30
|
+
unless offset && offset[2].to_i < 24 && offset[3].to_i < 60
|
|
31
|
+
@issues << { code: :clock_offset_unknown }
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
unless clock && clock[1].split(":").map(&:to_i).zip([ 24, 60, 60 ]).all? { |value, limit| value < limit }
|
|
35
|
+
raise ArgumentError, "Invalid clock"
|
|
36
|
+
end
|
|
37
|
+
date = Date.strptime("#{clock[3]} #{clock[4]} #{clock[5]}", "%b %d %Y")
|
|
38
|
+
raise ArgumentError, "Invalid clock weekday" unless Date::ABBR_DAYNAMES[date.wday] == clock[2]
|
|
39
|
+
|
|
40
|
+
Time.iso8601("#{date.iso8601}T#{clock[1]}#{offset[1]}#{offset[2]}:#{offset[3]}").iso8601
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
@issues << { code: :invalid_clock_time }
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 报告声明的供电组合,不虚构单个电源模块身份。
|
|
9
|
+
class InfoSysPs < CtxChecker::Parsers::Base
|
|
10
|
+
STATES = { "ok" => :ok, "failed" => :failed, "failure" => :failed, "warning" => :warning }.freeze
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 未识别的汇总状态保持不可用,不推定冗余供电正常。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
rows = []
|
|
17
|
+
clean(text).each_line.with_index(1) do |raw, number|
|
|
18
|
+
line = raw.strip
|
|
19
|
+
if (match = /\A(Single|Dual) Power Supply Status:\s*(.+)\z/i.match(line))
|
|
20
|
+
state = STATES.fetch(match[2].downcase, :unknown)
|
|
21
|
+
@issues << { code: :unrecognized_power_state, line: number } if state == :unknown
|
|
22
|
+
rows << { component: "#{match[1].downcase}_power_supply_system", kind: :power_supply, status: state, present: nil }
|
|
23
|
+
elsif !line.empty?
|
|
24
|
+
@issues << { code: :unrecognized_power_line, line: number }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
@issues << { code: :conflicting_power_arrangements } if rows.map { |row| row[:component] }.uniq.size > 1
|
|
28
|
+
rows = [] if rows.map { |row| row[:component] }.uniq.size > 1
|
|
29
|
+
build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component kind])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 读取同一命令报告的传感器值及阈值语义。
|
|
9
|
+
class InfoSysTemp < CtxChecker::Parsers::Base
|
|
10
|
+
NUMBER = /-?\d+(?:\.\d+)?/
|
|
11
|
+
STATES = { "ok" => :ok, "high" => :warning, "warning" => :warning, "critical" => :critical }.freeze
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# 阈值缺失时保留未知,拒绝矛盾的汇总或范围证据。
|
|
16
|
+
def parse_output(text, complete:)
|
|
17
|
+
rows, highs, criticals, summaries = [], [], [], []
|
|
18
|
+
clean(text).each_line.with_index(1) do |raw, number|
|
|
19
|
+
line = raw.strip
|
|
20
|
+
if (match = /\ASensor (\d+) has (\S+) degree Celsius\z/.match(line))
|
|
21
|
+
value = /\A#{NUMBER}\z/.match?(match[2]) ? Float(match[2], exception: false) : nil
|
|
22
|
+
@issues << { code: :invalid_temperature_measurement, line: number } unless value&.finite?
|
|
23
|
+
rows << { component: "sensor#{match[1]}", value: value&.finite? ? value : nil, status: :unknown }
|
|
24
|
+
elsif (match = /\ACurrent device temperature is (.+)\z/.match(line))
|
|
25
|
+
summaries << STATES.fetch(match[1].downcase, :unknown)
|
|
26
|
+
elsif (match = /\AHigh temperature is (#{NUMBER})-(#{NUMBER}) degree Celsius\z/.match(line))
|
|
27
|
+
highs << match.captures.map { |value| Float(value) }
|
|
28
|
+
elsif (match = /\ACritical temperature (#{NUMBER}) and above degree Celsius\z/.match(line))
|
|
29
|
+
criticals << Float(match[1])
|
|
30
|
+
elsif !line.empty? && ![ "Note:", "System will shutdown if it reaches Critical temperature" ].include?(line)
|
|
31
|
+
@issues << { code: :unrecognized_temperature_line, line: number }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
valid_bounds = highs.one? && criticals.one? && highs.first.all?(&:finite?) && criticals.first.finite? &&
|
|
35
|
+
highs.first.first < criticals.first && highs.first.last == criticals.first
|
|
36
|
+
if valid_bounds
|
|
37
|
+
rows.each { |row| row.merge!(high_warning: highs.first.first, high_alarm: criticals.first, high_comparison: :gte) }
|
|
38
|
+
elsif highs.any? || criticals.any?
|
|
39
|
+
@issues << { code: :temperature_thresholds_invalid }
|
|
40
|
+
end
|
|
41
|
+
state = summaries.one? ? summaries.first : :unknown
|
|
42
|
+
@issues << { code: :temperature_summary_unavailable } if state == :unknown
|
|
43
|
+
if %i[warning critical].include?(state)
|
|
44
|
+
rows << { component: "device_temperature", value: nil, status: state }
|
|
45
|
+
elsif state == :ok && valid_bounds && rows.any? { |row| row[:value] && row[:value] >= highs.first.first }
|
|
46
|
+
@issues << { code: :temperature_state_disagreement }
|
|
47
|
+
end
|
|
48
|
+
build_result(rows, complete: complete, recognized: rows.any?, identity: [ :component ])
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 模块只有测量值而无阈值时,不能证明光学健康。
|
|
9
|
+
class InfoTransceiver < CtxChecker::Parsers::Base
|
|
10
|
+
HEADER = /\APort\s+Temperature\s+Voltage\s+Current\s+Optical\s+Optical\s+Vendor Name\s+Vendor PN\s+VendorRev\s+VendorsN\s+VendorDateCode\z/i
|
|
11
|
+
LABELS = /\A\(Celsius\)\s+\(Volts\)\s+\(mA\)\s+Tx Power\s+Rx Power\s+\(yymmdd\)\z/i
|
|
12
|
+
UNITS = /\A\(dBm\)\s+\(dBm\)\z/i
|
|
13
|
+
ROW = /\A(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+\S+(?:\s+\S+){4,}\z/
|
|
14
|
+
NUMBER = /\A[+-]?\d+(?:\.\d+)?\z/
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# 指定光学方向或单位前,先校验多行表头。
|
|
19
|
+
def parse_output(text, complete:)
|
|
20
|
+
rows, issues, headings = [], [], []
|
|
21
|
+
clean(text).each_line do |raw|
|
|
22
|
+
line = raw.strip
|
|
23
|
+
next if line.empty? || /\A[-\s]+\z/.match?(line)
|
|
24
|
+
|
|
25
|
+
if HEADER.match?(line) || LABELS.match?(line) || UNITS.match?(line)
|
|
26
|
+
heading = HEADER.match?(line) ? :header : LABELS.match?(line) ? :labels : :units
|
|
27
|
+
issues << { code: :duplicate_optics_header } if headings.include?(heading)
|
|
28
|
+
headings << heading
|
|
29
|
+
elsif headings == %i[header labels units] && (match = /\A(\d+)\s+NA\z/i.match(line)) && match[1].to_i.positive?
|
|
30
|
+
rows.concat(measurements(match[1], nil, nil))
|
|
31
|
+
elsif headings == %i[header labels units] && (match = ROW.match(line)) && match[1].to_i.positive?
|
|
32
|
+
values = match.captures[1, 5]
|
|
33
|
+
unless values.all? { |value| value.casecmp?("NA") || !number(value).nil? }
|
|
34
|
+
issues << { code: :invalid_optics_value, interface: "port#{Integer(match[1], 10)}" }
|
|
35
|
+
end
|
|
36
|
+
rows.concat(measurements(match[1], number(values[3]), number(values[4])))
|
|
37
|
+
else
|
|
38
|
+
issues << { code: :unrecognized_optics_line }
|
|
39
|
+
headings = [] if /\APort\b|\A\(|Optical|Power/.match?(line)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
issues << { code: :optics_header_missing } unless headings == %i[header labels units]
|
|
43
|
+
issues << { code: :optics_measurements_missing } if rows.empty?
|
|
44
|
+
build_result(rows, complete: complete, recognized: headings.include?(:header), issues: issues,
|
|
45
|
+
identity: %i[interface lane metric])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 长、短 NA 行均保留模块是否存在及 DOM 是否支持的未知状态。
|
|
49
|
+
def measurements(port, tx, rx)
|
|
50
|
+
measured = !tx.nil? || !rx.nil?
|
|
51
|
+
%i[tx_power rx_power].zip([ tx, rx ]).map do |metric, value|
|
|
52
|
+
{ interface: "port#{Integer(port, 10)}", lane: nil, metric: metric, value: value, unit: :dbm,
|
|
53
|
+
low_alarm: nil, low_warning: nil, high_warning: nil, high_alarm: nil, alarm: :unknown,
|
|
54
|
+
present: measured ? true : nil, dom_supported: measured ? true : nil, applicable: nil }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 保留有限 dBm 值,不将不可用单元格视为零。
|
|
59
|
+
def number(value)
|
|
60
|
+
return unless NUMBER.match?(value)
|
|
61
|
+
|
|
62
|
+
parsed = Float(value)
|
|
63
|
+
parsed if parsed.finite?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 区分管理处理器与业务处理器的利用率采样。
|
|
9
|
+
class StatsMpCpu < CtxChecker::Parsers::Base
|
|
10
|
+
WINDOWS = [ 1, 4, 64 ].freeze
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 要求各文档规定的 MP 时间窗完整,无效读数保持未知。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
rows, headers = [], 0
|
|
17
|
+
clean(text).each_line.with_index(1) do |raw, number|
|
|
18
|
+
line = raw.strip
|
|
19
|
+
if line == "CPU utilization:"
|
|
20
|
+
headers += 1
|
|
21
|
+
elsif (match = /\AcpuUtil(\d+)Seconds? \(time base\):\s*(\S+)\z/.match(line))
|
|
22
|
+
seconds = match[1].to_i
|
|
23
|
+
value = /\A\d+(?:\.\d+)?%\z/.match?(match[2]) ? Float(match[2].delete_suffix("%"), exception: false) : nil
|
|
24
|
+
valid = WINDOWS.include?(seconds) && value&.finite? && value.between?(0, 100)
|
|
25
|
+
@issues << { code: :invalid_cpu_measurement, line: number } unless valid
|
|
26
|
+
rows << { component: "MP", scope: "management_plane", window_seconds: seconds, utilization: valid ? value : nil }
|
|
27
|
+
elsif !line.empty? && !/\A-+\z/.match?(line)
|
|
28
|
+
@issues << { code: :unrecognized_cpu_line, line: number }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
@issues << { code: :cpu_header_invalid } unless headers == 1
|
|
32
|
+
missing = WINDOWS - rows.map { |row| row[:window_seconds] }
|
|
33
|
+
@issues << { code: :missing_cpu_windows, count: missing.size } if missing.any?
|
|
34
|
+
build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component scope window_seconds])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 设备 RAM 与进程虚拟内存、RSS 属于不同内存作用域。
|
|
9
|
+
class StatsMpMem < CtxChecker::Parsers::Base
|
|
10
|
+
LABELS = { "Total device" => :total, "Free device" => :free, "Cached (Linux)" => :cached,
|
|
11
|
+
"MP Virtual Memory" => :virtual, "MP Resident(RSS Linux) Memory" => :rss }.freeze
|
|
12
|
+
UNITS = { "KB" => 1024, "MB" => 1024**2, "GB" => 1024**3 }.freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# 扣除明确报告的 Linux 缓存;原始进程数值仅作为来源证据保留。
|
|
17
|
+
def parse_output(text, complete:)
|
|
18
|
+
values, headers = Hash.new { |hash, key| hash[key] = [] }, 0
|
|
19
|
+
clean(text).each_line.with_index(1) do |raw, number|
|
|
20
|
+
line = raw.strip
|
|
21
|
+
if line == "Memory stats:"
|
|
22
|
+
headers += 1
|
|
23
|
+
elsif (match = /\A(.+?):\s*(\S+)\s+(\S+)\z/.match(line))
|
|
24
|
+
label = LABELS[match[1].gsub(/\s+/, " ")]
|
|
25
|
+
unless label
|
|
26
|
+
@issues << { code: :unrecognized_memory_line, line: number }
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
value = /\A\d+(?:\.\d+)?\z/.match?(match[2]) ? Float(match[2], exception: false) : nil
|
|
30
|
+
multiplier = UNITS[match[3]]
|
|
31
|
+
amount = value && multiplier && value * multiplier
|
|
32
|
+
valid = amount&.finite?
|
|
33
|
+
@issues << { code: :invalid_memory_measurement, field: label } unless valid
|
|
34
|
+
values[label] << (valid ? amount.round : nil)
|
|
35
|
+
elsif !line.empty? && !/\A-+\z/.match?(line)
|
|
36
|
+
@issues << { code: :unrecognized_memory_line, line: number }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
@issues << { code: :memory_header_invalid } unless headers == 1
|
|
40
|
+
duplicates = values.count { |_name, readings| readings.size > 1 }
|
|
41
|
+
@issues << { code: :duplicate_memory_measurement, count: duplicates } if duplicates.positive?
|
|
42
|
+
total, free, cached = %i[total free cached].map { |key| values[key].one? ? values[key].first : nil }
|
|
43
|
+
valid = total&.positive? && free && cached && free + cached <= total
|
|
44
|
+
@issues << { code: :memory_available_unknown } unless valid
|
|
45
|
+
rows = if %i[total free cached].any? { |key| values[key].any? }
|
|
46
|
+
[ { component: "system", scope: "physical", total_bytes: total, free_bytes: free,
|
|
47
|
+
used_bytes: valid ? total - free - cached : nil, available_bytes: valid ? free + cached : nil,
|
|
48
|
+
utilization: valid ? (total - free - cached).fdiv(total) * 100 : nil,
|
|
49
|
+
basis: valid ? :used_minus_cache : :unknown } ]
|
|
50
|
+
else
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component scope])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../port_counter_parser"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 以太网子计数器与接口错误总数分别保留。
|
|
9
|
+
class StatsPortEther < CtxChecker::RadwareAlteon::PortCounterParser
|
|
10
|
+
HEADER = /\AEthernet statistics for port (\d+):\z/i
|
|
11
|
+
METRICS = { "dot3StatsFCSErrors" => :crc_errors, "dot3StatsCarrierSenseErrors" => :out_carrier_errors }.freeze
|
|
12
|
+
CONTEXT = %w[dot3StatsAlignmentErrors dot3StatsSingleCollisionFrames dot3StatsMultipleCollisionFrames
|
|
13
|
+
dot3StatsSQETestErrors dot3StatsDeferredTransmissions dot3StatsLateCollisions dot3StatsExcessiveCollisions
|
|
14
|
+
dot3StatsInternalMacTransmitErrors dot3StatsFrameTooLongs dot3StatsInternalMacReceiveErrors].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# 将各 IEEE 计数器绑定到其明确的数据端口标题。
|
|
19
|
+
def parse_output(text, complete:)
|
|
20
|
+
blocks, issues, current = [], [], nil
|
|
21
|
+
clean(text).each_line do |raw|
|
|
22
|
+
line = raw.strip
|
|
23
|
+
next if line.empty? || /\A-+\z/.match?(line)
|
|
24
|
+
|
|
25
|
+
if (match = HEADER.match(line)) && match[1].to_i.positive?
|
|
26
|
+
current = { interface: "port#{Integer(match[1], 10)}", values: {}, seen: Hash.new(0) }
|
|
27
|
+
blocks << current
|
|
28
|
+
elsif current && (match = /\A(dot3\w+):\s+(\S+)\z/.match(line)) && (METRICS.key?(match[1]) || CONTEXT.include?(match[1]))
|
|
29
|
+
record_value(current, match, issues)
|
|
30
|
+
elsif current && /\Adot3CollFrequencies \[1-15\]:\s+NA\z/.match?(line)
|
|
31
|
+
next
|
|
32
|
+
else
|
|
33
|
+
issues << { code: :unrecognized_counter_line }
|
|
34
|
+
current = nil if /statistics/i.match?(line)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
rows = blocks.flat_map { |block| counter_records(block, issues) }
|
|
38
|
+
build_port_result(rows, complete: complete, recognized: blocks.any?, issues: issues)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 保留精确整数采样,重复测量使对应指标失效。
|
|
42
|
+
def record_value(block, match, issues)
|
|
43
|
+
value = match[2]
|
|
44
|
+
issues << { code: :invalid_counter_value, interface: block[:interface] } unless /\A(?:\d+|NA)\z/.match?(value)
|
|
45
|
+
return unless (metric = METRICS[match[1]])
|
|
46
|
+
|
|
47
|
+
block[:seen][metric] += 1
|
|
48
|
+
if block[:seen][metric] > 1
|
|
49
|
+
block[:values][metric] = nil
|
|
50
|
+
issues << { code: :duplicate_counter_value, interface: block[:interface], metric: metric }
|
|
51
|
+
else
|
|
52
|
+
block[:values][metric] = /\A\d+\z/.match?(value) ? Integer(value, 10) : nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 不能根据无关的零计数推断缺失的 FCS 或载波错误。
|
|
57
|
+
def counter_records(block, issues)
|
|
58
|
+
METRICS.values.map do |metric|
|
|
59
|
+
issues << { code: :counter_value_missing, interface: block[:interface], metric: metric } unless block[:seen][metric].positive?
|
|
60
|
+
{ interface: block[:interface], metric: metric, value: block[:values][metric], discontinuity: nil,
|
|
61
|
+
unit: metric == :out_carrier_errors ? :events : :packets, scope: :mac }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../port_counter_parser"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module RadwareAlteon
|
|
7
|
+
module Parsers
|
|
8
|
+
# 接口累计计数与后续每秒速率表属于独立事实。
|
|
9
|
+
class StatsPortIf < CtxChecker::RadwareAlteon::PortCounterParser
|
|
10
|
+
HEADER = /\AInterface statistics for port (\d+):\z/i
|
|
11
|
+
COLUMNS = /\AifHCIn Counters\s+ifHCOut Counters\z/i
|
|
12
|
+
ROW = /\A(Octets|UcastPkts|BroadcastPkts|MulticastPkts|Discards|Errors):\s+(\S+)\s+(\S+)\z/i
|
|
13
|
+
METRICS = { "discards" => %i[in_discards out_discards], "errors" => %i[in_errors out_errors] }.freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# 解释方向性测量前,要求存在带标签的累计计数列。
|
|
18
|
+
def parse_output(text, complete:)
|
|
19
|
+
blocks, issues, current, section = [], [], nil, nil
|
|
20
|
+
clean(text).each_line do |raw|
|
|
21
|
+
line = raw.strip
|
|
22
|
+
next if line.empty? || /\A-+\z/.match?(line)
|
|
23
|
+
|
|
24
|
+
if (match = HEADER.match(line)) && match[1].to_i.positive?
|
|
25
|
+
current = { interface: "port#{Integer(match[1], 10)}", values: {}, seen: Hash.new(0) }
|
|
26
|
+
blocks << current
|
|
27
|
+
section = :header
|
|
28
|
+
elsif current && section == :header && COLUMNS.match?(line)
|
|
29
|
+
section = :cumulative
|
|
30
|
+
elsif current && section == :cumulative && line.casecmp?("Per second Interface statistics:")
|
|
31
|
+
section = :rate
|
|
32
|
+
elsif current && %i[cumulative rate].include?(section) && (match = ROW.match(line))
|
|
33
|
+
record_values(current, match, section, issues)
|
|
34
|
+
else
|
|
35
|
+
issues << { code: :unrecognized_counter_line }
|
|
36
|
+
current = nil if /statistics|Counters/i.match?(line)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
rows = blocks.flat_map { |block| counter_records(block, issues) }
|
|
40
|
+
build_port_result(rows, complete: complete, recognized: blocks.any?, issues: issues)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 缺失或重复采样保持不可用;速率不能替代累计值。
|
|
44
|
+
def record_values(block, match, section, issues)
|
|
45
|
+
values = match.captures.last(2)
|
|
46
|
+
unless values.all? { |value| /\A(?:\d+|NA)\z/i.match?(value) }
|
|
47
|
+
issues << { code: :invalid_counter_value, interface: block[:interface] }
|
|
48
|
+
end
|
|
49
|
+
return unless section == :cumulative && (metrics = METRICS[match[1].downcase])
|
|
50
|
+
|
|
51
|
+
metrics.zip(values).each do |metric, value|
|
|
52
|
+
block[:seen][metric] += 1
|
|
53
|
+
if block[:seen][metric] > 1
|
|
54
|
+
block[:values][metric] = nil
|
|
55
|
+
issues << { code: :duplicate_counter_value, interface: block[:interface], metric: metric }
|
|
56
|
+
else
|
|
57
|
+
block[:values][metric] = /\A\d+\z/.match?(value) ? Integer(value, 10) : nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 输出所有请求方向的指标,包括明确不可用的单元格。
|
|
63
|
+
def counter_records(block, issues)
|
|
64
|
+
METRICS.values.flatten.map do |metric|
|
|
65
|
+
unless block[:seen][metric].positive?
|
|
66
|
+
issues << { code: :counter_value_missing, interface: block[:interface], metric: metric }
|
|
67
|
+
end
|
|
68
|
+
{ interface: block[:interface], metric: metric, value: block[:values][metric], discontinuity: nil }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require "ctx_parser/result_set"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module RadwareAlteon
|
|
8
|
+
module Parsers
|
|
9
|
+
# 各 SP 时间窗与内存章节独立保留覆盖诊断。
|
|
10
|
+
class StatsSpAllutils < CtxChecker::Parsers::Base
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# 校验回显的 SP 数量及汇总图,不混用 CPU 和内存数据。
|
|
14
|
+
def parse_output(text, complete:)
|
|
15
|
+
rows = { cpu: [], memory: [] }
|
|
16
|
+
issues = rows.transform_values { [] }
|
|
17
|
+
sections, current = [], nil
|
|
18
|
+
clean(text).each_line.with_index(1) do |raw, number|
|
|
19
|
+
line = raw.strip
|
|
20
|
+
if (match = /\ASPs CPU utilization percentage for (\d+) sec sampling\s*:\z/.match(line))
|
|
21
|
+
current = section(:cpu, match[1].to_i)
|
|
22
|
+
sections << current
|
|
23
|
+
elsif line == "SPs memory percentage:"
|
|
24
|
+
current = section(:memory, nil)
|
|
25
|
+
sections << current
|
|
26
|
+
elsif line.empty? || /\A-+\z/.match?(line)
|
|
27
|
+
next
|
|
28
|
+
elsif current
|
|
29
|
+
parse_section_line(line, number, current, rows, issues)
|
|
30
|
+
else
|
|
31
|
+
issues.each_value { |items| items << { code: :unrecognized_sp_line, line: number } }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
sections.each { |item| validate_section(item, issues.fetch(item[:dimension])) }
|
|
35
|
+
validate_sections(sections, issues)
|
|
36
|
+
CtxParser::ResultSet.new(results: rows.to_h do |dimension, records|
|
|
37
|
+
identity = dimension == :cpu ? %i[component scope window_seconds] : %i[component scope]
|
|
38
|
+
[ dimension, build_result(records, complete: complete, recognized: records.any?, issues: issues.fetch(dimension), identity: identity) ]
|
|
39
|
+
end)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 分别跟踪各章节,不用单个命令成功标志代替全部章节状态。
|
|
43
|
+
def section(dimension, seconds)
|
|
44
|
+
{ dimension: dimension, seconds: seconds, ids: [], summaries: [], graphs: [] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 柱状图仅作展示上下文,明确百分比字段才是数值来源。
|
|
48
|
+
def parse_section_line(line, number, current, rows, issues)
|
|
49
|
+
errors = issues.fetch(current[:dimension])
|
|
50
|
+
if (match = /\ASP\s+(\d+)\s*:\s*\[[#.]+\]\s+(\S+)\z/.match(line))
|
|
51
|
+
id = match[1].to_i
|
|
52
|
+
value = percentage(match[2], errors, number)
|
|
53
|
+
current[:ids] << id
|
|
54
|
+
errors << { code: :invalid_sp_identity, line: number } if id.zero?
|
|
55
|
+
base = { component: "SP#{id}", scope: "service_plane", utilization: id.positive? ? value : nil }
|
|
56
|
+
record = current[:dimension] == :cpu ? base.merge(window_seconds: current[:seconds]) : base.merge(basis: :used_total)
|
|
57
|
+
rows.fetch(current[:dimension]) << record
|
|
58
|
+
elsif (match = /\AAverage\s*:\s*(\S+)\s+\(total (\d+) sp's\)\z/.match(line))
|
|
59
|
+
current[:summaries] << { value: percentage(match[1], errors, number), count: match[2].to_i }
|
|
60
|
+
elsif (match = /\A\[[#.]+\]\s+(\S+)\z/.match(line))
|
|
61
|
+
current[:graphs] << percentage(match[1], errors, number)
|
|
62
|
+
else
|
|
63
|
+
errors << { code: :unrecognized_sp_line, line: number }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 数值缺失、非有限值或超过 100 的百分比不能变成零。
|
|
68
|
+
def percentage(text, issues, number)
|
|
69
|
+
value = /\A\d+(?:\.\d+)?%\z/.match?(text) ? Float(text.delete_suffix("%"), exception: false) : nil
|
|
70
|
+
return value if value&.finite? && value.between?(0, 100)
|
|
71
|
+
issues << { code: :invalid_sp_measurement, line: number }
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 要求汇总完整,且所有声明的 SP 身份均存在且不重复。
|
|
76
|
+
def validate_section(item, issues)
|
|
77
|
+
valid = item[:summaries].one? && item[:graphs].one?
|
|
78
|
+
summary = item[:summaries].first
|
|
79
|
+
valid &&= summary[:value] && summary[:count].positive? && summary[:count] == item[:ids].uniq.size &&
|
|
80
|
+
item[:ids].uniq.size == item[:ids].size && summary[:value] == item[:graphs].first
|
|
81
|
+
issues << { code: :incomplete_sp_section, window_seconds: item[:seconds] } unless valid
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# CPU 时间窗或内存表缺失仅影响其各自结果视图。
|
|
85
|
+
def validate_sections(sections, issues)
|
|
86
|
+
cpu = sections.select { |item| item[:dimension] == :cpu }
|
|
87
|
+
memory = sections.select { |item| item[:dimension] == :memory }
|
|
88
|
+
unless cpu.map { |item| item[:seconds] }.sort == [ 4, 64 ]
|
|
89
|
+
issues[:cpu] << { code: :incomplete_sp_cpu_windows }
|
|
90
|
+
end
|
|
91
|
+
issues[:memory] << { code: :incomplete_sp_memory_sections } unless memory.one?
|
|
92
|
+
cpu.each_cons(2) do |first, last|
|
|
93
|
+
issues[:cpu] << { code: :inconsistent_sp_identities } unless first[:ids].sort == last[:ids].sort
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|