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,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module Checks
|
|
7
|
+
# 逐光通道使用模块自身阈值,避免猜测统一的 dBm 限制。
|
|
8
|
+
class Optics < Base
|
|
9
|
+
ALARMS = %i[high_alarm low_alarm high_warning low_warning].freeze
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# 按策略评估资源,同时保留测量值缺失的状态。
|
|
14
|
+
def assess(record)
|
|
15
|
+
entity = record.slice(:interface, :lane, :metric)
|
|
16
|
+
if inapplicable?(record)
|
|
17
|
+
return finding(:unknown, :required_optics_inapplicable, entity: entity) if @policy.required_optics?(record)
|
|
18
|
+
|
|
19
|
+
return finding(:not_applicable, :optics_not_applicable, entity: entity)
|
|
20
|
+
end
|
|
21
|
+
context = interface(record[:interface])
|
|
22
|
+
if context && record_complete?(context, @snapshot.observation(:interfaces)) &&
|
|
23
|
+
context[:admin_status] == :down && !@policy.required_interfaces.include?(record[:interface]) && !@policy.required_optics?(record)
|
|
24
|
+
return finding(:not_applicable, :administratively_down, entity: entity)
|
|
25
|
+
end
|
|
26
|
+
if (record[:present] == false || record[:dom_supported] == false) && !ALARMS.include?(record[:alarm])
|
|
27
|
+
return finding(:unknown, record[:present] == false ? :transceiver_absent : :dom_unavailable, entity: entity)
|
|
28
|
+
end
|
|
29
|
+
policy = @policy.optical_thresholds_for(record)
|
|
30
|
+
thresholds = policy ? record.merge(Observation::OPTICAL_THRESHOLDS.to_h { |key| [ key, policy[key] ] }) : record
|
|
31
|
+
measured = assess_thresholds(thresholds, entity)
|
|
32
|
+
alarm = record[:alarm]
|
|
33
|
+
explicit = if ALARMS.include?(alarm)
|
|
34
|
+
finding(:fail, :optical_alarm, entity: entity, actual: alarm, severity: alarm.to_s.end_with?("warning") ? :warning : :critical)
|
|
35
|
+
end
|
|
36
|
+
results = explicit ? [ explicit ] : [ measured ]
|
|
37
|
+
if explicit && measured.status == :fail && (policy || measured.severity == :critical && explicit.severity != :critical)
|
|
38
|
+
results << measured
|
|
39
|
+
end
|
|
40
|
+
if explicit && measured.reason == :invalid_optical_thresholds
|
|
41
|
+
results << measured
|
|
42
|
+
elsif !policy && disagreement?(record, measured)
|
|
43
|
+
results << finding(:unknown, :optical_alarm_threshold_disagreement, entity: entity,
|
|
44
|
+
actual: { value: record[:value], alarm: alarm }, expected: record.slice(*Observation::OPTICAL_THRESHOLDS))
|
|
45
|
+
end
|
|
46
|
+
results
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 判定目标缺失需要完整枚举;未编号光通道仍保留身份歧义。
|
|
50
|
+
def missing
|
|
51
|
+
fields = Observation::IDENTITIES.fetch(:optics)
|
|
52
|
+
identities = @observation.records.to_h { |row| [ row.values_at(*fields), true ] }
|
|
53
|
+
unnumbered = @observation.records.select { |row| row[:lane].nil? }.to_h { |row| [ row.values_at(:interface, :metric), true ] }
|
|
54
|
+
@policy.required_optics.filter_map do |identity|
|
|
55
|
+
next if identities.key?(identity.values_at(*fields))
|
|
56
|
+
|
|
57
|
+
ambiguous = unnumbered.key?(identity.values_at(:interface, :metric))
|
|
58
|
+
finding(ambiguous ? :unknown : missing_status, :required_optics_missing, entity: identity, severity: :critical)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 仅完整且无矛盾的设备证据可以豁免铜缆端口的光学检查。
|
|
63
|
+
def inapplicable?(record)
|
|
64
|
+
record[:applicable] == false && record_complete?(record, @observation) &&
|
|
65
|
+
record[:present] != true && record[:dom_supported] != true && record[:value].nil? &&
|
|
66
|
+
record.values_at(*Observation::OPTICAL_THRESHOLDS).all?(&:nil?) && !ALARMS.include?(record[:alarm])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 将支持的测量值与适用策略阈值比较。
|
|
70
|
+
def assess_thresholds(record, entity)
|
|
71
|
+
value = record[:value]
|
|
72
|
+
return finding(:unknown, :optical_value_unavailable, entity: entity) if value.nil? || record[:unit] != :dbm
|
|
73
|
+
|
|
74
|
+
bounds = record.values_at(*Observation::OPTICAL_THRESHOLDS)
|
|
75
|
+
unless bounds.compact.each_cons(2).all? { |left, right| left <= right }
|
|
76
|
+
return finding(:unknown, :invalid_optical_thresholds, entity: entity, actual: value)
|
|
77
|
+
end
|
|
78
|
+
severity = if bounds[0] && value < bounds[0] || bounds[3] && value > bounds[3]
|
|
79
|
+
:critical
|
|
80
|
+
elsif bounds[1] && value < bounds[1] || bounds[2] && value > bounds[2]
|
|
81
|
+
:warning
|
|
82
|
+
else
|
|
83
|
+
:info
|
|
84
|
+
end
|
|
85
|
+
paired = bounds[0] && bounds[3] || bounds[1] && bounds[2]
|
|
86
|
+
if severity == :info && !paired
|
|
87
|
+
return finding(:unknown, :optical_thresholds_unavailable, entity: entity, actual: value)
|
|
88
|
+
end
|
|
89
|
+
finding(severity == :info ? :pass : :fail, :optical_threshold, entity: entity,
|
|
90
|
+
actual: value, expected: record.slice(*Observation::OPTICAL_THRESHOLDS), severity: severity)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# 保留告警故障并报告来源冲突,不随意选择有利证据。
|
|
94
|
+
def disagreement?(record, measured)
|
|
95
|
+
alarm = record[:alarm]
|
|
96
|
+
return measured.status == :fail if alarm == :normal
|
|
97
|
+
return false unless ALARMS.include?(alarm) && record[:value] && record[:unit] == :dbm
|
|
98
|
+
return true if measured.status == :fail && measured.severity == :critical && alarm.to_s.end_with?("warning")
|
|
99
|
+
|
|
100
|
+
threshold = record[alarm]
|
|
101
|
+
return false unless threshold
|
|
102
|
+
|
|
103
|
+
alarm.to_s.start_with?("low") ? record[:value] >= threshold : record[:value] <= threshold
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module Checks
|
|
7
|
+
# 按作用域比较百分比,避免混用 CPU 时间窗、处理平面或内存池。
|
|
8
|
+
class Resources < Base
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# 按策略评估资源,同时保留测量值缺失的状态。
|
|
12
|
+
def assess(record)
|
|
13
|
+
entity = record.slice(:component, :scope, :window_seconds)
|
|
14
|
+
results = []
|
|
15
|
+
state = record[:status]
|
|
16
|
+
if %i[warning critical failed].include?(state)
|
|
17
|
+
results << finding(:fail, :memory_pressure_state, entity: entity, actual: state, severity: state == :warning ? :warning : :critical)
|
|
18
|
+
end
|
|
19
|
+
if @observation.name == :memory && !%i[used_total available used_minus_cache].include?(record[:basis])
|
|
20
|
+
return results + [ finding(:unknown, :memory_basis_unavailable, entity: entity, actual: record) ]
|
|
21
|
+
end
|
|
22
|
+
if inconsistent?(record)
|
|
23
|
+
return results + [ finding(:unknown, :resource_measurements_inconsistent, entity: entity, actual: record) ]
|
|
24
|
+
end
|
|
25
|
+
value = record[:utilization]
|
|
26
|
+
return results + [ finding(:unknown, :resource_utilization_unavailable, entity: entity, actual: record) ] unless value
|
|
27
|
+
|
|
28
|
+
limits = @policy.system.resource_thresholds.fetch(@observation.name)
|
|
29
|
+
severity = if value >= limits[:critical]
|
|
30
|
+
:critical
|
|
31
|
+
elsif value >= limits[:warning]
|
|
32
|
+
:warning
|
|
33
|
+
else
|
|
34
|
+
:info
|
|
35
|
+
end
|
|
36
|
+
results << finding(severity == :info ? :pass : :fail, :resource_utilization, entity: entity,
|
|
37
|
+
actual: record, expected: limits, severity: severity)
|
|
38
|
+
results
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 总量或中断子集数值不合理时,不能据此判定利用率健康。
|
|
42
|
+
def inconsistent?(record)
|
|
43
|
+
if @observation.name == :cpu
|
|
44
|
+
record[:interrupt_utilization] && record[:utilization] && record[:interrupt_utilization] > record[:utilization]
|
|
45
|
+
else
|
|
46
|
+
return true if record[:status] == :absent
|
|
47
|
+
total = record[:total_bytes]
|
|
48
|
+
return false unless total
|
|
49
|
+
return true if total.zero? || record.values_at(:used_bytes, :free_bytes, :available_bytes).compact.any? { |value| value > total }
|
|
50
|
+
if record[:basis] == :used_total && record[:used_bytes] && record[:free_bytes]
|
|
51
|
+
return true if record[:used_bytes] + record[:free_bytes] != total
|
|
52
|
+
end
|
|
53
|
+
if %i[available used_minus_cache].include?(record[:basis]) && record[:used_bytes] && record[:available_bytes]
|
|
54
|
+
return true if record[:used_bytes] + record[:available_bytes] != total
|
|
55
|
+
end
|
|
56
|
+
used = record[:basis] == :available && record[:available_bytes] ? total - record[:available_bytes] : record[:used_bytes]
|
|
57
|
+
return true if record[:basis] == :available && used && record[:used_bytes] && used != record[:used_bytes]
|
|
58
|
+
used && record[:utilization] && (used.fdiv(total) * 100 - record[:utilization]).abs > 1
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module Checks
|
|
7
|
+
# 在所属 VRF、地址族或进程作用域中解释协议会话状态。
|
|
8
|
+
class Routing < Base
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# 按策略评估资源,同时保留测量值缺失的状态。
|
|
12
|
+
def assess(record)
|
|
13
|
+
identity = record.slice(*identity_fields)
|
|
14
|
+
state = record[:state]
|
|
15
|
+
status, reason = if state.nil? || state == :unknown
|
|
16
|
+
[ :unknown, :protocol_state_unavailable ]
|
|
17
|
+
elsif @observation.name == :bgp
|
|
18
|
+
state == :established ? [ :pass, :bgp_established ] : [ :fail, :bgp_not_established ]
|
|
19
|
+
elsif state == :full
|
|
20
|
+
[ :pass, :ospf_full ]
|
|
21
|
+
elsif state == :two_way
|
|
22
|
+
two_way_state(record)
|
|
23
|
+
else
|
|
24
|
+
[ :fail, :ospf_not_adjacent ]
|
|
25
|
+
end
|
|
26
|
+
status, reason = :unknown, :protocol_scope_unavailable if status == :pass && identity.values.any?(&:nil?)
|
|
27
|
+
finding(status, reason, entity: identity, actual: record, severity: status == :fail ? :critical : :info)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 返回用于跨采样匹配记录的稳定身份字段。
|
|
31
|
+
def identity_fields = @observation.name == :bgp ? %i[vrf afi_safi peer] : %i[vrf process_id interface router_id]
|
|
32
|
+
|
|
33
|
+
# 仅在拓扑明确不要求建立邻接时,将 OSPF 2-Way 状态视为正常。
|
|
34
|
+
def two_way_state(record)
|
|
35
|
+
if %i[point_to_point point_to_multipoint virtual_link].include?(record[:network_type]) ||
|
|
36
|
+
%i[dr bdr].include?(record[:local_role]) || %i[dr bdr].include?(record[:neighbor_role])
|
|
37
|
+
[ :fail, :ospf_expected_adjacency_missing ]
|
|
38
|
+
elsif %i[broadcast nbma].include?(record[:network_type]) && record[:local_role] == :drother && record[:neighbor_role] == :drother
|
|
39
|
+
[ :pass, :ospf_expected_two_way ]
|
|
40
|
+
else
|
|
41
|
+
[ :unknown, :ospf_two_way_requires_topology ]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 为策略要求但未观察到的资源生成证据缺失结果。
|
|
46
|
+
def missing
|
|
47
|
+
expected = @observation.name == :bgp ? @policy.expected_bgp : @policy.expected_ospf
|
|
48
|
+
if expected.empty?
|
|
49
|
+
reason = @observation.issues.map { |issue| issue[:code] }.find { |code| %i[protocol_not_configured protocol_disabled].include?(code) }
|
|
50
|
+
return [] unless @observation.records.empty? && reason
|
|
51
|
+
|
|
52
|
+
return [ finding(:unknown, reason) ]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
fields = identity_fields
|
|
56
|
+
scope = @observation.name == :bgp ? %i[vrf afi_safi] : %i[vrf process_id interface]
|
|
57
|
+
identities = {}
|
|
58
|
+
scopes = {}
|
|
59
|
+
incomplete = Hash.new { |hash, keys| hash[keys] = {} }
|
|
60
|
+
@observation.records.each do |record|
|
|
61
|
+
identities[record.values_at(*fields)] = true
|
|
62
|
+
scopes[record.values_at(*scope)] = true
|
|
63
|
+
present = fields.reject { |key| record[key].nil? }
|
|
64
|
+
incomplete[present][record.values_at(*present)] = true unless present == fields
|
|
65
|
+
end
|
|
66
|
+
expected.filter_map do |identity|
|
|
67
|
+
next if identities.key?(identity.values_at(*fields))
|
|
68
|
+
|
|
69
|
+
ambiguous = incomplete.any? { |keys, values| values.key?(identity.values_at(*keys)) }
|
|
70
|
+
enumerated = @observation.covered_scopes.any? { |entry| entry.all? { |key, value| identity[key] == value } }
|
|
71
|
+
scope_observed = scopes.key?(identity.values_at(*scope))
|
|
72
|
+
status = if ambiguous
|
|
73
|
+
:unknown
|
|
74
|
+
elsif enumerated
|
|
75
|
+
:fail
|
|
76
|
+
elsif scope_observed
|
|
77
|
+
missing_status
|
|
78
|
+
else
|
|
79
|
+
:unknown
|
|
80
|
+
end
|
|
81
|
+
finding(status, :expected_peer_missing, entity: identity, severity: :critical)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hardware"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module Checks
|
|
7
|
+
# 优先使用设备或精确传感器的摄氏度阈值,不虚构通用温度限制。
|
|
8
|
+
class Temperature < Hardware
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# 按策略评估资源,同时保留测量值缺失的状态。
|
|
12
|
+
def assess(record)
|
|
13
|
+
return super if record[:status] == :absent && record[:value].nil?
|
|
14
|
+
entity = { component: record[:component] }
|
|
15
|
+
override = @policy.system.temperature_thresholds[record[:component]]
|
|
16
|
+
bounds = (override || record).slice(*Observation::OPTICAL_THRESHOLDS).compact
|
|
17
|
+
comparison = override ? override.fetch(:high_comparison, :gt) : record[:high_comparison]
|
|
18
|
+
state = record[:status]
|
|
19
|
+
explicit_fault = %i[warning critical failed].include?(state)
|
|
20
|
+
results = []
|
|
21
|
+
results << finding(:unknown, :temperature_presence_inconsistent, entity: entity) if state == :absent
|
|
22
|
+
results << finding(:fail, :temperature_alarm, entity: entity, actual: state, severity: state == :warning ? :warning : :critical) if explicit_fault
|
|
23
|
+
unless bounds.values_at(*Observation::OPTICAL_THRESHOLDS).compact.each_cons(2).all? { |low, high| low <= high }
|
|
24
|
+
return results + [ finding(:unknown, :temperature_thresholds_invalid, entity: entity) ]
|
|
25
|
+
end
|
|
26
|
+
value = record[:value]
|
|
27
|
+
if value && bounds.any?
|
|
28
|
+
severity = exceeded(value, bounds, comparison)
|
|
29
|
+
boundary_unknown = comparison.nil? && bounds.values_at(:high_warning, :high_alarm).compact.include?(value)
|
|
30
|
+
status = severity ? :fail : boundary_unknown || state == :absent ? :unknown : :pass
|
|
31
|
+
results << finding(status, boundary_unknown && !severity ? :temperature_boundary_unknown : :temperature_threshold, entity: entity,
|
|
32
|
+
actual: value, expected: bounds.merge(high_comparison: comparison), severity: severity || :info)
|
|
33
|
+
if !override && (state == :ok && severity || explicit_fault && !severity)
|
|
34
|
+
results << finding(:unknown, :temperature_state_disagreement, entity: entity)
|
|
35
|
+
end
|
|
36
|
+
elsif !explicit_fault
|
|
37
|
+
results << finding(state == :ok ? :pass : :unknown, :temperature_state, entity: entity, actual: record)
|
|
38
|
+
end
|
|
39
|
+
results
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 为测量值超出阈值生成检查结果。
|
|
43
|
+
def exceeded(value, bounds, comparison)
|
|
44
|
+
return :critical if bounds[:low_alarm] && value < bounds[:low_alarm] || above?(value, bounds[:high_alarm], comparison)
|
|
45
|
+
return :warning if bounds[:low_warning] && value < bounds[:low_warning] || above?(value, bounds[:high_warning], comparison)
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 将可用测量值与配置的上限比较。
|
|
50
|
+
def above?(value, limit, comparison) = limit && (value > limit || comparison == :gte && value == limit)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "system_values"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module CiscoIos
|
|
8
|
+
module Parsers
|
|
9
|
+
# 时区缩写不等于 UTC 偏移;有歧义的本地时间保持未知。
|
|
10
|
+
class ShowClockDetail < CtxChecker::Parsers::Base
|
|
11
|
+
include SystemValues
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# 解析 show clock detail,保留不完整或不支持的证据。
|
|
16
|
+
def parse_output(text, complete:)
|
|
17
|
+
records, issues = [], []
|
|
18
|
+
time_source = nil
|
|
19
|
+
clean(text).each_line.with_index(1) do |source, number|
|
|
20
|
+
line = source.strip
|
|
21
|
+
next if line.empty?
|
|
22
|
+
|
|
23
|
+
if (match = /\A([*.]?)(\d{1,2}:\d{2}:\d{2}(?:\.\d+)?)\s+(\S+)\s+\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{4})\z/.match(line))
|
|
24
|
+
offset = match[3]
|
|
25
|
+
offset = "+0000" if %w[UTC GMT].include?(offset)
|
|
26
|
+
timestamp = nil
|
|
27
|
+
offset_parts = /\A[+-](\d{2}):?(\d{2})\z/.match(offset)
|
|
28
|
+
if offset_parts && offset_parts[1].to_i < 24 && offset_parts[2].to_i < 60
|
|
29
|
+
begin
|
|
30
|
+
raise Date::Error unless match[2].split(":").map(&:to_f).zip([ 24, 60, 60 ]).all? { |value, limit| value < limit }
|
|
31
|
+
|
|
32
|
+
format = match[2].include?(".") ? "%H:%M:%S.%N %z %b %d %Y" : "%H:%M:%S %z %b %d %Y"
|
|
33
|
+
timestamp = DateTime.strptime("#{match[2]} #{offset} #{match[4]} #{match[5]} #{match[6]}", format).iso8601(3)
|
|
34
|
+
rescue Date::Error
|
|
35
|
+
issues << { code: :invalid_clock_time, line: number }
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
issues << { code: :clock_timezone_unknown, line: number }
|
|
39
|
+
end
|
|
40
|
+
records << { component: "system", time: timestamp, source: nil, synchronized: match[1].empty? ? nil : false }
|
|
41
|
+
elsif (match = /\ATime source is (.+)\z/i.match(line))
|
|
42
|
+
issues << { code: :duplicate_clock_source, line: number } if time_source
|
|
43
|
+
time_source = match[1]
|
|
44
|
+
elsif /\A(?:Summer time|Time zone|Timezone|No time source)/i.match?(line)
|
|
45
|
+
next
|
|
46
|
+
else
|
|
47
|
+
issues << unparsed(number)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
records.each { |record| record[:source] = time_source }
|
|
51
|
+
build_result(records, complete: complete, recognized: records.any?, issues: issues, identity: [ :component ])
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "system_values"
|
|
5
|
+
require "ctx_parser/result_set"
|
|
6
|
+
|
|
7
|
+
module CtxChecker
|
|
8
|
+
module CiscoIos
|
|
9
|
+
module Parsers
|
|
10
|
+
# Catalyst 组件块与 ASR 传感器表分别保留覆盖状态。
|
|
11
|
+
class ShowEnvironmentAll < CtxChecker::Parsers::Base
|
|
12
|
+
include SystemValues
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# 解析 show environment all,保留不完整或不支持的证据。
|
|
17
|
+
def parse_output(text, complete:)
|
|
18
|
+
@rows = { temperature: [], environment: [], power: [] }
|
|
19
|
+
@problems = @rows.transform_values { [] }
|
|
20
|
+
@switch = "active"
|
|
21
|
+
@temperature = nil
|
|
22
|
+
@sensor_table = false
|
|
23
|
+
@component_table = nil
|
|
24
|
+
clean(text).each_line.with_index(1) do |source, number|
|
|
25
|
+
line = source.strip
|
|
26
|
+
next if line.empty? || /\A[- ]+\z/.match?(line)
|
|
27
|
+
next if catalyst_line(line) || stack_component_line(line) || sensor_line(line)
|
|
28
|
+
next if /\A(?:Sensor List:|Sensor\s+Location|SW\s+PID\s+Serial#)/.match?(line)
|
|
29
|
+
|
|
30
|
+
@problems.each_value { |issues| issues << unparsed(number) }
|
|
31
|
+
end
|
|
32
|
+
results = @rows.to_h do |dimension, rows|
|
|
33
|
+
identity = dimension == :temperature ? [ :component ] : %i[component kind]
|
|
34
|
+
[ dimension, build_result(rows, complete: complete, recognized: rows.any?, issues: @problems[dimension], identity: identity) ]
|
|
35
|
+
end
|
|
36
|
+
CtxParser::ResultSet.new(results: results)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Catalyst StackWise Virtual 在传感器后按交换机报告电源和风扇托盘。
|
|
40
|
+
def stack_component_line(line)
|
|
41
|
+
case line
|
|
42
|
+
when /\ASwitch:(\d+)\z/i
|
|
43
|
+
@switch = Regexp.last_match(1)
|
|
44
|
+
@sensor_table, @component_table = false, nil
|
|
45
|
+
when /\APower\s+Fan States\z/i
|
|
46
|
+
@component_table = :power
|
|
47
|
+
when /\AFan\s+Fan States\z/i
|
|
48
|
+
@component_table = :fan
|
|
49
|
+
when /\ASupply\s+Model No\s+Type\s+Capacity\s+Status\s+(?:\d+\s*)+\z/i,
|
|
50
|
+
/\ATray\s+Status\s+(?:\d+\s*)+\z/i
|
|
51
|
+
return false unless @component_table
|
|
52
|
+
else
|
|
53
|
+
if @component_table == :power && (match = /\A(PS\d+)\s+\S+\s+(?:AC|DC)\s+(\d+(?:\.\d+)?)\s+W\s+(\S+)\s+(.+)\z/i.match(line))
|
|
54
|
+
status = stack_component_status(match[3], match[4])
|
|
55
|
+
return false unless status
|
|
56
|
+
@rows[:power] << { component: "switch #{@switch}/power supply #{match[1]}", kind: :power_supply,
|
|
57
|
+
status: status, present: status != :absent, watts: nil, capacity_watts: Float(match[2]) }
|
|
58
|
+
elsif @component_table == :fan && (match = /\A(FM\d+)\s+(\S+)\s+(.+)\z/i.match(line))
|
|
59
|
+
status = stack_component_status(match[2], match[3])
|
|
60
|
+
return false unless status
|
|
61
|
+
@rows[:environment] << { component: "switch #{@switch}/fan tray #{match[1]}", kind: :fan, status: status }
|
|
62
|
+
else
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 单个风扇故障优先于电源或托盘的整体正常状态。
|
|
70
|
+
def stack_component_status(parent, fans)
|
|
71
|
+
values = fans.split
|
|
72
|
+
return unless values.all? { |value| /\A(?:good|bad|ok|fail|failed|N\/A)\z/i.match?(value) }
|
|
73
|
+
|
|
74
|
+
states = [ component_status(parent), *values.reject { |value| value.casecmp?("N/A") }.map { |value| component_status(value) } ]
|
|
75
|
+
most_severe_status(states)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 保留当前交换机身份;本命令不承诺覆盖整个堆叠。
|
|
79
|
+
def catalyst_line(line)
|
|
80
|
+
case line
|
|
81
|
+
when /\A(?:Switch (\d+) )?FAN (\S+) is (.+)\z/i
|
|
82
|
+
@switch = Regexp.last_match(1) if Regexp.last_match(1)
|
|
83
|
+
@rows[:environment] << { component: "switch #{@switch}/fan #{Regexp.last_match(2)}", kind: :fan,
|
|
84
|
+
status: component_status(Regexp.last_match(3)) }
|
|
85
|
+
when /\ASwitch (\d+): SYSTEM TEMPERATURE is (.+)\z/i
|
|
86
|
+
@switch = Regexp.last_match(1)
|
|
87
|
+
@rows[:environment] << { component: "switch #{@switch}/temperature", kind: :overall, status: component_status(Regexp.last_match(2)) }
|
|
88
|
+
when /\A(.+?) Temperature Value:\s*(#{NUMBER}) Degree Celsius\z/i
|
|
89
|
+
component = "switch #{@switch}/#{Regexp.last_match(1).downcase}"
|
|
90
|
+
value = checked_number(Regexp.last_match(2), issues: @problems[:temperature], component: component)
|
|
91
|
+
@temperature = { component: component, value: value,
|
|
92
|
+
low_alarm: nil, low_warning: nil, high_warning: nil, high_alarm: nil, status: :unknown }
|
|
93
|
+
@rows[:temperature] << @temperature
|
|
94
|
+
when /\A(?:System )?Temperature State:\s*(\S+)\z/i
|
|
95
|
+
return false unless @temperature
|
|
96
|
+
|
|
97
|
+
state = component_status(Regexp.last_match(1))
|
|
98
|
+
if @temperature[:status] == :unknown
|
|
99
|
+
@temperature[:status] = state
|
|
100
|
+
else
|
|
101
|
+
@problems[:temperature] << { code: :duplicate_sensor_state, component: @temperature[:component] }
|
|
102
|
+
@temperature[:status] = most_severe_status([ @temperature[:status], state ])
|
|
103
|
+
end
|
|
104
|
+
when /\A(Yellow|Red) Threshold\s*:\s*(#{NUMBER}) Degree Celsius\z/i
|
|
105
|
+
return false unless @temperature
|
|
106
|
+
|
|
107
|
+
field = Regexp.last_match(1).casecmp?("yellow") ? :high_warning : :high_alarm
|
|
108
|
+
@problems[:temperature] << { code: :duplicate_sensor_threshold, component: @temperature[:component] } if @temperature[field]
|
|
109
|
+
@temperature[field] = checked_number(Regexp.last_match(2), issues: @problems[:temperature], component: @temperature[:component])
|
|
110
|
+
when /\A(\d+[AB])\s+\S+\s+\S+\s+(.+?)\s+(Good|Bad|N\/A)\s+(Good|Bad|N\/A)\s+(\d+)\z/i
|
|
111
|
+
match = Regexp.last_match
|
|
112
|
+
state = component_status(match[2])
|
|
113
|
+
state = most_severe_status([ state, :failed ]) if [ match[3], match[4] ].any? { |value| value.casecmp?("bad") }
|
|
114
|
+
@rows[:power] << { component: "power supply #{match[1]}", kind: :power_supply, status: state,
|
|
115
|
+
present: state == :absent ? false : nil, watts: nil, capacity_watts: Integer(match[5], 10) }
|
|
116
|
+
when /\ASensor List:.*Environmental Monitoring/i
|
|
117
|
+
@sensor_table = true
|
|
118
|
+
else
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 根据读数单位区分传感器类型;仅有转速或风扇百分比不能判定健康。
|
|
125
|
+
def sensor_line(line)
|
|
126
|
+
return false unless @sensor_table
|
|
127
|
+
|
|
128
|
+
cells = line.split(/\s{2,}/)
|
|
129
|
+
return false unless cells.size == 4
|
|
130
|
+
|
|
131
|
+
sensor, location, state, reading = cells
|
|
132
|
+
component = "#{location}/#{sensor}"
|
|
133
|
+
status = component_status(state)
|
|
134
|
+
if (match = /\A(#{NUMBER}) Celsius\z/i.match(reading))
|
|
135
|
+
value = checked_number(match[1], issues: @problems[:temperature], component: component)
|
|
136
|
+
@rows[:temperature] << { component: component, value: value, low_alarm: nil, low_warning: nil,
|
|
137
|
+
high_warning: nil, high_alarm: nil, status: status }
|
|
138
|
+
if state.start_with?("Fan Speed")
|
|
139
|
+
@rows[:environment] << { component: component, kind: :fan, status: :unknown }
|
|
140
|
+
end
|
|
141
|
+
elsif /\A(?:PEM|Power)/i.match?(sensor)
|
|
142
|
+
@rows[:power] << { component: component, kind: :power_rail, status: status, present: nil, watts: nil, capacity_watts: nil }
|
|
143
|
+
elsif /\A#{NUMBER}\s+(?:mV|V|V AC|mA|A)\z/i.match?(reading)
|
|
144
|
+
@rows[:environment] << { component: component, kind: :voltage, status: status }
|
|
145
|
+
else
|
|
146
|
+
return false
|
|
147
|
+
end
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "../../parsers/interface_names"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module CiscoIos
|
|
8
|
+
module Parsers
|
|
9
|
+
# 保留聚合父接口与成员状态,包括合法的 LACP 备用成员。
|
|
10
|
+
class ShowEtherchannelSummary < CtxChecker::Parsers::Base
|
|
11
|
+
PLATFORM = :cisco_ios
|
|
12
|
+
MEMBER_STATES = { "P" => :active, "H" => :standby, "D" => :down, "s" => :suspended,
|
|
13
|
+
"I" => :unselected, "w" => :unselected, "M" => :unselected, "r" => :down, "p" => :active,
|
|
14
|
+
"b" => :unselected }.freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# 要求存在数据表或明确空表证据;标志图例不能作为成员记录。
|
|
19
|
+
def parse_output(text, complete:)
|
|
20
|
+
text = clean(text)
|
|
21
|
+
rows, issues, groups, totals = [], [], [], []
|
|
22
|
+
current, header, empty = nil, false, false
|
|
23
|
+
text.each_line.with_index(1) do |source, number|
|
|
24
|
+
line = source.strip
|
|
25
|
+
next if line.empty? || line.match?(/\A[-=+ ]+\z/)
|
|
26
|
+
|
|
27
|
+
if line.match?(/\AGroup\s+Port-(?:channel|Channel)?\s+(?:Type\s+)?Protocol\s+(?:Ports|Member Ports)\z/i)
|
|
28
|
+
header = true
|
|
29
|
+
elsif (match = /\A(\d+)\s+((?:Po|port-channel)\d+)\(([^()\s]+)\)\s+(?:(?:Eth|FC)\s+)?(?:LACP|PAgP|NONE|--|-|None)\s*(.*)\z/i.match(line))
|
|
30
|
+
current = { group: normalize_name(match[2]), oper_status: group_state(match[3], issues, number) }
|
|
31
|
+
groups << current
|
|
32
|
+
append_members(rows, current, match[4], issues, number)
|
|
33
|
+
elsif line.match?(/\A\d+\s+(?:Po|port-channel)\d+\b/i)
|
|
34
|
+
current = nil
|
|
35
|
+
issues << { code: :unparsed_aggregation_group, line: number }
|
|
36
|
+
elsif current && line.match?(/\A\S+\s*\([^()\s]+\)/)
|
|
37
|
+
append_members(rows, current, line, issues, number)
|
|
38
|
+
elsif (match = /\ANumber of (?:port-channels|channel-groups in use):\s*(\d+)\z/i.match(line))
|
|
39
|
+
totals << Integer(match[1], 10)
|
|
40
|
+
empty ||= match[1] == "0"
|
|
41
|
+
elsif line.match?(/\ANo (?:port-channels|channel-groups|EtherChannels)(?: found)?\.?\z/i)
|
|
42
|
+
empty = true
|
|
43
|
+
elsif legend?(line)
|
|
44
|
+
next
|
|
45
|
+
else
|
|
46
|
+
issues << { code: :unparsed_row, line: number }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
totals.each do |total|
|
|
50
|
+
issues << { code: :count_mismatch, expected: total, observed: groups.size } unless total == groups.size
|
|
51
|
+
end
|
|
52
|
+
groups.each do |group|
|
|
53
|
+
next if rows.any? { |row| row[:group] == group[:group] }
|
|
54
|
+
|
|
55
|
+
rows << group.merge(member: nil, member_status: :unknown)
|
|
56
|
+
issues << { code: :aggregation_members_missing, group: group[:group] }
|
|
57
|
+
end
|
|
58
|
+
repeated_groups = groups.group_by { |group| group[:group] }.select { |_, entries| entries.size > 1 }.keys
|
|
59
|
+
if repeated_groups.any?
|
|
60
|
+
issues << { code: :duplicate_aggregation_group, count: repeated_groups.size }
|
|
61
|
+
rows.reject! { |row| repeated_groups.include?(row[:group]) }
|
|
62
|
+
end
|
|
63
|
+
issues << { code: :contradictory_empty } if empty && groups.any?
|
|
64
|
+
build_result(rows, complete: complete, recognized: empty || (header && groups.any?), issues: issues, identity: %i[group member])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 将物理成员关联到其逻辑聚合接口。
|
|
68
|
+
def append_members(rows, group, text, issues, number)
|
|
69
|
+
return if text.empty?
|
|
70
|
+
if text == "--"
|
|
71
|
+
rows << group.merge(member: nil, member_status: :unknown)
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
text.scan(/[^\s()]+\s*\([^()\s]+\)|\S+/).each do |word|
|
|
76
|
+
match = /\A([^()\s]+)\s*\(([^()\s]+)\)\z/.match(word)
|
|
77
|
+
unless match
|
|
78
|
+
issues << { code: :unparsed_member, line: number }
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
state = MEMBER_STATES.fetch(match[2], :unknown)
|
|
82
|
+
issues << { code: :unknown_member_status, line: number } if state == :unknown
|
|
83
|
+
rows << group.merge(member: normalize_name(match[1]), member_status: state)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 归一化聚合组报告的运行状态。
|
|
88
|
+
def group_state(flags, issues, number)
|
|
89
|
+
unless flags.match?(/\A[SRUDM]+\z/) && !(flags.include?("U") && (flags.include?("D") || flags.include?("M")))
|
|
90
|
+
issues << { code: :unknown_aggregation_status, line: number }
|
|
91
|
+
return :unknown
|
|
92
|
+
end
|
|
93
|
+
return :down if flags.include?("D") || flags.include?("M")
|
|
94
|
+
return :up if flags.include?("U")
|
|
95
|
+
|
|
96
|
+
issues << { code: :unknown_aggregation_status, line: number }
|
|
97
|
+
:unknown
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 识别表格图例,避免将其作为资源记录。
|
|
101
|
+
def legend?(line)
|
|
102
|
+
line == "Channel" || line.match?(/\A(?:Flags:|[A-Za-z] - |[A-Za-z]\s+-\s+|Number of aggregators:|Number of aggregators in use:)/)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# 将接口别名归一化为平台标准身份。
|
|
106
|
+
def normalize_name(name) = CtxChecker::Parsers::InterfaceNames.normalize(name, platform: self.class::PLATFORM)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module CiscoIos
|
|
7
|
+
module Parsers
|
|
8
|
+
# Facility 告警表示当前状态,与历史 syslog 记录区分处理。
|
|
9
|
+
class ShowFacilityAlarmStatus < CtxChecker::Parsers::Base
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# 解析 show facility alarm status,保留不完整或不支持的证据。
|
|
13
|
+
def parse_output(text, complete:)
|
|
14
|
+
records, issues, totals, severities = [], [], [], []
|
|
15
|
+
clean(text).each_line.with_index(1) do |source, number|
|
|
16
|
+
line = source.strip
|
|
17
|
+
next if line.empty? || /\A[- ]+\z/.match?(line) || /\ASource\s+(?:Time\s+)?Severity\s+/.match?(line)
|
|
18
|
+
|
|
19
|
+
if (match = /\ASystem Totals\s+Critical:\s*(\d+)\s+Major:\s*(\d+)\s+Minor:\s*(\d+)\z/i.match(line))
|
|
20
|
+
totals << match.captures.map { |value| Integer(value, 10) }
|
|
21
|
+
elsif (match = /\A(.+?)\s+(?:(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d+\s+\d{4}\s+\d{2}:\d{2}:\d{2}\s+)?(CRITICAL|MAJOR|MINOR|INFO)\s+.+?\s+\[(\d+)\]\z/i.match(line))
|
|
22
|
+
severity = match[2].upcase
|
|
23
|
+
severities << severity
|
|
24
|
+
records << { component: match[1].strip, alarm_id: match[3],
|
|
25
|
+
severity: { "CRITICAL" => :critical, "MAJOR" => :critical, "MINOR" => :warning, "INFO" => :info }.fetch(severity), active: true, count: 1 }
|
|
26
|
+
else
|
|
27
|
+
issues << { code: :unparsed_row, line: number }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
if !totals.one? || totals.first != %w[CRITICAL MAJOR MINOR].map { |severity| severities.count(severity) }
|
|
31
|
+
issues << { code: :alarm_count_mismatch }
|
|
32
|
+
end
|
|
33
|
+
if records.empty? && totals == [ [ 0, 0, 0 ] ] && issues.empty?
|
|
34
|
+
records << { component: "system", alarm_id: "facility_summary", severity: :info, active: false, count: 0 }
|
|
35
|
+
end
|
|
36
|
+
build_result(records, complete: complete, recognized: totals.any? || records.any?, issues: issues, identity: %i[component alarm_id])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|