ctx_checker 0.1.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README-zh.md +10 -0
- data/README.md +10 -0
- data/docs/architecture-zh.md +2 -2
- data/lib/ctx_checker/checks/base.rb +4 -4
- data/lib/ctx_checker/checks/clock.rb +3 -3
- data/lib/ctx_checker/checks/counters.rb +7 -7
- data/lib/ctx_checker/checks/device_counters.rb +1 -1
- data/lib/ctx_checker/checks/interfaces.rb +1 -1
- data/lib/ctx_checker/checks/optics.rb +1 -1
- data/lib/ctx_checker/checks/temperature.rb +2 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +3 -3
- data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +6 -3
- data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +19 -5
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +3 -2
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +5 -5
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +3 -3
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +1 -1
- data/lib/ctx_checker/cisco_ios/profile.rb +3 -3
- data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +3 -3
- data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +13 -8
- data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +2 -2
- data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +8 -5
- data/lib/ctx_checker/cisco_nxos/profile.rb +3 -3
- data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +2 -2
- data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +3 -2
- data/lib/ctx_checker/h3c/parsers/display_interface.rb +2 -2
- data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +2 -2
- data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +1 -1
- data/lib/ctx_checker/h3c/profile.rb +3 -3
- data/lib/ctx_checker/hillstone/parsers/show_environment.rb +3 -3
- data/lib/ctx_checker/hillstone/parsers/show_interface.rb +2 -2
- data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +2 -2
- data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +1 -1
- data/lib/ctx_checker/hillstone/profile.rb +3 -3
- data/lib/ctx_checker/observation.rb +13 -8
- data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +3 -3
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +4 -4
- data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +3 -3
- data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +3 -3
- data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +1 -1
- data/lib/ctx_checker/palo_alto/profile.rb +3 -3
- data/lib/ctx_checker/parsers/base.rb +11 -4
- data/lib/ctx_checker/parsers/diagnostics.rb +47 -0
- data/lib/ctx_checker/policy.rb +20 -12
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +3 -3
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +4 -4
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +3 -3
- data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +3 -3
- data/lib/ctx_checker/radware_alteon/profile.rb +9 -9
- data/lib/ctx_checker/runtime_parser.rb +16 -10
- data/lib/ctx_checker/{system_record.rb → system_fields.rb} +3 -3
- data/lib/ctx_checker/system_policy.rb +14 -14
- data/lib/ctx_checker/version.rb +1 -1
- data/lib/ctx_checker.rb +1 -1
- metadata +11 -10
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
# 解析 show transceiver all,保留不完整或不支持的证据。
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
text = clean(text)
|
|
17
|
-
rows, issues, header = [],
|
|
17
|
+
rows, issues, header = [], diagnostics, false
|
|
18
18
|
text.each_line do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if /\APort\s+Temperature\s+Voltage\s+Current\s+Tx Power\s+Rx Power\z/i.match?(line)
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
module PaloAlto
|
|
19
19
|
# 声明传统路由引擎查询;高级路由格式需要独立平台策略。
|
|
20
20
|
class Profile < CtxParser::Profile
|
|
21
|
-
|
|
21
|
+
DECLARATIONS = CtxParser::Commands.new([
|
|
22
22
|
{ dimensions: [ :interfaces ], command: "show interface all", parser: Parsers::ShowInterfaceAll },
|
|
23
23
|
{ dimensions: [ :counters ], command: "show counter interface all", parser: Parsers::ShowCounterInterfaceAll },
|
|
24
24
|
{ dimensions: [ :aggregation ], command: "show lacp aggregate-ethernet all", parser: Parsers::ShowLacpAggregateEthernet },
|
|
@@ -32,8 +32,8 @@ module CtxChecker
|
|
|
32
32
|
{ dimensions: [ :alarms ], command: "show system state | match alarm", parser: Parsers::ShowSystemStateAlarms },
|
|
33
33
|
{ dimensions: [ :device_counters ], command: "show counter global", parser: Parsers::ShowCounterGlobal }
|
|
34
34
|
])
|
|
35
|
-
COMMANDS =
|
|
36
|
-
PARSERS =
|
|
35
|
+
COMMANDS = DECLARATIONS.by_dimension
|
|
36
|
+
PARSERS = DECLARATIONS.parsers
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -4,6 +4,7 @@ require "ctx_parser"
|
|
|
4
4
|
require "ctx_parser/input"
|
|
5
5
|
require_relative "interface_names"
|
|
6
6
|
require_relative "scoped_result"
|
|
7
|
+
require_relative "diagnostics"
|
|
7
8
|
|
|
8
9
|
module CtxChecker
|
|
9
10
|
module Parsers
|
|
@@ -13,27 +14,33 @@ module CtxChecker
|
|
|
13
14
|
|
|
14
15
|
private
|
|
15
16
|
|
|
17
|
+
# 各命令和共享视图使用相同的诊断预算。
|
|
18
|
+
def diagnostics = Diagnostics.new
|
|
19
|
+
|
|
16
20
|
# 从归一化观测记录中移除不支持的数据。
|
|
17
21
|
def clean(text)
|
|
18
|
-
@issues =
|
|
22
|
+
@issues = diagnostics
|
|
19
23
|
@unsupported = false
|
|
24
|
+
@command_failed = false
|
|
20
25
|
source = CtxParser::Input.text(text, encoding: :transcode, newlines: :lf, max_bytes: MAX_BYTES)
|
|
21
26
|
source.each_line.with_index(1) do |line, number|
|
|
22
27
|
if /\A\s*(?:(?:%\s*)?(?:Error:\s*)?(?:Invalid (?:input|command|syntax)|Unrecognized command|Unknown command|command not (?:found|supported)|not supported on this|syntax error)|\^-+\s*unrecognized keyword\b)/i.match?(line)
|
|
23
28
|
@unsupported = true
|
|
24
29
|
@issues << { code: :unsupported_command, line: number }
|
|
25
30
|
elsif /\A\s*(?:%\s*(?:Error|Incomplete|Ambiguous)|Error:|Permission denied|Insufficient privilege|\^-+\s*(?:incomplete command|ambiguous command|error\b))/i.match?(line)
|
|
31
|
+
@command_failed = true
|
|
26
32
|
@issues << { code: :command_error, line: number }
|
|
27
33
|
elsif /--\s*More\s*--|----\s*More\s*----|Press (?:any key|ENTER|<SPACE>)/i.match?(line)
|
|
28
34
|
@issues << { code: :paginated_output, line: number }
|
|
29
35
|
end
|
|
30
36
|
end
|
|
31
|
-
|
|
37
|
+
# 明确失败的命令不会保留记录,无需再次逐行解析其错误正文。
|
|
38
|
+
@unsupported || @command_failed ? "" : source
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
# 隔离身份冲突,保留无歧义记录并降低覆盖完整性。
|
|
35
42
|
def build_result(records, complete:, recognized:, issues: [], identity: nil, covered_scopes: [])
|
|
36
|
-
problems = Array(@issues)
|
|
43
|
+
problems = diagnostics.concat(Array(@issues)).concat(issues)
|
|
37
44
|
if identity
|
|
38
45
|
grouped = CtxParser::Records.new(records, identity: identity)
|
|
39
46
|
if grouped.duplicates?
|
|
@@ -46,7 +53,7 @@ module CtxChecker
|
|
|
46
53
|
status = if @unsupported
|
|
47
54
|
records = []
|
|
48
55
|
:unsupported
|
|
49
|
-
elsif
|
|
56
|
+
elsif @command_failed
|
|
50
57
|
records = []
|
|
51
58
|
:failed
|
|
52
59
|
elsif !recognized && records.empty?
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CtxChecker
|
|
4
|
+
module Parsers
|
|
5
|
+
# 解析期只保留有界诊断样本;省略数量仍进入结果,不能将截断当作完整。
|
|
6
|
+
class Diagnostics < Array
|
|
7
|
+
MAX_SAMPLES = 128
|
|
8
|
+
|
|
9
|
+
# 每个解析视图独立拥有样本和省略计数。
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
@samples = 0
|
|
13
|
+
@summary = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 合并多个来源时累加原有省略数,不把摘要误计为一条普通诊断。
|
|
17
|
+
def <<(issue)
|
|
18
|
+
if issue[:code] == :diagnostics_truncated
|
|
19
|
+
omit(issue[:count])
|
|
20
|
+
elsif @samples < MAX_SAMPLES
|
|
21
|
+
insert(@samples, issue)
|
|
22
|
+
@samples += 1
|
|
23
|
+
else
|
|
24
|
+
omit(1)
|
|
25
|
+
end
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# 跨视图合并仍经过同一预算,不恢复已省略的诊断。
|
|
30
|
+
def concat(issues)
|
|
31
|
+
issues.each { |issue| self << issue }
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# 原地更新本集合持有的摘要,避免每次省略都创建一个对象。
|
|
38
|
+
def omit(count)
|
|
39
|
+
unless @summary
|
|
40
|
+
@summary = { code: :diagnostics_truncated, count: 0 }
|
|
41
|
+
self[size] = @summary
|
|
42
|
+
end
|
|
43
|
+
@summary[:count] += count
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/ctx_checker/policy.rb
CHANGED
|
@@ -15,22 +15,27 @@ module CtxChecker
|
|
|
15
15
|
@system = SystemPolicy.new(resource_thresholds: resource_thresholds, temperature_thresholds: temperature_thresholds,
|
|
16
16
|
required_components: required_components, required_dimensions: required_dimensions, max_clock_skew_seconds: max_clock_skew_seconds,
|
|
17
17
|
max_ntp_offset_ms: max_ntp_offset_ms, device_counter_limits: device_counter_limits)
|
|
18
|
-
|
|
19
|
-
unless required_members.is_a?(Hash) && required_members.all? { |group, members| text?(group) &&
|
|
18
|
+
validate_interface_names!(required_interfaces)
|
|
19
|
+
unless required_members.is_a?(Hash) && required_members.all? { |group, members| text?(group) && validate_interface_names!(members) }
|
|
20
20
|
raise ArgumentError, "Required members must map groups to interface names"
|
|
21
21
|
end
|
|
22
22
|
unless minimum_active_members.is_a?(Hash) && minimum_active_members.all? { |group, count| text?(group) && count.is_a?(Integer) && count.positive? }
|
|
23
23
|
raise ArgumentError, "Minimum active members must be positive integers"
|
|
24
24
|
end
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
validate_neighbor_identities!(expected_bgp, %i[vrf afi_safi peer])
|
|
26
|
+
validate_neighbor_identities!(expected_ospf, %i[vrf process_id interface router_id])
|
|
27
|
+
expected_ospf = expected_ospf.map do |identity|
|
|
28
|
+
identity[:process_id].is_a?(Integer) ? identity.merge(process_id: identity[:process_id].to_s) : identity
|
|
29
|
+
end
|
|
30
|
+
validate_neighbor_identities!(expected_ospf, %i[vrf process_id interface router_id])
|
|
27
31
|
unless counter_limits.is_a?(Hash) && counter_limits.all? { |metric, count| Observation::METRICS.include?(metric) && count.is_a?(Integer) && count >= 0 }
|
|
28
32
|
raise ArgumentError, "Counter limits must be nonnegative integer deltas"
|
|
29
33
|
end
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
validate_optical_thresholds!(optical_thresholds)
|
|
35
|
+
validate_required_optics!(required_optics)
|
|
32
36
|
@required_interfaces, @required_members, @minimum_active_members, @expected_bgp, @expected_ospf, @counter_limits =
|
|
33
37
|
[ required_interfaces, required_members, minimum_active_members, expected_bgp, expected_ospf, counter_limits ].map { |value| CtxParser::Immutable.copy(value) }
|
|
38
|
+
@required_interface_index = @required_interfaces.to_h { |name| [ name, true ] }.freeze
|
|
34
39
|
@optical_thresholds = CtxParser::Immutable.copy(optical_thresholds)
|
|
35
40
|
@optical_index = @optical_thresholds.to_h { |row| [ row.values_at(:interface, :lane, :metric).freeze, row ] }.freeze
|
|
36
41
|
@required_optics = CtxParser::Immutable.copy(required_optics)
|
|
@@ -38,14 +43,17 @@ module CtxChecker
|
|
|
38
43
|
freeze
|
|
39
44
|
end
|
|
40
45
|
|
|
46
|
+
# 按接口身份查询策略要求,避免逐条测量重复遍历列表。
|
|
47
|
+
def required_interface?(name) = @required_interface_index.key?(name)
|
|
48
|
+
|
|
41
49
|
# 策略仅替换精确光通道及指标的阈值,不改变设备告警证据。
|
|
42
50
|
def optical_thresholds_for(record) = @optical_index[record.values_at(:interface, :lane, :metric)]
|
|
43
51
|
# 检查策略是否要求此接口提供光学诊断。
|
|
44
52
|
def required_optics?(record) = @required_optical_index.key?(record.values_at(:interface, :lane, :metric))
|
|
45
53
|
|
|
46
54
|
# 检查策略是否明确要求此资源或功能。
|
|
47
|
-
def
|
|
48
|
-
return true if system.
|
|
55
|
+
def required_dimension?(name)
|
|
56
|
+
return true if system.required_dimension?(name)
|
|
49
57
|
case name
|
|
50
58
|
when :interfaces then required_interfaces.any?
|
|
51
59
|
when :aggregation then required_members.any? || minimum_active_members.any?
|
|
@@ -62,13 +70,13 @@ module CtxChecker
|
|
|
62
70
|
def text?(value) = value.is_a?(String) && !value.strip.empty? && !/[\x00-\x1f\x7f]/.match?(value)
|
|
63
71
|
|
|
64
72
|
# 校验策略选择器使用的资源名称是否有效且不重复。
|
|
65
|
-
def
|
|
73
|
+
def validate_interface_names!(values)
|
|
66
74
|
raise ArgumentError, "Interface names must be unique text" unless values.is_a?(Array) && values.all? { |value| text?(value) } && values.uniq == values
|
|
67
75
|
true
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
# 校验协议对等体身份及对应策略要求。
|
|
71
|
-
def
|
|
79
|
+
def validate_neighbor_identities!(values, fields)
|
|
72
80
|
unless values.is_a?(Array) && values.uniq == values && values.all? do |row|
|
|
73
81
|
row.is_a?(Hash) && row.keys.sort == fields.sort && row.all? { |key, value| text?(value) || key == :process_id && value.is_a?(Integer) && value >= 0 }
|
|
74
82
|
end
|
|
@@ -77,7 +85,7 @@ module CtxChecker
|
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
# 校验光功率阈值及必需接口选择。
|
|
80
|
-
def
|
|
88
|
+
def validate_optical_thresholds!(values)
|
|
81
89
|
fields = %i[interface lane metric] + Observation::OPTICAL_THRESHOLDS
|
|
82
90
|
unless values.is_a?(Array) && values.all? { |row| row.is_a?(Hash) && (row.keys - fields).empty? }
|
|
83
91
|
raise ArgumentError, "Optical policies require scoped threshold records"
|
|
@@ -93,7 +101,7 @@ module CtxChecker
|
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
# 必需测量必须有明确身份;仅配置阈值不表示要求资源存在。
|
|
96
|
-
def
|
|
104
|
+
def validate_required_optics!(values)
|
|
97
105
|
fields = %i[interface lane metric]
|
|
98
106
|
unless values.is_a?(Array) && values.all? { |row| row.is_a?(Hash) && row.keys.sort == fields }
|
|
99
107
|
raise ArgumentError, "Required optics need exact interface, lane and metric identities"
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
|
|
15
15
|
# 活跃模式需要额外运行证据,不能由重复的 trunk 记录替代。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
|
-
issues, ports, header =
|
|
17
|
+
issues, ports, header = diagnostics, {}, false
|
|
18
18
|
clean(text).each_line do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
@@ -26,8 +26,8 @@ module CtxChecker
|
|
|
26
26
|
next
|
|
27
27
|
elsif header && (match = ROW.match(line)) && match[1].to_i.positive?
|
|
28
28
|
port = "port#{Integer(match[1], 10)}"
|
|
29
|
-
issues << { code: :duplicate_lacp_port, interface: port } if ports.
|
|
30
|
-
ports
|
|
29
|
+
issues << { code: :duplicate_lacp_port, interface: port } if ports.key?(port)
|
|
30
|
+
ports[port] = true
|
|
31
31
|
if match[2].casecmp?("off")
|
|
32
32
|
unless match.captures[2..] == %w[n -- -- n]
|
|
33
33
|
issues << { code: :inconsistent_lacp_disabled_state, interface: port }
|
|
@@ -14,15 +14,15 @@ module CtxChecker
|
|
|
14
14
|
|
|
15
15
|
# 将成员限定在其所属 trunk 中,明确保留不完整聚合组。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
|
-
rows, issues, groups, current = [],
|
|
17
|
+
rows, issues, groups, current = [], diagnostics, {}, nil
|
|
18
18
|
clean(text).each_line do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
21
21
|
|
|
22
22
|
if (match = GROUP.match(line)) && match[1].to_i.positive?
|
|
23
23
|
current = "trunk#{Integer(match[1], 10)}"
|
|
24
|
-
issues << { code: :duplicate_aggregation_header, group: current } if groups.
|
|
25
|
-
groups
|
|
24
|
+
issues << { code: :duplicate_aggregation_header, group: current } if groups.key?(current)
|
|
25
|
+
groups[current] = true
|
|
26
26
|
elsif current && (match = MEMBER.match(line)) && match[1].to_i.positive?
|
|
27
27
|
member = "port#{Integer(match[1], 10)}"
|
|
28
28
|
state = match[2].downcase
|
|
@@ -34,7 +34,7 @@ module CtxChecker
|
|
|
34
34
|
issues << { code: :unrecognized_aggregation_line }
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
|
-
(groups.
|
|
37
|
+
(groups.keys - rows.map { |row| row[:group] }).each do |group|
|
|
38
38
|
rows << { group: group, member: nil, member_status: :unknown, oper_status: :unknown }
|
|
39
39
|
issues << { code: :aggregation_members_unavailable, group: group }
|
|
40
40
|
end
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
|
|
15
15
|
# 将接口行限定在明确章节内,排除带编号的网关记录。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
|
-
rows, issues, section = [],
|
|
17
|
+
rows, issues, section = [], diagnostics, :preamble
|
|
18
18
|
header, interfaces, ipv6, closed = false, false, false, false
|
|
19
19
|
clean(text).each_line do |raw|
|
|
20
20
|
line = raw.strip
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
|
|
15
15
|
# 链路行缺失不能证明端口已禁用或健康。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
|
-
rows, issues, header = [],
|
|
17
|
+
rows, issues, header = [], diagnostics, false
|
|
18
18
|
clean(text).each_line do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty? || /\A[-\s]+\z/.match?(line)
|
|
@@ -17,7 +17,7 @@ module CtxChecker
|
|
|
17
17
|
|
|
18
18
|
# 指定光学方向或单位前,先校验多行表头。
|
|
19
19
|
def parse_output(text, complete:)
|
|
20
|
-
rows, issues, headings = [],
|
|
20
|
+
rows, issues, headings = [], diagnostics, []
|
|
21
21
|
clean(text).each_line do |raw|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
next if line.empty? || /\A[-\s]+\z/.match?(line)
|
|
@@ -17,7 +17,7 @@ module CtxChecker
|
|
|
17
17
|
|
|
18
18
|
# 将各 IEEE 计数器绑定到其明确的数据端口标题。
|
|
19
19
|
def parse_output(text, complete:)
|
|
20
|
-
blocks, issues, current = [],
|
|
20
|
+
blocks, issues, current = [], diagnostics, nil
|
|
21
21
|
clean(text).each_line do |raw|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
|
|
17
17
|
# 解释方向性测量前,要求存在带标签的累计计数列。
|
|
18
18
|
def parse_output(text, complete:)
|
|
19
|
-
blocks, issues, current, section = [],
|
|
19
|
+
blocks, issues, current, section = [], diagnostics, nil, nil
|
|
20
20
|
clean(text).each_line do |raw|
|
|
21
21
|
line = raw.strip
|
|
22
22
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../parsers/base"
|
|
4
|
-
require "ctx_parser/
|
|
4
|
+
require "ctx_parser/results"
|
|
5
5
|
|
|
6
6
|
module CtxChecker
|
|
7
7
|
module RadwareAlteon
|
|
@@ -13,7 +13,7 @@ module CtxChecker
|
|
|
13
13
|
# 校验回显的 SP 数量及汇总图,不混用 CPU 和内存数据。
|
|
14
14
|
def parse_output(text, complete:)
|
|
15
15
|
rows = { cpu: [], memory: [] }
|
|
16
|
-
issues = rows.transform_values {
|
|
16
|
+
issues = rows.transform_values { diagnostics }
|
|
17
17
|
sections, current = [], nil
|
|
18
18
|
clean(text).each_line.with_index(1) do |raw, number|
|
|
19
19
|
line = raw.strip
|
|
@@ -33,7 +33,7 @@ module CtxChecker
|
|
|
33
33
|
end
|
|
34
34
|
sections.each { |item| validate_section(item, issues.fetch(item[:dimension])) }
|
|
35
35
|
validate_sections(sections, issues)
|
|
36
|
-
CtxParser::
|
|
36
|
+
CtxParser::Results.new(by_dimension: rows.to_h do |dimension, records|
|
|
37
37
|
identity = dimension == :cpu ? %i[component scope window_seconds] : %i[component scope]
|
|
38
38
|
[ dimension, build_result(records, complete: complete, recognized: records.any?, issues: issues.fetch(dimension), identity: identity) ]
|
|
39
39
|
end)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
4
|
require_relative "../../parsers/base"
|
|
5
|
-
require "ctx_parser/
|
|
5
|
+
require "ctx_parser/results"
|
|
6
6
|
|
|
7
7
|
module CtxChecker
|
|
8
8
|
module RadwareAlteon
|
|
@@ -17,7 +17,7 @@ module CtxChecker
|
|
|
17
17
|
def parse_output(text, complete:)
|
|
18
18
|
source = clean(text)
|
|
19
19
|
rows, headers, sections, time_fields = [], 0, [], []
|
|
20
|
-
counter_issues, ntp_issues, section =
|
|
20
|
+
counter_issues, ntp_issues, section = diagnostics, diagnostics, nil
|
|
21
21
|
source.each_line.with_index(1) do |raw, number|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
@@ -52,7 +52,7 @@ module CtxChecker
|
|
|
52
52
|
ntp_issues << { code: :ambiguous_ntp_time_field, field: field, count: time_fields.count(field) } unless time_fields.count(field) == 1
|
|
53
53
|
end
|
|
54
54
|
ntp_rows = headers.positive? ? [ { component: "system", synchronized: nil, last_sync_status: :unknown, last_sync_at: nil } ] : []
|
|
55
|
-
CtxParser::
|
|
55
|
+
CtxParser::Results.new(by_dimension: {
|
|
56
56
|
ntp: build_result(ntp_rows, complete: complete, recognized: headers.positive?, issues: ntp_issues, identity: [ :component ]),
|
|
57
57
|
device_counters: build_result(rows, complete: complete, recognized: rows.any?, issues: counter_issues, identity: %i[component metric])
|
|
58
58
|
})
|
|
@@ -25,7 +25,7 @@ module CtxChecker
|
|
|
25
25
|
# Alteon 菜单查询独立于其他 Radware 产品系列。
|
|
26
26
|
class Profile < CtxParser::Profile
|
|
27
27
|
MAX_DISCOVERED_PORTS = 256
|
|
28
|
-
|
|
28
|
+
DECLARATIONS = CtxParser::Commands.new([
|
|
29
29
|
{ dimensions: [ :interfaces ], command: "/info/link", parser: Parsers::InfoLink },
|
|
30
30
|
{ dimensions: [ :interfaces ], command: "/info/l3/ip", parser: Parsers::InfoL3Ip },
|
|
31
31
|
{ dimensions: [ :aggregation ], command: "/info/l2/trunk", parser: Parsers::InfoL2Trunk },
|
|
@@ -41,33 +41,33 @@ module CtxChecker
|
|
|
41
41
|
{ dimensions: [ :clock ], command: "/info/sys/general", parser: Parsers::InfoSysGeneral },
|
|
42
42
|
{ dimensions: %i[ntp device_counters], command: "/stats/sys/ntp", parser: Parsers::StatsSysNtp }
|
|
43
43
|
], multi_command: true)
|
|
44
|
-
COMMANDS =
|
|
45
|
-
PARSERS =
|
|
44
|
+
COMMANDS = DECLARATIONS.by_dimension
|
|
45
|
+
PARSERS = DECLARATIONS.parsers
|
|
46
46
|
|
|
47
47
|
# 打开连接器前,拒绝已知不兼容的产品系列。
|
|
48
48
|
def initialize(model: nil)
|
|
49
49
|
super
|
|
50
50
|
raise ArgumentError, "DefensePro requires a separate checker profile" if /defen[cs]e[\s_-]*pro/i.match?(model.to_s)
|
|
51
51
|
|
|
52
|
-
@
|
|
52
|
+
@declarations = DECLARATIONS
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# 扩展后的实例保留不可变命令与实现索引。
|
|
56
|
-
def commands = @
|
|
56
|
+
def commands = @declarations.by_dimension
|
|
57
57
|
|
|
58
58
|
# 绑定端口的解析器复用基础型号覆盖及命令选择契约。
|
|
59
|
-
def parsers = @
|
|
59
|
+
def parsers = @declarations.parsers
|
|
60
60
|
|
|
61
61
|
# 已知光模块命令缺陷根据固件事实判断,不依据型号名称猜测。
|
|
62
62
|
def with_software_version(value)
|
|
63
63
|
raise ArgumentError, "Software version must be text" unless value.nil? || value.is_a?(String)
|
|
64
64
|
|
|
65
|
-
entries = @
|
|
65
|
+
entries = @declarations.entries.reject { |entry| entry[:dimensions].include?(:optics) }
|
|
66
66
|
if transceiver_fixed?(value)
|
|
67
67
|
entries += [ { dimensions: [ :optics ], command: "/info/transceiver", parser: Parsers::InfoTransceiver } ]
|
|
68
68
|
end
|
|
69
69
|
expanded = dup
|
|
70
|
-
expanded.instance_variable_set(:@
|
|
70
|
+
expanded.instance_variable_set(:@declarations, CtxParser::Commands.new(entries, multi_command: true))
|
|
71
71
|
expanded
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -91,7 +91,7 @@ module CtxChecker
|
|
|
91
91
|
{ dimensions: [ :counters ], command: "/stats/port #{port}/ether", parser: Parsers::StatsPortEther.for_port(port) } ]
|
|
92
92
|
end
|
|
93
93
|
expanded = dup
|
|
94
|
-
expanded.instance_variable_set(:@
|
|
94
|
+
expanded.instance_variable_set(:@declarations, CtxParser::Commands.new(@declarations.entries + additions, multi_command: true))
|
|
95
95
|
expanded
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -39,11 +39,15 @@ module CtxChecker
|
|
|
39
39
|
|
|
40
40
|
return names.to_h { |name| [ name, unavailable_observation(name) ] }
|
|
41
41
|
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
unless command
|
|
43
|
+
candidates = names.map { |name| @implementations.fetch(name).keys }.reduce(:&)
|
|
44
|
+
command = candidates.first if candidates.one?
|
|
45
|
+
end
|
|
46
|
+
unless command && names.all? { |name| @implementations.fetch(name).key?(command) }
|
|
47
|
+
raise ArgumentError, "Select one declared command or use parse_outputs for multiple sources"
|
|
48
|
+
end
|
|
49
|
+
implementation = @implementations.fetch(names.first).fetch(command)
|
|
50
|
+
command_counts = names.to_h { |name| [ name, @implementations.fetch(name).size ] }
|
|
47
51
|
evidence = [ { command: command, sha256: Digest::SHA256.hexdigest(text), bytes: text.bytesize } ]
|
|
48
52
|
source = begin
|
|
49
53
|
normalized = CtxParser::Input.text(text, encoding: :transcode, newlines: :lf, max_bytes: Parsers::Base::MAX_BYTES)
|
|
@@ -178,19 +182,21 @@ module CtxChecker
|
|
|
178
182
|
unless profile.commands.is_a?(Hash) && profile.commands.any? && (profile.commands.keys - DIMENSIONS).empty?
|
|
179
183
|
raise ArgumentError, "Checker profile must declare supported dimensions"
|
|
180
184
|
end
|
|
181
|
-
profile.commands.
|
|
182
|
-
profile.commands_for(name).
|
|
185
|
+
@implementations = profile.commands.to_h do |name, _commands|
|
|
186
|
+
implementations = profile.commands_for(name).to_h do |command|
|
|
183
187
|
implementation = profile.parser_for(name, command: command)
|
|
184
188
|
unless implementation.is_a?(Class) && implementation < CtxParser::Parser
|
|
185
189
|
raise ArgumentError, "Command parser must inherit from Parser"
|
|
186
190
|
end
|
|
191
|
+
[ command, implementation ]
|
|
187
192
|
end
|
|
188
|
-
|
|
193
|
+
[ name, implementations.freeze ]
|
|
194
|
+
end.freeze
|
|
189
195
|
end
|
|
190
196
|
|
|
191
197
|
# 单个投影缺失不能使同一回显的其他视图失效。
|
|
192
198
|
def project(name, result, command:, command_count:, evidence:)
|
|
193
|
-
if result.is_a?(CtxParser::
|
|
199
|
+
if result.is_a?(CtxParser::Results)
|
|
194
200
|
begin
|
|
195
201
|
result = result.fetch(name)
|
|
196
202
|
rescue StandardError => error
|
|
@@ -203,7 +209,7 @@ module CtxChecker
|
|
|
203
209
|
|
|
204
210
|
# 来源状态归属于单个脚本,即使归并维度仅部分完整也独立保留。
|
|
205
211
|
def build_observation(name, result, command:, command_count:, evidence:)
|
|
206
|
-
raise ArgumentError, "Command parser must return a Result or
|
|
212
|
+
raise ArgumentError, "Command parser must return a Result or Results" unless result.is_a?(CtxParser::Result)
|
|
207
213
|
|
|
208
214
|
status, issues = result.status, result.issues.dup
|
|
209
215
|
# 仅明确的全功能声明可证明不适用;局部空邻居表不能证明。
|
|
@@ -5,8 +5,8 @@ require "date"
|
|
|
5
5
|
require "ctx_parser/immutable"
|
|
6
6
|
|
|
7
7
|
module CtxChecker
|
|
8
|
-
#
|
|
9
|
-
module
|
|
8
|
+
# 定义各系统观测维度的字段、身份及取值规则。
|
|
9
|
+
module SystemFields
|
|
10
10
|
FIELDS = CtxParser::Immutable.copy(
|
|
11
11
|
cpu: %i[component scope window_seconds utilization interrupt_utilization],
|
|
12
12
|
memory: %i[component scope utilization total_bytes used_bytes free_bytes available_bytes basis status],
|
|
@@ -34,7 +34,7 @@ module CtxChecker
|
|
|
34
34
|
CATEGORIES = %i[error discard policy informational unknown].freeze
|
|
35
35
|
|
|
36
36
|
# 校验测量值,不将不可用读数转换为零或 false。
|
|
37
|
-
def self.
|
|
37
|
+
def self.valid_value?(name, key, value)
|
|
38
38
|
case key
|
|
39
39
|
when :status then STATES.include?(value)
|
|
40
40
|
when :basis then BASES.include?(value)
|
|
@@ -12,46 +12,46 @@ module CtxChecker
|
|
|
12
12
|
# 校验并冻结运行限制及必需组件选择。
|
|
13
13
|
def initialize(resource_thresholds: {}, temperature_thresholds: {}, required_components: {}, required_dimensions: [],
|
|
14
14
|
max_clock_skew_seconds: 300, max_ntp_offset_ms: 100, device_counter_limits: {})
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
validate_resource_thresholds!(resource_thresholds)
|
|
16
|
+
validate_temperature_thresholds!(temperature_thresholds)
|
|
17
|
+
validate_required_components!(required_components)
|
|
18
18
|
unless required_dimensions.is_a?(Array) && required_dimensions.uniq == required_dimensions && (required_dimensions - Observation::NAMES).empty?
|
|
19
19
|
raise ArgumentError, "Required dimensions must be unique known names"
|
|
20
20
|
end
|
|
21
21
|
[ max_clock_skew_seconds, max_ntp_offset_ms ].each do |value|
|
|
22
|
-
raise ArgumentError, "Time limits must be finite and nonnegative" unless
|
|
22
|
+
raise ArgumentError, "Time limits must be finite and nonnegative" unless SystemFields.number?(value) && value >= 0
|
|
23
23
|
end
|
|
24
|
-
unless device_counter_limits.is_a?(Hash) && device_counter_limits.all? { |metric, limit|
|
|
24
|
+
unless device_counter_limits.is_a?(Hash) && device_counter_limits.all? { |metric, limit| SystemFields.text?(metric) && limit.is_a?(Integer) && limit >= 0 }
|
|
25
25
|
raise ArgumentError, "Device counter limits require named metrics and nonnegative integer deltas"
|
|
26
26
|
end
|
|
27
27
|
@resource_thresholds = CtxParser::Immutable.copy(DEFAULT_RESOURCES.merge(resource_thresholds))
|
|
28
28
|
@temperature_thresholds, @required_components, @required_dimensions, @device_counter_limits =
|
|
29
29
|
[ temperature_thresholds, required_components, required_dimensions, device_counter_limits ].map { |value| CtxParser::Immutable.copy(value) }
|
|
30
|
-
@max_clock_skew_seconds, @max_ntp_offset_ms = max_clock_skew_seconds, max_ntp_offset_ms
|
|
30
|
+
@max_clock_skew_seconds, @max_ntp_offset_ms = CtxParser::Immutable.copy([ max_clock_skew_seconds, max_ntp_offset_ms ])
|
|
31
31
|
freeze
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# 检查策略是否明确要求此资源或功能。
|
|
35
|
-
def
|
|
35
|
+
def required_dimension?(name) = required_dimensions.include?(name) || required_components.fetch(name, []).any?
|
|
36
36
|
|
|
37
37
|
private
|
|
38
38
|
|
|
39
39
|
# 两个资源阈值必须明确且有序,拒绝仅替换其中一个。
|
|
40
|
-
def
|
|
40
|
+
def validate_resource_thresholds!(values)
|
|
41
41
|
unless values.is_a?(Hash) && (values.keys - DEFAULT_RESOURCES.keys).empty? && values.values.all? do |row|
|
|
42
|
-
row.is_a?(Hash) && row.keys.sort == %i[critical warning] && row.values.all? { |value|
|
|
42
|
+
row.is_a?(Hash) && row.keys.sort == %i[critical warning] && row.values.all? { |value| SystemFields.number?(value) && value.between?(0, 100) } && row[:warning] <= row[:critical]
|
|
43
43
|
end
|
|
44
44
|
raise ArgumentError, "Resource thresholds require ordered warning and critical percentages"
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# 校验温度限制及各传感器的策略覆盖值。
|
|
49
|
-
def
|
|
49
|
+
def validate_temperature_thresholds!(values)
|
|
50
50
|
unless values.is_a?(Hash) && values.all? do |component, row|
|
|
51
|
-
|
|
51
|
+
SystemFields.text?(component) && row.is_a?(Hash) && (row.keys & Observation::OPTICAL_THRESHOLDS).any? &&
|
|
52
52
|
(row.keys - Observation::OPTICAL_THRESHOLDS - [ :high_comparison ]).empty? &&
|
|
53
53
|
(!row.key?(:high_comparison) || %i[gt gte].include?(row[:high_comparison])) &&
|
|
54
|
-
row.except(:high_comparison).values.all? { |value|
|
|
54
|
+
row.except(:high_comparison).values.all? { |value| SystemFields.number?(value) } &&
|
|
55
55
|
row.values_at(*Observation::OPTICAL_THRESHOLDS).compact.each_cons(2).all? { |low, high| low <= high }
|
|
56
56
|
end
|
|
57
57
|
raise ArgumentError, "Temperature policies require exact sensors and ordered Celsius limits"
|
|
@@ -59,9 +59,9 @@ module CtxChecker
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# 校验必需环境组件与允许的状态。
|
|
62
|
-
def
|
|
62
|
+
def validate_required_components!(values)
|
|
63
63
|
unless values.is_a?(Hash) && (values.keys - %i[temperature environment power]).empty? && values.values.all? do |names|
|
|
64
|
-
names.is_a?(Array) && names.uniq == names && names.all? { |name|
|
|
64
|
+
names.is_a?(Array) && names.uniq == names && names.all? { |name| SystemFields.text?(name) }
|
|
65
65
|
end
|
|
66
66
|
raise ArgumentError, "Required hardware components must be unique names by dimension"
|
|
67
67
|
end
|
data/lib/ctx_checker/version.rb
CHANGED
data/lib/ctx_checker.rb
CHANGED
|
@@ -7,7 +7,7 @@ module CtxChecker
|
|
|
7
7
|
{
|
|
8
8
|
Observation: "observation", Snapshot: "snapshot", Policy: "policy", Finding: "finding",
|
|
9
9
|
Report: "report", Evaluator: "evaluator", RuntimeParser: "runtime_parser", Collector: "collector", Applicability: "applicability",
|
|
10
|
-
SystemPolicy: "system_policy",
|
|
10
|
+
SystemPolicy: "system_policy", SystemFields: "system_fields", ObservationMerger: "observation_merger", Failure: "failure",
|
|
11
11
|
ParserRegistry: "parser_registry", Parsers: "parsers/base", H3c: "h3c/profile",
|
|
12
12
|
CiscoIos: "cisco_ios/profile", CiscoNxos: "cisco_nxos/profile", PaloAlto: "palo_alto/profile",
|
|
13
13
|
Hillstone: "hillstone/profile", RadwareAlteon: "radware_alteon"
|