ctx_checker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/COUNTER_CONTRACT.md +33 -0
  4. data/LICENSE +22 -0
  5. data/README-zh.md +93 -0
  6. data/README.md +93 -0
  7. data/docs/architecture-zh.md +487 -0
  8. data/docs/release.md +13 -0
  9. data/examples/offline.rb +17 -0
  10. data/lib/ctx_checker/applicability.rb +187 -0
  11. data/lib/ctx_checker/checks/aggregation.rb +73 -0
  12. data/lib/ctx_checker/checks/alarms.rb +25 -0
  13. data/lib/ctx_checker/checks/base.rb +66 -0
  14. data/lib/ctx_checker/checks/clock.rb +30 -0
  15. data/lib/ctx_checker/checks/counters.rb +77 -0
  16. data/lib/ctx_checker/checks/device_counters.rb +32 -0
  17. data/lib/ctx_checker/checks/hardware.rb +45 -0
  18. data/lib/ctx_checker/checks/interfaces.rb +35 -0
  19. data/lib/ctx_checker/checks/ntp.rb +47 -0
  20. data/lib/ctx_checker/checks/optics.rb +107 -0
  21. data/lib/ctx_checker/checks/resources.rb +63 -0
  22. data/lib/ctx_checker/checks/routing.rb +86 -0
  23. data/lib/ctx_checker/checks/temperature.rb +53 -0
  24. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +56 -0
  25. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +153 -0
  26. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +110 -0
  27. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +41 -0
  28. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +140 -0
  29. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +107 -0
  30. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +164 -0
  31. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +96 -0
  32. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +65 -0
  33. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +36 -0
  34. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +44 -0
  35. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +45 -0
  36. data/lib/ctx_checker/cisco_ios/parsers/system_values.rb +62 -0
  37. data/lib/ctx_checker/cisco_ios/profile.rb +39 -0
  38. data/lib/ctx_checker/cisco_nxos/parsers/show_bgp_ipv4_unicast_summary.rb +69 -0
  39. data/lib/ctx_checker/cisco_nxos/parsers/show_clock.rb +13 -0
  40. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +157 -0
  41. data/lib/ctx_checker/cisco_nxos/parsers/show_interface.rb +78 -0
  42. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +141 -0
  43. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +14 -0
  44. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +151 -0
  45. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +116 -0
  46. data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +21 -0
  47. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +89 -0
  48. data/lib/ctx_checker/cisco_nxos/profile.rb +35 -0
  49. data/lib/ctx_checker/collector.rb +175 -0
  50. data/lib/ctx_checker/evaluator.rb +49 -0
  51. data/lib/ctx_checker/failure.rb +24 -0
  52. data/lib/ctx_checker/finding.rb +19 -0
  53. data/lib/ctx_checker/h3c/parsers/counters_table.rb +56 -0
  54. data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +43 -0
  55. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +44 -0
  56. data/lib/ctx_checker/h3c/parsers/display_clock.rb +72 -0
  57. data/lib/ctx_checker/h3c/parsers/display_counters_inbound_interface.rb +21 -0
  58. data/lib/ctx_checker/h3c/parsers/display_counters_outbound_interface.rb +21 -0
  59. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +64 -0
  60. data/lib/ctx_checker/h3c/parsers/display_environment.rb +74 -0
  61. data/lib/ctx_checker/h3c/parsers/display_fan.rb +72 -0
  62. data/lib/ctx_checker/h3c/parsers/display_interface.rb +127 -0
  63. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +72 -0
  64. data/lib/ctx_checker/h3c/parsers/display_memory.rb +132 -0
  65. data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +84 -0
  66. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +47 -0
  67. data/lib/ctx_checker/h3c/parsers/display_power.rb +63 -0
  68. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +109 -0
  69. data/lib/ctx_checker/h3c/parsers/system_values.rb +46 -0
  70. data/lib/ctx_checker/h3c/profile.rb +49 -0
  71. data/lib/ctx_checker/hillstone/parsers/show_clock.rb +68 -0
  72. data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +44 -0
  73. data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +114 -0
  74. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +90 -0
  75. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +82 -0
  76. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +53 -0
  77. data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +22 -0
  78. data/lib/ctx_checker/hillstone/parsers/show_ip_ospf_neighbor.rb +21 -0
  79. data/lib/ctx_checker/hillstone/parsers/show_memory.rb +85 -0
  80. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +110 -0
  81. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +101 -0
  82. data/lib/ctx_checker/hillstone/profile.rb +40 -0
  83. data/lib/ctx_checker/observation.rb +165 -0
  84. data/lib/ctx_checker/observation_merger.rb +80 -0
  85. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +50 -0
  86. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +47 -0
  87. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +68 -0
  88. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +70 -0
  89. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +52 -0
  90. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +78 -0
  91. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +66 -0
  92. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +54 -0
  93. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +142 -0
  94. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +102 -0
  95. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +54 -0
  96. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +44 -0
  97. data/lib/ctx_checker/palo_alto/profile.rb +39 -0
  98. data/lib/ctx_checker/parser_registry.rb +43 -0
  99. data/lib/ctx_checker/parsers/base.rb +68 -0
  100. data/lib/ctx_checker/parsers/interface_names.rb +65 -0
  101. data/lib/ctx_checker/parsers/scoped_result.rb +35 -0
  102. data/lib/ctx_checker/policy.rb +104 -0
  103. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +48 -0
  104. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +46 -0
  105. data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +22 -0
  106. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +71 -0
  107. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +22 -0
  108. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +44 -0
  109. data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +47 -0
  110. data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +48 -0
  111. data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +34 -0
  112. data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +53 -0
  113. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +68 -0
  114. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +39 -0
  115. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +58 -0
  116. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +67 -0
  117. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +74 -0
  118. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +99 -0
  119. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +86 -0
  120. data/lib/ctx_checker/radware_alteon/port_counter_parser.rb +34 -0
  121. data/lib/ctx_checker/radware_alteon/profile.rb +145 -0
  122. data/lib/ctx_checker/radware_alteon.rb +8 -0
  123. data/lib/ctx_checker/report.rb +112 -0
  124. data/lib/ctx_checker/runtime_parser.rb +234 -0
  125. data/lib/ctx_checker/snapshot.rb +44 -0
  126. data/lib/ctx_checker/system_policy.rb +70 -0
  127. data/lib/ctx_checker/system_record.rb +81 -0
  128. data/lib/ctx_checker/version.rb +6 -0
  129. data/lib/ctx_checker.rb +17 -0
  130. metadata +271 -0
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 本地成员选中状态不能从远端 LACP 记录推断。
10
+ class DisplayLinkAggregationVerbose < CtxChecker::Parsers::Base
11
+ GROUP = /\A(?:Bridge-Aggregation|Route-Aggregation|BAGG|RAGG)\d+\z/i
12
+ LOCAL_HEADER = /\APort\s+Status\s+Priority(?:\s+(Index))?\s+Oper-Key(?:\s+(Flag))?\z/i
13
+ MEMBER = /\A([A-Za-z][A-Za-z-]*\d+(?:[\/.]\d+)*)(?:\(([AMR])\))?\s+([SUI])\s+(\d+)\s+(\d+)(?:\s+(\d+))?(?:\s+(\{[A-H]*\}))?\z/
14
+ METADATA = /\A(?:-+|Loadsharing Type:|Port Status:|Port:|Flags:|[A-H] --|Creation Mode:|Aggregation Mode:|System ID:)/
15
+
16
+ private
17
+
18
+ # 按聚合组及声明的列布局限定各本地表的作用域。
19
+ def parse_output(text, complete:)
20
+ text = clean(text)
21
+ if /\AInfo:\s+No link aggregation group defined at present\.\z/i.match?(text.strip)
22
+ return build_result([], complete: complete, recognized: true, issues: [ { code: :aggregation_not_configured } ])
23
+ end
24
+ rows, issues, group, remote, groups, columns, tables = [], [], nil, false, {}, nil, []
25
+ text.each_line do |raw|
26
+ line = raw.strip
27
+ if (match = /\AAggregate Interface:\s*(.*)\z/i.match(line))
28
+ group, remote, columns = nil, false, nil
29
+ if GROUP.match?(match[1])
30
+ group = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :h3c)
31
+ issues << { code: :duplicate_aggregation_group, group: group } if groups.key?(group)
32
+ groups[group] ||= 0
33
+ else
34
+ issues << { code: :invalid_aggregation_group }
35
+ end
36
+ elsif line == "Remote:"
37
+ remote = true
38
+ columns = nil
39
+ elsif line == "Local:"
40
+ remote = false
41
+ columns = nil
42
+ elsif !remote && (match = LOCAL_HEADER.match(line))
43
+ columns = { indexed: !match[1].nil?, flags: !match[2].nil?, group: group, count: 0 }
44
+ tables << columns
45
+ elsif !remote && (record = local_member(line, group: group, columns: columns))
46
+ rows << record
47
+ groups[group] += 1
48
+ columns[:count] += 1
49
+ elsif !remote && line.match?(/\A[A-Za-z][A-Za-z-]*\d+(?:[\/.]\d+)*(?:\([^\s]*\))?\s/)
50
+ issues << { code: :invalid_aggregation_member }
51
+ elsif !remote && !line.empty? && !METADATA.match?(line) && !/\AManagement VLANs:\s*(?:None|\d+(?:-\d+)?(?:,\s*\d+(?:-\d+)?)*)\z/i.match?(line)
52
+ issues << { code: :unrecognized_aggregation_line }
53
+ end
54
+ end
55
+ groups.each { |name, count| issues << { code: :aggregation_members_missing, group: name } if count.zero? }
56
+ tables.each { |table| issues << { code: :aggregation_members_missing, group: table[:group] } if table[:count].zero? }
57
+ build_result(rows, complete: complete, recognized: groups.any?, issues: issues, identity: %i[group member])
58
+ end
59
+
60
+ # 仅在身份匹配前移除已知端口角色标记,不修改状态文本。
61
+ def local_member(line, group:, columns:)
62
+ return unless group && columns
63
+ match = MEMBER.match(line)
64
+ return unless match && columns[:indexed] == !match[6].nil? && columns[:flags] == !match[7].nil?
65
+
66
+ { group: group, member: CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :h3c),
67
+ member_status: { "S" => :active, "U" => :unselected, "I" => :unselected }.fetch(match[3]), oper_status: :unknown }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 应用内存使用量必须来自同一物理内存池中经过确认的缓存调整行。
10
+ class DisplayMemory < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+ LEGACY_FIELDS = { "System Total Memory(bytes)" => :total, "Total Used Memory(bytes)" => :used, "Used Rate" => :rate }.freeze
13
+
14
+ private
15
+
16
+ # 解析 display memory,保留不完整或不支持的证据。
17
+ def parse_output(text, complete:)
18
+ source = clean(text)
19
+ return legacy_memory(source, complete: complete) if /^\s*System Total Memory\(bytes\):/.match?(source)
20
+
21
+ rows, contexts, sections = [], [], []
22
+ context, scope, unit, columns, current, section = nil, "physical", nil, nil, nil, nil
23
+ source.each_line do |raw|
24
+ line = raw.strip
25
+ if /\A(?:Container memory statistics|Memory statistics|The statistics about memory).*measured in (KB|bytes)(?:\(Available\))?:\z/i.match?(line)
26
+ unit = line.match(/measured in (KB|bytes)/i)[1].casecmp?("KB") ? 1024 : 1
27
+ scope = line.start_with?("Container") ? "container" : "physical"
28
+ context, columns, current = nil, nil, nil
29
+ sections << (section = { scope: scope, samples: 0 })
30
+ elsif (match = /\A((?:Chassis \d+ )?Slot \d+(?: CPU \d+)?):\z/i.match(line))
31
+ contexts << (context = { component: scope_name(match[1]), scope: scope, samples: 0 })
32
+ columns, current = nil, nil
33
+ elsif /\ATotal\s+Used\s+Free\b/.match?(line)
34
+ columns = line.split
35
+ elsif (match = /\AMem:\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+.*)?\z/.match(line))
36
+ if context && unit
37
+ values = line.delete_prefix("Mem:").split
38
+ if columns.nil? || values.size != columns.size
39
+ @issues << { code: :incomplete_memory_row, component: context[:component] }
40
+ end
41
+ record = memory_record(context[:component], scope, unit, match.captures)
42
+ rows << record
43
+ current = { record: record, raw: record.dup, values: (columns || []).zip(values).to_h, adjustments: 0 }
44
+ context[:samples] += 1
45
+ section[:samples] += 1 if section
46
+ else
47
+ @issues << { code: :missing_memory_scope_or_unit }
48
+ end
49
+ elsif (match = /\A-\/\+ Buffers\/Cache:\s+(\S+)\s+(\S+)\z/i.match(line))
50
+ adjust_cache(current, match.captures, unit)
51
+ elsif !line.empty? && !/\A(?:Swap:|LowMem:|HighMem:)/i.match?(line)
52
+ @issues << { code: :unrecognized_memory_line }
53
+ end
54
+ end
55
+ contexts.each do |entry|
56
+ @issues << { code: :missing_memory_sample, component: entry[:component], scope: entry[:scope] } if entry[:samples].zero?
57
+ end
58
+ sections.each do |entry|
59
+ @issues << { code: :missing_memory_section, scope: entry[:scope] } if entry[:samples].zero?
60
+ end
61
+ build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component scope])
62
+ end
63
+
64
+ # 旧交换机仅报告物理内存使用量,不能证明缓存调整后的应用使用量。
65
+ def legacy_memory(source, complete:)
66
+ values, seen = {}, {}
67
+ source.each_line do |raw|
68
+ line = raw.strip
69
+ next if line.empty?
70
+
71
+ key, value = line.split(/:\s*/, 2)
72
+ field = LEGACY_FIELDS[key]
73
+ unless field && value
74
+ @issues << { code: :unrecognized_memory_line }
75
+ next
76
+ end
77
+ if seen[field]
78
+ values[field] = nil
79
+ @issues << { code: :duplicate_memory_field, field: field, component: "system" }
80
+ else
81
+ values[field] = if field == :rate
82
+ match = /\A(#{NUMBER})%\z/i.match(value)
83
+ measurement(match && match[1], field: :utilization, component: "system", percent: true)
84
+ else
85
+ count(value, field: :memory, component: "system")
86
+ end
87
+ end
88
+ seen[field] = true
89
+ end
90
+ total, used, rate = values.values_at(:total, :used, :rate)
91
+ valid = total && total.positive? && used && used <= total && rate && (rate - used.fdiv(total) * 100).abs < 1
92
+ @issues << { code: :inconsistent_memory_totals, component: "system" } unless valid
93
+ row = { component: "system", scope: "physical", utilization: valid ? used.fdiv(total) * 100 : nil,
94
+ total_bytes: total, used_bytes: used, free_bytes: nil, basis: :used_including_cache }
95
+ build_result([ row ], complete: complete, recognized: true, identity: %i[component scope])
96
+ end
97
+
98
+ # 由兼容的容量与使用量测量计算内存利用率。
99
+ def memory_record(component, scope, unit, values)
100
+ total, used, free = values.map { |value| count(value, field: :memory, component: component) }
101
+ valid = total && total.positive? && used && free && used <= total && free <= total && (scope == "container" || used + free == total)
102
+ @issues << { code: :inconsistent_memory_totals, component: component } unless valid
103
+ { component: component, scope: scope, utilization: valid ? used.fdiv(total) * 100 : nil,
104
+ total_bytes: total && total * unit, used_bytes: used && used * unit, free_bytes: scope == "physical" && free ? free * unit : nil,
105
+ basis: scope == "container" ? :used_total : :used_including_cache }
106
+ end
107
+
108
+ # 仅在平台明确报告时扣除可回收内存。
109
+ def adjust_cache(current, values, unit)
110
+ unless current && current[:record][:scope] == "physical"
111
+ @issues << { code: :unscoped_memory_cache_adjustment }
112
+ return
113
+ end
114
+ row, raw, fields = current.values_at(:record, :raw, :values)
115
+ current[:adjustments] += 1
116
+ used, free = values.map { |value| count(value, field: :memory, component: row[:component]) }
117
+ buffers, cached = fields.values_at("Buffers", "Cached").map { |value| count(value, field: :memory, component: row[:component]) }
118
+ valid = raw[:utilization] && used && free && buffers && cached && current[:adjustments] == 1 &&
119
+ (used + free) * unit == raw[:total_bytes] && used * unit == raw[:used_bytes] - (buffers + cached) * unit &&
120
+ free * unit == raw[:free_bytes] + (buffers + cached) * unit
121
+ unless valid
122
+ row.replace(raw.merge(utilization: nil, basis: :unknown))
123
+ @issues << { code: :invalid_memory_cache_adjustment, component: row[:component] }
124
+ return
125
+ end
126
+ row.merge!(used_bytes: used * unit, available_bytes: free * unit,
127
+ utilization: (used * unit).fdiv(raw[:total_bytes]) * 100, basis: :used_minus_cache)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 系统对等体才是同步服务器;reference ID 可能是本地时钟或 IPv6 哈希。
10
+ class DisplayNtpServiceStatus < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+ FIELDS = { "clock status" => :synchronized, "clock stratum" => :stratum,
13
+ "reference clock id" => :reference, "system peer" => :peer,
14
+ "clock offset" => :offset_ms, "leap indicator" => :leap }.freeze
15
+ AUXILIARY = /\A(?:Local mode|NTP version|Clock jitter|Stability|Clock precision|Root delay|Root dispersion|Peer dispersion|Nominal frequency|Actual frequency|Reference time|System poll interval):/i
16
+
17
+ private
18
+
19
+ # 解析 display ntp service status,保留不完整或不支持的证据。
20
+ def parse_output(text, complete:)
21
+ source = clean(text)
22
+ row, seen, local_sync = { component: "system" }, {}, []
23
+ source.each_line do |raw|
24
+ line = raw.strip
25
+ next if line.empty?
26
+ key, value = line.split(/:\s*/, 2)
27
+ field = FIELDS[key.downcase]
28
+ if field && value
29
+ if seen[field]
30
+ row[field] = nil
31
+ @issues << { code: :duplicate_ntp_field, field: field }
32
+ else
33
+ row[field] = parse_field(field, value)
34
+ end
35
+ seen[field] = true
36
+ elsif key.casecmp?("Sync state from NTP server to local clock")
37
+ local_sync << value&.downcase
38
+ elsif !line.empty? && !AUXILIARY.match?(line)
39
+ @issues << { code: :unrecognized_ntp_line }
40
+ end
41
+ end
42
+ @issues << { code: :missing_ntp_status } unless seen[:synchronized]
43
+ apply_local_sync(row, local_sync) unless local_sync.empty?
44
+ if local_clock?(row[:peer]) || row[:peer].nil? && local_clock?(row[:reference])
45
+ row[:synchronized] = false
46
+ @issues << { code: :local_ntp_reference }
47
+ end
48
+ build_result(seen.any? ? [ row ] : [], complete: complete, recognized: seen.any?, identity: [ :component ])
49
+ end
50
+
51
+ # 仅存在服务器关联不能证明设备时钟已受同步校准。
52
+ def apply_local_sync(row, states)
53
+ if states.any? { |state| [ "clock not synced", "clock unsynced" ].include?(state) }
54
+ row[:synchronized] = false
55
+ @issues << { code: :ntp_local_clock_unsynchronized }
56
+ elsif states != [ "clock synced" ]
57
+ row[:synchronized] = nil unless row[:synchronized] == false
58
+ @issues << { code: :unknown_ntp_local_clock_state }
59
+ end
60
+ end
61
+
62
+ # 识别本地参考时钟,避免视为已同步的外部对等体。
63
+ def local_clock?(value)
64
+ /\A(?:LOCAL\(\d+\)|LOCL|127\.127\.1\.\d+)\z/i.match?(value.to_s)
65
+ end
66
+
67
+ # 解释单个支持的命令字段,不丢弃其他无关事实。
68
+ def parse_field(field, value)
69
+ parsed = case field
70
+ when :synchronized then { "synchronized" => true, "unsynchronized" => false }[value.downcase]
71
+ when :stratum then value.to_i if /\A\d+\z/.match?(value) && (0..16).cover?(value.to_i)
72
+ when :leap then value.to_i(2) if /\A[01]{2}\z/.match?(value)
73
+ when :offset_ms
74
+ match = /\A(-?\d+(?:\.\d+)?)\s*ms\z/i.match(value)
75
+ measurement(match[1], field: field, component: "system") if match
76
+ when :reference, :peer then value unless /\A(?:none|--)\z/i.match?(value)
77
+ end
78
+ @issues << { code: :invalid_ntp_field, field: field } if parsed.nil? && !%i[reference peer].include?(field)
79
+ parsed
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 邻居表作用域由进程和区域标题确定。
10
+ class DisplayOspfPeer < CtxChecker::Parsers::Base
11
+ private
12
+
13
+ # 解析 display ospf peer,保留不完整或不支持的证据。
14
+ def parse_output(text, complete:)
15
+ text = clean(text)
16
+ if /\AOSPF is not configured\.\z/i.match?(text.strip)
17
+ return build_result([], complete: complete, recognized: true, issues: [ { code: :protocol_not_configured } ])
18
+ end
19
+ rows, issues, process, area, header = [], [], nil, nil, false
20
+ text.each_line do |raw|
21
+ line = raw.strip
22
+ if (match = /\AOSPF Process (\d+) with Router ID ([\d.]+)\z/i.match(line))
23
+ process, area = match[1], nil
24
+ elsif (match = /\AArea:\s*([\d.]+)\z/i.match(line))
25
+ area = match[1]
26
+ elsif /\ARouter ID\s+Address\s+Pri\s+Dead-Time\s+State\s+Interface\z/i.match?(line)
27
+ header = true
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])
30
+ issues << { code: :ospf_scope_missing } unless process && area
31
+ rows << { vrf: nil, process_id: process, area: area,
32
+ interface: CtxChecker::Parsers::InterfaceNames.normalize(match[5], platform: :h3c), router_id: match[1], neighbor_address: match[2],
33
+ state: match[3].downcase == "2-way" ? :two_way : match[3].downcase.to_sym,
34
+ network_type: :unknown, local_role: :unknown, neighbor_role: match[4] && match[4] != "-" ? match[4].downcase.to_sym : :unknown }
35
+ else
36
+ issues << { code: :invalid_ospf_peer }
37
+ end
38
+ elsif !line.empty? && line != "Neighbor Brief Information"
39
+ issues << { code: :unrecognized_ospf_line }
40
+ end
41
+ end
42
+ build_result(rows, complete: complete, recognized: header && rows.any?, issues: issues, identity: %i[vrf process_id interface router_id])
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 逐电源评估;电源缺失时仍需区分槽位是否应有模块。
10
+ class DisplayPower < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+
13
+ private
14
+
15
+ # 解析 display power,保留不完整或不支持的证据。
16
+ def parse_output(text, complete:)
17
+ source = clean(text)
18
+ rows, location, columns, contexts, context = [], "system", nil, [], nil
19
+ source.each_line do |raw|
20
+ line = raw.strip
21
+ if (match = /\A((?:Chassis \d+ )?Slot \d+|Chassis \d+)\s*:\z/i.match(line))
22
+ location, columns = scope_name(match[1]), nil
23
+ contexts << (context = { component: location, count: 0 })
24
+ elsif (match = /\APowerID\s+State\s+Mode\s+Current\(A\)\s+Voltage\(V\)\s+Power\(W\)(?:\s+(FanDirection))?(?:\s+(Switch))?\z/i.match(line))
25
+ columns = match.captures.compact.map(&:downcase)
26
+ elsif columns && (match = /\A(\d+)\s+(\S+)\s+(?:AC|DC|--)\s+#{NUMBER}\s+#{NUMBER}\s+(#{NUMBER})(?:\s+(\S+))?(?:\s+(\S+))?\z/i.match(line)) && match.captures.drop(3).compact.size == columns.size
27
+ row = power_record(location, match[1], match[2], match[3])
28
+ metadata = columns.zip(match.captures.drop(3).compact).to_h
29
+ if metadata.key?("switch") && !metadata["switch"].casecmp?("on")
30
+ row[:status] = :unknown if row[:status] == :ok
31
+ @issues << { code: :power_switch_not_on, component: row[:component] }
32
+ end
33
+ rows << row
34
+ context[:count] += 1 if context
35
+ elsif (match = /\APower\s+(\d+)\s+State:\s*(\S+)\z/i.match(line))
36
+ rows << power_record(location, match[1], match[2], nil)
37
+ context[:count] += 1 if context
38
+ elsif !line.empty? && !/\A(?:-+|Input Power:\s*(?:#{NUMBER}\s*(?:W|\(W\))|--|N\/?A|error))\z/i.match?(line)
39
+ @issues << { code: :unrecognized_power_line }
40
+ end
41
+ end
42
+ contexts.each do |entry|
43
+ @issues << { code: :missing_power_scope_records, component: entry[:component] } if entry[:count].zero?
44
+ end
45
+ build_result(rows, complete: complete, recognized: rows.any?, identity: %i[component kind])
46
+ end
47
+
48
+ # 构建包含状态与测量值的归一化电源记录。
49
+ def power_record(location, id, state, value)
50
+ component = "#{location}/power #{id}"
51
+ status = component_state(state, component: component)
52
+ watts = measurement(value, field: :watts, component: component)
53
+ if watts&.negative?
54
+ watts = nil
55
+ @issues << { code: :invalid_measurement, component: component, field: :watts }
56
+ end
57
+ { component: component, kind: :power_supply, status: status,
58
+ present: status == :absent ? false : (status == :unknown ? nil : true), watts: watts }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module H3c
8
+ module Parsers
9
+ # 诊断表保留各光通道及模块自身阈值。
10
+ class DisplayTransceiverDiagnosis < CtxChecker::Parsers::Base
11
+ NUMBER = /(?:-?\d+(?:\.\d+)?|N\/A|--)/i
12
+ ABSENT = /\A(?:The )?(?:transceiver is absent|no transceiver|transceiver is not present)\.?\z/i
13
+ DOM_UNSUPPORTED = /\A(?:(?:The )?transceiver does not support (?:digital diagnosis|diagnostic(?: information)?|this function)|(?:The )?diagnostic information is not supported)\.?\z/i
14
+ OPERATION_UNSUPPORTED = /\AThe operation is not supported\.\z/i
15
+
16
+ private
17
+
18
+ # 解析 display transceiver diagnosis,保留不完整或不支持的证据。
19
+ def parse_output(text, complete:)
20
+ text = clean(text)
21
+ rows, issues = [], []
22
+ text.split(/(?=^\S+ transceiver diagnostic information:)/i).each do |block|
23
+ next if block.strip.empty?
24
+ match = /\A(\S+) transceiver diagnostic information:/i.match(block)
25
+ unless match
26
+ issues << { code: :unrecognized_optics_block }
27
+ next
28
+ end
29
+ name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :h3c)
30
+ lines = block.lines.drop(1).map(&:strip).reject(&:empty?)
31
+ if (state = availability(lines, name))
32
+ rows.concat(measurements(name, nil, nil, nil, **state))
33
+ next
34
+ end
35
+ samples, limits, section, mode = [], {}, nil, nil
36
+ lines.each do |line|
37
+ case line
38
+ when /\ACurrent diagnostic parameters:/i then section, mode = :current, nil
39
+ when /\AAlarm thresholds:/i then section, mode = :alarm, nil
40
+ when /\AWarning thresholds:/i then section, mode = :warning, nil
41
+ when /RX power\(dBm\)\s+TX power\(dBm\)\z/i
42
+ mode = line.start_with?("[channel]") ? :lanes : :single
43
+ else
44
+ if section == :current && mode == :single && (sample = /\A#{NUMBER}\s+#{NUMBER}\s+#{NUMBER}\s+(#{NUMBER})\s+(#{NUMBER})\z/i.match(line))
45
+ samples << [ nil, number(sample[1], interface: name), number(sample[2], interface: name) ]
46
+ elsif section == :current && mode == :lanes && (sample = /\A(\d+)\s+#{NUMBER}\s+(#{NUMBER})\s+(#{NUMBER})\z/i.match(line))
47
+ samples << [ sample[1].to_i, number(sample[2], interface: name), number(sample[3], interface: name) ]
48
+ elsif %i[alarm warning].include?(section) && mode && (limit = /\A(High|Low)\s+#{NUMBER}\s+#{NUMBER}\s+#{NUMBER}\s+(#{NUMBER})\s+(#{NUMBER})\z/i.match(line))
49
+ key = "#{limit[1].downcase}_#{section}".to_sym
50
+ if limits.key?(key)
51
+ issues << { code: :duplicate_optics_threshold, interface: name }
52
+ else
53
+ limits[key] = [ number(limit[2], interface: name), number(limit[3], interface: name) ]
54
+ end
55
+ elsif line.match?(/\A(?:High|Low)\b/) || (mode && line.match?(/\A[-\d]/))
56
+ issues << { code: :invalid_optics_measurement, interface: name }
57
+ elsif !line.empty? && !line.match?(/\A-+\z/)
58
+ issues << { code: :unrecognized_optics_field, interface: name }
59
+ end
60
+ end
61
+ end
62
+ issues << { code: :optics_measurements_missing, interface: name } if samples.empty?
63
+ samples.each do |lane, rx, tx|
64
+ measurements(name, lane, rx, tx, present: true, dom_supported: true).each_with_index do |record, index|
65
+ limits.each { |key, values| record[key] = values[index] }
66
+ rows << record
67
+ end
68
+ end
69
+ end
70
+ build_result(rows, complete: complete, recognized: rows.any?, issues: issues, identity: %i[interface lane metric])
71
+ end
72
+
73
+ # 状态标记不能掩盖后续数据、重复状态或未知诊断内容。
74
+ def availability(lines, name)
75
+ absent = lines.any? { |line| ABSENT.match?(line) }
76
+ unsupported = lines.any? { |line| DOM_UNSUPPORTED.match?(line) }
77
+ operation_unsupported = lines.any? { |line| OPERATION_UNSUPPORTED.match?(line) }
78
+ return unless absent || unsupported || operation_unsupported
79
+
80
+ if lines.size != 1
81
+ @issues << { code: :ambiguous_optics_availability, interface: name }
82
+ return { present: nil, dom_supported: nil }
83
+ end
84
+ { present: operation_unsupported ? nil : !absent, dom_supported: absent ? nil : false }
85
+ end
86
+
87
+ # 解析数值测量,不将未知文本转换为零。
88
+ def number(value, interface:)
89
+ return unless value.match?(/\A-?\d+(?:\.\d+)?\z/)
90
+
91
+ number = Float(value)
92
+ return number if number.finite?
93
+
94
+ @issues << { code: :invalid_optics_measurement, interface: interface }
95
+ nil
96
+ end
97
+
98
+ # 提取支持的传感器值,保留测量不可用的状态。
99
+ def measurements(name, lane, rx, tx, present:, dom_supported:)
100
+ [ [ :rx_power, rx ], [ :tx_power, tx ] ].map do |metric, value|
101
+ { interface: name, lane: lane, metric: metric, value: value, unit: :dbm,
102
+ low_alarm: nil, low_warning: nil, high_warning: nil, high_alarm: nil, alarm: :unknown,
103
+ present: present, dom_supported: dom_supported }
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CtxChecker
4
+ module H3c
5
+ module Parsers
6
+ # 共享 Comware 标量转换;测量缺失始终不能变成零。
7
+ module SystemValues
8
+ NUMBER = /(?:-?\d+(?:\.\d+)?|--|N\/?A|error)/i
9
+ COMPONENT_STATES = { "normal" => :ok, "ok" => :ok, "fault" => :failed,
10
+ "faulty" => :failed, "error" => :failed, "fandirectionfault" => :failed,
11
+ "absent" => :absent, "warning" => :warning }.freeze
12
+
13
+ private
14
+
15
+ # 解析支持的传感器测量值及回显单位。
16
+ def measurement(value, field:, component:, percent: false)
17
+ return if value.nil? || /\A(?:--|N\/?A|error)\z/i.match?(value)
18
+ number = Float(value, exception: false)
19
+ return number if number&.finite? && (!percent || (0..100).cover?(number))
20
+
21
+ @issues << { code: :invalid_measurement, field: field, component: component }
22
+ nil
23
+ end
24
+
25
+ # 解析非负计数,同时保留未知值。
26
+ def count(value, field:, component:)
27
+ return value.to_i if /\A\d+\z/.match?(value.to_s)
28
+
29
+ @issues << { code: :invalid_measurement, field: field, component: component }
30
+ nil
31
+ end
32
+
33
+ # 按平台支持的标签归一化硬件组件状态。
34
+ def component_state(value, component:)
35
+ COMPONENT_STATES.fetch(value.downcase) do
36
+ @issues << { code: :unrecognized_component_state, component: component }
37
+ :unknown
38
+ end
39
+ end
40
+
41
+ # 确定模块或路由上下文的身份。
42
+ def scope_name(value) = value.downcase.strip.gsub(/\s+/, " ")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ctx_parser/profile"
4
+ require_relative "parsers/display_interface"
5
+ require_relative "parsers/display_link_aggregation_verbose"
6
+ require_relative "parsers/display_transceiver_diagnosis"
7
+ require_relative "parsers/display_bgp_peer"
8
+ require_relative "parsers/display_ospf_peer"
9
+
10
+ require_relative "parsers/display_cpu_usage"
11
+ require_relative "parsers/display_memory"
12
+ require_relative "parsers/display_environment"
13
+ require_relative "parsers/display_fan"
14
+ require_relative "parsers/display_power"
15
+ require_relative "parsers/display_clock"
16
+ require_relative "parsers/display_ntp_service_status"
17
+ require_relative "parsers/display_alarm_active"
18
+ require_relative "parsers/display_counters_inbound_interface"
19
+ require_relative "parsers/display_counters_outbound_interface"
20
+
21
+ module CtxChecker
22
+ module H3c
23
+ # 声明 Comware 只读查询;BGP 明确限定为公网 IPv4 单播。
24
+ class Profile < CtxParser::Profile
25
+ CATALOG = CtxParser::CommandCatalog.new([
26
+ { dimensions: %i[interfaces counters], command: "display interface", parser: Parsers::DisplayInterface },
27
+ { dimensions: [ :counters ], command: "display counters inbound interface", parser: Parsers::DisplayCountersInboundInterface },
28
+ { dimensions: [ :counters ], command: "display counters outbound interface", parser: Parsers::DisplayCountersOutboundInterface },
29
+ { dimensions: [ :aggregation ], command: "display link-aggregation verbose", parser: Parsers::DisplayLinkAggregationVerbose },
30
+ { dimensions: [ :optics ], command: "display transceiver diagnosis interface", parser: Parsers::DisplayTransceiverDiagnosis },
31
+ { dimensions: [ :bgp ], command: "display bgp peer ipv4", parser: Parsers::DisplayBgpPeer },
32
+ { dimensions: [ :ospf ], command: "display ospf peer", parser: Parsers::DisplayOspfPeer },
33
+ { dimensions: [ :cpu ], command: "display cpu-usage", parser: Parsers::DisplayCpuUsage },
34
+ { dimensions: [ :memory ], command: "display memory", parser: Parsers::DisplayMemory },
35
+ { dimensions: [ :temperature ], command: "display environment", parser: Parsers::DisplayEnvironment },
36
+ { dimensions: [ :environment ], command: "display fan", parser: Parsers::DisplayFan },
37
+ { dimensions: [ :power ], command: "display power", parser: Parsers::DisplayPower },
38
+ { dimensions: [ :clock ], command: "display clock", parser: Parsers::DisplayClock },
39
+ { dimensions: [ :ntp ], command: "display ntp-service status", parser: Parsers::DisplayNtpServiceStatus },
40
+ { dimensions: [ :alarms ], command: "display alarm active", parser: Parsers::DisplayAlarmActive }
41
+ ], multi_command: true)
42
+ COMMANDS = CATALOG.commands
43
+ PARSERS = CATALOG.parsers
44
+
45
+ # 每个已观察接口都必须保留入站与出站两个方向的证据状态。
46
+ def counter_metrics = %i[in_errors out_errors].freeze
47
+ end
48
+ end
49
+ end