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