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,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