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
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  # 保留 DOM 不可用和模块缺失状态,不虚构光功率。
17
17
  def parse_output(text, complete:)
18
18
  text = clean(text)
19
- rows, issues, devices = [], [], []
19
+ rows, issues, devices = [], diagnostics, []
20
20
  current, lane, header, lane_valid = nil, nil, false, true
21
21
  text.each_line.with_index(1) do |source, number|
22
22
  line = source.strip
@@ -62,7 +62,8 @@ module CtxChecker
62
62
  issues << { code: :unparsed_row, line: number }
63
63
  end
64
64
  end
65
- devices.each { |device| complete_measurements(rows, device, issues) }
65
+ by_interface = rows.group_by { |row| row[:interface] }
66
+ devices.each { |device| complete_measurements(rows, device, issues, by_interface) }
66
67
  build_result(rows, complete: complete, recognized: devices.any?, issues: issues, identity: %i[interface lane metric])
67
68
  end
68
69
 
@@ -80,8 +81,9 @@ module CtxChecker
80
81
  end
81
82
 
82
83
  # 即使配对指标解析成功,缺失指标仍须明确保留。
83
- def complete_measurements(rows, device, issues)
84
- device_rows = rows.select { |row| row[:interface] == device[:interface] }
84
+ def complete_measurements(rows, device, issues, by_interface)
85
+ device_rows = by_interface[device[:interface]] ||= []
86
+ measurements = device_rows.to_h { |row| [ row.values_at(:lane, :metric), true ] }
85
87
  if device[:applicable] == false && (!device[:presence_states].empty? || device[:lanes].any? || device_rows.any?)
86
88
  issues << { code: :contradictory_optical_applicability, interface: device[:interface] }
87
89
  device[:applicable] = nil
@@ -90,15 +92,18 @@ module CtxChecker
90
92
  if device[:presence_states].uniq.size > 1
91
93
  issues << { code: :contradictory_transceiver_presence, interface: device[:interface] }
92
94
  end
93
- if device[:dom_unsupported] && rows.any? { |row| row[:interface] == device[:interface] && !row[:value].nil? }
95
+ if device[:dom_unsupported] && device_rows.any? { |row| !row[:value].nil? }
94
96
  issues << { code: :contradictory_dom_support, interface: device[:interface] }
95
97
  end
96
98
  lanes = device[:lanes].empty? ? [ nil ] : device[:lanes].uniq
97
99
  lanes.each do |lane|
98
100
  %i[tx_power rx_power].each do |metric|
99
- next if rows.any? { |row| row.values_at(:interface, :lane, :metric) == [ device[:interface], lane, metric ] }
101
+ next if measurements.key?([ lane, metric ])
100
102
 
101
- rows << base_record(device, lane, metric)
103
+ record = base_record(device, lane, metric)
104
+ rows << record
105
+ device_rows << record
106
+ measurements[[ lane, metric ]] = true
102
107
  unless device[:present] == false || device[:dom_supported] == false
103
108
  issues << { code: :optical_measurement_missing, interface: device[:interface], lane: lane, metric: metric }
104
109
  end
@@ -107,7 +112,7 @@ module CtxChecker
107
112
  if device[:present].nil? && device[:applicable] != false
108
113
  issues << { code: :transceiver_presence_unknown, interface: device[:interface] }
109
114
  end
110
- if device[:present] == false && rows.any? { |row| row[:interface] == device[:interface] && !row[:value].nil? }
115
+ if device[:present] == false && device_rows.any? { |row| !row[:value].nil? }
111
116
  issues << { code: :contradictory_transceiver_presence, interface: device[:interface] }
112
117
  end
113
118
  end
@@ -52,7 +52,7 @@ module CtxChecker
52
52
 
53
53
  # 解析 show ip traffic,保留不完整或不支持的证据。
54
54
  def parse_output(text, complete:)
55
- records, issues = [], []
55
+ records, issues = [], diagnostics
56
56
  recognized = false
57
57
  section = nil
58
58
  fragment_sections, acl_sections = 0, 0
@@ -12,7 +12,7 @@ module CtxChecker
12
12
 
13
13
  # 解析 show ntp peer status,保留不完整或不支持的证据。
14
14
  def parse_output(text, complete:)
15
- peers, issues, totals = [], [], []
15
+ peers, issues, totals = [], diagnostics, []
16
16
  vrf_column = nil
17
17
  each_ntp_line(clean(text), issues) do |line, number|
18
18
  next if line.empty? || /\A-+\z/.match?(line)
@@ -12,8 +12,8 @@ module CtxChecker
12
12
  private
