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,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "ctx_parser/document"
5
+ require "ctx_parser/banner"
6
+ require "ctx_parser/immutable"
7
+ require_relative "parser_registry"
8
+
9
+ module CtxChecker
10
+ # 配置证据可证明功能存在或缺失;运行回显为空不能证明任何一者。
11
+ class Applicability
12
+ DIMENSIONS = %i[aggregation bgp ospf].freeze
13
+ MAX_BYTES = 16 * 1024 * 1024
14
+ SUPPORTED_PLATFORMS = %w[h3c cisco_ios cisco_nxos].freeze
15
+ ROOT_PATTERNS = {
16
+ h3c: {
17
+ aggregation: /\A(?:interface\s+(?:Bridge-Aggregation|Route-Aggregation)\s*\d|link-aggregation\s+group\b)/i,
18
+ bgp: /\Abgp\b/i, ospf: /\Aospf(?:v3)?\b/i
19
+ },
20
+ cisco: {
21
+ aggregation: /\Ainterface\s+(?:Port-channel|Bundle-Ether)\s*\d/i,
22
+ bgp: /\Arouter\s+bgp\b/i, ospf: /\A(?:router\s+ospf(?:v3)?|ipv6\s+router\s+ospf)\b/i
23
+ }
24
+ }.transform_values { |patterns| patterns.transform_values(&:freeze).freeze }.freeze
25
+ INTERFACE_PATTERNS = {
26
+ h3c: { aggregation: /\A(?:port\s+link-aggregation\s+group|link-aggregation)\b/i,
27
+ ospf: /\A(?:ip\s+)?ospf(?:v3)?\b/i },
28
+ cisco: { aggregation: /\Achannel-group\b/i,
29
+ ospf: /\A(?:(?:ip|ipv6)\s+(?:router\s+)?ospf|ospfv3)\b/i }
30
+ }.transform_values { |patterns| patterns.transform_values(&:freeze).freeze }.freeze
31
+ VRF = /\A(?:vrf\s+(?:definition|context)\s+|ip\s+(?:vrf|vpn-instance)\s+)/i
32
+ ADDRESS_FAMILY = /\A(?:address-family|ipv[46]-family)\b/i
33
+ HINTS = { aggregation: /\b(?:link-aggregation|bridge-aggregation|route-aggregation|channel-group|port-channel|bundle-ether|lacp)\b/i,
34
+ bgp: /\bbgp\b/i, ospf: /\bospf(?:v3|6)?\b/i }.freeze
35
+
36
+ attr_reader :platform, :evidence
37
+
38
+ # 仅使用可追溯的配置证据推断可选功能是否适用。
39
+ def self.from_configuration(platform:, text:, complete: false, reference: nil)
40
+ new(platform: platform, text: text, complete: complete, reference: reference)
41
+ end
42
+
43
+ # 冻结配置推导的可选功能状态及其证据。
44
+ def initialize(platform:, text:, complete:, reference: nil)
45
+ raise ArgumentError, "Configuration must be text" unless text.is_a?(String)
46
+ raise ArgumentError, "Completeness must be boolean" unless [ true, false ].include?(complete)
47
+ unless (platform.is_a?(String) || platform.is_a?(Symbol)) && !platform.to_s.strip.empty? && !/[\x00-\x1f\x7f]/.match?(platform.to_s)
48
+ raise ArgumentError, "Platform must be text"
49
+ end
50
+ unless reference.nil? || reference.is_a?(String) && !reference.strip.empty? && !/[\x00-\x1f\x7f]/.match?(reference)
51
+ raise ArgumentError, "Configuration reference must be text"
52
+ end
53
+
54
+ @platform = canonical_platform(platform).freeze
55
+ source = { sha256: Digest::SHA256.hexdigest(text), bytes: text.bytesize }
56
+ source[:reference] = reference unless reference.nil?
57
+ @evidence = CtxParser::Immutable.copy([ source ])
58
+ @states = CtxParser::Immutable.copy(configuration_states(text, complete: complete))
59
+ freeze
60
+ end
61
+
62
+ # 返回功能已配置、未配置或未知的证据状态。
63
+ def state(name) = @states.fetch(name, :unknown)
64
+ # 检查证据是否明确证明可选功能未配置。
65
+ def not_applicable?(name) = state(name) == :not_configured
66
+ # 展示对象身份与状态,避免泄露原始回显或凭据。
67
+ def inspect = "#<#{self.class} platform=#{platform} states=#{@states}>"
68
+ alias to_s inspect
69
+
70
+ private
71
+
72
+ # 不支持的平台保留扩展入口,不继承其他厂商的语法判断。
73
+ def canonical_platform(value)
74
+ ParserRegistry.canonical(value)
75
+ rescue ArgumentError
76
+ value.to_s.strip.downcase
77
+ end
78
+
79
+ # 推导可选功能已配置、未配置或未知的状态。
80
+ def configuration_states(text, complete:)
81
+ unknown = DIMENSIONS.to_h { |name| [ name, :unknown ] }
82
+ return unknown unless SUPPORTED_PLATFORMS.include?(platform)
83
+
84
+ source = CtxParser::Input.text(text, encoding: :transcode, newlines: :lf, max_bytes: MAX_BYTES)
85
+ return unknown if /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.match?(source)
86
+
87
+ document = Document.new(source)
88
+ return unknown if document.issues.any?
89
+
90
+ roots = document.roots
91
+ identity = platform == "h3c" ? /\Asysname\s+\S+\z/i : /\Ahostname\s+\S+\z/i
92
+ recognized = roots.any? { |entry| identity.match?(entry.text) }
93
+ family = platform == "h3c" ? :h3c : :cisco
94
+ DIMENSIONS.to_h do |name|
95
+ present, unresolved = feature_state(roots, family, name)
96
+ value = if present
97
+ :configured
98
+ elsif complete && recognized && !unresolved
99
+ :not_configured
100
+ else
101
+ :unknown
102
+ end
103
+ [ name, value ]
104
+ end
105
+ rescue ArgumentError, EncodingError
106
+ unknown
107
+ end
108
+
109
+ # 仅在已声明的配置所属作用域中识别嵌套功能语句。
110
+ def feature_state(roots, family, name)
111
+ configured, unresolved = false, false
112
+ roots.each do |entry|
113
+ configured ||= ROOT_PATTERNS.fetch(family).fetch(name).match?(entry.text)
114
+ if /\Ainterface\s+/i.match?(entry.text)
115
+ pattern = INTERFACE_PATTERNS.fetch(family)[name]
116
+ configured ||= pattern && entry.children.any? { |child| pattern.match?(child.text) }
117
+ unresolved ||= feature_hint?(entry.text, name) || entry.children.any? { |child| feature_hint?(child.text, name) }
118
+ elsif VRF.match?(entry.text)
119
+ present, ambiguous = vrf_feature_state(entry.children, family, name)
120
+ configured ||= present
121
+ unresolved ||= ambiguous
122
+ elsif HINTS.fetch(name).match?(entry.text) && !unrelated_setting?(entry.text)
123
+ unresolved = true
124
+ end
125
+ end
126
+ [ configured, unresolved ]
127
+ end
128
+
129
+ # 结合全局与 VRF 配置作用域判断协议是否适用。
130
+ def vrf_feature_state(children, family, name)
131
+ pending = children.dup
132
+ unresolved = false
133
+ until pending.empty?
134
+ entry = pending.pop
135
+ return [ true, unresolved ] if ROOT_PATTERNS.fetch(family).fetch(name).match?(entry.text)
136
+ unresolved ||= feature_hint?(entry.text, name)
137
+ pending.concat(entry.children) if ADDRESS_FAMILY.match?(entry.text)
138
+ end
139
+ [ false, unresolved ]
140
+ end
141
+
142
+ # 识别不能安全推断为功能缺失的声明。
143
+ def feature_hint?(text, name) = HINTS.fetch(name).match?(text) && !unrelated_setting?(text)
144
+
145
+ # 功能开关或描述值本身不能证明已配置运行实例。
146
+ def unrelated_setting?(text)
147
+ text.match?(/\A(?:no |undo )?(?:feature\s+\S+|logging\s+level\s+\S+\s+\d+|description\s+.*|hostname\s+\S+|sysname\s+\S+)\z/i)
148
+ end
149
+
150
+ # 领域语法负责注释、终端错误和横幅处理;缩进结构继续复用公共实现。
151
+ class Document < CtxParser::Document
152
+ protected
153
+
154
+ # 区分配置结构行与引号、横幅中的文本内容。
155
+ def classify(line)
156
+ text = line.text
157
+ return :ignored if text.empty?
158
+
159
+ @banner ||= CtxParser::Banner.new
160
+ # Comware 的可选关键字本身不能作为横幅分隔符。
161
+ if !@banner.open? && /\Aheader\s+\S+\s+information\s*\z/i.match?(text)
162
+ issues << { code: :invalid_banner, line: line.number }
163
+ return :ignored
164
+ end
165
+ return :ignored if @banner.consume(text)
166
+ if /\A(?:banner|header)\b/i.match?(text)
167
+ issues << { code: :invalid_banner, line: line.number }
168
+ return :ignored
169
+ end
170
+ if /--\s*More\s*--|Press (?:any key|ENTER|<SPACE>)|\A%\s*(?:Invalid|Incomplete|Ambiguous|Error)\b|\A(?:%\s*)?(?:Error:|Invalid (?:command|input|syntax)|Incomplete command|Ambiguous command|Permission denied|Insufficient privilege|Unrecognized command|Unknown command|command not (?:found|supported)|syntax error|\^-+\s*(?:unrecognized keyword|incomplete command|ambiguous command|error\b))/i.match?(text)
171
+ issues << { code: :incomplete_configuration, line: line.number }
172
+ return :ignored
173
+ end
174
+ return :boundary if text.start_with?("!", "#") || %w[end return].include?(text)
175
+ return :ignored if /\A(?:Building configuration|Current configuration)/i.match?(text)
176
+
177
+ :entry
178
+ end
179
+
180
+ # 汇总已解析记录,并报告未解决或不完整的来源状态。
181
+ def finish
182
+ issues << { code: :incomplete_banner } if @banner&.open?
183
+ end
184
+ end
185
+ private_constant :Document
186
+ end
187
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 逐个评估成员;即使聚合接口仍为 up,也保留成员故障。
8
+ class Aggregation < Base
9
+ private
10
+
11
+ # 按策略评估资源,同时保留测量值缺失的状态。
12
+ def assess(record)
13
+ record = resolved_state(record)
14
+ if record[:member].nil?
15
+ return finding(record[:oper_status] == :down ? :fail : :unknown, :aggregation_members_unavailable,
16
+ entity: record[:group], actual: record, severity: :warning)
17
+ end
18
+ status, reason = if record[:oper_status] == :down || member_down?(record) || %i[down suspended unselected].include?(record[:member_status])
19
+ [ :fail, :aggregation_member_down ]
20
+ elsif %i[active standby].include?(record[:member_status]) && record[:oper_status] == :up
21
+ [ :pass, record[:member_status] == :standby ? :aggregation_standby : :aggregation_active ]
22
+ else
23
+ [ :unknown, :aggregation_state_unavailable ]
24
+ end
25
+ finding(status, reason, entity: record.slice(:group, :member), actual: record, severity: :warning)
26
+ end
27
+
28
+ # 为策略要求但未观察到的资源生成证据缺失结果。
29
+ def missing
30
+ rows = @observation.records.map { |record| resolved_state(record) }.group_by { |record| record[:group] }
31
+ results = @policy.required_members.flat_map do |group, members|
32
+ group_rows = rows.fetch(group, [])
33
+ status = group_rows.any? { |row| row[:member].nil? } ? :unknown : missing_status
34
+ (members - group_rows.map { |row| row[:member] }).map do |member|
35
+ finding(status, :required_member_missing, entity: { group: group, member: member }, severity: :critical)
36
+ end
37
+ end
38
+ @policy.minimum_active_members.each do |group, minimum|
39
+ group_rows = rows.fetch(group, [])
40
+ members = group_rows.reject { |row| row[:member].nil? }
41
+ active = members.count { |row| row[:member_status] == :active && row[:oper_status] == :up && !member_down?(row) }
42
+ known = group_rows.none? { |row| row[:member].nil? } && members.all? { |row| row[:member_status] && row[:member_status] != :unknown && %i[up down].include?(row[:oper_status]) }
43
+ status = if active >= minimum
44
+ :pass
45
+ elsif @observation.complete? && known
46
+ :fail
47
+ else
48
+ :unknown
49
+ end
50
+ results << finding(status, :aggregation_capacity, entity: group, actual: active, expected: minimum, severity: :critical)
51
+ end
52
+ results
53
+ end
54
+
55
+ # Comware 成员表不提供链路状态;仅关联完整的父级接口证据。
56
+ def resolved_state(record)
57
+ return record if %i[up down].include?(record[:oper_status])
58
+
59
+ parent = interface(record[:group])
60
+ return record unless parent && parent[:kind] == :aggregate &&
61
+ record_complete?(parent, @snapshot.observation(:interfaces))
62
+
63
+ record.merge(oper_status: parent[:oper_status])
64
+ end
65
+
66
+ # 成员处于选中状态,不能覆盖已确认的物理链路故障。
67
+ def member_down?(record)
68
+ physical = interface(record[:member])
69
+ physical && record_complete?(physical, @snapshot.observation(:interfaces)) && physical[:oper_status] == :down
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 当前告警独立于历史事件日志进行评估。
8
+ class Alarms < Base
9
+ private
10
+
11
+ # 按策略评估资源,同时保留测量值缺失的状态。
12
+ def assess(record)
13
+ entity = record.slice(:component, :alarm_id)
14
+ active = record[:active]
15
+ if active.nil? || active == false && record[:count]&.positive? || active == true && record[:count] == 0
16
+ return finding(:unknown, :alarm_state_unavailable, entity: entity, actual: record)
17
+ end
18
+ return finding(:pass, :alarm_clear, entity: entity, actual: record) unless active
19
+ return finding(:not_applicable, :informational_alarm, entity: entity, actual: record) if record[:severity] == :info
20
+
21
+ finding(:fail, :active_alarm, entity: entity, actual: record, severity: record[:severity] == :critical ? :critical : :warning)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../finding"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 共用证据边界;厂商特定文本不进入评估规则。
8
+ class Base
9
+ # 绑定当前维度评估使用的快照与策略。
10
+ def initialize(snapshot:, policy:, previous: nil)
11
+ @snapshot, @policy, @previous = snapshot, policy, previous
12
+ end
13
+
14
+ # 评估已知记录,分别表达必需、缺失与不完整证据。
15
+ def call(observation)
16
+ @observation = observation
17
+ if observation.status == :not_applicable
18
+ return [ finding(:fail, :required_feature_not_configured, severity: :critical) ] if @policy.required?(observation.name)
19
+
20
+ return [ finding(:not_applicable, :feature_not_configured) ]
21
+ end
22
+ if observation.status == :not_requested && !@policy.required?(observation.name)
23
+ return [ finding(:not_applicable, :not_requested) ]
24
+ end
25
+ results = observation.records.flat_map do |record|
26
+ @current_record = record
27
+ assess(record)
28
+ ensure
29
+ @current_record = nil
30
+ end
31
+ results.concat(missing)
32
+ results << finding(:unknown, observation.complete? ? :empty_evidence : observation.status) if results.empty?
33
+ results << finding(:unknown, :incomplete_evidence) unless observation.complete? || results.any? { |row| row.status == :unknown }
34
+ results
35
+ end
36
+
37
+ private
38
+
39
+ # 局部采集仍保留已知故障;判定成功则要求完整的支持证据。
40
+ def finding(status, reason, entity: nil, actual: nil, expected: nil, severity: :info, suffix: nil)
41
+ if status == :pass && !record_complete?(@current_record, @observation)
42
+ status, reason = :unknown, :incomplete_evidence
43
+ end
44
+ Finding.new(id: [ @observation.name, suffix || reason ].join("."), dimension: @observation.name,
45
+ entity: entity, status: status, reason: reason, actual: actual, expected: expected, severity: severity)
46
+ end
47
+
48
+ # 为策略要求但未观察到的资源生成证据缺失结果。
49
+ def missing = []
50
+ # 为不可用证据选择结果状态,不据此声明健康。
51
+ def missing_status = @observation.complete? ? :fail : :unknown
52
+ # 仅在需要关联接口状态的检查中建立接口索引。
53
+ def interface(name)
54
+ @interfaces ||= @snapshot.observation(:interfaces).records.to_h { |row| [ row[:name], row ] }
55
+ @interfaces[name]
56
+ end
57
+
58
+ # 同级查询失败不能使其他来源的完整测量失效。
59
+ def record_complete?(record, observation)
60
+ return false if record && record[:source_complete] == false
61
+
62
+ observation.complete? || observation.status == :partial && record && record[:source_complete] == true
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 依据明确的 UTC 偏移,以命令采集时间计算设备时钟偏差。
8
+ class Clock < Base
9
+ private
10
+
11
+ # 按策略评估资源,同时保留测量值缺失的状态。
12
+ def assess(record)
13
+ entity = { component: record[:component] }
14
+ results = []
15
+ if record[:synchronized] == false
16
+ results << finding(:fail, :clock_unsynchronized, entity: entity, severity: :warning)
17
+ end
18
+ unless record[:time]
19
+ return results + [ finding(:unknown, :clock_time_unavailable, entity: entity) ]
20
+ end
21
+ observed_at = @observation.observed_at || @snapshot.observed_at
22
+ skew = (Time.iso8601(record[:time]) - observed_at).abs
23
+ limit = @policy.system.max_clock_skew_seconds
24
+ results << finding(skew > limit ? :fail : :pass, :clock_skew, entity: entity,
25
+ actual: { time: record[:time], skew_seconds: skew }, expected: { maximum_seconds: limit }, severity: skew > limit ? :warning : :info)
26
+ results
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 区分可能重叠的累计指标;仅在采样连续性得到证明时计算增量。
8
+ class Counters < Base
9
+ # 按稳定计数器身份索引前次采样,使比较保持线性复杂度。
10
+ def initialize(**options)
11
+ super
12
+ @previous_counters = @previous&.observation(dimension)
13
+ @previous_records = @previous_counters&.records&.to_h { |row| [ row.values_at(*identity_fields), row ] } || {}
14
+ end
15
+
16
+ private
17
+
18
+ # 按策略评估资源,同时保留测量值缺失的状态。
19
+ def assess(record)
20
+ entity = record.slice(*identity_fields)
21
+ value = record[:value]
22
+ return finding(:unknown, :counter_unavailable, entity: entity) if value.nil?
23
+ previous = @previous_counters
24
+ prior = @previous_records[record.values_at(*identity_fields)]
25
+ if (reason = packet_semantics_reason(record, prior))
26
+ return finding(:unknown, reason, entity: entity, actual: record.slice(:value, :unit, :scope))
27
+ end
28
+ unless prior
29
+ # 单次非零累计值无法证明当前窗口新增错误,等待可比较的前次采样。
30
+ return finding(value.zero? ? :pass : :unknown, value.zero? ? :zero_cumulative_counter : :cumulative_errors,
31
+ entity: entity, actual: value, severity: value.zero? ? :info : :warning)
32
+ end
33
+ interval = sample_time(record, @observation, @snapshot) - sample_time(prior, previous, @previous)
34
+ # 先验证时间与重置标记;数值下降可能是清零或重启,不能按负增量计算。
35
+ reason = if interval <= 0
36
+ :counter_sample_time_invalid
37
+ elsif record[:discontinuity].nil? || prior[:discontinuity].nil?
38
+ :counter_continuity_unconfirmed
39
+ elsif record[:discontinuity] != prior[:discontinuity] || prior[:value].nil? || value < prior[:value]
40
+ :counter_discontinuity
41
+ end
42
+ return finding(:unknown, reason, entity: entity, actual: value, severity: :warning) if reason
43
+
44
+ delta = value - prior[:value]
45
+ limit = limit_for(record)
46
+ # 超限是已观察故障;未超限仍需前后来源完整才能证明通过。
47
+ if delta <= limit && (!record_complete?(prior, previous) || !record_complete?(record, @observation))
48
+ return finding(:unknown, :counter_evidence_incomplete, entity: entity, actual: { delta: delta, seconds: interval })
49
+ end
50
+ finding(delta > limit ? :fail : :pass, :counter_delta, entity: entity,
51
+ actual: delta_measurement(value, delta, interval), expected: { maximum_delta: limit }, severity: :warning)
52
+ end
53
+
54
+ # 选择当前评估负责的观测维度。
55
+ def dimension = :counters
56
+ # 返回用于跨采样匹配记录的稳定身份字段。
57
+ def identity_fields = Observation::IDENTITIES.fetch(dimension)
58
+ # 优先使用资源专用阈值,再回退到策略默认值。
59
+ def limit_for(record) = @policy.counter_limits.fetch(record[:metric], 0)
60
+ # 将已计算增量与采样间隔组织为报告测量值。
61
+ def delta_measurement(_value, delta, interval) = { delta: delta, seconds: interval }
62
+ # 解析速率或增量分析所需的采样时间。
63
+ def sample_time(record, observation, snapshot) = record[:sampled_at] ? Time.iso8601(record[:sampled_at]) : observation.observed_at || snapshot.observed_at
64
+
65
+ # 包数阈值不能比较字节、事件计数,也不能比较测量语义已变化的采样。
66
+ def packet_semantics_reason(record, prior)
67
+ return unless dimension == :counters
68
+ return :counter_unit_unknown if record[:unit] == :unknown
69
+ return :counter_unit_not_packets unless record[:unit].nil? || record[:unit] == :packets
70
+ return :counter_scope_unknown if record[:scope] == :unknown
71
+ return unless prior
72
+ return :counter_unit_changed if record[:unit] != prior[:unit]
73
+ :counter_scope_changed if record[:scope] != prior[:scope]
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "counters"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 区分设备级指标;策略主动丢弃仅在运维明确配置阈值时评估。
8
+ class DeviceCounters < Counters
9
+ private
10
+
11
+ # 按策略评估资源,同时保留测量值缺失的状态。
12
+ def assess(record)
13
+ unless @policy.system.device_counter_limits.key?(record[:metric])
14
+ case record[:category]
15
+ when :policy, :informational
16
+ return finding(:not_applicable, :device_counter_context, entity: record.slice(*identity_fields), actual: record)
17
+ when nil, :unknown
18
+ return finding(:unknown, :counter_category_unavailable, entity: record.slice(*identity_fields), actual: record)
19
+ end
20
+ end
21
+ super
22
+ end
23
+
24
+ # 选择当前评估负责的观测维度。
25
+ def dimension = :device_counters
26
+ # 优先使用资源专用阈值,再回退到策略默认值。
27
+ def limit_for(record) = @policy.system.device_counter_limits.fetch(record[:metric], 0)
28
+ # 组织累计量、已验证增量及采样间隔,并计算每秒速率。
29
+ def delta_measurement(value, delta, interval) = { cumulative: value, delta: delta, seconds: interval, per_second: delta / interval }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 区分可选空槽位、组件故障与策略明确要求的组件。
8
+ class Hardware < Base
9
+ private
10
+
11
+ # 按策略评估资源,同时保留测量值缺失的状态。
12
+ def assess(record)
13
+ entity = record.slice(:component, :kind)
14
+ state = record[:status]
15
+ if record[:watts] && record[:capacity_watts] && record[:watts] > record[:capacity_watts]
16
+ return [ finding(:fail, :power_capacity_exceeded, entity: entity,
17
+ actual: record[:watts], expected: { maximum_watts: record[:capacity_watts] }, severity: :critical),
18
+ finding(:unknown, :power_measurements_inconsistent, entity: entity, actual: record) ]
19
+ end
20
+ if %i[warning critical failed].include?(state)
21
+ return finding(:fail, :hardware_fault, entity: entity, actual: record, severity: state == :warning ? :warning : :critical)
22
+ end
23
+ if record[:present] == false || state == :absent
24
+ if state == :ok || record[:present] == true || record[:watts]&.positive?
25
+ return finding(:unknown, :hardware_presence_inconsistent, entity: entity, actual: record)
26
+ end
27
+ required = required_components.include?(record[:component])
28
+ return finding(required ? :fail : :not_applicable, :hardware_absent, entity: entity,
29
+ actual: record, severity: required ? :critical : :info)
30
+ end
31
+ finding(state == :ok ? :pass : :unknown, :hardware_state, entity: entity, actual: record)
32
+ end
33
+
34
+ # 解析策略明确要求的环境组件。
35
+ def required_components = @policy.system.required_components.fetch(@observation.name, [])
36
+
37
+ # 为策略要求但未观察到的资源生成证据缺失结果。
38
+ def missing
39
+ (required_components - @observation.records.map { |row| row[:component] }).map do |component|
40
+ finding(missing_status, :required_component_missing, entity: { component: component }, severity: :critical)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 评估物理与逻辑链路,同时尊重明确的管理关闭意图。
8
+ class Interfaces < Base
9
+ private
10
+
11
+ # 按策略评估资源,同时保留测量值缺失的状态。
12
+ def assess(record)
13
+ name = record[:name]
14
+ required = @policy.required_interfaces.include?(name)
15
+ status, reason = if record[:admin_status] == :down && !required
16
+ [ :not_applicable, :administratively_down ]
17
+ elsif record[:oper_status] == :down || record[:protocol_status] == :down && record[:protocol_applicable] != false || required && record[:admin_status] == :down
18
+ [ :fail, :interface_down ]
19
+ elsif record[:oper_status] == :up && (record[:protocol_applicable] == false || record[:protocol_status] == :up || %i[physical aggregate].include?(record[:kind]))
20
+ [ :pass, :interface_up ]
21
+ else
22
+ [ :unknown, :interface_state_unavailable ]
23
+ end
24
+ finding(status, reason, entity: name, actual: record, severity: required ? :critical : :warning)
25
+ end
26
+
27
+ # 为策略要求但未观察到的资源生成证据缺失结果。
28
+ def missing
29
+ (@policy.required_interfaces - @observation.records.map { |row| row[:name] }).map do |name|
30
+ finding(missing_status, :required_interface_missing, entity: name, severity: :critical)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module CtxChecker
6
+ module Checks
7
+ # 设备可达或已配置对等体不能代替系统同步证据。
8
+ class Ntp < Base
9
+ private
10
+
11
+ # 按策略评估资源,同时保留测量值缺失的状态。
12
+ def assess(record)
13
+ entity = { component: record[:component] }
14
+ return peer_attempt(record, entity) unless record[:component] == "system"
15
+
16
+ unsynchronized = record[:synchronized] == false || record[:stratum] == 16 || record[:leap] == 3
17
+ status = unsynchronized ? :fail : record[:synchronized] == true && record[:stratum] != 0 ? :pass : :unknown
18
+ results = [ finding(status,
19
+ :ntp_synchronization, entity: entity, actual: record, severity: unsynchronized ? :warning : :info) ]
20
+ if record[:synchronized] == true && (unsynchronized || record[:stratum] == 0)
21
+ results << finding(:unknown, :ntp_state_inconsistent, entity: entity)
22
+ end
23
+ if record[:offset_ms]
24
+ exceeded = record[:offset_ms].abs > @policy.system.max_ntp_offset_ms
25
+ results << finding(exceeded ? :fail : :pass, :ntp_offset, entity: entity, actual: record[:offset_ms],
26
+ expected: { maximum_absolute_ms: @policy.system.max_ntp_offset_ms }, severity: exceeded ? :warning : :info)
27
+ end
28
+ results
29
+ end
30
+
31
+ # 可独立报告对等体尝试失败,但不能据此断言系统已失去同步。
32
+ def peer_attempt(record, entity)
33
+ if record[:last_sync_status] == :failed
34
+ finding(:fail, :ntp_last_sync_failed, entity: entity, actual: record, severity: :warning)
35
+ else
36
+ finding(:not_applicable, :ntp_peer_context, entity: entity, actual: record)
37
+ end
38
+ end
39
+
40
+ # 为策略要求但未观察到的资源生成证据缺失结果。
41
+ def missing
42
+ return [] if @observation.records.any? { |row| row[:component] == "system" }
43
+ [ finding(:unknown, :ntp_system_state_unavailable) ]
44
+ end
45
+ end
46
+ end
47
+ end