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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module CiscoIos
8
+ module Parsers
9
+ # IOS 进程 CPU 包含中断工作量,不代表 IOS-XE 的 Linux 整机 CPU。
10
+ class ShowProcessesCpu < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+
13
+ private
14
+
15
+ # 解析 show processes cpu,保留不完整或不支持的证据。
16
+ def parse_output(text, complete:)
17
+ records, issues = [], []
18
+ recognized = false
19
+ clean(text).each_line.with_index(1) do |source, number|
20
+ line = source.strip
21
+ next if line.empty?
22
+
23
+ if (match = /\ACPU utilization for five seconds:\s*([^%]+)%\/([^%]+)%; one minute:\s*([^%]+)%; five minutes:\s*([^%]+)%\z/i.match(line))
24
+ recognized = true
25
+ [ [ 5, match[1], match[2] ], [ 60, match[3], nil ], [ 300, match[4], nil ] ].each do |window, value, interrupt|
26
+ utilization = percentage(value)
27
+ interrupt_utilization = percentage(interrupt)
28
+ valid = utilization && (!interrupt || interrupt_utilization && interrupt_utilization <= utilization)
29
+ issues << { code: :invalid_cpu_value, line: number } unless valid
30
+ records << { component: "processor", scope: "ios_processes", window_seconds: window,
31
+ utilization: utilization, interrupt_utilization: interrupt_utilization }
32
+ end
33
+ elsif /\A(?:PID\s+Runtime|\d+\s+\d+\s+\d+\s+\d+\s+[\d.]+%\s+[\d.]+%\s+[\d.]+%\s+\d+\s+)/.match?(line)
34
+ next
35
+ else
36
+ issues << unparsed(number)
37
+ end
38
+ end
39
+ build_result(records, complete: complete, recognized: recognized, issues: issues,
40
+ identity: %i[component scope window_seconds])
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module CtxChecker
6
+ module CiscoIos
7
+ module Parsers
8
+ # 仅在 Cisco 命令语义一致时共享小型值转换方法。
9
+ module SystemValues
10
+ NUMBER = /[+-]?\d+(?:\.\d+)?/
11
+
12
+ private
13
+
14
+ # 映射组件回显状态,不将缺失解释为健康。
15
+ def component_status(value)
16
+ case value.to_s.strip.downcase
17
+ when "ok", "normal", "good", "green", "on", "powered-up" then :ok
18
+ when "warning", "minor", "yellow" then :warning
19
+ when "critical", "major", "red", "shutdown" then :critical
20
+ when "fail", "failed", "faulty", "bad", "off", "no input power", "non-operational" then :failed
21
+ when "absent", "not present", "not installed", "notsupported/none" then :absent
22
+ else :unknown
23
+ end
24
+ end
25
+
26
+ # 解析支持的数值字段,并拒绝格式错误的测量值。
27
+ def numeric(token)
28
+ return unless /\A#{NUMBER}\z/.match?(token.to_s)
29
+
30
+ value = Float(token)
31
+ value if value.finite?
32
+ end
33
+
34
+ # 外部数值无效时降低证据完整性,不使类型化记录构建失败。
35
+ def checked_number(token, issues:, component:, nonnegative: false)
36
+ return if %w[N/A --].include?(token.to_s.upcase)
37
+
38
+ value = numeric(token)
39
+ return value if value && (!nonnegative || value >= 0)
40
+
41
+ issues << { code: :invalid_numeric_value, component: component }
42
+ nil
43
+ end
44
+
45
+ # 将利用率归一化为经过校验的百分比。
46
+ def percentage(token)
47
+ value = numeric(token)
48
+ value if value && value.between?(0, 100)
49
+ end
50
+
51
+ # 保留无法解释的回显行诊断。
52
+ def unparsed(number) = { code: :unparsed_row, line: number }
53
+
54
+ # 读数冲突不能抹除已明确观察到的组件故障。
55
+ def most_severe_status(states)
56
+ ranks = { ok: 0, absent: 1, unknown: 2, warning: 3, failed: 4, critical: 5 }
57
+ states.max_by { |state| ranks.fetch(state) }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ctx_parser/profile"
4
+ require_relative "parsers/show_interfaces"
5
+ require_relative "parsers/show_etherchannel_summary"
6
+ require_relative "parsers/show_interfaces_transceiver_detail"
7
+ require_relative "parsers/show_ip_bgp_summary"
8
+ require_relative "parsers/show_ip_ospf_neighbor"
9
+ require_relative "parsers/show_processes_cpu"
10
+ require_relative "parsers/show_memory_statistics"
11
+ require_relative "parsers/show_environment_all"
12
+ require_relative "parsers/show_clock_detail"
13
+ require_relative "parsers/show_ntp_status"
14
+ require_relative "parsers/show_facility_alarm_status"
15
+ require_relative "parsers/show_ip_traffic"
16
+
17
+ module CtxChecker
18
+ module CiscoIos
19
+ # IOS 与 IOS-XE 共用命令契约;型号特有语法通过解析器覆盖。
20
+ class Profile < CtxParser::Profile
21
+ CATALOG = CtxParser::CommandCatalog.new([
22
+ { dimensions: %i[interfaces counters], command: "show interfaces", parser: Parsers::ShowInterfaces },
23
+ { dimensions: [ :aggregation ], command: "show etherchannel summary", parser: Parsers::ShowEtherchannelSummary },
24
+ { dimensions: [ :optics ], command: "show interfaces transceiver detail", parser: Parsers::ShowInterfacesTransceiverDetail },
25
+ { dimensions: [ :bgp ], command: "show ip bgp summary", parser: Parsers::ShowIpBgpSummary },
26
+ { dimensions: [ :ospf ], command: "show ip ospf neighbor", parser: Parsers::ShowIpOspfNeighbor },
27
+ { dimensions: [ :cpu ], command: "show processes cpu", parser: Parsers::ShowProcessesCpu },
28
+ { dimensions: [ :memory ], command: "show memory statistics", parser: Parsers::ShowMemoryStatistics },
29
+ { dimensions: %i[temperature environment power], command: "show environment all", parser: Parsers::ShowEnvironmentAll },
30
+ { dimensions: [ :clock ], command: "show clock detail", parser: Parsers::ShowClockDetail },
31
+ { dimensions: [ :ntp ], command: "show ntp status", parser: Parsers::ShowNtpStatus },
32
+ { dimensions: [ :alarms ], command: "show facility-alarm status", parser: Parsers::ShowFacilityAlarmStatus },
33
+ { dimensions: [ :device_counters ], command: "show ip traffic", parser: Parsers::ShowIpTraffic }
34
+ ])
35
+ COMMANDS = CATALOG.commands
36
+ PARSERS = CATALOG.parsers
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../cisco_ios/parsers/show_ip_bgp_summary"
4
+
5
+ module CtxChecker
6
+ module CiscoNxos
7
+ module Parsers
8
+ # NX-OS 在各 VRF、地址族表内独立校验已配置对等体总数。
9
+ class ShowBgpIpv4UnicastSummary < CiscoIos::Parsers::ShowIpBgpSummary
10
+ PEER_COUNTS = /\ABGP table version is \d+,\s*(.+?)\s+config peers (\d+),\s*capable peers (\d+)\z/i
11
+ STATISTICS = [
12
+ /\ADampening configured, \d+ history paths, \d+ dampened paths\z/i,
13
+ /\A\d+ received paths for inbound soft reconfiguration\z/i,
14
+ /\A\d+ identical, \d+ modified, \d+ filtered received paths using \d+ bytes\z/i
15
+ ].freeze
16
+
17
+ private
18
+
19
+ # 缺少地址族能力时保留已观察对等体,但不声明会话健康。
20
+ def peer_state(text)
21
+ return super unless text.match?(/\A\d+\s+\(No Cap\)\z/i)
22
+
23
+ @issues << { code: :peer_capability_unavailable }
24
+ [ :unknown, nil ]
25
+ end
26
+
27
+ # 配置数量覆盖全部记录;具备能力的对等体数量不等于已建立数量。
28
+ def parse_statistics(line, section, scope, issues, number)
29
+ if (match = PEER_COUNTS.match(line))
30
+ counts = (section[:peer_counts] ||= [])
31
+ issues << { code: :duplicate_peer_count, line: number } unless counts.empty?
32
+ counts << { configured: Integer(match[2], 10), capable: Integer(match[3], 10) }
33
+ if address_family(match[1]) != scope[:afi_safi]
34
+ issues << { code: :conflicting_address_family, line: number }
35
+ end
36
+ elsif line.match?(/\ABGP table version\b.*\b(?:config|capable) peers\b/i)
37
+ issues << { code: :invalid_peer_count, line: number }
38
+ else
39
+ return false
40
+ end
41
+ true
42
+ end
43
+
44
+ # 仅当能力计数同样一致时,零数量才能证明空表。
45
+ def section_empty?(section)
46
+ super || section[:peer_counts] == [ { configured: 0, capable: 0 } ]
47
+ end
48
+
49
+ # 校验每条总数声明,避免重复或矛盾总数掩盖记录缺失。
50
+ def validate_section(section, issues)
51
+ super
52
+ Array(section[:peer_counts]).each do |counts|
53
+ unless counts[:configured] == section[:count]
54
+ issues << { code: :count_mismatch, expected: counts[:configured], observed: section[:count] }
55
+ end
56
+ if counts[:capable] > counts[:configured]
57
+ issues << { code: :invalid_capable_peer_count, expected: counts[:configured], observed: counts[:capable] }
58
+ end
59
+ end
60
+ end
61
+
62
+ # 可选路由统计仅接受文档明确的完整格式。
63
+ def auxiliary?(line)
64
+ super || STATISTICS.any? { |pattern| pattern.match?(line) }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../cisco_ios/parsers/show_clock_detail"
4
+
5
+ module CtxChecker
6
+ module CiscoNxos
7
+ module Parsers
8
+ # NX-OS 使用相同的时钟正文,不含 IOS 的 detail 关键字。
9
+ class ShowClock < CtxChecker::CiscoIos::Parsers::ShowClockDetail
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../cisco_ios/parsers/system_values"
5
+ require "ctx_parser/result_set"
6
+
7
+ module CtxChecker
8
+ module CiscoNxos
9
+ module Parsers
10
+ # 每个环境章节分别产生类型化组件观测。
11
+ class ShowEnvironment < CtxChecker::Parsers::Base
12
+ include CtxChecker::CiscoIos::Parsers::SystemValues
13
+ HEADERS = /\A(?:[- ]+|\(?Watts\s*\)?(?:\s+\(?Watts\s*\)?)*|\(?Celsius\)?(?:\s+\(?Celsius\)?)*|Module\s+Sensor|Clock\s+Model|Fan\s+Model|Power\s+Actual|Supply\s+Model|Actual\s+Power|Module\s+Model|Output\s+Capacity|N\/A - Per module|Voltage:)/i
14
+ POWER_VALUE = /(?:#{NUMBER}\s*W|N\/A(?:\s*W)?|--)/i
15
+
16
+ private
17
+
18
+ # 解析 show environment,保留不完整或不支持的证据。
19
+ def parse_output(text, complete:)
20
+ @rows = { temperature: [], environment: [], power: [] }
21
+ @problems = @rows.transform_values { [] }
22
+ @section = nil
23
+ @section_size = 0
24
+ @power_columns = nil
25
+ clean(text).each_line.with_index(1) do |source, number|
26
+ line = source.strip
27
+ next if line.empty?
28
+ next if section_line(line) || header_line(line)
29
+ next if @section == :fan && /\AFan Zone Speed:\s*Zone\s+\d+:/i.match?(line)
30
+ next if parse_temperature(line) || parse_component(line) || parse_power(line)
31
+ next if @section == :summary && /\A(?:Power Supply redundancy|Total Power |Total Grid|Cumulative Power)/i.match?(line)
32
+
33
+ dimension = { temperature: :temperature, fan: :environment, clock: :environment, power: :power, module: :environment, summary: :power }[@section]
34
+ (dimension ? [ @problems[dimension] ] : @problems.values).each { |issues| issues << unparsed(number) }
35
+ end
36
+ finish_section
37
+ CtxParser::ResultSet.new(results: @rows.to_h do |dimension, rows|
38
+ identity = dimension == :temperature ? [ :component ] : %i[component kind]
39
+ [ dimension, build_result(rows, complete: complete, recognized: rows.any?, issues: @problems[dimension], identity: identity) ]
40
+ end)
41
+ end
42
+
43
+ # 识别环境章节内的行类型。
44
+ def section_line(line)
45
+ section = case line
46
+ when /\ATemperature:\z/i then :temperature
47
+ when /\AFan:\z/i then :fan
48
+ when /\AClock:\z/i then :clock
49
+ when /\APower Supply:\z/i then :power
50
+ when /\APower Usage Summary:\z/i then :summary
51
+ when /\AModule\s+Model.*Status/i then :module
52
+ else return false
53
+ end
54
+ finish_section
55
+ @section = section
56
+ @section_size = section_rows.size
57
+ @power_columns = nil if section == :power
58
+ true
59
+ end
60
+
61
+ # 行被截断时,不能将已声明的输入列误当作额定容量。
62
+ def header_line(line)
63
+ if @section == :power && /\ASupply\s+Model\s+Output\b/i.match?(line)
64
+ @power_columns = /\bInput\b/i.match?(line) ? 3 : 2
65
+ end
66
+ HEADERS.match?(line)
67
+ end
68
+
69
+ # 选择属于当前环境章节的数据行。
70
+ def section_rows
71
+ dimension = { temperature: :temperature, fan: :environment, clock: :environment, power: :power, module: :environment }[@section]
72
+ dimension ? @rows[dimension] : []
73
+ end
74
+
75
+ # 独立完成当前环境章节,不影响其他章节的结果。
76
+ def finish_section
77
+ return unless @section && @section != :summary && section_rows.size == @section_size
78
+
79
+ dimension = %i[fan clock module].include?(@section) ? :environment : @section
80
+ @problems[dimension] << { code: :empty_environment_section, scope: @section.to_s }
81
+ end
82
+
83
+ # 归一化温度传感器及其回显阈值。
84
+ def parse_temperature(line)
85
+ return false unless @section == :temperature
86
+
87
+ match = /\A(\S+)\s+(.+?)\s+(#{NUMBER}|N\/A)\s+(#{NUMBER}|N\/A)\s+(#{NUMBER}|N\/A)\s+(.+)\z/i.match(line)
88
+ return false unless match
89
+
90
+ component = "#{match[1]}/#{match[2]}"
91
+ high_alarm, high_warning, value = match.captures[2, 3].map do |token|
92
+ checked_number(token, issues: @problems[:temperature], component: component)
93
+ end
94
+ # NX-OS 使用 -127 表示传感器阈值不可用或不适用。
95
+ high_alarm = nil if high_alarm == -127
96
+ high_warning = nil if high_warning == -127
97
+ @rows[:temperature] << { component: component, value: value, low_alarm: nil, low_warning: nil,
98
+ high_warning: high_warning, high_alarm: high_alarm, high_comparison: :gt, status: component_status(match[6]) }
99
+ true
100
+ end
101
+
102
+ # 归一化单个环境组件及其运行状态。
103
+ def parse_component(line)
104
+ return false unless %i[fan clock].include?(@section)
105
+
106
+ if (match = /\AFan Air Filter\s*:\s*(.+)\z/i.match(line))
107
+ @rows[:environment] << { component: "fan air filter", kind: :unknown,
108
+ status: match[1].casecmp?("NotSupported") ? :absent : component_status(match[1]) }
109
+ elsif @section == :fan && (match = /\A(\S+)\s+\S+\s+\S+(?:\s+(?:front-to-back|back-to-front))?\s+(.+)\z/i.match(line))
110
+ @rows[:environment] << { component: "fan #{match[1]}", kind: :fan, status: component_status(match[2]) }
111
+ elsif (match = /\A(\S+)\s+.+?\s+(Ok|Normal|Good|Fail|Failed|Faulty|Absent|Not Present|NotSupported\/None)\z/i.match(line))
112
+ @rows[:environment] << { component: "#{@section} #{match[1]}", kind: @section, status: component_status(match[2]) }
113
+ else
114
+ return false
115
+ end
116
+ true
117
+ end
118
+
119
+ # 归一化电源测量值及回显状态。
120
+ def parse_power(line)
121
+ return false unless %i[power module].include?(@section)
122
+
123
+ match = /\A(\S+)\s+\S+\s+(#{POWER_VALUE})\s+(#{POWER_VALUE})(?:\s+(#{POWER_VALUE}))?\s+(.+)\z/i.match(line)
124
+ return false unless match
125
+ return false if @section == :module && match[4]
126
+ return false if @section == :power && @power_columns && @power_columns != (match[4] ? 3 : 2)
127
+
128
+ state = component_status(match[5])
129
+ if @section == :module
130
+ @rows[:environment] << { component: "module #{match[1]}", kind: :module, status: state }
131
+ else
132
+ component = "power supply #{match[1]}"
133
+ # 部分平台在输出与容量之间插入 Actual Input 列。
134
+ watts, capacity = [ match[2], match[4] || match[3] ].map do |token|
135
+ checked_number(token.sub(/W\z/i, "").strip, issues: @problems[:power], component: component, nonnegative: true)
136
+ end
137
+ if match[4]
138
+ checked_number(match[3].sub(/W\z/i, "").strip, issues: @problems[:power], component: component, nonnegative: true)
139
+ end
140
+ @rows[:power] << { component: component, kind: :power_supply, status: state,
141
+ present: state == :absent ? false : nil, watts: watts, capacity_watts: capacity }
142
+ end
143
+ true
144
+ end
145
+
146
+ # NX-OS 拼写差异仅在本平台处理;未报告的风扇状态保持未知。
147
+ def component_status(value)
148
+ case value.to_s.strip.downcase
149
+ when "shut" then :critical
150
+ when "failure", "fail/shutdown", "powered-dn" then :failed
151
+ else super
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../cisco_ios/parsers/show_interfaces"
4
+
5
+ module CtxChecker
6
+ module CiscoNxos
7
+ module Parsers
8
+ # NX-OS 与 IOS 共享标签式计数器,但单独提供管理状态。
9
+ class ShowInterface < CiscoIos::Parsers::ShowInterfaces
10
+ PLATFORM = :cisco_nxos
11
+ HEADER = /\A(\S+) is (administratively down|up|down|reset|deleted)(?:\s*\([^)]*\))?(?:, line protocol is (up|down)(?: \([^)]*\))?)?(?:, autostate (?:enabled|disabled))?\s*\z/i
12
+ AUXILIARY = /\A(?:\d+ seconds? (?:input|output) rate\b|Auto-mdix is (?:turned )?(?:on|off)\z|Rate mode is \S+\z|EEE \(efficient-ethernet\)\s*:|admin fec state is \S+, oper fec state is \S+\z|L3 Switched:\z|(?:input|ucast): \d+ pkts, \d+ bytes - (?:output|mcast): \d+ pkts, \d+ bytes\z)/i
13
+ DETAIL_METADATA = /\A(?:\d+ broadcast packets \d+ bytes|\d+ multicast frames \d+ compressed|No members|ucast: \d+ pkts, \d+ bytes)\z/i
14
+ COUNTERS = CiscoIos::Parsers::ShowInterfaces::COUNTERS.merge(
15
+ stomped_crc_errors: /(?<!\S)(\d+) Stomped\s+CRC(?=\s|\z)/i,
16
+ out_carrier_errors: /(?<!\S)(\d+) out_carrier_errors(?=\s|\z)/i).freeze
17
+ COUNTER_LABELS = CiscoIos::Parsers::ShowInterfaces::COUNTER_LABELS.merge(
18
+ stomped_crc_errors: /\bStomped\s+CRC\b/i, out_carrier_errors: /\bout_carrier_errors\b/i).freeze
19
+
20
+ private
21
+
22
+ # NX-OS 的 SVI 头可附带 autostate,不改变协议状态语义。
23
+ def interface_header(line) = HEADER.match(line)
24
+ # 声明当前平台支持的接口错误与丢弃计数模式。
25
+ def counter_patterns = COUNTERS
26
+ # 将平台计数器标签映射到归一化指标名称。
27
+ def counter_labels = COUNTER_LABELS
28
+ # 识别包含接口计数测量值的数据行。
29
+ def counter_line?(line) = COUNTER_LABELS.values.any? { |pattern| pattern.match?(line) } || super
30
+
31
+ # CRC 总数已包含 stomped 帧,该子集应独立保留。
32
+ def counter_source(line, metric)
33
+ metric == :crc_errors ? line.gsub(/\bStomped\s+CRC\b/i, "stomped_checksum") : line
34
+ end
35
+
36
+ # NX-OS 在 SVI 运行状态头中报告明确的 shutdown 原因。
37
+ def interface_record(match)
38
+ super.tap do |record|
39
+ record[:admin_status] = :down if match[0].match?(/\A\S+ is down\s*\(Administratively down\)/i)
40
+ end
41
+ end
42
+
43
+ # 保留物理成员端口回显的聚合归属关系。
44
+ def interface_detail(record, line)
45
+ if (match = /\ABelongs to ((?:Po|port-channel)\d+)\z/i.match(line))
46
+ record[:parent] = normalize_name(match[1])
47
+ elsif record[:kind] == :aggregate && (match = /\AvPC Status: (Up|Down), vPC number: \d+(?: \[packets forwarded via vPC peer-link\])?\z/i.match(line))
48
+ return record[:oper_status] == match[1].downcase.to_sym
49
+ elsif /\Amgmt\d+\z/i.match?(record[:name]) && /\AManagement transceiver: (?:Absent|Present)\z/i.match?(line)
50
+ (record[:management_metadata] ||= []) << :transceiver
51
+ elsif /\Amgmt\d+\z/i.match?(record[:name]) && /\AActive connector: (?:RJ45|SFP)\z/i.match?(line)
52
+ (record[:management_metadata] ||= []) << :connector
53
+ else
54
+ return false
55
+ end
56
+ true
57
+ end
58
+
59
+ # 部分管理接口表以连接器信息结尾,且仅提供包总数。
60
+ def complete_counter_coverage(record, counters, issues)
61
+ metadata = record.delete(:management_metadata)
62
+ return super unless metadata == %i[transceiver connector] && counters.empty? && issues.empty?
63
+
64
+ %i[in_errors out_errors crc_errors].map do |metric|
65
+ { interface: record[:name], metric: metric, value: nil, discontinuity: nil }
66
+ end
67
+ end
68
+
69
+ # 仅忽略已识别的描述字段;新出现的状态语法使结果保持部分完整。
70
+ def auxiliary?(line)
71
+ return false if line.match?(/\Aadmin state is\b/i)
72
+
73
+ AUXILIARY.match?(line) || DETAIL_METADATA.match?(line) || super
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module CiscoNxos
8
+ module Parsers
9
+ # NX-OS DOM 按接口及可选光通道组织测量值。
10
+ class ShowInterfaceTransceiverDetails < CtxChecker::Parsers::Base
11
+ POWER = /([+-]?\d+(?:\.\d+)?\s+dBm|(?:N\/A|NA|--)(?:\s+dBm)?)/i
12
+ MARKERS = { "++" => :high_alarm, "+" => :high_warning, "--" => :low_alarm, "-" => :low_warning }.freeze
13
+
14
+ private
15
+
16
+ # 保留 DOM 不可用和模块缺失状态,不虚构光功率。
17
+ def parse_output(text, complete:)
18
+ text = clean(text)
19
+ rows, issues, devices = [], [], []
20
+ current, lane, header, lane_valid = nil, nil, false, true
21
+ text.each_line.with_index(1) do |source, number|
22
+ line = source.strip
23
+ next if line.empty? || line.match?(/\A[-= ]+\z/)
24
+
25
+ if line.match?(/\A(?:Eth(?:ernet)?|mgmt)\d[\d\/.]*\z/i)
26
+ current = { interface: CtxChecker::Parsers::InterfaceNames.normalize(line, platform: :cisco_nxos),
27
+ applicable: nil, present: nil, dom_supported: nil, lanes: [], presence_states: [], dom_unsupported: false }
28
+ devices << current
29
+ lane, header, lane_valid = nil, false, true
30
+ elsif current && (match = /\Atransceiver is (not )?present\z/i.match(line))
31
+ current[:present] = match[1].nil?
32
+ current[:presence_states] << current[:present]
33
+ elsif current && line.match?(/\Atransceiver is not applicable\z/i)
34
+ current[:applicable] = false
35
+ current[:dom_supported] = false
36
+ elsif current && line.match?(/\ADOM is not supported\z/i)
37
+ current[:dom_supported] = false
38
+ current[:dom_unsupported] = true
39
+ elsif current && (match = /\ALane Number\s*:\s*(\d+)(?:\s+Network Lane)?\z/i.match(line))
40
+ lane = Integer(match[1], 10)
41
+ current[:lanes] << lane
42
+ header, lane_valid = false, true
43
+ elsif current && line.match?(/\ALane Number\b/i)
44
+ lane, header, lane_valid = nil, false, false
45
+ issues << { code: :invalid_optical_lane, line: number, interface: current[:interface] }
46
+ elsif current && line.match?(/\AMeasurement\s+High\s+Low\s+High\s+Low\z/i)
47
+ header = true
48
+ elsif current && (match = /\A(Tx Power|Rx Power)\s+(.+)\z/i.match(line))
49
+ next unless lane_valid
50
+
51
+ row = measurement(match[1], match[2], current, lane)
52
+ if row
53
+ rows << row
54
+ current[:dom_supported] = true
55
+ issues << { code: :missing_optical_header, line: number } unless header
56
+ else
57
+ issues << { code: :unparsed_optical_measurement, line: number }
58
+ end
59
+ elsif current && auxiliary?(line)
60
+ next
61
+ else
62
+ issues << { code: :unparsed_row, line: number }
63
+ end
64
+ end
65
+ devices.each { |device| complete_measurements(rows, device, issues) }
66
+ build_result(rows, complete: complete, recognized: devices.any?, issues: issues, identity: %i[interface lane metric])
67
+ end
68
+
69
+ # 解析支持的传感器测量值及回显单位。
70
+ def measurement(label, values, device, lane)
71
+ match = /\A#{POWER}\s*(\+\+|\+|--|-)?\s+#{POWER}\s+#{POWER}\s+#{POWER}\s+#{POWER}\z/i.match(values)
72
+ return unless match
73
+
74
+ current, marker, high_alarm, low_alarm, high_warning, low_warning = match.captures
75
+ value = decimal(current)
76
+ base_record(device, lane, label.downcase.start_with?("tx") ? :tx_power : :rx_power).merge(
77
+ value: value, high_alarm: decimal(high_alarm), low_alarm: decimal(low_alarm),
78
+ high_warning: decimal(high_warning), low_warning: decimal(low_warning),
79
+ alarm: marker ? MARKERS.fetch(marker) : (value.nil? ? :unknown : :normal), dom_supported: true)
80
+ end
81
+
82
+ # 即使配对指标解析成功,缺失指标仍须明确保留。
83
+ def complete_measurements(rows, device, issues)
84
+ device_rows = rows.select { |row| row[:interface] == device[:interface] }
85
+ if device[:applicable] == false && (!device[:presence_states].empty? || device[:lanes].any? || device_rows.any?)
86
+ issues << { code: :contradictory_optical_applicability, interface: device[:interface] }
87
+ device[:applicable] = nil
88
+ device_rows.each { |row| row[:applicable] = nil }
89
+ end
90
+ if device[:presence_states].uniq.size > 1
91
+ issues << { code: :contradictory_transceiver_presence, interface: device[:interface] }
92
+ end
93
+ if device[:dom_unsupported] && rows.any? { |row| row[:interface] == device[:interface] && !row[:value].nil? }
94
+ issues << { code: :contradictory_dom_support, interface: device[:interface] }
95
+ end
96
+ lanes = device[:lanes].empty? ? [ nil ] : device[:lanes].uniq
97
+ lanes.each do |lane|
98
+ %i[tx_power rx_power].each do |metric|
99
+ next if rows.any? { |row| row.values_at(:interface, :lane, :metric) == [ device[:interface], lane, metric ] }
100
+
101
+ rows << base_record(device, lane, metric)
102
+ unless device[:present] == false || device[:dom_supported] == false
103
+ issues << { code: :optical_measurement_missing, interface: device[:interface], lane: lane, metric: metric }
104
+ end
105
+ end
106
+ end
107
+ if device[:present].nil? && device[:applicable] != false
108
+ issues << { code: :transceiver_presence_unknown, interface: device[:interface] }
109
+ end
110
+ if device[:present] == false && rows.any? { |row| row[:interface] == device[:interface] && !row[:value].nil? }
111
+ issues << { code: :contradictory_transceiver_presence, interface: device[:interface] }
112
+ end
113
+ end
114
+
115
+ # 构建同一资源各测量值共用的身份字段。
116
+ def base_record(device, lane, metric)
117
+ { interface: device[:interface], lane: lane, metric: metric, value: nil, unit: :dbm,
118
+ low_alarm: nil, low_warning: nil, high_warning: nil, high_alarm: nil, alarm: :unknown,
119
+ applicable: device[:applicable], present: device[:present], dom_supported: device[:dom_supported] }
120
+ end
121
+
122
+ # 解析有限小数,不将占位符转换为零。
123
+ def decimal(value)
124
+ value = value.sub(/\s+dBm\z/i, "")
125
+ return unless value.match?(/\A[+-]?\d+(?:\.\d+)?\z/)
126
+
127
+ parsed = Float(value)
128
+ return parsed if parsed.finite?
129
+
130
+ @issues << { code: :invalid_optical_value }
131
+ nil
132
+ end
133
+
134
+ # 识别说明性回显,不将其作为数据记录。
135
+ def auxiliary?(line)
136
+ line.match?(/\A(?:(?:type|name|part number|revision|serial number|nominal bitrate|cisco [\w ]+|connector type|encoding|host lane count|power class) is\b|Link length supported |(?:SFP|QSFP) Detail Diagnostics Information |Current\s+Alarms\s+Warnings\z|(?:Temperature|Voltage|Current)\s+(?:[+-]?\d|N\/A)|Transmit Fault Count\s*=\s*\d+\z|Note:|\+\+ :|\*\* :|\+ :|\- :|\-\- :)/i)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../cisco_ios/parsers/show_ip_ospf_neighbor"
4
+
5
+ module CtxChecker
6
+ module CiscoNxos
7
+ module Parsers
8
+ # NX-OS 复用已校验的 OSPFv2 行,同时保留分进程总数与明确的 VRF 作用域。
9
+ class ShowIpOspfNeighbors < CiscoIos::Parsers::ShowIpOspfNeighbor
10
+ PLATFORM = :cisco_nxos
11
+ end
12
+ end
13
+ end
14
+ end