13
13
 
14
14
  # 部分终端会将下一表格行附加在占满终端宽度的分隔线后。
15
- def parse_output(text, complete:)
16
- super(clean(text).gsub(/^([ \t]*-{3,})(?=Group\s+Port-|\d+\s+(?:Po|port-channel)\d+\()/i, "\\1\n"), complete: complete)
15
+ def clean(text)
16
+ super.gsub(/^([ \t]*-{3,})(?=Group\s+Port-|\d+\s+(?:Po|port-channel)\d+\()/i, "\\1\n")
17
17
  end
18
18
  end
19
19
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "../../parsers/base"
4
4
  require_relative "../../cisco_ios/parsers/system_values"
5
- require "ctx_parser/result_set"
5
+ require "ctx_parser/results"
6
6
 
7
7
  module CtxChecker
8
8
  module CiscoNxos
@@ -15,9 +15,10 @@ module CtxChecker
15
15
 
16
16
  # 解析 show system resources,保留不完整或不支持的证据。
17
17
  def parse_output(text, complete:)
18
- cpu, memory, cpu_issues, memory_issues = [], [], [], []
18
+ cpu, memory, cpu_issues, memory_issues = [], [], diagnostics, diagnostics
19
19
  caches, statuses = [], []
20
20
  kernel_cache = {}
21
+ duplicate_cache = false
21
22
  clean(text).each_line.with_index(1) do |source, number|
22
23
  line = source.strip
23
24
  next if line.empty?
@@ -38,7 +39,10 @@ module CtxChecker
38
39
  caches << match.captures.sum { |value| Integer(value, 10) * 1024 }
39
40
  elsif (match = /\AKernel (buffers|cached)\s*:\s*(\d+)K Used\z/i.match(line))
40
41
  key = match[1].downcase
41
- memory_issues << { code: :duplicate_memory_cache, line: number } if kernel_cache.key?(key)
42
+ if kernel_cache.key?(key)
43
+ duplicate_cache = true
44
+ memory_issues << { code: :duplicate_memory_cache, line: number }
45
+ end
42
46
  kernel_cache[key] = Integer(match[2], 10) * 1024
43
47
  elsif (match = /\ACurrent memory status:\s*(.+)\z/i.match(line))
44
48
  state = match[1].casecmp?("severe") ? :critical : component_status(match[1])
@@ -56,7 +60,6 @@ module CtxChecker
56
60
  end
57
61
  end
58
62
  memory_issues << { code: :incomplete_memory_cache } if kernel_cache.size == 1
59
- duplicate_cache = memory_issues.any? { |issue| issue[:code] == :duplicate_memory_cache }
60
63
  caches << kernel_cache.values.sum if kernel_cache.size == 2 && !duplicate_cache
61
64
  memory_issues << { code: :duplicate_memory_cache } if caches.size > 1
62
65
  memory_issues << { code: :duplicate_memory_status } if statuses.size > 1
@@ -78,7 +81,7 @@ module CtxChecker
78
81
  memory_issues << { code: :inconsistent_memory_cache }
79
82
  end
80
83
  end
81
- CtxParser::ResultSet.new(results: {
84
+ CtxParser::Results.new(by_dimension: {
82
85
  cpu: build_result(cpu, complete: complete, recognized: cpu.any?, issues: cpu_issues, identity: %i[component scope window_seconds]),
83
86
  memory: build_result(memory, complete: complete, recognized: memory.any?, issues: memory_issues, identity: %i[component scope])
84
87
  })
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  module CiscoNxos
17
17
  # NX-OS 命令选择保留完整接口详情,并明确 IPv4 BGP 作用域。
18
18
  class Profile < CtxParser::Profile
19
- CATALOG = CtxParser::CommandCatalog.new([
19
+ DECLARATIONS = CtxParser::Commands.new([
20
20
  { dimensions: %i[interfaces counters], command: "show interface", parser: Parsers::ShowInterface },
21
21
  { dimensions: [ :aggregation ], command: "show port-channel summary", parser: Parsers::ShowPortChannelSummary },
22
22
  { dimensions: [ :optics ], command: "show interface transceiver details", parser: Parsers::ShowInterfaceTransceiverDetails },
@@ -28,8 +28,8 @@ module CtxChecker
28
28
  { dimensions: [ :ntp ], command: "show ntp peer-status", parser: Parsers::ShowNtpPeerStatus },
29
29
  { dimensions: [ :device_counters ], command: "show ip traffic", parser: Parsers::ShowIpTraffic }
30
30
  ])
31
- COMMANDS = CATALOG.commands
32
- PARSERS = CATALOG.parsers
31
+ COMMANDS = DECLARATIONS.by_dimension
32
+ PARSERS = DECLARATIONS.parsers
33
33
  end
34
34
  end
35
35
  end
@@ -14,7 +14,7 @@ module CtxChecker
14
14
  # 解析 display bgp peer,保留不完整或不支持的证据。
15
15
  def parse_output(text, complete:)
16
16
  text = clean(text)
17
- rows, issues, total, header = [], [], nil, false
17
+ rows, issues, total, header = [], diagnostics, nil, false
18
18
  text.each_line do |raw|
19
19
  line = raw.strip
20
20
  if (match = /\ATotal number of peers:\s*(\d+)\z/i.match(line))
@@ -24,7 +24,7 @@ module CtxChecker
24
24
  header = true
25
25
  elsif (match = /\A\*?\s*([\d.]+)\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+\S+\s+(.+)\z/.match(line))
26
26
  state = match[4].downcase.delete(" ")
27
- if CtxParser::Values.valid_host?(match[1]) && STATES.include?(state) && (1..4_294_967_295).cover?(match[2].to_i)
27
+ if CtxParser::Values.ip_address?(match[1]) && STATES.include?(state) && (1..4_294_967_295).cover?(match[2].to_i)
28
28
  rows << { vrf: "default", afi_safi: "ipv4_unicast", peer: match[1], state: state == "idle(admin)" ? :shutdown : state.to_sym,
29
29
  received_prefixes: match[3].to_i, remote_as: match[2].to_i }
30
30
  else
@@ -45,8 +45,9 @@ module CtxChecker
45
45
  @issues << { code: :unrecognized_cpu_line }
46
46
  end
47
47
  end
48
- components.each do |component|
49
- missing = [ 5, 60, 300 ] - rows.select { |row| row[:component] == component }.map { |row| row[:window_seconds] }
48
+ windows = rows.group_by { |row| row[:component] }.transform_values { |samples| samples.map { |row| row[:window_seconds] }.uniq }
49
+ components.uniq.each do |component|
50
+ missing = [ 5, 60, 300 ] - windows.fetch(component, [])
50
51
  @issues << { code: :missing_cpu_windows, component: component, count: missing.size } if missing.any?
51
52
  end
52
53
  build_result(rows, complete: complete, recognized: components.any?, identity: %i[component scope window_seconds])
@@ -24,7 +24,7 @@ module CtxChecker
24
24
  # 接口状态独立于各配置块提供的计数器进行投影。
25
25
  def parse_output(text, complete:)
26
26
  text = clean(text)
27
- interfaces, counters, interface_issues, counter_issues = [], [], [], []
27
+ interfaces, counters, interface_issues, counter_issues = [], [], diagnostics, diagnostics
28
28
  sections = text.split(/(?=^[ \t]*#{INTERFACE}(?:[ \t]+current state[ \t]*:.*)?[ \t]*$)/i)
29
29
  sections.each do |section|
30
30
  header = /\A[ \t]*(#{INTERFACE})(?:[ \t]+current state[ \t]*:[ \t]*([^\n]+))?[ \t]*\n/i.match(section)
@@ -73,7 +73,7 @@ module CtxChecker
73
73
  end
74
74
  counters.concat(section_counters)
75
75
  end
76
- CtxParser::ResultSet.new(results: {
76
+ CtxParser::Results.new(by_dimension: {
77
77
  interfaces: build_result(interfaces, complete: complete, recognized: interfaces.any?, issues: interface_issues, identity: [ :name ]),
78
78
  counters: build_result(counters, complete: complete, recognized: counters.any?, issues: counter_issues + interface_issues, identity: %i[interface metric])
79
79
  })
@@ -21,7 +21,7 @@ module CtxChecker
21
21
  if /\AInfo:\s+No link aggregation group defined at present\.\z/i.match?(text.strip)
22
22
  return build_result([], complete: complete, recognized: true, issues: [ { code: :aggregation_not_configured } ])
23
23
  end
24
- rows, issues, group, remote, groups, columns, tables = [], [], nil, false, {}, nil, []
24
+ rows, issues, group, remote, groups, columns, tables = [], diagnostics, nil, false, {}, nil, []
25
25
  text.each_line do |raw|
26
26
  line = raw.strip
27
27
  if (match = /\AAggregate Interface:\s*(.*)\z/i.match(line))
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  if /\AOSPF is not configured\.\z/i.match?(text.strip)
17
17
  return build_result([], complete: complete, recognized: true, issues: [ { code: :protocol_not_configured } ])
18
18
  end
19
- rows, issues, process, area, header = [], [], nil, nil, false
19
+ rows, issues, process, area, header = [], diagnostics, nil, nil, false
20
20
  text.each_line do |raw|
21
21
  line = raw.strip
22
22
  if (match = /\AOSPF Process (\d+) with Router ID ([\d.]+)\z/i.match(line))
@@ -26,7 +26,7 @@ module CtxChecker
26
26
  elsif /\ARouter ID\s+Address\s+Pri\s+Dead-Time\s+State\s+Interface\z/i.match?(line)
27
27
  header = true
28
28
  elsif (match = /\A([\d.]+)\s+([\d.]+)\s+\d+\s+\S+\s+(Down|Init|Attempt|2-Way|Exstart|Exchange|Loading|Full)(?:\/[ \t]*(DR|BDR|DROther|-))?\s+(\S+)\z/i.match(line))
29
- if CtxParser::Values.valid_host?(match[1]) && CtxParser::Values.valid_host?(match[2])
29
+ if CtxParser::Values.ip_address?(match[1]) && CtxParser::Values.ip_address?(match[2])
30
30
  issues << { code: :ospf_scope_missing } unless process && area
31
31
  rows << { vrf: nil, process_id: process, area: area,
32
32
  interface: CtxChecker::Parsers::InterfaceNames.normalize(match[5], platform: :h3c), router_id: match[1], neighbor_address: match[2],
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  # 解析 display transceiver diagnosis,保留不完整或不支持的证据。
19
19
  def parse_output(text, complete:)
20
20
  text = clean(text)
21
- rows, issues = [], []
21
+ rows, issues = [], diagnostics
22
22
  text.split(/(?=^\S+ transceiver diagnostic information:)/i).each do |block|
23
23
  next if block.strip.empty?
24
24
  match = /\A(\S+) transceiver diagnostic information:/i.match(block)
@@ -22,7 +22,7 @@ module CtxChecker
22
22
  module H3c
23
23
  # 声明 Comware 只读查询;BGP 明确限定为公网 IPv4 单播。
24
24
  class Profile < CtxParser::Profile
25
- CATALOG = CtxParser::CommandCatalog.new([
25
+ DECLARATIONS = CtxParser::Commands.new([
26
26
  { dimensions: %i[interfaces counters], command: "display interface", parser: Parsers::DisplayInterface },
27
27
  { dimensions: [ :counters ], command: "display counters inbound interface", parser: Parsers::DisplayCountersInboundInterface },
28
28
  { dimensions: [ :counters ], command: "display counters outbound interface", parser: Parsers::DisplayCountersOutboundInterface },
@@ -39,8 +39,8 @@ module CtxChecker
39
39
  { dimensions: [ :ntp ], command: "display ntp-service status", parser: Parsers::DisplayNtpServiceStatus },
40
40
  { dimensions: [ :alarms ], command: "display alarm active", parser: Parsers::DisplayAlarmActive }
41
41
  ], multi_command: true)
42
- COMMANDS = CATALOG.commands
43
- PARSERS = CATALOG.parsers
42
+ COMMANDS = DECLARATIONS.by_dimension
43
+ PARSERS = DECLARATIONS.parsers
44
44
 
45
45
  # 每个已观察接口都必须保留入站与出站两个方向的证据状态。
46
46
  def counter_metrics = %i[in_errors out_errors].freeze
@@ -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 Hillstone
@@ -20,7 +20,7 @@ module CtxChecker
20
20
  # 跟踪各已声明章节,并分别要求其具备表头与测量值。
21
21
  def parse_output(text, complete:)
22
22
  rows = { temperature: [], environment: [], power: [] }
23
- issues = rows.transform_values { [] }
23
+ issues = rows.transform_values { diagnostics }
24
24
  contexts, section, header = [], nil, false
25
25
  clean(text).each_line.with_index(1) do |raw, number|
26
26
  line = raw.strip
@@ -52,7 +52,7 @@ module CtxChecker
52
52
  issues[context[:section]] << { code: :incomplete_environment_section }
53
53
  end
54
54
  end
55
- CtxParser::ResultSet.new(results: rows.to_h do |dimension, records|
55
+ CtxParser::Results.new(by_dimension: rows.to_h do |dimension, records|
56
56
  identity = dimension == :temperature ? [ :component ] : %i[component kind]
57
57
  [ dimension, build_result(records, complete: complete, recognized: records.any?, issues: issues[dimension], identity: identity) ]
58
58
  end)
@@ -17,7 +17,7 @@ module CtxChecker
17
17
 
18
18
  # 识别无地址链路时,仍保留 IP 协议 down 状态。
19
19
  def parse_output(text, complete:)
20
- rows, issues, header = [], [], false
20
+ rows, issues, header = [], diagnostics, false
21
21
  clean(text).each_line do |raw|
22
22
  line = raw.strip
23
23
  next if line.empty? || /\A[-=]+\z/.match?(line) || LEGEND.match?(line)
@@ -38,7 +38,7 @@ module CtxChecker
38
38
  { group: row[:name], member: nil, member_status: :unknown, oper_status: row[:oper_status] }
39
39
  end
40
40
  missing_members = groups.map { |group| { code: :aggregation_members_unavailable, group: group[:group] } }
41
- CtxParser::ResultSet.new(results: {
41
+ CtxParser::Results.new(by_dimension: {
42
42
  interfaces: interfaces,
43
43
  aggregation: build_result(groups, complete: complete, recognized: header,
44
44
  issues: interfaces.issues + missing_members, identity: %i[group member])
@@ -14,7 +14,7 @@ module CtxChecker
14
14
 
15
15
  # 解释计数器前先校验列顺序;缺少采样时保留 nil。
16
16
  def parse_output(text, complete:)
17
- rows, issues, header, count = [], [], false, 0
17
+ rows, issues, header, count = [], diagnostics, false, 0
18
18
  clean(text).each_line do |raw|
19
19
  line = raw.strip
20
20
  next if line.empty? || /\A[-=]+\z/.match?(line)
@@ -59,7 +59,7 @@ module CtxChecker
59
59
  fields[key] = value
60
60
  end
61
61
  end
62
- peer = CtxParser::Values.valid_host?(host) ? CtxParser::Values.host(host) : host.downcase
62
+ peer = CtxParser::Values.ip_address?(host) ? CtxParser::Values.ip_address(host) : host.downcase
63
63
  { component: "peer/#{fields[:vrouter] || 'unspecified'}/#{peer}", peer: peer, synchronized: nil,
64
64
  last_sync_status: fields[:last_sync_status] || :unknown, last_sync_at: nil }
65
65
  end
@@ -99,7 +99,7 @@ module CtxChecker
99
99
 
100
100
  # 服务器名称仅接受主机名或主机地址,不接受任意 CLI 片段。
101
101
  def valid_peer?(text)
102
- return true if CtxParser::Values.valid_host?(text)
102
+ return true if CtxParser::Values.ip_address?(text)
103
103
  return false if text.match?(/\A[\d.]+\z/) || text.length > 253
104
104
 
105
105
  text.delete_suffix(".").split(".", -1).all? { |label| /\A[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\z/.match?(label) }
@@ -16,7 +16,7 @@ module CtxChecker
16
16
 
17
17
  # 逐模块隔离,避免未知块将测量值错误关联到前一模块。
18
18
  def parse_output(text, complete:)
19
- rows, issues, blocks, current, header = [], [], [], nil, false
19
+ rows, issues, blocks, current, header = [], diagnostics, [], nil, false
20
20
  clean(text).each_line do |raw|
21
21
  line = raw.strip
22
22
  next if line.empty? || /\A=+\z/.match?(line)
@@ -17,7 +17,7 @@ module CtxChecker
17
17
  module Hillstone
18
18
  # StoneOS 命令已在 SG-6000-A3700 验证;未覆盖作用域明确保留不可用。
19
19
  class Profile < CtxParser::Profile
20
- CATALOG = CtxParser::CommandCatalog.new([
20
+ DECLARATIONS = CtxParser::Commands.new([
21
21
  { dimensions: %i[interfaces aggregation], command: "show interface", parser: Parsers::ShowInterface },
22
22
  { dimensions: [ :counters ], command: "show interface counter", parser: Parsers::ShowInterfaceCounter },
23
23
  { dimensions: [ :optics ], command: "show transceiver", parser: Parsers::ShowTransceiver },
@@ -30,8 +30,8 @@ module CtxChecker
30
30
  { dimensions: [ :ntp ], command: "show ntp status", parser: Parsers::ShowNtpStatus },
31
31
  { dimensions: [ :device_counters ], command: "show cpu-cntr", parser: Parsers::ShowCpuCntr }
32
32
  ])
33
- COMMANDS = CATALOG.commands
34
- PARSERS = CATALOG.parsers
33
+ COMMANDS = DECLARATIONS.by_dimension
34
+ PARSERS = DECLARATIONS.parsers
35
35
 
36
36
  # 在线接口丢弃表不提供错误包或 CRC 测量。
37
37
  def counter_metrics = %i[in_errors out_errors crc_errors].freeze
@@ -2,12 +2,12 @@
2
2
 
3
3
  require "ctx_parser/immutable"
4
4
  require "time"
5
- require_relative "system_record"
5
+ require_relative "system_fields"
6
6
 
7
7
  module CtxChecker
8
8
  # 提供经过校验的不可变运行事实,与后续健康评估相互独立。
9
9
  class Observation
10
- NAMES = (%i[interfaces counters aggregation optics bgp ospf] + SystemRecord::FIELDS.keys).freeze
10
+ NAMES = (%i[interfaces counters aggregation optics bgp ospf] + SystemFields::FIELDS.keys).freeze
11
11
  STATUSES = %i[complete partial failed unsupported not_requested not_applicable].freeze
12
12
  METRICS = %i[in_errors out_errors in_discards out_discards crc_errors stomped_crc_errors out_carrier_errors].freeze
13
13
  COUNTER_UNITS = %i[packets bytes events unknown].freeze
@@ -19,13 +19,13 @@ module CtxChecker
19
19
  aggregation: %i[group member member_status oper_status],
20
20
  optics: %i[interface lane metric value unit low_alarm low_warning high_warning high_alarm alarm present dom_supported applicable],
21
21
  bgp: %i[vrf afi_safi peer state received_prefixes remote_as],
22
- ospf: %i[vrf process_id area interface router_id neighbor_address state network_type local_role neighbor_role], **SystemRecord::FIELDS
22
+ ospf: %i[vrf process_id area interface router_id neighbor_address state network_type local_role neighbor_role], **SystemFields::FIELDS
23
23
  }.transform_values { |fields| fields + [ :source_complete ] })
24
24
  IDENTITIES = CtxParser::Immutable.copy(interfaces: %i[name], counters: %i[interface metric],
25
25
  aggregation: %i[group member], optics: %i[interface lane metric], bgp: %i[vrf afi_safi peer],
26
- ospf: %i[vrf process_id interface router_id neighbor_address], **SystemRecord::IDENTITIES)
26
+ ospf: %i[vrf process_id interface router_id neighbor_address], **SystemFields::IDENTITIES)
27
27
  REQUIRED = CtxParser::Immutable.copy(interfaces: %i[name], counters: %i[interface metric],
28
- aggregation: %i[group], optics: %i[interface metric], bgp: %i[peer], ospf: %i[router_id], **SystemRecord::REQUIRED)
28
+ aggregation: %i[group], optics: %i[interface metric], bgp: %i[peer], ospf: %i[router_id], **SystemFields::REQUIRED)
29
29
  ISSUE_FIELDS = %i[code line lines field count skipped interface metric metrics lane section expected observed group stage component scope window_seconds command error_type].freeze
30
30
  ERROR_TYPE_PATTERN = /\A(?:[A-Z][A-Za-z0-9_]*::)*[A-Z][A-Za-z0-9_]*\z/
31
31
  ENUMS = CtxParser::Immutable.copy(
@@ -48,6 +48,10 @@ module CtxChecker
48
48
  end
49
49
  @name, @status = name, status
50
50
  validate_scopes!(covered_scopes)
51
+ covered_scopes = covered_scopes.map do |scope|
52
+ scope[:process_id].is_a?(Integer) ? scope.merge(process_id: scope[:process_id].to_s) : scope
53
+ end
54
+ validate_scopes!(covered_scopes)
51
55
  normalized = records.map { |record| validate_record(record) }
52
56
  identities = normalized.map { |record| identity(record) }
53
57
  raise ArgumentError, "Duplicate observation identity" unless identities.uniq.size == identities.size
@@ -128,10 +132,10 @@ module CtxChecker
128
132
  next if value.nil?
129
133
  valid = if key == :source_complete
130
134
  value == true || value == false
131
- elsif SystemRecord::FIELDS.key?(name)
132
- SystemRecord.valid?(name, key, value)
135
+ elsif SystemFields::FIELDS.key?(name)
136
+ SystemFields.valid_value?(name, key, value)
133
137
  elsif key == :sampled_at
134
- SystemRecord.timestamp?(value)
138
+ SystemFields.timestamp?(value)
135
139
  elsif name == :counters && key == :unit
136
140
  COUNTER_UNITS.include?(value)
137
141
  elsif name == :counters && key == :scope
@@ -156,6 +160,7 @@ module CtxChecker
156
160
  end
157
161
  raise ArgumentError, "Invalid #{name} field #{key}" unless valid
158
162
  end
163
+ row[:process_id] = row[:process_id].to_s if name == :ospf && row[:process_id].is_a?(Integer)
159
164
  row
160
165
  end
161
166
 
@@ -14,7 +14,7 @@ module CtxChecker
14
14
  # 解析 show clock,保留不完整或不支持的证据。
15
15
  def parse_output(text, complete:)
16
16
  source = clean(text)
17
- rows, issues = [], []
17
+ rows, issues = [], diagnostics
18
18
  source.each_line.with_index(1) do |raw, number|
19
19
  next if raw.strip.empty?
20
20
  match = /\A(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+([A-Z][a-z]{2})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\d{4})\z/.match(raw.strip)
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  # 解析 show counter global,保留不完整或不支持的证据。
17
17
  def parse_output(text, complete:)
18
18
  source = clean(text)
19
- rows, totals, issues = [], [], []
19
+ rows, totals, issues = [], [], diagnostics
20
20
  title, header = false, false
21
21
  source.each_line.with_index(1) do |raw, number|
22
22
  line = raw.strip
@@ -19,7 +19,7 @@ module CtxChecker
19
19
  # 解析 show counter interface all,保留不完整或不支持的证据。
20
20
  def parse_output(text, complete:)
21
21
  text = clean(text)
22
- rows, issues, interfaces = [], [], {}
22
+ rows, issues, interfaces = [], diagnostics, {}
23
23
  text.split(/(?=^\s*Interface:)/i).each do |block|
24
24
  next if block.strip.empty?
25
25
  match = /\A\s*Interface:\s*([^\n]+)\n/i.match(block)
@@ -13,7 +13,7 @@ module CtxChecker
13
13
  # 解析 show interface all,保留不完整或不支持的证据。
14
14
  def parse_output(text, complete:)
15
15
  text = clean(text)
16
- rows, issues, section, totals, counts, duplicates = {}, [], nil, {}, Hash.new(0), []
16
+ rows, issues, section, totals, counts, duplicates = {}, diagnostics, nil, {}, Hash.new(0), []
17
17
  logical_names = {}
18
18
  text.each_line do |raw|
19
19
  line = raw.strip
@@ -13,7 +13,7 @@ module CtxChecker
13
13
  # 解析 show lacp aggregate ethernet,保留不完整或不支持的证据。
14
14
  def parse_output(text, complete:)
15
15
  text = clean(text)
16
- rows, issues, group, header, groups = [], [], nil, false, {}
16
+ rows, issues, group, header, groups = [], diagnostics, nil, false, {}
17
17
  text.each_line do |raw|
18
18
  line = raw.strip
19
19
  if (match = /\AAE group:\s*(ae\d+)\z/i.match(line))
@@ -12,7 +12,7 @@ module CtxChecker
12
12
  # 解析 show ntp,保留不完整或不支持的证据。
13
13
  def parse_output(text, complete:)
14
14
  source = clean(text)
15
- rows, globals, issues = [], [], []
15
+ rows, globals, issues = [], [], diagnostics
16
16
  current, fields, header, peer_fields = nil, {}, false, {}
17
17
  source.each_line.with_index(1) do |raw, number|
18
18
  line = raw.strip
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  # 解析 show routing protocol bgp peer,保留不完整或不支持的证据。
16
16
  def parse_output(text, complete:)
17
17
  text = clean(text)
18
- rows, issues = [], []
18
+ rows, issues = [], diagnostics
19
19
  text.split(/(?=^\s*Peer:\s)/i).each do |block|
20
20
  next if block.strip.empty? || block.strip.match?(/\A=+\z/)
21
21
  unless block.match?(/\A\s*Peer:\s/i)
@@ -32,11 +32,11 @@ module CtxChecker
32
32
  address = block[/^\s*Remote Address:\s*(\S+)/i, 1]
33
33
  peer = address&.sub(/\A\[([^\]]+)\]:\d+\z/, '\1')
34
34
  peer = peer.sub(/:\d+\z/, "") if peer&.match?(/\A\d+(?:\.\d+){3}:\d+\z/)
35
- unless CtxParser::Values.valid_host?(peer)
35
+ unless CtxParser::Values.ip_address?(peer)
36
36
  issues << { code: :bgp_peer_address_missing }
37
37
  next
38
38
  end
39
- peer = CtxParser::Values.host(peer)
39
+ peer = CtxParser::Values.ip_address(peer)
40
40
  state = block[/^\s*Peer status:\s*([^,\n]+)/i, 1]&.strip&.downcase
41
41
  state = STATES.include?(state) ? state.to_sym : :unknown
42
42
  remote_as = block[/^\s*Remote AS:\s*(\d+)\s*$/i, 1]&.to_i
@@ -17,7 +17,7 @@ module CtxChecker
17
17
  # 解析 show routing protocol ospf neighbor,保留不完整或不支持的证据。
18
18
  def parse_output(text, complete:)
19
19
  text = clean(text)
20
- rows, issues = [], []
20
+ rows, issues = [], diagnostics
21
21
  text.split(/(?=^\s*virtual router:)/i).each do |block|
22
22
  unless block.match?(/\A\s*virtual router:/i)
23
23
  # 文档规定的图例位于首个邻居前,任意前导文本不构成证据。
@@ -34,12 +34,12 @@ module CtxChecker
34
34
  end
35
35
  neighbor = block[/^\s*neighbor address:\s*(\S+)/i, 1]
36
36
  router_id = block[/^\s*neighbor router ID:\s*(\S+)/i, 1]
37
- unless CtxParser::Values.valid_host?(neighbor) && CtxParser::Values.valid_host?(router_id)
37
+ unless CtxParser::Values.ip_address?(neighbor) && CtxParser::Values.ip_address?(router_id)
38
38
  issues << { code: :ospf_neighbor_identity_missing }
39
39
  next
40
40
  end
41
- neighbor = CtxParser::Values.host(neighbor)
42
- router_id = CtxParser::Values.host(router_id)
41
+ neighbor = CtxParser::Values.ip_address(neighbor)
42
+ router_id = CtxParser::Values.ip_address(router_id)
43
43
  state = STATES.fetch(block[/^\s*status:\s*(.+)$/i, 1]&.strip&.downcase, :unknown)
44
44
  issues << { code: :ospf_state_unknown } if state == :unknown
45
45
  rows << { vrf: block[/^\s*virtual router:\s*(\S+)/i, 1], process_id: nil,
@@ -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 PaloAlto
@@ -17,7 +17,7 @@ module CtxChecker
17
17
  def parse_output(text, complete:)
18
18
  source = clean(text)
19
19
  rows = { temperature: [], environment: [], power: [] }
20
- issues = rows.transform_values { [] }
20
+ issues = rows.transform_values { diagnostics }
21
21
  section, header, sections, section_rows = nil, nil, [], 0
22
22
  source.each_line.with_index(1) do |raw, number|
23
23
  line = raw.strip
@@ -88,7 +88,7 @@ module CtxChecker
88
88
  if rows[:power].any? && !sections.include?("power supplies")
89
89
  issues[:power] << { code: :power_supply_status_missing }
90
90
  end
91
- CtxParser::ResultSet.new(results: rows.to_h do |dimension, values|
91
+ CtxParser::Results.new(by_dimension: rows.to_h do |dimension, values|
92
92
  identity = dimension == :temperature ? [ :component ] : %i[component kind]
93
93
  [ dimension, build_result(values, complete: complete, recognized: values.any?, issues: issues.fetch(dimension), identity: identity) ]
94
94
  end)
@@ -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 PaloAlto
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  # 解析 show system resources,保留不完整或不支持的证据。
17
17
  def parse_output(text, complete:)
18
18
  source = clean(text)
19
- cpus, memories, cpu_issues, memory_issues, shared_issues = [], [], [], [], []
19
+ cpus, memories, cpu_issues, memory_issues, shared_issues = [], [], diagnostics, diagnostics, diagnostics
20
20
  swap_lines = source.lines.grep(/\A\s*(?:(?:KiB|MiB|GiB|TiB)\s+)?Swap\s*:/)
21
21
  process_table = false
22
22
  source.each_line.with_index(1) do |raw, number|
@@ -33,7 +33,7 @@ module CtxChecker
33
33
  shared_issues << { code: :unrecognized_resource_row, line: number }
34
34
  end
35
35
  end
36
- CtxParser::ResultSet.new(results: {
36
+ CtxParser::Results.new(by_dimension: {
37
37
  cpu: build_result(cpus, complete: complete, recognized: cpus.any?, issues: shared_issues + cpu_issues,
38
38
  identity: %i[component scope window_seconds]),
39
39
  memory: build_result(memories, complete: complete, recognized: memories.any?, issues: shared_issues + memory_issues,
@@ -12,7 +12,7 @@ module CtxChecker
12
12
  # 解析 show system state alarms,保留不完整或不支持的证据。
13
13
  def parse_output(text, complete:)
14
14
  source = clean(text)
15
- rows, issues = [], []
15
+ rows, issues = [], diagnostics
16
16
  source.each_line.with_index(1) do |raw, number|
17
17
  line = raw.strip
18
18
  # 配置阈值和复制的 HA 对端状态不属于本地当前告警。