ctx_checker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/COUNTER_CONTRACT.md +33 -0
  4. data/LICENSE +22 -0
  5. data/README-zh.md +93 -0
  6. data/README.md +93 -0
  7. data/docs/architecture-zh.md +487 -0
  8. data/docs/release.md +13 -0
  9. data/examples/offline.rb +17 -0
  10. data/lib/ctx_checker/applicability.rb +187 -0
  11. data/lib/ctx_checker/checks/aggregation.rb +73 -0
  12. data/lib/ctx_checker/checks/alarms.rb +25 -0
  13. data/lib/ctx_checker/checks/base.rb +66 -0
  14. data/lib/ctx_checker/checks/clock.rb +30 -0
  15. data/lib/ctx_checker/checks/counters.rb +77 -0
  16. data/lib/ctx_checker/checks/device_counters.rb +32 -0
  17. data/lib/ctx_checker/checks/hardware.rb +45 -0
  18. data/lib/ctx_checker/checks/interfaces.rb +35 -0
  19. data/lib/ctx_checker/checks/ntp.rb +47 -0
  20. data/lib/ctx_checker/checks/optics.rb +107 -0
  21. data/lib/ctx_checker/checks/resources.rb +63 -0
  22. data/lib/ctx_checker/checks/routing.rb +86 -0
  23. data/lib/ctx_checker/checks/temperature.rb +53 -0
  24. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +56 -0
  25. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +153 -0
  26. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +110 -0
  27. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +41 -0
  28. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +140 -0
  29. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +107 -0
  30. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +164 -0
  31. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +96 -0
  32. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +65 -0
  33. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +36 -0
  34. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +44 -0
  35. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +45 -0
  36. data/lib/ctx_checker/cisco_ios/parsers/system_values.rb +62 -0
  37. data/lib/ctx_checker/cisco_ios/profile.rb +39 -0
  38. data/lib/ctx_checker/cisco_nxos/parsers/show_bgp_ipv4_unicast_summary.rb +69 -0
  39. data/lib/ctx_checker/cisco_nxos/parsers/show_clock.rb +13 -0
  40. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +157 -0
  41. data/lib/ctx_checker/cisco_nxos/parsers/show_interface.rb +78 -0
  42. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +141 -0
  43. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +14 -0
  44. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +151 -0
  45. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +116 -0
  46. data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +21 -0
  47. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +89 -0
  48. data/lib/ctx_checker/cisco_nxos/profile.rb +35 -0
  49. data/lib/ctx_checker/collector.rb +175 -0
  50. data/lib/ctx_checker/evaluator.rb +49 -0
  51. data/lib/ctx_checker/failure.rb +24 -0
  52. data/lib/ctx_checker/finding.rb +19 -0
  53. data/lib/ctx_checker/h3c/parsers/counters_table.rb +56 -0
  54. data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +43 -0
  55. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +44 -0
  56. data/lib/ctx_checker/h3c/parsers/display_clock.rb +72 -0
  57. data/lib/ctx_checker/h3c/parsers/display_counters_inbound_interface.rb +21 -0
  58. data/lib/ctx_checker/h3c/parsers/display_counters_outbound_interface.rb +21 -0
  59. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +64 -0
  60. data/lib/ctx_checker/h3c/parsers/display_environment.rb +74 -0
  61. data/lib/ctx_checker/h3c/parsers/display_fan.rb +72 -0
  62. data/lib/ctx_checker/h3c/parsers/display_interface.rb +127 -0
  63. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +72 -0
  64. data/lib/ctx_checker/h3c/parsers/display_memory.rb +132 -0
  65. data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +84 -0
  66. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +47 -0
  67. data/lib/ctx_checker/h3c/parsers/display_power.rb +63 -0
  68. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +109 -0
  69. data/lib/ctx_checker/h3c/parsers/system_values.rb +46 -0
  70. data/lib/ctx_checker/h3c/profile.rb +49 -0
  71. data/lib/ctx_checker/hillstone/parsers/show_clock.rb +68 -0
  72. data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +44 -0
  73. data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +114 -0
  74. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +90 -0
  75. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +82 -0
  76. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +53 -0
  77. data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +22 -0
  78. data/lib/ctx_checker/hillstone/parsers/show_ip_ospf_neighbor.rb +21 -0
  79. data/lib/ctx_checker/hillstone/parsers/show_memory.rb +85 -0
  80. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +110 -0
  81. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +101 -0
  82. data/lib/ctx_checker/hillstone/profile.rb +40 -0
  83. data/lib/ctx_checker/observation.rb +165 -0
  84. data/lib/ctx_checker/observation_merger.rb +80 -0
  85. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +50 -0
  86. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +47 -0
  87. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +68 -0
  88. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +70 -0
  89. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +52 -0
  90. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +78 -0
  91. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +66 -0
  92. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +54 -0
  93. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +142 -0
  94. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +102 -0
  95. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +54 -0
  96. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +44 -0
  97. data/lib/ctx_checker/palo_alto/profile.rb +39 -0
  98. data/lib/ctx_checker/parser_registry.rb +43 -0
  99. data/lib/ctx_checker/parsers/base.rb +68 -0
  100. data/lib/ctx_checker/parsers/interface_names.rb +65 -0
  101. data/lib/ctx_checker/parsers/scoped_result.rb +35 -0
  102. data/lib/ctx_checker/policy.rb +104 -0
  103. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +48 -0
  104. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +46 -0
  105. data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +22 -0
  106. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +71 -0
  107. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +22 -0
  108. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +44 -0
  109. data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +47 -0
  110. data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +48 -0
  111. data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +34 -0
  112. data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +53 -0
  113. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +68 -0
  114. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +39 -0
  115. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +58 -0
  116. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +67 -0
  117. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +74 -0
  118. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +99 -0
  119. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +86 -0
  120. data/lib/ctx_checker/radware_alteon/port_counter_parser.rb +34 -0
  121. data/lib/ctx_checker/radware_alteon/profile.rb +145 -0
  122. data/lib/ctx_checker/radware_alteon.rb +8 -0
  123. data/lib/ctx_checker/report.rb +112 -0
  124. data/lib/ctx_checker/runtime_parser.rb +234 -0
  125. data/lib/ctx_checker/snapshot.rb +44 -0
  126. data/lib/ctx_checker/system_policy.rb +70 -0
  127. data/lib/ctx_checker/system_record.rb +81 -0
  128. data/lib/ctx_checker/version.rb +6 -0
  129. data/lib/ctx_checker.rb +17 -0
  130. metadata +271 -0
@@ -0,0 +1,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