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,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "observation"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
# 单条检查结果包含原因说明;严重级别不能代替实际结果状态。
|
|
7
|
+
Finding = Data.define(:id, :dimension, :entity, :status, :severity, :reason, :actual, :expected) do
|
|
8
|
+
# 独立校验并冻结检查结果及其说明,不由严重级别推断状态。
|
|
9
|
+
def initialize(id:, dimension:, entity: nil, status:, severity: :info, reason:, actual: nil, expected: nil)
|
|
10
|
+
raise ArgumentError, "Invalid finding dimension" unless Observation::NAMES.include?(dimension) || dimension == :collection
|
|
11
|
+
raise ArgumentError, "Invalid finding status" unless %i[pass fail unknown not_applicable].include?(status)
|
|
12
|
+
raise ArgumentError, "Invalid finding severity" unless %i[info warning critical].include?(severity)
|
|
13
|
+
unless id.is_a?(String) && /\A[a-z][a-z0-9_.]*\z/.match?(id) && reason.is_a?(Symbol) && /\A[a-z][a-z0-9_]*\z/.match?(reason.to_s)
|
|
14
|
+
raise ArgumentError, "Findings require an ID and reason code"
|
|
15
|
+
end
|
|
16
|
+
super(**CtxParser::Immutable.copy(id: id, dimension: dimension, entity: entity, status: status, severity: severity, reason: reason, actual: actual, expected: expected))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "../../parsers/interface_names"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module H3c
|
|
8
|
+
module Parsers
|
|
9
|
+
# 入站、出站查询使用相同包计数表;仅 Err 列表示错误计数。
|
|
10
|
+
module CountersTable
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# 按来源标签归一化支持的接口或设备计数器。
|
|
14
|
+
def parse_counters(text, complete:, metric:)
|
|
15
|
+
source = clean(text)
|
|
16
|
+
rows, sections, section = [], [], nil
|
|
17
|
+
source.each_line do |raw|
|
|
18
|
+
line = raw.strip
|
|
19
|
+
if /\AInterface\s+Total\s*\(pkts?\)\s+Broadcast\s*\(pkts?\)\s+Multicast\s*\(pkts?\)\s+Err\s*\(pkts?\)\z/i.match?(line)
|
|
20
|
+
sections << (section = { count: 0 })
|
|
21
|
+
elsif (match = /\A([A-Za-z][A-Za-z-]*\d+(?:[\/.]\d+)*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\z/.match(line))
|
|
22
|
+
unless section
|
|
23
|
+
@issues << { code: :missing_counter_header }
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :h3c)
|
|
27
|
+
traffic = match.captures[1, 3]
|
|
28
|
+
unless traffic.all? { |value| /\A(?:\d+|Overflow|--)\z/i.match?(value) }
|
|
29
|
+
@issues << { code: :invalid_traffic_counter, interface: name }
|
|
30
|
+
end
|
|
31
|
+
rows << { interface: name, metric: metric, value: error_count(match[5], name, metric), discontinuity: nil }
|
|
32
|
+
section[:count] += 1
|
|
33
|
+
elsif !line.empty? && !/\A(?:-+|Overflow:\s*More than \d+ (?:decimal )?digits.*|--:\s*Not supported\.?)\z/i.match?(line)
|
|
34
|
+
@issues << { code: :unrecognized_counter_line }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
@issues << { code: :empty_counter_table } if sections.any? { |entry| entry[:count].zero? }
|
|
38
|
+
build_result(rows, complete: complete, recognized: sections.any? && rows.any?, identity: %i[interface metric])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 提取错误计数器,不将缺少证据替换为零。
|
|
42
|
+
def error_count(value, interface, metric)
|
|
43
|
+
return value.to_i if /\A\d+\z/.match?(value)
|
|
44
|
+
|
|
45
|
+
code = case value.downcase
|
|
46
|
+
when "overflow" then :counter_overflow
|
|
47
|
+
when "--" then :counter_unavailable
|
|
48
|
+
else :invalid_counter_value
|
|
49
|
+
end
|
|
50
|
+
@issues << { code: code, interface: interface, metric: metric }
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module H3c
|
|
7
|
+
module Parsers
|
|
8
|
+
# 保留当前告警身份与严重级别,不复制设备自由文本消息。
|
|
9
|
+
class DisplayAlarmActive < CtxChecker::Parsers::Base
|
|
10
|
+
SEVERITIES = { "critical" => :critical, "major" => :critical, "minor" => :warning,
|
|
11
|
+
"error" => :critical, "warning" => :warning, "notice" => :info, "info" => :info }.freeze
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# 解析 display alarm active,保留不完整或不支持的证据。
|
|
16
|
+
def parse_output(text, complete:)
|
|
17
|
+
source = clean(text)
|
|
18
|
+
rows, header, empty = [], false, false
|
|
19
|
+
source.each_line do |raw|
|
|
20
|
+
line = raw.strip
|
|
21
|
+
if /\ASeq\s+Level\s+Time\s+Info\z/i.match?(line)
|
|
22
|
+
header = true
|
|
23
|
+
elsif (match = /\A(\d+)\s+(\S+)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+.+\z/.match(line))
|
|
24
|
+
severity = SEVERITIES.fetch(match[2].downcase, :unknown)
|
|
25
|
+
@issues << { code: :unrecognized_alarm_severity } if severity == :unknown
|
|
26
|
+
rows << { component: "system", alarm_id: match[1], severity: severity, active: true, count: 1 }
|
|
27
|
+
elsif /\ANo active alarms\.?\z/i.match?(line)
|
|
28
|
+
empty = true
|
|
29
|
+
elsif !line.empty? && !/\A-+\z/.match?(line)
|
|
30
|
+
@issues << { code: :unrecognized_alarm_line }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
if empty && rows.empty?
|
|
34
|
+
rows << { component: "system", alarm_id: "summary", severity: :info, active: false, count: 0 }
|
|
35
|
+
elsif empty
|
|
36
|
+
@issues << { code: :contradictory_alarm_summary }
|
|
37
|
+
end
|
|
38
|
+
build_result(rows, complete: complete, recognized: (header && rows.any?) || empty, identity: %i[component alarm_id])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module H3c
|
|
7
|
+
module Parsers
|
|
8
|
+
# 公网 IPv4 单播查询具有明确的地址族作用域。
|
|
9
|
+
class DisplayBgpPeer < CtxChecker::Parsers::Base
|
|
10
|
+
STATES = %w[idle connect active opensent openconfirm established idle(admin)].freeze
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 解析 display bgp peer,保留不完整或不支持的证据。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
text = clean(text)
|
|
17
|
+
rows, issues, total, header = [], [], nil, false
|
|
18
|
+
text.each_line do |raw|
|
|
19
|
+
line = raw.strip
|
|
20
|
+
if (match = /\ATotal number of peers:\s*(\d+)\z/i.match(line))
|
|
21
|
+
issues << { code: :duplicate_peer_count } unless total.nil?
|
|
22
|
+
total = Integer(match[1], 10)
|
|
23
|
+
elsif /\APeer\s+AS\s+MsgRcvd\s+MsgSent\s+OutQ\s+PrefRcv\s+Up\/Down\s+State\z/i.match?(line)
|
|
24
|
+
header = true
|
|
25
|
+
elsif (match = /\A\*?\s*([\d.]+)\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+\S+\s+(.+)\z/.match(line))
|
|
26
|
+
state = match[4].downcase.delete(" ")
|
|
27
|
+
if CtxParser::Values.valid_host?(match[1]) && STATES.include?(state) && (1..4_294_967_295).cover?(match[2].to_i)
|
|
28
|
+
rows << { vrf: "default", afi_safi: "ipv4_unicast", peer: match[1], state: state == "idle(admin)" ? :shutdown : state.to_sym,
|
|
29
|
+
received_prefixes: match[3].to_i, remote_as: match[2].to_i }
|
|
30
|
+
else
|
|
31
|
+
issues << { code: :invalid_bgp_peer }
|
|
32
|
+
end
|
|
33
|
+
elsif !line.empty? && !line.match?(/\A(?:BGP local router ID:|Local AS number:|\* - Dynamically created peer)/i)
|
|
34
|
+
issues << { code: :unrecognized_bgp_line }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
issues << { code: :count_mismatch, expected: total, observed: rows.size } if total && total != rows.size
|
|
38
|
+
build_result(rows, complete: complete, recognized: header && (rows.any? || total == 0), issues: issues,
|
|
39
|
+
identity: %i[vrf afi_safi peer], covered_scopes: [ { vrf: "default", afi_safi: "ipv4_unicast" } ])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../../parsers/base"
|
|
6
|
+
|
|
7
|
+
module CtxChecker
|
|
8
|
+
module H3c
|
|
9
|
+
module Parsers
|
|
10
|
+
# 转换 UTC 需要明确的数字时区偏移,不猜测时区名称。
|
|
11
|
+
class DisplayClock < CtxChecker::Parsers::Base
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 解析 display clock,保留不完整或不支持的证据。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
source = clean(text)
|
|
17
|
+
timestamps, zones, summer = [], [], false
|
|
18
|
+
source.each_line do |raw|
|
|
19
|
+
line = raw.strip
|
|
20
|
+
if (match = /\A(\d{1,2}:\d{2}:\d{2}(?:\.\d+)?)\s+(\S+)\s+[A-Za-z]{3}\s+(\d{2})\/(\d{2})\/(\d{4})\z/.match(line))
|
|
21
|
+
timestamps << match.captures
|
|
22
|
+
elsif (match = /\ATime Zone\s*:\s*(\S+)\s+(add|minus)\s+(\d{2}):(\d{2}):(\d{2})\z/i.match(line))
|
|
23
|
+
zones << match.captures
|
|
24
|
+
elsif /\ASummer Time\s*:/i.match?(line)
|
|
25
|
+
summer = true
|
|
26
|
+
elsif !line.empty?
|
|
27
|
+
@issues << { code: :unrecognized_clock_line }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
if timestamps.size == 1
|
|
31
|
+
instant = normalize_time(timestamps.first, zones, summer)
|
|
32
|
+
rows = [ { component: "system", time: instant, source: nil, synchronized: nil } ]
|
|
33
|
+
else
|
|
34
|
+
rows = []
|
|
35
|
+
@issues << { code: :ambiguous_clock_time, count: timestamps.size }
|
|
36
|
+
end
|
|
37
|
+
build_result(rows, complete: complete, recognized: timestamps.any?, identity: [ :component ])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 归一化时间戳,同时保留显式声明的时区语义。
|
|
41
|
+
def normalize_time(parts, zones, summer)
|
|
42
|
+
clock, zone, month, day, year = parts
|
|
43
|
+
if summer || zones.size > 1
|
|
44
|
+
@issues << { code: summer ? :ambiguous_daylight_saving_time : :duplicate_clock_timezone }
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
offset = zone.casecmp?("UTC") && zones.empty? ? 0 : zone_offset(zone, zones.first)
|
|
48
|
+
if offset.nil?
|
|
49
|
+
@issues << { code: :unknown_clock_timezone }
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
hours, minutes, fraction = clock.split(":")
|
|
53
|
+
hours, minutes, seconds = hours.to_i, minutes.to_i, Rational(fraction)
|
|
54
|
+
unless Date.valid_date?(year.to_i, month.to_i, day.to_i) && (0...24).cover?(hours) && (0...60).cover?(minutes) && (0...60).cover?(seconds)
|
|
55
|
+
@issues << { code: :invalid_clock_time }
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
(Time.utc(year.to_i, month.to_i, day.to_i, hours, minutes, seconds) - offset).iso8601(3)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 解析支持的时区偏移,不猜测有歧义的时区标签。
|
|
62
|
+
def zone_offset(zone, settings)
|
|
63
|
+
return unless settings && settings.first == zone
|
|
64
|
+
_name, sign, hours, minutes, seconds = settings
|
|
65
|
+
return unless (0...24).cover?(hours.to_i) && (0...60).cover?(minutes.to_i) && (0...60).cover?(seconds.to_i)
|
|
66
|
+
|
|
67
|
+
(hours.to_i * 3600 + minutes.to_i * 60 + seconds.to_i) * (sign.casecmp?("minus") ? -1 : 1)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "counters_table"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module H3c
|
|
7
|
+
module Parsers
|
|
8
|
+
# 解析接收错误包数量,避免把正常流量计入错误。
|
|
9
|
+
class DisplayCountersInboundInterface < CtxChecker::Parsers::Base
|
|
10
|
+
include CountersTable
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 解析 display counters inbound interface,保留不完整或不支持的证据。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
parse_counters(text, complete: complete, metric: :in_errors)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "counters_table"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module H3c
|
|
7
|
+
module Parsers
|
|
8
|
+
# 解析发送错误包数量;CRC 与丢弃量需要独立证据。
|
|
9
|
+
class DisplayCountersOutboundInterface < CtxChecker::Parsers::Base
|
|
10
|
+
include CountersTable
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 解析 display counters outbound interface,保留不完整或不支持的证据。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
parse_counters(text, complete: complete, metric: :out_errors)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "system_values"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module H3c
|
|
8
|
+
module Parsers
|
|
9
|
+
# 保留每个 CPU 及其采样周期,不对机框成员取平均。
|
|
10
|
+
class DisplayCpuUsage < CtxChecker::Parsers::Base
|
|
11
|
+
include SystemValues
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# 解析 display cpu usage,保留不完整或不支持的证据。
|
|
16
|
+
def parse_output(text, complete:)
|
|
17
|
+
source = clean(text)
|
|
18
|
+
rows, components, current, table = [], [], nil, nil
|
|
19
|
+
source.each_line do |raw|
|
|
20
|
+
line = raw.strip
|
|
21
|
+
if (match = /\A((?:Chassis \d+ )?Slot \d+(?: CPU \d+(?: Core \d+)?)?) CPU usage:\z/i.match(line))
|
|
22
|
+
current = scope_name(match[1])
|
|
23
|
+
components << current
|
|
24
|
+
elsif /\A(?:Chassis\s+)?Slot\s+CPU\s+Last 5 sec\s+Last 1 min\s+Last 5 min\z/i.match?(line)
|
|
25
|
+
table = line.start_with?("Chassis") ? 3 : 2
|
|
26
|
+
elsif table && (match = /\A(\d+(?:\s+\d+){1,2})\s+(#{NUMBER})%?\s+(#{NUMBER})%?\s+(#{NUMBER})%?\z/i.match(line))
|
|
27
|
+
identifiers = match[1].split
|
|
28
|
+
if identifiers.size == table
|
|
29
|
+
current = identifiers.zip(table == 3 ? %w[chassis slot cpu] : %w[slot cpu]).map { |id, kind| "#{kind} #{id}" }.join(" ")
|
|
30
|
+
components << current
|
|
31
|
+
rows.concat([ 5, 60, 300 ].zip(match.captures.drop(1)).map { |seconds, value| sample(current, seconds, value) })
|
|
32
|
+
else
|
|
33
|
+
@issues << { code: :missing_cpu_scope }
|
|
34
|
+
end
|
|
35
|
+
elsif (match = /\A(#{NUMBER})%? in last (5 seconds|1 minute|5 minutes)\z/i.match(line))
|
|
36
|
+
if current
|
|
37
|
+
seconds = { "5 seconds" => 5, "1 minute" => 60, "5 minutes" => 300 }.fetch(match[2].downcase)
|
|
38
|
+
rows << sample(current, seconds, match[1])
|
|
39
|
+
else
|
|
40
|
+
@issues << { code: :missing_cpu_scope }
|
|
41
|
+
end
|
|
42
|
+
elsif line.empty?
|
|
43
|
+
next
|
|
44
|
+
else
|
|
45
|
+
@issues << { code: :unrecognized_cpu_line }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
components.each do |component|
|
|
49
|
+
missing = [ 5, 60, 300 ] - rows.select { |row| row[:component] == component }.map { |row| row[:window_seconds] }
|
|
50
|
+
@issues << { code: :missing_cpu_windows, component: component, count: missing.size } if missing.any?
|
|
51
|
+
end
|
|
52
|
+
build_result(rows, complete: complete, recognized: components.any?, identity: %i[component scope window_seconds])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 构建包含采样上下文的归一化测量记录。
|
|
56
|
+
def sample(component, seconds, value)
|
|
57
|
+
utilization = measurement(value, field: :utilization, component: component, percent: true)
|
|
58
|
+
@issues << { code: :missing_cpu_measurement, component: component, window_seconds: seconds } if utilization.nil?
|
|
59
|
+
{ component: component, scope: "system", window_seconds: seconds, utilization: utilization }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "system_values"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module H3c
|
|
8
|
+
module Parsers
|
|
9
|
+
# 传感器阈值取自各板卡,不使用统一猜测的温度限制。
|
|
10
|
+
class DisplayEnvironment < CtxChecker::Parsers::Base
|
|
11
|
+
include SystemValues
|
|
12
|
+
SENSOR_ROW = /\A(?:(\d+)\s+)?(\d+|Vent)\s+((?:hotspot|inflow|outflow)\s+\d+(?:\(sub\d+\))?)\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})(?:\s+#{NUMBER})?\z/i
|
|
13
|
+
HOTSPOT_ROW = /\A(\d+)\s+hotspot(\d+)\s+[A-Za-z][A-Za-z0-9_-]*(?:[ \t]+\d+-\d+)?\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})\s+(#{NUMBER})\s+#{NUMBER}\z/i
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# 解析 display environment,保留不完整或不支持的证据。
|
|
18
|
+
def parse_output(text, complete:)
|
|
19
|
+
source = clean(text)
|
|
20
|
+
rows, chassis, header, auxiliary, contexts, context = [], nil, nil, nil, [], nil
|
|
21
|
+
source.each_line do |raw|
|
|
22
|
+
line = raw.strip
|
|
23
|
+
if (match = /\AChassis (\d+):\z/i.match(line))
|
|
24
|
+
chassis = "chassis #{match[1]} "
|
|
25
|
+
contexts << (context = { component: chassis.strip, count: 0 })
|
|
26
|
+
header, auxiliary = nil, nil
|
|
27
|
+
elsif (match = /\A(Chassis\s+)?Slot\s+Sensor\s+Temperature\s+Lower(?:Limit)?\s+Warning(?:Limit)?\s+Alarm(?:Limit)?(?:\s+Shutdown(?:Limit)?)?\z/i.match(line))
|
|
28
|
+
header, auxiliary = match[1] ? :chassis : :slot, nil
|
|
29
|
+
elsif /\ASlotNo\s+Temperature\s+Lower limit\s+Upper limit\z/i.match?(line)
|
|
30
|
+
header, auxiliary = :legacy, nil
|
|
31
|
+
elsif /\ASlot\s+Hotspot\s+(?:Sensor|Name)\s+Temperature\s+Lower\s+Warning\s+Alarm\s+Shutdown\z/i.match?(line)
|
|
32
|
+
header, auxiliary = :hotspot, nil
|
|
33
|
+
elsif header == :hotspot && (match = HOTSPOT_ROW.match(line))
|
|
34
|
+
component = "#{chassis}slot #{match[1]}/hotspot #{match[2]}"
|
|
35
|
+
values = match.captures.drop(2).map { |value| measurement(value, field: :temperature, component: component) }
|
|
36
|
+
rows << temperature_record(component, values[0], low_warning: values[1], high_warning: values[2], high_alarm: values[3])
|
|
37
|
+
context[:count] += 1 if context
|
|
38
|
+
elsif %i[chassis slot].include?(header) && (match = SENSOR_ROW.match(line)) && (header == :chassis) == !match[1].nil?
|
|
39
|
+
prefix = match[1] ? "chassis #{match[1]} " : chassis
|
|
40
|
+
component = "#{prefix}#{match[2].casecmp?("vent") ? 'vent' : "slot #{match[2]}"}/#{scope_name(match[3])}"
|
|
41
|
+
values = match.captures.drop(3).map { |value| measurement(value, field: :temperature, component: component) }
|
|
42
|
+
rows << temperature_record(component, values[0], low_warning: values[1], high_warning: values[2], high_alarm: values[3])
|
|
43
|
+
context[:count] += 1 if context
|
|
44
|
+
elsif header == :legacy && (match = /\A(\d+)\s+(#{NUMBER})\s+#{NUMBER}\s+#{NUMBER}\z/i.match(line))
|
|
45
|
+
component = "#{chassis}slot #{match[1]}"
|
|
46
|
+
rows << temperature_record(component, measurement(match[2], field: :temperature, component: component))
|
|
47
|
+
# 旧版表列不能区分警告与告警级别。
|
|
48
|
+
@issues << { code: :temperature_threshold_semantics_unknown, component: component }
|
|
49
|
+
context[:count] += 1 if context
|
|
50
|
+
elsif (match = /\A(Fan|Power) temperature information:\z/i.match(line))
|
|
51
|
+
header, auxiliary = nil, match[1].downcase
|
|
52
|
+
elsif chassis && auxiliary && (match = /\A(Fan|Power) (\d+):\s*(#{NUMBER})\z/i.match(line)) && match[1].casecmp?(auxiliary)
|
|
53
|
+
component = "#{chassis.strip}/#{auxiliary} #{match[2]}"
|
|
54
|
+
rows << temperature_record(component, measurement(match[3], field: :temperature, component: component))
|
|
55
|
+
context[:count] += 1 if context
|
|
56
|
+
elsif !line.empty? && !/\A(?:-+|System temperature information \(degree centigrade\):)\z/i.match?(line)
|
|
57
|
+
@issues << { code: :unrecognized_temperature_line }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
contexts.each do |entry|
|
|
61
|
+
@issues << { code: :missing_temperature_scope_records, component: entry[:component] } if entry[:count].zero?
|
|
62
|
+
end
|
|
63
|
+
build_result(rows, complete: complete, recognized: rows.any?, identity: [ :component ])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 能读取但无设备阈值的传感器仅提供测量值,不据此判定健康。
|
|
67
|
+
def temperature_record(component, value, **limits)
|
|
68
|
+
@issues << { code: :missing_temperature_measurement, component: component } if value.nil?
|
|
69
|
+
{ component: component, value: value, high_comparison: :gte, status: :unknown, **limits }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "system_values"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module H3c
|
|
8
|
+
module Parsers
|
|
9
|
+
# 风扇托盘保留机框与槽位身份;空槽位不直接判为故障。
|
|
10
|
+
class DisplayFan < CtxChecker::Parsers::Base
|
|
11
|
+
include SystemValues
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# 解析 display fan,保留不完整或不支持的证据。
|
|
16
|
+
def parse_output(text, complete:)
|
|
17
|
+
source = clean(text)
|
|
18
|
+
rows, location, current, table, seen, contexts, context, fanless = [], "system", nil, false, {}, [], nil, {}
|
|
19
|
+
source.each_line do |raw|
|
|
20
|
+
line = raw.strip
|
|
21
|
+
if (match = /\A((?:Chassis \d+ )?Slot \d+|Chassis \d+)\s*:\z/i.match(line))
|
|
22
|
+
location, current, table = scope_name(match[1]), nil, false
|
|
23
|
+
contexts << (context = { component: location, count: 0 })
|
|
24
|
+
elsif context && line.casecmp?("Fan-less")
|
|
25
|
+
row = { component: "#{location}/fan", kind: :fan, status: :absent }
|
|
26
|
+
rows << row
|
|
27
|
+
fanless[location] = row
|
|
28
|
+
context[:count] += 1
|
|
29
|
+
current, table = nil, false
|
|
30
|
+
elsif (match = /\AFan(?: Frame)?\s+(\d+)(?:\s*:\s*|\s+)(?:State\s*:\s*(\S+))?\z/i.match(line))
|
|
31
|
+
current = { component: "#{location}/fan #{match[1]}", kind: :fan, status: :unknown }
|
|
32
|
+
rows << current
|
|
33
|
+
context[:count] += 1 if context
|
|
34
|
+
if match[2]
|
|
35
|
+
current[:status] = component_state(match[2], component: current[:component])
|
|
36
|
+
seen[current.object_id] = true
|
|
37
|
+
end
|
|
38
|
+
elsif (match = /\AState\s*:\s*(\S+)\z/i.match(line)) && current
|
|
39
|
+
if seen[current.object_id]
|
|
40
|
+
current[:status] = :unknown
|
|
41
|
+
@issues << { code: :duplicate_component_state, component: current[:component] }
|
|
42
|
+
else
|
|
43
|
+
current[:status] = component_state(match[1], component: current[:component])
|
|
44
|
+
end
|
|
45
|
+
seen[current.object_id] = true
|
|
46
|
+
elsif /\AFan No\.\s+State\s+Speed(?:\s+Description)?\z/i.match?(line)
|
|
47
|
+
table = true
|
|
48
|
+
elsif table && (match = /\A(\d+)\s+(\S+)\s+(?:\d+|--)(?:\s+\S+)?\z/.match(line))
|
|
49
|
+
component = "#{location}/fan #{match[1]}"
|
|
50
|
+
rows << { component: component, kind: :fan, status: component_state(match[2], component: component) }
|
|
51
|
+
context[:count] += 1 if context
|
|
52
|
+
elsif !line.empty? && !/\A(?:-+|(?:Prefer )?Airflow Direction:.*)\z/i.match?(line)
|
|
53
|
+
@issues << { code: :unrecognized_fan_line }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
contexts.each do |entry|
|
|
57
|
+
@issues << { code: :missing_fan_scope_records, component: entry[:component] } if entry[:count].zero?
|
|
58
|
+
end
|
|
59
|
+
# 同一作用域已有实际风扇托盘时,fanless 声明不能覆盖它。
|
|
60
|
+
fanless.each do |scope, row|
|
|
61
|
+
next unless rows.any? { |entry| entry[:component].start_with?("#{scope}/fan ") }
|
|
62
|
+
|
|
63
|
+
row[:status] = :unknown
|
|
64
|
+
@issues << { code: :conflicting_fan_presence, component: row[:component] }
|
|
65
|
+
end
|
|
66
|
+
@issues << { code: :missing_component_state } if rows.any? { |row| row[:status] == :unknown }
|
|
67
|
+
build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component kind])
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "../../parsers/interface_names"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module H3c
|
|
8
|
+
module Parsers
|
|
9
|
+
# Comware 接口块分别产生状态与计数器观测。
|
|
10
|
+
class DisplayInterface < CtxChecker::Parsers::Base
|
|
11
|
+
INTERFACE = /(?:[A-Za-z][A-Za-z-]*[\d\/.:]+|NULL\d+)/
|
|
12
|
+
COUNTERS = {
|
|
13
|
+
crc_errors: /(?<![\w.+-])(\d+) CRC\b/i,
|
|
14
|
+
in_discards: /^[ \t]*(?:Input:[ \t]*\d+ packets?|\d+ packets? input),[ \t]*\d+ bytes?,[ \t]*(\d+) drops?[ \t]*$/i,
|
|
15
|
+
out_discards: /^[ \t]*(?:Output:[ \t]*\d+ packets?|\d+ packets? output),[ \t]*\d+ bytes?,[ \t]*(\d+) drops?[ \t]*$/i
|
|
16
|
+
}.freeze
|
|
17
|
+
DISCARD_LABELS = { in_discards: /^[ \t]*(?:Input:|[^,\n]+ packets? input,).*\bdrops?\b/i, out_discards: /^[ \t]*(?:Output:|[^,\n]+ packets? output,).*\bdrops?\b/i }.freeze
|
|
18
|
+
DETAILS = /\A(?:Current state:|Line protocol (?:current )?state:|Description:|Bandwidth:|Maximum |Allow jumbo|Broadcast |Multicast |Unicast |Known[ -]unicast |Internet |IP(?:v[46])? |Loopback |Media type |Port |[\w.-]+-speed mode|Link speed |Flow-control |The Maximum |Last |Current system time:|Peak |Input\b|Output\b|PVID:|MDI type:|Tagged VLANs:|UnTagged VLANs:|(?:\d+|-) (?:CRC|ignored|unicasts|aborts|lost carrier))/i
|
|
19
|
+
NON_METRIC_DETAILS = /\A(?:Trunk port encapsulation:\s*IEEE 802\.1q|Physical:\s*(?:InLoopBack|Loopback|Unknown)|Physical(?:\s+is|:)\s*NULL DEV(?:, baudrate:\s*\d+ kbps)?)\z/i
|
|
20
|
+
COUNTER_DETAILS = /\A(?:(?:\d+|-) (?:broadcasts,[ \t]*(?:\d+|-) multicasts(?:,[ \t]*(?:\d+|-) pauses)?|carrier,[ \t]*(?:\d+|-) no carrier|frame,[ \t]*(?:\d+|-) overruns,[ \t]*(?:\d+|-) aborts,[ \t]*(?:\d+|-) ignored,[ \t]*(?:\d+|-) parity errors)|\d+ packets? (?:input|output),[ \t]*\d+ bytes?,[ \t]*\d+ drops?)\z/i
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# 接口状态独立于各配置块提供的计数器进行投影。
|
|
25
|
+
def parse_output(text, complete:)
|
|
26
|
+
text = clean(text)
|
|
27
|
+
interfaces, counters, interface_issues, counter_issues = [], [], [], []
|
|
28
|
+
sections = text.split(/(?=^[ \t]*#{INTERFACE}(?:[ \t]+current state[ \t]*:.*)?[ \t]*$)/i)
|
|
29
|
+
sections.each do |section|
|
|
30
|
+
header = /\A[ \t]*(#{INTERFACE})(?:[ \t]+current state[ \t]*:[ \t]*([^\n]+))?[ \t]*\n/i.match(section)
|
|
31
|
+
next if section.strip.empty?
|
|
32
|
+
unless header
|
|
33
|
+
interface_issues << { code: :unrecognized_interface_block }
|
|
34
|
+
next
|
|
35
|
+
end
|
|
36
|
+
name = CtxChecker::Parsers::InterfaceNames.normalize(header[1], platform: :h3c)
|
|
37
|
+
unless known_details?(section)
|
|
38
|
+
interface_issues << { code: :unrecognized_interface_field, interface: name }
|
|
39
|
+
end
|
|
40
|
+
states = [ header[2], *section.scan(/^[ \t]*Current state\s*:\s*(.+)$/i).flatten ].compact
|
|
41
|
+
protocols = section.scan(/^[ \t]*Line protocol (?:current )?state\s*:\s*(.+)$/i).flatten
|
|
42
|
+
if states.size > 1 || protocols.size > 1
|
|
43
|
+
interface_issues << { code: :duplicate_interface_state, interface: name }
|
|
44
|
+
end
|
|
45
|
+
state = states.one? ? states.first : nil
|
|
46
|
+
protocol = protocols.one? ? protocols.first : nil
|
|
47
|
+
admin, oper = state_values(state)
|
|
48
|
+
protocol = status(protocol)
|
|
49
|
+
interface_issues << { code: :interface_state_missing, interface: name } if oper == :unknown || protocol == :unknown
|
|
50
|
+
interfaces << { name: name, kind: CtxChecker::Parsers::InterfaceNames.kind(name), admin_status: admin,
|
|
51
|
+
oper_status: oper, protocol_status: protocol, parent: name.include?(".") ? name.split(".").first : nil }
|
|
52
|
+
# Never 或相对时间都不能证明跨重启的计数连续性。
|
|
53
|
+
discontinuity = nil
|
|
54
|
+
section_counters = []
|
|
55
|
+
counter_source = section.lines.reject { |line| /\A\s*Description:/i.match?(line) }.join
|
|
56
|
+
COUNTERS.each do |metric, pattern|
|
|
57
|
+
values = counter_source.scan(pattern).flatten
|
|
58
|
+
if DISCARD_LABELS.key?(metric) && values.size != counter_source.scan(DISCARD_LABELS.fetch(metric)).size
|
|
59
|
+
counter_issues << { code: :invalid_counter_value, interface: name, metric: metric }
|
|
60
|
+
section_counters << { interface: name, metric: metric, value: nil, discontinuity: discontinuity }
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
if values.one?
|
|
64
|
+
section_counters << { interface: name, metric: metric, value: Integer(values.first, 10), discontinuity: discontinuity }
|
|
65
|
+
elsif values.any?
|
|
66
|
+
counter_issues << { code: :duplicate_counter, interface: name, metric: metric }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
if interfaces.last[:kind] == :physical
|
|
70
|
+
missing = %i[in_discards out_discards crc_errors] - section_counters.map { |row| row[:metric] }
|
|
71
|
+
missing.each { |metric| section_counters << { interface: name, metric: metric, value: nil, discontinuity: nil } }
|
|
72
|
+
counter_issues << { code: :missing_counter_metrics, interface: name, metrics: missing } if missing.any?
|
|
73
|
+
end
|
|
74
|
+
counters.concat(section_counters)
|
|
75
|
+
end
|
|
76
|
+
CtxParser::ResultSet.new(results: {
|
|
77
|
+
interfaces: build_result(interfaces, complete: complete, recognized: interfaces.any?, issues: interface_issues, identity: [ :name ]),
|
|
78
|
+
counters: build_result(counters, complete: complete, recognized: counters.any?, issues: counter_issues + interface_issues, identity: %i[interface metric])
|
|
79
|
+
})
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 换行 VLAN 列表仅在紧接前一列表时作为元数据处理。
|
|
83
|
+
def known_details?(section)
|
|
84
|
+
vlan_list = false
|
|
85
|
+
section.lines.drop(1).all? do |raw|
|
|
86
|
+
line = raw.strip
|
|
87
|
+
if (match = /\A(?:VLAN (?:Passing|permitted)|(?:Tagged|Untagged)\s+VLAN ID)\s*:\s*(.+)\z/i.match(line))
|
|
88
|
+
vlan_list = valid_vlan_list?(match[1])
|
|
89
|
+
elsif vlan_list && /\A\s+\d/.match?(raw)
|
|
90
|
+
valid_vlan_list?(line)
|
|
91
|
+
else
|
|
92
|
+
vlan_list = false
|
|
93
|
+
line.empty? || DETAILS.match?(line) || NON_METRIC_DETAILS.match?(line) || COUNTER_DETAILS.match?(line)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 校验 VLAN 元数据,避免任意数字尾行掩盖截断。
|
|
99
|
+
def valid_vlan_list?(value)
|
|
100
|
+
return true if value.casecmp?("none")
|
|
101
|
+
|
|
102
|
+
value.split(/,\s*/, -1).all? do |token|
|
|
103
|
+
match = /\A(\d+)(?:-(\d+))?(?:\(default vlan\))?\z/i.match(token)
|
|
104
|
+
next false unless match
|
|
105
|
+
first = match[1].to_i
|
|
106
|
+
last = (match[2] || match[1]).to_i
|
|
107
|
+
first.between?(1, 4094) && last.between?(first, 4094)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 区分主动管理关闭与已启用链路故障。
|
|
112
|
+
def state_values(value)
|
|
113
|
+
return [ :down, :down ] if value&.match?(/\AAdministratively\s+DOWN\b/i)
|
|
114
|
+
observed = status(value)
|
|
115
|
+
[ observed == :unknown ? :unknown : :up, observed ]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# 新出现或格式错误的状态文本明确保持未解析。
|
|
119
|
+
def status(value)
|
|
120
|
+
return :up if value&.match?(/\AUP(?:\s|\(|$)/i)
|
|
121
|
+
return :down if value&.match?(/\A(?:DOWN|Administratively\s+DOWN|Link-Flap DOWN|mac-address moving down|MAD ShutDown|OFP DOWN|Storm-Constrain|STP DOWN|M-LAG MAD Shutdown)(?:\s|\(|$)/i)
|
|
122
|
+
:unknown
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|