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