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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+ require_relative "../../parsers/base"
6
+
7
+ module CtxChecker
8
+ module Hillstone
9
+ module Parsers
10
+ # StoneOS 命名时区可能由用户自定义,不能用于确定 UTC 偏移。
11
+ class ShowClock < CtxChecker::Parsers::Base
12
+ private
13
+
14
+ # 保留已识别的设备本地时间,不猜测时区或同步状态。
15
+ def parse_output(text, complete:)
16
+ source = clean(text)
17
+ zones, clocks = [], []
18
+ source.each_line.with_index(1) do |raw, number|
19
+ line = raw.strip
20
+ next if line.empty?
21
+
22
+ if (match = /\ACurrent time zone:\s*(\S+)\z/i.match(line))
23
+ zones << match[1]
24
+ elsif (match = /\A(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\z/.match(line))
25
+ clocks << match.captures
26
+ else
27
+ @issues << { code: :unrecognized_clock_line, line: number }
28
+ end
29
+ end
30
+ @issues << { code: :ambiguous_clock_time, count: clocks.size } unless clocks.one?
31
+ @issues << { code: :ambiguous_clock_timezone, count: zones.size } unless zones.one?
32
+ instant = normalize_time(clocks.first, zones.first) if clocks.one? && zones.one?
33
+ rows = clocks.any? ? [ { component: "system", time: instant, source: "system_clock", synchronized: nil } ] : []
34
+ build_result(rows, complete: complete, recognized: clocks.any?, identity: [ :component ])
35
+ end
36
+
37
+ # 仅明确数字偏移或 UTC 标签允许计算时钟偏差。
38
+ def normalize_time(parts, zone)
39
+ date, clock, weekday = parts
40
+ parsed_date = Date.iso8601(date)
41
+ unless Date::DAYNAMES[parsed_date.wday] == weekday && clock.split(":").map(&:to_i).zip([ 24, 60, 60 ]).all? { |value, limit| value < limit }
42
+ @issues << { code: :invalid_clock_time }
43
+ return
44
+ end
45
+ offset = zone_offset(zone)
46
+ unless offset
47
+ @issues << { code: :clock_offset_unknown }
48
+ return
49
+ end
50
+ Time.iso8601("#{date}T#{clock}#{offset}").iso8601
51
+ rescue ArgumentError
52
+ @issues << { code: :invalid_clock_time }
53
+ nil
54
+ end
55
+
56
+ # 用户自定义时区名称不能替代实际偏移值。
57
+ def zone_offset(zone)
58
+ return "+00:00" if %w[UTC GMT Z].include?(zone)
59
+
60
+ match = /\A(?:UTC|GMT)?([+-])(\d{2}):?(\d{2})\z/.match(zone)
61
+ return unless match && match[2].to_i < 24 && match[3].to_i < 60
62
+
63
+ "#{match[1]}#{match[2]}:#{match[3]}"
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module Hillstone
7
+ module Parsers
8
+ # 保留设备报告的时间窗,不为整体平均值虚构周期。
9
+ class ShowCpu < CtxChecker::Parsers::Base
10
+ WINDOWS = { "1 minute" => 60, "5 minutes" => 300, "15 minutes" => 900 }.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(Average|Current) cpu utilization\s*:\s*(\S+)\s*\z/i.match(line))
20
+ rows << sample(match[2], scope: match[1].casecmp?("average") ? "system_average" : "system", seconds: nil)
21
+ elsif (match = /\ALast (1 minute|5 minutes|15 minutes)\s*:\s*(\S+)\s*\z/i.match(line))
22
+ rows << sample(match[2], scope: "system", seconds: WINDOWS.fetch(match[1].downcase))
23
+ elsif !line.empty?
24
+ @issues << { code: :unrecognized_cpu_line, line: number }
25
+ end
26
+ end
27
+ windows = rows.select { |row| row[:scope] == "system" }.map { |row| row[:window_seconds] }
28
+ missing = [ nil, *WINDOWS.values ] - windows
29
+ @issues << { code: :missing_cpu_windows, count: missing.size } if missing.any?
30
+ build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component scope window_seconds])
31
+ end
32
+
33
+ # 无效百分比保持不可用,避免导致观测记录校验失败。
34
+ def sample(text, scope:, seconds:)
35
+ match = /\A(\d+(?:\.\d+)?)%\z/.match(text)
36
+ value = match && Float(match[1], exception: false)
37
+ valid = value&.finite? && value.between?(0, 100)
38
+ @issues << { code: :invalid_cpu_measurement, scope: scope, window_seconds: seconds } unless valid
39
+ { component: "system", scope: scope, window_seconds: seconds, utilization: valid ? value : nil }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module Hillstone
7
+ module Parsers
8
+ # 命名 CPU 端口与队列分别保留计数器,不叠加可能重叠的值。
9
+ class ShowCpuCntr < CtxChecker::Parsers::Base
10
+ ERROR_FIELDS = %w[InBadOctets InBadPkts InMACRcvErrors InCRCAlignErrors].freeze
11
+ INFORMATIONAL_FIELDS = %w[InGoodOctets InGoodPkts InUnicastPkts InBroadcastPkts InMulticastPkts InControlPkts
12
+ InOver1518Pkts OutGoodOctets OutGoodPkts OutUnicastPkts OutBroadcastPkts OutMulticastPkts OutControlPkts
13
+ Pkts64Octets Pkts65to127Octets Pkts128to255Octets Pkts256to511Octets Pkts512to1023Octets Pkts1024toMaxOctets].freeze
14
+ UNKNOWN_FIELDS = %w[InUndersizePkts InOversizePkts InFragments InJabbers DropEvents Collisions OutDropDeferrals
15
+ SingleCollisions MultipleCollisions ExcessiveCollisions LateCollisions].freeze
16
+ FIELDS = (ERROR_FIELDS + INFORMATIONAL_FIELDS + UNKNOWN_FIELDS).freeze
17
+ CELL = /\G\s*([A-Za-z][A-Za-z0-9]*):\s*(\S+)(?=\s|\z)/
18
+
19
+ private
20
+
21
+ # 顶部流量汇总包含歧义拼接列,不据此生成健康事实。
22
+ def parse_output(text, complete:)
23
+ records, contexts = [], []
24
+ context, queue, summary = nil, nil, false
25
+ clean(text).each_line.with_index(1) do |raw, number|
26
+ line = raw.strip
27
+ if /\Acpu Port Send\s+Rev Counter:\z/i.match?(line)
28
+ summary = true
29
+ elsif (match = /\A([A-Za-z][A-Za-z0-9_\/.-]*), physical port (\d+):\z/i.match(line))
30
+ context = { component: "cpu/#{match[1]}/physical_port_#{match[2]}", fields: [], queues: [] }
31
+ contexts << context
32
+ queue, summary = nil, false
33
+ elsif context && (match = /\A(\d+) (RX|TX) Queue\(s\):\z/i.match(line))
34
+ queue = { direction: match[2].downcase, count: match[1].to_i, ids: [], headers: 0 }
35
+ context[:queues] << queue
36
+ elsif line.empty? || /\A[-=]+\z/.match?(line)
37
+ next
38
+ elsif summary && /\A(?:Port\s+Receive\s+Send|port-\d+(?:\s+\d+){1,2})\z/i.match?(line)
39
+ next
40
+ elsif context && queue
41
+ parse_queue(line, number, context, queue, records)
42
+ elsif context
43
+ parse_fields(line, number, context, records)
44
+ else
45
+ @issues << { code: :unrecognized_cpu_counter_row, line: number }
46
+ end
47
+ end
48
+ contexts.each { |entry| validate_context(entry) }
49
+ build_result(records, complete: complete, recognized: contexts.any?, identity: %i[component metric])
50
+ end
51
+
52
+ # 独立解析每个带标签字段,保留指标语义未知的状态。
53
+ def parse_fields(line, number, context, records)
54
+ position = 0
55
+ until /\A\s*\z/.match?(line[position..])
56
+ match = CELL.match(line, position)
57
+ unless match
58
+ @issues << { code: :unrecognized_cpu_counter_row, line: number }
59
+ break
60
+ end
61
+ field, raw = match.captures
62
+ context[:fields] << field
63
+ unless FIELDS.include?(field)
64
+ @issues << { code: :unrecognized_cpu_counter_field, line: number }
65
+ end
66
+ value = /\A\d+\z/.match?(raw) ? Integer(raw, 10) : nil
67
+ @issues << { code: :invalid_cpu_counter_value, line: number } unless value
68
+ category = ERROR_FIELDS.include?(field) ? :error : INFORMATIONAL_FIELDS.include?(field) ? :informational : :unknown
69
+ metric = field.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
70
+ records << counter(context[:component], metric, category, value)
71
+ position = match.end(0)
72
+ end
73
+ end
74
+
75
+ # 队列错误作为独立观测,不叠加到硬件端口总数。
76
+ def parse_queue(line, number, context, queue, records)
77
+ direction = queue[:direction] == "rx" ? "In" : "Out"
78
+ if /\AQueue ID\s+#{direction} Pkts\s+#{direction} Bytes\s+Errors\z/i.match?(line)
79
+ queue[:headers] += 1
80
+ elsif queue[:headers].positive? && (match = /\A(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\z/.match(line))
81
+ id, packets, bytes, errors = match.captures.map { |value| Integer(value, 10) }
82
+ queue[:ids] << id
83
+ component = "#{context[:component]}/#{queue[:direction]}_queue_#{id}"
84
+ records << counter(component, "packets", :informational, packets)
85
+ records << counter(component, "bytes", :informational, bytes)
86
+ records << counter(component, "errors", :error, errors)
87
+ else
88
+ @issues << { code: :unrecognized_cpu_queue_row, line: number }
89
+ end
90
+ end
91
+
92
+ # 使用显式字段与队列数量识别截断或重复的组件块。
93
+ def validate_context(context)
94
+ missing = FIELDS - context[:fields]
95
+ @issues << { code: :cpu_counter_fields_missing, component: context[:component], count: missing.size } if missing.any?
96
+ directions = context[:queues].map { |queue| queue[:direction] }
97
+ unless directions.sort == %w[rx tx]
98
+ @issues << { code: :cpu_counter_queue_sections_incomplete, component: context[:component] }
99
+ end
100
+ context[:queues].each do |queue|
101
+ unless queue[:headers] == 1 && queue[:ids].size == queue[:count] && queue[:ids].uniq.size == queue[:count]
102
+ @issues << { code: :cpu_counter_queue_count_mismatch, component: context[:component], scope: queue[:direction] }
103
+ end
104
+ end
105
+ end
106
+
107
+ # 本命令计数器未提供运行时长或重置时间点。
108
+ def counter(component, metric, category, value)
109
+ { component: component, metric: metric, category: category, value: value, discontinuity: nil }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require "ctx_parser/result_set"
5
+
6
+ module CtxChecker
7
+ module Hillstone
8
+ module Parsers
9
+ # 分离传感器视图,避免单个硬件章节格式错误抹除其他结果。
10
+ class ShowEnvironment < CtxChecker::Parsers::Base
11
+ SECTIONS = { "Temperature information(C)" => :temperature, "Fan information" => :environment, "Power information" => :power }.freeze
12
+ HEADERS = { temperature: /\ADescription\s+Temperature\s+Range\z/, environment: /\ADescription\s+Status\z/,
13
+ power: /\ADescription\s+Status\z/ }.freeze
14
+ STATES = { "fine" => :ok, "normal" => :ok, "ok" => :ok, "failed" => :failed, "fault" => :failed,
15
+ "faulty" => :failed, "error" => :failed, "warning" => :warning, "absent" => :absent }.freeze
16
+ NUMBER = /-?\d+(?:\.\d+)?/
17
+
18
+ private
19
+
20
+ # 跟踪各已声明章节,并分别要求其具备表头与测量值。
21
+ def parse_output(text, complete:)
22
+ rows = { temperature: [], environment: [], power: [] }
23
+ issues = rows.transform_values { [] }
24
+ contexts, section, header = [], nil, false
25
+ clean(text).each_line.with_index(1) do |raw, number|
26
+ line = raw.strip
27
+ if (match = /\A(.+ information(?:\(C\))?):\z/.match(line))
28
+ section, header = SECTIONS[match[1]], false
29
+ contexts << { section: section, rows: 0, headers: 0 } if section
30
+ issues.each_value { |items| items << { code: :unrecognized_environment_section, line: number } } unless section
31
+ elsif section && HEADERS.fetch(section).match?(line)
32
+ header = true
33
+ contexts.last[:headers] += 1
34
+ elsif line.empty? || /\A[-=]+\z/.match?(line)
35
+ next
36
+ elsif section && header
37
+ record = section == :temperature ? temperature(line, issues[section]) : hardware(line, section, issues[section])
38
+ if record
39
+ rows[section] << record
40
+ contexts.last[:rows] += 1
41
+ else
42
+ issues[section] << { code: :unrecognized_environment_row, line: number }
43
+ end
44
+ elsif section
45
+ issues[section] << { code: :environment_header_missing, line: number }
46
+ else
47
+ issues.each_value { |items| items << { code: :unrecognized_environment_row, line: number } }
48
+ end
49
+ end
50
+ contexts.each do |context|
51
+ if context[:rows].zero? || context[:headers] != 1
52
+ issues[context[:section]] << { code: :incomplete_environment_section }
53
+ end
54
+ end
55
+ CtxParser::ResultSet.new(results: rows.to_h do |dimension, records|
56
+ identity = dimension == :temperature ? [ :component ] : %i[component kind]
57
+ [ dimension, build_result(records, complete: complete, recognized: records.any?, issues: issues[dimension], identity: identity) ]
58
+ end)
59
+ end
60
+
61
+ # 保留设备给出的范围,不推测边界相等时是否触发告警。
62
+ def temperature(line, issues)
63
+ match = /\A(\S+)\s+(\S+)\s+(#{NUMBER})\s*-\s*(#{NUMBER})\z/.match(line)
64
+ return unless match
65
+ component, raw, low, high = match.captures
66
+ value = /\A#{NUMBER}\z/.match?(raw) ? Float(raw, exception: false) : nil
67
+ low, high = Float(low), Float(high)
68
+ valid = value&.finite? && low.finite? && high.finite? && low <= high
69
+ issues << { code: :invalid_temperature_measurement, component: component } unless valid
70
+ { component: component, value: valid ? value : nil, low_alarm: low.finite? ? low : nil,
71
+ high_alarm: high.finite? ? high : nil, high_comparison: nil, status: :unknown }
72
+ end
73
+
74
+ # 仅明确状态词才能证明风扇和电源正常。
75
+ def hardware(line, dimension, issues)
76
+ match = /\A(\S+)\s+(\S+(?:\s+\S+)*)\z/.match(line)
77
+ return unless match
78
+ component, text = match.captures
79
+ state = STATES.fetch(text.downcase, :unknown)
80
+ issues << { code: :unrecognized_component_state, component: component } if state == :unknown
81
+ if dimension == :power
82
+ { component: component, kind: :power_supply, status: state, present: state == :absent ? false : (state == :unknown ? nil : true) }
83
+ else
84
+ { component: component, kind: :fan, status: state }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require_relative "../../parsers/base"
5
+
6
+ module CtxChecker
7
+ module Hillstone
8
+ module Parsers
9
+ # StoneOS 分别展示物理、管理、链路与 IP 协议状态。
10
+ class ShowInterface < CtxChecker::Parsers::Base
11
+ NAME = /(?:x?ethernet|cellular)\d+(?:\/\d+)+(?:\.\d+)?|(?:ha|mgt|aggregate|redundant|bgroup|vswitchif|vlan|loopback|tunnel)\d+(?:\.\d+)?/i
12
+ HEADER = /\AInterface name\s+IP address\/mask\s+Zone name\s+H\s+A\s+L\s+P\s+MAC address(?:\s+Description)?\z/i
13
+ LEGEND = /\AH:physical state;\s*A:admin state;\s*L:link state;\s*P:protocol state;\s*U:up;\s*D:down(?:;\s*K:ha keep up)?(?:;\s*C:lacp down)?\z/i
14
+ ROW = /\A(#{NAME})\s+(\S+)\s+\S+\s+(\S)\s+(\S)\s+(\S)\s+(\S)\s+(?:[\da-f]{4}\.){2}[\da-f]{4}(?:\s+.*)?\z/i
15
+
16
+ private
17
+
18
+ # 识别无地址链路时,仍保留 IP 协议 down 状态。
19
+ def parse_output(text, complete:)
20
+ rows, issues, header = [], [], false
21
+ clean(text).each_line do |raw|
22
+ line = raw.strip
23
+ next if line.empty? || /\A[-=]+\z/.match?(line) || LEGEND.match?(line)
24
+
25
+ if HEADER.match?(line)
26
+ issues << { code: :duplicate_interface_header } if header
27
+ header = true
28
+ elsif header && (match = ROW.match(line))
29
+ row = interface_record(match, issues)
30
+ rows << row if row
31
+ else
32
+ issues << { code: :unrecognized_interface_line }
33
+ end
34
+ end
35
+ issues << { code: :interface_records_missing } if header && rows.empty?
36
+ interfaces = build_result(rows, complete: complete, recognized: header, issues: issues, identity: [ :name ])
37
+ groups = interfaces.records.select { |row| row[:kind] == :aggregate }.map do |row|
38
+ { group: row[:name], member: nil, member_status: :unknown, oper_status: row[:oper_status] }
39
+ end
40
+ missing_members = groups.map { |group| { code: :aggregation_members_unavailable, group: group[:group] } }
41
+ CtxParser::ResultSet.new(results: {
42
+ interfaces: interfaces,
43
+ aggregation: build_result(groups, complete: complete, recognized: header,
44
+ issues: interfaces.issues + missing_members, identity: %i[group member])
45
+ })
46
+ end
47
+
48
+ # 仅当前一单元格具有地址形式时,才解释相邻状态列。
49
+ def interface_record(match, issues)
50
+ name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :hillstone)
51
+ unless /\A[\da-f:.]+\/\d+\z/i.match?(match[2]) && IPAddr.new(match[2])
52
+ issues << { code: :invalid_interface_address, interface: name }
53
+ return
54
+ end
55
+ flags = match.captures[2, 4].map(&:upcase)
56
+ unless flags.all? { |flag| %w[U D C].include?(flag) }
57
+ issues << { code: :unknown_interface_state, interface: name }
58
+ end
59
+ if flags[2] == "U" && flags.first(2).include?("D")
60
+ issues << { code: :inconsistent_interface_state, interface: name }
61
+ end
62
+ { name: name, kind: CtxChecker::Parsers::InterfaceNames.kind(name),
63
+ admin_status: state(flags[1]), oper_status: state(flags[2]), protocol_status: state(flags[3]),
64
+ protocol_applicable: !%w[0.0.0.0/0 ::/0].include?(match[2]),
65
+ parent: name.include?(".") ? name.split(".").first : nil }
66
+ rescue IPAddr::InvalidAddressError, IPAddr::InvalidPrefixError
67
+ issues << { code: :invalid_interface_address, interface: name }
68
+ nil
69
+ end
70
+
71
+ # HA keep-up 不能证明接口可转发普通流量。
72
+ def state(flag)
73
+ case flag
74
+ when "U" then :up
75
+ when "D", "C" then :down
76
+ else :unknown
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module Hillstone
7
+ module Parsers
8
+ # StoneOS 仅列出在线接口的累计丢弃量,不提供 CRC 或错误总数。
9
+ class ShowInterfaceCounter < CtxChecker::Parsers::Base
10
+ HEADER = /\Ainterface\s+in frames\s+in octets\s+out frames\s+out octets\s+in discard\s+out discard\z/i
11
+ ROW = /\A([a-z][\w\/.-]*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\z/i
12
+
13
+ private
14
+
15
+ # 解释计数器前先校验列顺序;缺少采样时保留 nil。
16
+ def parse_output(text, complete:)
17
+ rows, issues, header, count = [], [], false, 0
18
+ clean(text).each_line do |raw|
19
+ line = raw.strip
20
+ next if line.empty? || /\A[-=]+\z/.match?(line)
21
+
22
+ if HEADER.match?(line)
23
+ issues << { code: :duplicate_counter_header } if header
24
+ header = true
25
+ elsif /\Ainterface\b/i.match?(line)
26
+ header = false
27
+ issues << { code: :unrecognized_counter_header }
28
+ elsif header && (match = ROW.match(line))
29
+ count += 1
30
+ name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :hillstone)
31
+ if CtxChecker::Parsers::InterfaceNames.kind(name) == :unknown
32
+ issues << { code: :unknown_counter_interface, interface: name }
33
+ end
34
+ unless match.captures[1, 4].all? { |value| /\A\d+\z/.match?(value) }
35
+ issues << { code: :invalid_traffic_counter, interface: name }
36
+ end
37
+ %i[in_discards out_discards].zip(match.captures.last(2)).each do |metric, value|
38
+ issues << { code: :invalid_counter_value, interface: name, metric: metric } unless /\A\d+\z/.match?(value)
39
+ rows << { interface: name, metric: metric, value: /\A\d+\z/.match?(value) ? Integer(value, 10) : nil,
40
+ discontinuity: nil }
41
+ end
42
+ else
43
+ issues << { code: :unrecognized_counter_line }
44
+ end
45
+ end
46
+ issues << { code: :empty_counter_table } if header && count.zero?
47
+ build_result(rows, complete: complete, recognized: header || rows.any?, issues: issues,
48
+ identity: %i[interface metric])
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module Hillstone
7
+ module Parsers
8
+ # BGP 配置为空属于明确证据,但不能证明所有 VRouter 均健康。
9
+ class ShowIpBgpSummary < CtxChecker::Parsers::Base
10
+ private
11
+
12
+ # 仅支持已观察到的 StoneOS 格式,不假设使用 Cisco 表格语法。
13
+ def parse_output(text, complete:)
14
+ lines = clean(text).lines.map(&:strip).reject(&:empty?)
15
+ recognized = lines == [ "No IPv4 neighbor is configured" ]
16
+ issues = [ { code: recognized ? :protocol_not_configured : :unrecognized_bgp_output } ]
17
+ build_result([], complete: complete, recognized: recognized, issues: issues)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module Hillstone
7
+ module Parsers
8
+ # StoneOS 的 OSPF 正文可能为空,但这不能证明邻居或进程覆盖完整。
9
+ class ShowIpOspfNeighbor < CtxChecker::Parsers::Base
10
+ private
11
+
12
+ # 在取得真实或文档明确的邻居表前保留未知状态。
13
+ def parse_output(text, complete:)
14
+ source = clean(text)
15
+ issue = source.strip.empty? ? :ospf_neighbor_state_unavailable : :unrecognized_ospf_output
16
+ build_result([], complete: complete, recognized: false, issues: [ { code: issue } ])
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module Hillstone
7
+ module Parsers
8
+ # 使用当前总量计算使用率,不将历史曲线分桶作为当前采样。
9
+ class ShowMemory < CtxChecker::Parsers::Base
10
+ HISTORY_SAMPLES = { "60 seconds" => 60, "60 minutes" => 60, "24 hours" => 24, "7 days" => 168 }.freeze
11
+
12
+ private
13
+
14
+ # 检查历史数据结构,但仅保留当前物理内存池的测量值。
15
+ def parse_output(text, complete:)
16
+ percentages, totals, histories = [], [], []
17
+ header_count, history = 0, nil
18
+ clean(text).each_line.with_index(1) do |raw, number|
19
+ line = raw.strip
20
+ if (match = /\AThe percentage of memory utilization:\s*(\S+)\s*%\z/i.match(line))
21
+ percentages << percentage(match[1])
22
+ history = nil
23
+ elsif /\Atotal\(KB\)\s+used\(KB\)\s+free\(KB\)\z/i.match?(line)
24
+ header_count += 1
25
+ history = nil
26
+ elsif (match = /\AMemory utilization in last (60 seconds|60 minutes|24 hours|7 days)\(%\):\z/i.match(line))
27
+ history = { section: match[1].downcase, indices: [], header: 0 }
28
+ histories << history
29
+ elsif history && line == "memoryutil"
30
+ history[:header] += 1
31
+ elsif history && (match = /\A(\d+)\s*:\s*(\S+)\z/.match(line))
32
+ history[:indices] << match[1].to_i
33
+ @issues << { code: :invalid_memory_history_value, section: history[:section] } unless percentage(match[2])
34
+ elsif !history && header_count.positive? && (match = /\A(\d+)\s+(\d+)\s+(\d+)\z/.match(line))
35
+ totals << match.captures.map(&:to_i)
36
+ elsif !line.empty?
37
+ @issues << { code: :unrecognized_memory_line, line: number }
38
+ end
39
+ end
40
+ validate_histories(histories)
41
+ row = current_record(percentages, totals, header_count)
42
+ build_result(row ? [ row ] : [], complete: complete, recognized: percentages.any? || header_count.positive?, identity: %i[component scope])
43
+ end
44
+
45
+ # 仅接受有限且在设备合理范围内的百分比。
46
+ def percentage(text)
47
+ return unless /\A\d+(?:\.\d+)?\z/.match?(text)
48
+ value = Float(text, exception: false)
49
+ value if value&.finite? && value.between?(0, 100)
50
+ end
51
+
52
+ # 重复、缺失或截断的时间桶不能掩盖证据丢失。
53
+ def validate_histories(histories)
54
+ sections = histories.map { |history| history[:section] }
55
+ missing = HISTORY_SAMPLES.keys - sections
56
+ @issues << { code: :missing_memory_history_sections, count: missing.size } if histories.any? && missing.any?
57
+ histories.each do |history|
58
+ expected = (0...HISTORY_SAMPLES.fetch(history[:section])).to_a
59
+ unless history[:header] == 1 && history[:indices].sort == expected
60
+ @issues << { code: :incomplete_memory_history, section: history[:section] }
61
+ end
62
+ end
63
+ if sections.uniq.size != histories.size
64
+ @issues << { code: :duplicate_memory_history }
65
+ end
66
+ end
67
+
68
+ # 整数汇总百分比可因截断,与已校验的总量及使用量比值相差不足一个百分点。
69
+ def current_record(percentages, totals, header_count)
70
+ unless totals.one? && percentages.size == 1 && header_count == 1
71
+ @issues << { code: :missing_or_ambiguous_memory_sample }
72
+ return
73
+ end
74
+ total, used, free = totals.first
75
+ valid = total.positive? && used + free == total && percentages.first
76
+ utilization = total.positive? ? used.fdiv(total) * 100 : nil
77
+ valid &&= utilization && (utilization - percentages.first).abs < 1
78
+ @issues << { code: :inconsistent_memory_totals } unless valid
79
+ { component: "system", scope: "physical", total_bytes: total * 1024, used_bytes: used * 1024,
80
+ free_bytes: free * 1024, utilization: valid ? utilization : nil, basis: valid ? :used_total : :unknown }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end