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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README-zh.md +10 -0
  4. data/README.md +10 -0
  5. data/docs/architecture-zh.md +2 -2
  6. data/lib/ctx_checker/checks/base.rb +4 -4
  7. data/lib/ctx_checker/checks/clock.rb +3 -3
  8. data/lib/ctx_checker/checks/counters.rb +7 -7
  9. data/lib/ctx_checker/checks/device_counters.rb +1 -1
  10. data/lib/ctx_checker/checks/interfaces.rb +1 -1
  11. data/lib/ctx_checker/checks/optics.rb +1 -1
  12. data/lib/ctx_checker/checks/temperature.rb +2 -0
  13. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +1 -1
  14. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +3 -3
  15. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +6 -3
  16. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +1 -1
  17. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +19 -5
  18. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +3 -2
  19. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +5 -5
  20. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +3 -3
  21. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +1 -1
  22. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +1 -1
  23. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +1 -1
  24. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +1 -1
  25. data/lib/ctx_checker/cisco_ios/profile.rb +3 -3
  26. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +3 -3
  27. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +13 -8
  28. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +1 -1
  29. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +1 -1
  30. data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +2 -2
  31. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +8 -5
  32. data/lib/ctx_checker/cisco_nxos/profile.rb +3 -3
  33. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +2 -2
  34. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +3 -2
  35. data/lib/ctx_checker/h3c/parsers/display_interface.rb +2 -2
  36. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +1 -1
  37. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +2 -2
  38. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +1 -1
  39. data/lib/ctx_checker/h3c/profile.rb +3 -3
  40. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +3 -3
  41. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +2 -2
  42. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +1 -1
  43. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +2 -2
  44. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +1 -1
  45. data/lib/ctx_checker/hillstone/profile.rb +3 -3
  46. data/lib/ctx_checker/observation.rb +13 -8
  47. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +1 -1
  48. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +1 -1
  49. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +1 -1
  50. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +1 -1
  51. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +1 -1
  52. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +1 -1
  53. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +3 -3
  54. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +4 -4
  55. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +3 -3
  56. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +3 -3
  57. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +1 -1
  58. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +1 -1
  59. data/lib/ctx_checker/palo_alto/profile.rb +3 -3
  60. data/lib/ctx_checker/parsers/base.rb +11 -4
  61. data/lib/ctx_checker/parsers/diagnostics.rb +47 -0
  62. data/lib/ctx_checker/policy.rb +20 -12
  63. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +3 -3
  64. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +4 -4
  65. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +1 -1
  66. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +1 -1
  67. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +1 -1
  68. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +1 -1
  69. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +1 -1
  70. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +3 -3
  71. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +3 -3
  72. data/lib/ctx_checker/radware_alteon/profile.rb +9 -9
  73. data/lib/ctx_checker/runtime_parser.rb +16 -10
  74. data/lib/ctx_checker/{system_record.rb → system_fields.rb} +3 -3
  75. data/lib/ctx_checker/system_policy.rb +14 -14
  76. data/lib/ctx_checker/version.rb +1 -1
  77. data/lib/ctx_checker.rb +1 -1
  78. 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 = [], [], false
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
- CATALOG = CtxParser::CommandCatalog.new([
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 = CATALOG.commands
36
- PARSERS = CATALOG.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
- source
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) + 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 Array(@issues).any? { |issue| issue[:code] == :command_error }
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
@@ -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
- names!(required_interfaces)
19
- unless required_members.is_a?(Hash) && required_members.all? { |group, members| text?(group) && names!(members) }
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
- peers!(expected_bgp, %i[vrf afi_safi peer])
26
- peers!(expected_ospf, %i[vrf process_id interface router_id])
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
- optics!(optical_thresholds)
31
- required_optics!(required_optics)
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 required?(name)
48
- return true if system.required?(name)
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 names!(values)
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 peers!(values, fields)
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 optics!(values)
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 required_optics!(values)
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 = [], [], false
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.include?(port)
30
- ports << port
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 = [], [], [], nil
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.include?(current)
25
- groups << current
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.uniq - rows.map { |row| row[:group] }).each do |group|
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 = [], [], :preamble
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 = [], [], false
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 = [], [], nil
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 = [], [], nil, nil
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/result_set"
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::ResultSet.new(results: rows.to_h do |dimension, records|
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/result_set"
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 = [], [], nil
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::ResultSet.new(results: {
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
- CATALOG = CtxParser::CommandCatalog.new([
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 = CATALOG.commands
45
- PARSERS = CATALOG.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
- @catalog = CATALOG
52
+ @declarations = DECLARATIONS
53
53
  end
54
54
 
55
55
  # 扩展后的实例保留不可变命令与实现索引。
56
- def commands = @catalog.commands
56
+ def commands = @declarations.by_dimension
57
57
 
58
58
  # 绑定端口的解析器复用基础型号覆盖及命令选择契约。
59
- def parsers = @catalog.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 = @catalog.entries.reject { |entry| entry[:dimensions].include?(:optics) }
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(:@catalog, CtxParser::CommandCatalog.new(entries, multi_command: true))
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(:@catalog, CtxParser::CommandCatalog.new(@catalog.entries + additions, multi_command: true))
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
- candidates = names.map { |name| profile.commands_for(name) }.reduce(:&)
43
- command ||= candidates.first if candidates.one?
44
- raise ArgumentError, "Select one declared command or use parse_outputs for multiple sources" unless candidates.include?(command)
45
- implementation = profile.parser_for(names.first, command: command)
46
- command_counts = names.to_h { |name| [ name, profile.commands_for(name).size ] }
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.each_key do |name|
182
- profile.commands_for(name).each do |command|
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
- end
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::ResultSet)
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 ResultSet" unless result.is_a?(CtxParser::Result)
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 SystemRecord
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.valid?(name, key, value)
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
- resources!(resource_thresholds)
16
- temperatures!(temperature_thresholds)
17
- components!(required_components)
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 SystemRecord.number?(value) && value >= 0
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| SystemRecord.text?(metric) && limit.is_a?(Integer) && limit >= 0 }
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 required?(name) = required_dimensions.include?(name) || required_components.fetch(name, []).any?
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 resources!(values)
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| SystemRecord.number?(value) && value.between?(0, 100) } && row[:warning] <= row[:critical]
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 temperatures!(values)
49
+ def validate_temperature_thresholds!(values)
50
50
  unless values.is_a?(Hash) && values.all? do |component, row|
51
- SystemRecord.text?(component) && row.is_a?(Hash) && (row.keys & Observation::OPTICAL_THRESHOLDS).any? &&
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| SystemRecord.number?(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 components!(values)
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| SystemRecord.text?(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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CtxChecker
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.1"
6
6
  end
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", SystemRecord: "system_record", ObservationMerger: "observation_merger", Failure: "failure",
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"