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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module PaloAlto
7
+ module Parsers
8
+ # 汇总功率使用 mW;零和 N/A 不能转换为有限的 dBm 读数。
9
+ class ShowTransceiverAll < CtxChecker::Parsers::Base
10
+ NUMBER = /(?:\d+(?:\.\d+)?|n\/a)/i
11
+
12
+ private
13
+
14
+ # 解析 show transceiver all,保留不完整或不支持的证据。
15
+ def parse_output(text, complete:)
16
+ text = clean(text)
17
+ rows, issues, header = [], [], false
18
+ text.each_line do |raw|
19
+ line = raw.strip
20
+ if /\APort\s+Temperature\s+Voltage\s+Current\s+Tx Power\s+Rx Power\z/i.match?(line)
21
+ header = true
22
+ elsif (match = /\A(ethernet\d+\/\d+)\s+#{NUMBER}(?:\s+C)?\s+#{NUMBER}(?:\s+V)?\s+#{NUMBER}(?:\s+mA)?\s+(#{NUMBER})(?:\s+mW)?\s+(#{NUMBER})(?:\s+mW)?\z/i.match(line))
23
+ [ [ :tx_power, match[2] ], [ :rx_power, match[3] ] ].each do |metric, raw_value|
24
+ milliwatts = raw_value.match?(/\A\d+(?:\.\d+)?\z/) ? Float(raw_value) : nil
25
+ if milliwatts && !milliwatts.finite?
26
+ issues << { code: :invalid_optics_measurement, interface: match[1], metric: metric }
27
+ milliwatts = nil
28
+ end
29
+ value = milliwatts && milliwatts.positive? ? 10 * Math.log10(milliwatts) : nil
30
+ issues << { code: :zero_optical_power_unresolved, interface: match[1], metric: metric } if milliwatts == 0
31
+ rows << { interface: match[1].downcase, lane: nil, metric: metric, value: value, unit: :dbm,
32
+ low_alarm: nil, low_warning: nil, high_warning: nil, high_alarm: nil, alarm: :unknown,
33
+ present: true, dom_supported: milliwatts.nil? ? nil : true }
34
+ end
35
+ elsif !line.empty? && !line.match?(/\A-+\z/)
36
+ issues << { code: :unrecognized_optics_line }
37
+ end
38
+ end
39
+ build_result(rows, complete: complete, recognized: header && rows.any?, issues: issues, identity: %i[interface lane metric])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ctx_parser/profile"
4
+ require_relative "parsers/show_interface_all"
5
+ require_relative "parsers/show_counter_interface_all"
6
+ require_relative "parsers/show_lacp_aggregate_ethernet"
7
+ require_relative "parsers/show_transceiver_all"
8
+ require_relative "parsers/show_routing_protocol_bgp_peer"
9
+ require_relative "parsers/show_routing_protocol_ospf_neighbor"
10
+ require_relative "parsers/show_system_resources"
11
+ require_relative "parsers/show_system_environmentals"
12
+ require_relative "parsers/show_clock"
13
+ require_relative "parsers/show_ntp"
14
+ require_relative "parsers/show_system_state_alarms"
15
+ require_relative "parsers/show_counter_global"
16
+
17
+ module CtxChecker
18
+ module PaloAlto
19
+ # 声明传统路由引擎查询;高级路由格式需要独立平台策略。
20
+ class Profile < CtxParser::Profile
21
+ CATALOG = CtxParser::CommandCatalog.new([
22
+ { dimensions: [ :interfaces ], command: "show interface all", parser: Parsers::ShowInterfaceAll },
23
+ { dimensions: [ :counters ], command: "show counter interface all", parser: Parsers::ShowCounterInterfaceAll },
24
+ { dimensions: [ :aggregation ], command: "show lacp aggregate-ethernet all", parser: Parsers::ShowLacpAggregateEthernet },
25
+ { dimensions: [ :optics ], command: "show transceiver all", parser: Parsers::ShowTransceiverAll },
26
+ { dimensions: [ :bgp ], command: "show routing protocol bgp peer", parser: Parsers::ShowRoutingProtocolBgpPeer },
27
+ { dimensions: [ :ospf ], command: "show routing protocol ospf neighbor", parser: Parsers::ShowRoutingProtocolOspfNeighbor },
28
+ { dimensions: %i[cpu memory], command: "show system resources", parser: Parsers::ShowSystemResources },
29
+ { dimensions: %i[temperature environment power], command: "show system environmentals", parser: Parsers::ShowSystemEnvironmentals },
30
+ { dimensions: [ :clock ], command: "show clock", parser: Parsers::ShowClock },
31
+ { dimensions: [ :ntp ], command: "show ntp", parser: Parsers::ShowNtp },
32
+ { dimensions: [ :alarms ], command: "show system state | match alarm", parser: Parsers::ShowSystemStateAlarms },
33
+ { dimensions: [ :device_counters ], command: "show counter global", parser: Parsers::ShowCounterGlobal }
34
+ ])
35
+ COMMANDS = CATALOG.commands
36
+ PARSERS = CATALOG.parsers
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ctx_parser"
4
+ require_relative "h3c/profile"
5
+ require_relative "cisco_ios/profile"
6
+ require_relative "cisco_nxos/profile"
7
+ require_relative "palo_alto/profile"
8
+ require_relative "hillstone/profile"
9
+ require_relative "radware_alteon/profile"
10
+
11
+ module CtxChecker
12
+ # 注册明确的平台别名与命令策略,不猜测不支持的语法。
13
+ module ParserRegistry
14
+ PROFILES = { h3c: H3c::Profile, cisco_ios: CiscoIos::Profile,
15
+ cisco_nxos: CiscoNxos::Profile, palo_alto: PaloAlto::Profile, hillstone: Hillstone::Profile,
16
+ radware_alteon: RadwareAlteon::Profile }.freeze
17
+ ALIASES = { comware: :h3c, h3c_wireless: :h3c, ios: :cisco_ios, iosxe: :cisco_ios,
18
+ ios_xe: :cisco_ios, cisco_iosxe: :cisco_ios, nxos: :cisco_nxos,
19
+ paloalto: :palo_alto, panos: :palo_alto, stoneos: :hillstone, radware: :radware_alteon, alteon: :radware_alteon }.freeze
20
+ REGISTRY = CtxParser::Registry.new(profiles: PROFILES, aliases: ALIASES)
21
+
22
+ # 固件版本作为目标事实传入,仅由需要它的平台策略解释。
23
+ def self.fetch(platform, model: nil, software_version: nil)
24
+ unless software_version.nil? || software_version.is_a?(String) && !/[\x00-\x1f\x7f]/.match?(software_version)
25
+ raise ArgumentError, "Software version must be text"
26
+ end
27
+ profile = REGISTRY.fetch(platform, model: model)
28
+ profile.respond_to?(:with_software_version) ? profile.with_software_version(software_version) : profile
29
+ end
30
+
31
+ # 选择解析器与检查策略前,先归一化平台别名。
32
+ def self.canonical(platform)
33
+ unless platform.is_a?(String) || platform.is_a?(Symbol)
34
+ raise ArgumentError, "Platform must be text"
35
+ end
36
+ key = platform.to_s.strip.downcase.to_sym
37
+ key = ALIASES.fetch(key, key)
38
+ raise ArgumentError, "Unsupported checker platform" unless PROFILES.key?(key)
39
+
40
+ key.to_s
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ctx_parser"
4
+ require "ctx_parser/input"
5
+ require_relative "interface_names"
6
+ require_relative "scoped_result"
7
+
8
+ module CtxChecker
9
+ module Parsers
10
+ # 共享文本边界与完整性规则,不在厂商解析器中嵌入健康策略。
11
+ class Base < CtxParser::Parser
12
+ MAX_BYTES = 16 * 1024 * 1024
13
+
14
+ private
15
+
16
+ # 从归一化观测记录中移除不支持的数据。
17
+ def clean(text)
18
+ @issues = []
19
+ @unsupported = false
20
+ source = CtxParser::Input.text(text, encoding: :transcode, newlines: :lf, max_bytes: MAX_BYTES)
21
+ source.each_line.with_index(1) do |line, number|
22
+ if /\A\s*(?:(?:%\s*)?(?:Error:\s*)?(?:Invalid (?:input|command|syntax)|Unrecognized command|Unknown command|command not (?:found|supported)|not supported on this|syntax error)|\^-+\s*unrecognized keyword\b)/i.match?(line)
23
+ @unsupported = true
24
+ @issues << { code: :unsupported_command, line: number }
25
+ elsif /\A\s*(?:%\s*(?:Error|Incomplete|Ambiguous)|Error:|Permission denied|Insufficient privilege|\^-+\s*(?:incomplete command|ambiguous command|error\b))/i.match?(line)
26
+ @issues << { code: :command_error, line: number }
27
+ elsif /--\s*More\s*--|----\s*More\s*----|Press (?:any key|ENTER|<SPACE>)/i.match?(line)
28
+ @issues << { code: :paginated_output, line: number }
29
+ end
30
+ end
31
+ source
32
+ end
33
+
34
+ # 隔离身份冲突,保留无歧义记录并降低覆盖完整性。
35
+ def build_result(records, complete:, recognized:, issues: [], identity: nil, covered_scopes: [])
36
+ problems = Array(@issues) + issues
37
+ if identity
38
+ grouped = CtxParser::Records.new(records, identity: identity)
39
+ if grouped.duplicates?
40
+ problems << { code: :duplicate_identity, count: grouped.duplicate_identity_count }
41
+ records = grouped.unambiguous
42
+ end
43
+ end
44
+ problems << { code: :unrecognized_output } unless recognized
45
+ problems << { code: :incomplete_output } unless complete
46
+ status = if @unsupported
47
+ records = []
48
+ :unsupported
49
+ elsif Array(@issues).any? { |issue| issue[:code] == :command_error }
50
+ records = []
51
+ :failed
52
+ elsif !recognized && records.empty?
53
+ :failed
54
+ elsif problems.any?
55
+ :partial
56
+ else
57
+ :complete
58
+ end
59
+ if covered_scopes.any?
60
+ ScopedResult.new(status: status, records: records, issues: problems.uniq,
61
+ covered_scopes: status == :complete ? covered_scopes.uniq : [])
62
+ else
63
+ CtxParser::Result.new(status: status, records: records, issues: problems.uniq)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CtxChecker
4
+ module Parsers
5
+ # 仅展开已知厂商缩写,保留接口、子接口与成员的身份区别。
6
+ module InterfaceNames
7
+ H3C = {
8
+ "ge" => "GigabitEthernet", "gigabitethernet" => "GigabitEthernet",
9
+ "xge" => "Ten-GigabitEthernet", "ten-gigabitethernet" => "Ten-GigabitEthernet",
10
+ "ten-gige" => "Ten-GigabitEthernet", "fge" => "FortyGigE", "fortygige" => "FortyGigE",
11
+ "hge" => "HundredGigE", "hundredgige" => "HundredGigE",
12
+ "bagg" => "Bridge-Aggregation", "bridge-aggregation" => "Bridge-Aggregation",
13
+ "ragg" => "Route-Aggregation", "route-aggregation" => "Route-Aggregation",
14
+ "vlan" => "Vlan-interface", "vlan-interface" => "Vlan-interface"
15
+ }.freeze
16
+ CISCO = {
17
+ "eth" => "Ethernet", "et" => "Ethernet", "ethernet" => "Ethernet",
18
+ "gi" => "GigabitEthernet", "gig" => "GigabitEthernet", "gigabitethernet" => "GigabitEthernet",
19
+ "fa" => "FastEthernet", "fastethernet" => "FastEthernet",
20
+ "te" => "TenGigabitEthernet", "ten" => "TenGigabitEthernet", "tengigabitethernet" => "TenGigabitEthernet",
21
+ "twe" => "TwentyFiveGigE", "twentyfivegige" => "TwentyFiveGigE", "twentyfivegigabitethernet" => "TwentyFiveGigE",
22
+ "fo" => "FortyGigabitEthernet", "fortygigabitethernet" => "FortyGigabitEthernet",
23
+ "hu" => "HundredGigE", "hundredgige" => "HundredGigE",
24
+ "po" => "Port-channel", "port-channel" => "Port-channel", "vlan" => "Vlan"
25
+ }.freeze
26
+ COMMON = { "lo" => "Loopback", "loopback" => "Loopback", "tu" => "Tunnel", "tunnel" => "Tunnel", "nve" => "Nve" }.freeze
27
+
28
+ module_function
29
+
30
+ # 展开已知厂商接口缩写,同时保留端口后缀。
31
+ def normalize(name, platform:)
32
+ raise ArgumentError, "Interface name must be text" unless name.is_a?(String) && !name.strip.empty? && !/[\x00-\x1f\x7f]/.match?(name)
33
+
34
+ value = name.strip
35
+ return value.downcase if %w[palo_alto paloalto panos hillstone stoneos].include?(platform.to_s)
36
+ if %w[radware_alteon radware alteon].include?(platform.to_s)
37
+ return "port#{Integer(value, 10)}" if /\A[1-9]\d*\z/.match?(value)
38
+
39
+ return value.downcase.sub(/\A(port|mgmt|trunk)\s+(\d+)/, '\1\2')
40
+ end
41
+
42
+ match = /\A([a-z-]+)\s*(\d.*)\z/i.match(value)
43
+ return value unless match
44
+
45
+ aliases = platform.to_s.start_with?("h3c") ? H3C : CISCO
46
+ "#{aliases.fetch(match[1].downcase) { COMMON.fetch(match[1].downcase, match[1]) }}#{match[2]}"
47
+ end
48
+
49
+ # 将已知接口名分类为物理口、聚合口、子接口或逻辑口。
50
+ def kind(name)
51
+ case name.to_s
52
+ when /\Aloopback\.?\d/i then :loopback
53
+ when /\A(?:tunnel|nve)\.?\d/i then :tunnel
54
+ when /\.\d+\z/ then :subinterface
55
+ when /\A(?:port-channel|bridge-aggregation|route-aggregation|ae|aggregate|trunk)\d/i then :aggregate
56
+ when /\A(?:vswitchif|bgroup)\d/i then :bridge
57
+ when /\Aredundant\d/i then :redundant
58
+ when /\Avlan(?:-interface)?\d/i then :svi
59
+ when /\A(?:[\w-]*ethernet|[\w-]*gige|mgmt|mgt|ha|cellular|port)\d/i then :physical
60
+ else :unknown
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ctx_parser/result"
4
+ require "ctx_parser/immutable"
5
+
6
+ module CtxChecker
7
+ module Parsers
8
+ # 运行表即使没有实体,也可证明已枚举的作用域。
9
+ class ScopedResult < CtxParser::Result
10
+ attr_reader :covered_scopes
11
+
12
+ # 冻结各作用域证据,不合并无关的观测结果。
13
+ def initialize(covered_scopes: [], **attributes)
14
+ @covered_scopes = CtxParser::Immutable.copy(covered_scopes)
15
+ super(**attributes)
16
+ end
17
+
18
+ # 值相等、序列化与替换操作均保留覆盖范围语义。
19
+ def to_h(&block)
20
+ attributes = { status: status, records: records, issues: issues, covered_scopes: covered_scopes }
21
+ block ? attributes.to_h(&block) : attributes
22
+ end
23
+ # 向 Ruby 模式匹配提供结果字段与已覆盖作用域。
24
+ def deconstruct_keys(keys) = keys ? to_h.slice(*keys) : to_h
25
+ # 创建新的不可变结果,同时保留未修改的作用域证据。
26
+ def with(**changes) = self.class.new(**to_h.merge(changes))
27
+ # 比较全部结果字段,包括明确的作用域覆盖范围。
28
+ def ==(other) = other.instance_of?(self.class) && to_h == other.to_h
29
+ # 比较结果值时同时比较已覆盖作用域。
30
+ def eql?(other) = other.instance_of?(self.class) && to_h.eql?(other.to_h)
31
+ # 按与相等判断一致的字段和作用域计算哈希。
32
+ def hash = [ self.class, to_h ].hash
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "observation"
4
+ require_relative "system_policy"
5
+
6
+ module CtxChecker
7
+ # 使用显式设备预期,不猜测未声明的拓扑或光学阈值。
8
+ class Policy
9
+ attr_reader :required_interfaces, :required_members, :minimum_active_members, :expected_bgp, :expected_ospf, :counter_limits, :optical_thresholds, :required_optics, :system
10
+
11
+ # 在设备评估前校验并冻结策略定义。
12
+ def initialize(required_interfaces: [], required_members: {}, minimum_active_members: {}, expected_bgp: [], expected_ospf: [], counter_limits: {}, optical_thresholds: [],
13
+ resource_thresholds: {}, temperature_thresholds: {}, required_components: {}, required_dimensions: [], required_optics: [],
14
+ max_clock_skew_seconds: 300, max_ntp_offset_ms: 100, device_counter_limits: {})
15
+ @system = SystemPolicy.new(resource_thresholds: resource_thresholds, temperature_thresholds: temperature_thresholds,
16
+ required_components: required_components, required_dimensions: required_dimensions, max_clock_skew_seconds: max_clock_skew_seconds,
17
+ max_ntp_offset_ms: max_ntp_offset_ms, device_counter_limits: device_counter_limits)
18
+ names!(required_interfaces)
19
+ unless required_members.is_a?(Hash) && required_members.all? { |group, members| text?(group) && names!(members) }
20
+ raise ArgumentError, "Required members must map groups to interface names"
21
+ end
22
+ unless minimum_active_members.is_a?(Hash) && minimum_active_members.all? { |group, count| text?(group) && count.is_a?(Integer) && count.positive? }
23
+ raise ArgumentError, "Minimum active members must be positive integers"
24
+ end
25
+ peers!(expected_bgp, %i[vrf afi_safi peer])
26
+ peers!(expected_ospf, %i[vrf process_id interface router_id])
27
+ unless counter_limits.is_a?(Hash) && counter_limits.all? { |metric, count| Observation::METRICS.include?(metric) && count.is_a?(Integer) && count >= 0 }
28
+ raise ArgumentError, "Counter limits must be nonnegative integer deltas"
29
+ end
30
+ optics!(optical_thresholds)
31
+ required_optics!(required_optics)
32
+ @required_interfaces, @required_members, @minimum_active_members, @expected_bgp, @expected_ospf, @counter_limits =
33
+ [ required_interfaces, required_members, minimum_active_members, expected_bgp, expected_ospf, counter_limits ].map { |value| CtxParser::Immutable.copy(value) }
34
+ @optical_thresholds = CtxParser::Immutable.copy(optical_thresholds)
35
+ @optical_index = @optical_thresholds.to_h { |row| [ row.values_at(:interface, :lane, :metric).freeze, row ] }.freeze
36
+ @required_optics = CtxParser::Immutable.copy(required_optics)
37
+ @required_optical_index = @required_optics.to_h { |row| [ row.values_at(:interface, :lane, :metric).freeze, true ] }.freeze
38
+ freeze
39
+ end
40
+
41
+ # 策略仅替换精确光通道及指标的阈值,不改变设备告警证据。
42
+ def optical_thresholds_for(record) = @optical_index[record.values_at(:interface, :lane, :metric)]
43
+ # 检查策略是否要求此接口提供光学诊断。
44
+ def required_optics?(record) = @required_optical_index.key?(record.values_at(:interface, :lane, :metric))
45
+
46
+ # 检查策略是否明确要求此资源或功能。
47
+ def required?(name)
48
+ return true if system.required?(name)
49
+ case name
50
+ when :interfaces then required_interfaces.any?
51
+ when :aggregation then required_members.any? || minimum_active_members.any?
52
+ when :bgp then expected_bgp.any?
53
+ when :ospf then expected_ospf.any?
54
+ when :optics then required_optics.any?
55
+ else false
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ # 校验非空文本,拒绝将含控制字符的值作为标识。
62
+ def text?(value) = value.is_a?(String) && !value.strip.empty? && !/[\x00-\x1f\x7f]/.match?(value)
63
+
64
+ # 校验策略选择器使用的资源名称是否有效且不重复。
65
+ def names!(values)
66
+ raise ArgumentError, "Interface names must be unique text" unless values.is_a?(Array) && values.all? { |value| text?(value) } && values.uniq == values
67
+ true
68
+ end
69
+
70
+ # 校验协议对等体身份及对应策略要求。
71
+ def peers!(values, fields)
72
+ unless values.is_a?(Array) && values.uniq == values && values.all? do |row|
73
+ row.is_a?(Hash) && row.keys.sort == fields.sort && row.all? { |key, value| text?(value) || key == :process_id && value.is_a?(Integer) && value >= 0 }
74
+ end
75
+ raise ArgumentError, "Expected peers require complete scoped identities"
76
+ end
77
+ end
78
+
79
+ # 校验光功率阈值及必需接口选择。
80
+ def optics!(values)
81
+ fields = %i[interface lane metric] + Observation::OPTICAL_THRESHOLDS
82
+ unless values.is_a?(Array) && values.all? { |row| row.is_a?(Hash) && (row.keys - fields).empty? }
83
+ raise ArgumentError, "Optical policies require scoped threshold records"
84
+ end
85
+ observation = Observation.new(name: :optics, status: :complete, records: values)
86
+ observation.records.each do |row|
87
+ thresholds = row.values_at(*Observation::OPTICAL_THRESHOLDS)
88
+ paired = row[:low_alarm] && row[:high_alarm] || row[:low_warning] && row[:high_warning]
89
+ unless paired && thresholds.compact.each_cons(2).all? { |low, high| low <= high }
90
+ raise ArgumentError, "Optical policies require an ordered complete alarm or warning pair"
91
+ end
92
+ end
93
+ end
94
+
95
+ # 必需测量必须有明确身份;仅配置阈值不表示要求资源存在。
96
+ def required_optics!(values)
97
+ fields = %i[interface lane metric]
98
+ unless values.is_a?(Array) && values.all? { |row| row.is_a?(Hash) && row.keys.sort == fields }
99
+ raise ArgumentError, "Required optics need exact interface, lane and metric identities"
100
+ end
101
+ Observation.new(name: :optics, status: :complete, records: values)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module RadwareAlteon
7
+ module Parsers
8
+ # 端口模式表可证明 LACP 已禁用,但不能虚构活跃成员状态。
9
+ class InfoL2LacpDump < CtxChecker::Parsers::Base
10
+ HEADER = /\APort\s+lacp\s+adminkey\s+operkey\s+selected\s+prio\s+attached\s+trunk\s+Blocked\z/i
11
+ ROW = /\A(\d+)\s+(\S+)\s+\d+\s+\d+\s+([yn])\s+\d+\s+(\d+|--)\s+(\d+|--)\s+([yn])\z/i
12
+
13
+ private
14
+
15
+ # 活跃模式需要额外运行证据,不能由重复的 trunk 记录替代。
16
+ def parse_output(text, complete:)
17
+ issues, ports, header = [], [], false
18
+ clean(text).each_line do |raw|
19
+ line = raw.strip
20
+ next if line.empty? || /\A-+\z/.match?(line)
21
+
22
+ if HEADER.match?(line)
23
+ issues << { code: :duplicate_aggregation_header } if header
24
+ header = true
25
+ elsif header && line == "aggr"
26
+ next
27
+ elsif header && (match = ROW.match(line)) && match[1].to_i.positive?
28
+ port = "port#{Integer(match[1], 10)}"
29
+ issues << { code: :duplicate_lacp_port, interface: port } if ports.include?(port)
30
+ ports << port
31
+ if match[2].casecmp?("off")
32
+ unless match.captures[2..] == %w[n -- -- n]
33
+ issues << { code: :inconsistent_lacp_disabled_state, interface: port }
34
+ end
35
+ else
36
+ issues << { code: :lacp_operational_evidence_unavailable, interface: port }
37
+ end
38
+ else
39
+ issues << { code: :unrecognized_lacp_line }
40
+ end
41
+ end
42
+ issues << { code: :lacp_port_records_missing } if header && ports.empty?
43
+ build_result([], complete: complete, recognized: header, issues: issues)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module RadwareAlteon
7
+ module Parsers
8
+ # trunk 成员关系与 STP 转发状态不能独立证明链路状态。
9
+ class InfoL2Trunk < CtxChecker::Parsers::Base
10
+ GROUP = /\ATrunk group (\d+), bw contract \d+, name:.*?, port state:\z/i
11
+ MEMBER = /\A(\d+):\s+STG\s+\d+\s+(\S+)\z/i
12
+
13
+ private
14
+
15
+ # 将成员限定在其所属 trunk 中,明确保留不完整聚合组。
16
+ def parse_output(text, complete:)
17
+ rows, issues, groups, current = [], [], [], nil
18
+ clean(text).each_line do |raw|
19
+ line = raw.strip
20
+ next if line.empty? || /\A-+\z/.match?(line)
21
+
22
+ if (match = GROUP.match(line)) && match[1].to_i.positive?
23
+ current = "trunk#{Integer(match[1], 10)}"
24
+ issues << { code: :duplicate_aggregation_header, group: current } if groups.include?(current)
25
+ groups << current
26
+ elsif current && (match = MEMBER.match(line)) && match[1].to_i.positive?
27
+ member = "port#{Integer(match[1], 10)}"
28
+ state = match[2].downcase
29
+ issues << { code: :aggregation_forwarding_state_unknown, group: current, interface: member } unless state == "forwarding"
30
+ rows << { group: current, member: member,
31
+ member_status: state == "forwarding" ? :active : :unknown, oper_status: :unknown }
32
+ else
33
+ current = nil
34
+ issues << { code: :unrecognized_aggregation_line }
35
+ end
36
+ end
37
+ (groups.uniq - rows.map { |row| row[:group] }).each do |group|
38
+ rows << { group: group, member: nil, member_status: :unknown, oper_status: :unknown }
39
+ issues << { code: :aggregation_members_unavailable, group: group }
40
+ end
41
+ build_result(rows, complete: complete, recognized: groups.any?, issues: issues, identity: %i[group member])
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module RadwareAlteon
7
+ module Parsers
8
+ # 传统 BGP 表与 FRR BGP 表分别使用已验证的语法。
9
+ class InfoL3BgpSummary < CtxChecker::Parsers::Base
10
+ private
11
+
12
+ # 协议明确禁用不代表策略要求的对等体健康。
13
+ def parse_output(text, complete:)
14
+ lines = clean(text).lines.map(&:strip).reject(&:empty?)
15
+ disabled = lines == [ "BGP is disabled." ]
16
+ issues = [ { code: disabled ? :protocol_disabled : :unrecognized_bgp_output } ]
17
+ build_result([], complete: complete, recognized: disabled, issues: issues)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require_relative "../../parsers/base"
5
+
6
+ module CtxChecker
7
+ module RadwareAlteon
8
+ module Parsers
9
+ # IP 接口索引独立于 VLAN ID 和物理数据端口号。
10
+ class InfoL3Ip < CtxChecker::Parsers::Base
11
+ ROW = /\A(\d+):\s+IP4\s+(\S+)\s+(\S+)\s+([^,\s]+)\s*,\s*vlan\s+(\d+),\s*(\S+),\s*BFD\s+\S+\z/i
12
+
13
+ private
14
+
15
+ # 将接口行限定在明确章节内,排除带编号的网关记录。
16
+ def parse_output(text, complete:)
17
+ rows, issues, section = [], [], :preamble
18
+ header, interfaces, ipv6, closed = false, false, false, false
19
+ clean(text).each_line do |raw|
20
+ line = raw.strip
21
+ next if line.empty? || /\A-+\z/.match?(line)
22
+
23
+ if line.casecmp?("IP information:")
24
+ issues << { code: :duplicate_interface_header } if header
25
+ header = true
26
+ elsif /\A,?Interface information:\z/i.match?(line)
27
+ issues << { code: :duplicate_interface_header } if interfaces
28
+ interfaces = true
29
+ section = :interfaces
30
+ elsif line.casecmp?("IPv6 Link Local Address Information:")
31
+ ipv6 = true
32
+ section = :ipv6
33
+ elsif /\ADefault gateway information:/i.match?(line)
34
+ closed = section == :ipv6
35
+ section = :other
36
+ elsif section == :preamble && /\ARouter ID:/.match?(line) || section == :other
37
+ next
38
+ elsif section == :interfaces && (match = ROW.match(line)) && match[1].to_i.positive?
39
+ row = interface_record(match, issues)
40
+ rows << row if row
41
+ elsif section == :ipv6
42
+ issues << { code: :ipv6_interface_evidence_unavailable }
43
+ else
44
+ issues << { code: :unrecognized_interface_line }
45
+ end
46
+ end
47
+ issues << { code: :interface_section_incomplete } unless header && interfaces && ipv6 && closed
48
+ issues << { code: :interface_records_missing } if interfaces && rows.empty?
49
+ build_result(rows, complete: complete, recognized: header && interfaces, issues: issues, identity: [ :name ])
50
+ end
51
+
52
+ # 明确的 IP4 状态仅证明运行状态,不能推断未报告的线路协议状态。
53
+ def interface_record(match, issues)
54
+ name = "ipif#{Integer(match[1], 10)}"
55
+ unless match.captures[1, 3].all? { |value| IPAddr.new(value).ipv4? } && IPAddr.new("#{match[2]}/#{match[3]}")
56
+ issues << { code: :invalid_interface_address, interface: name }
57
+ return
58
+ end
59
+ state = match[6].downcase
60
+ issues << { code: :unknown_interface_state, interface: name } unless %w[up down].include?(state)
61
+ { name: name, kind: :svi, admin_status: :unknown,
62
+ oper_status: { "up" => :up, "down" => :down }.fetch(state, :unknown),
63
+ protocol_status: :unknown, protocol_applicable: true, parent: nil }
64
+ rescue IPAddr::InvalidAddressError, IPAddr::InvalidPrefixError
65
+ issues << { code: :invalid_interface_address, interface: name }
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module RadwareAlteon
7
+ module Parsers
8
+ # OSPF 正文为空不能推断邻居或进程覆盖完整。
9
+ class InfoL3OspfNbr < CtxChecker::Parsers::Base
10
+ private
11
+
12
+ # 在验证活跃 Alteon 邻居表前,保留明确禁用状态。
13
+ def parse_output(text, complete:)
14
+ lines = clean(text).lines.map(&:strip).reject(&:empty?)
15
+ disabled = lines == [ "OSPF is disabled." ]
16
+ issues = [ { code: disabled ? :protocol_disabled : :unrecognized_ospf_output } ]
17
+ build_result([], complete: complete, recognized: disabled, issues: issues)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module RadwareAlteon
7
+ module Parsers
8
+ # 运维修改端口别名时,物理端口号仍保持稳定。
9
+ class InfoLink < CtxChecker::Parsers::Base
10
+ HEADER = /\AAlias\s+Port\s+Speed\s+Duplex\s+Flow Ctrl\s+Link\z/i
11
+ ROW = /\A(\S+)\s+(\d+)\s+\S+\s+\S+\s+(?:yes|no)\*?\s+(?:yes|no)\*?\s+(\S+)\z/i
12
+
13
+ private
14
+
15
+ # 链路行缺失不能证明端口已禁用或健康。
16
+ def parse_output(text, complete:)
17
+ rows, issues, header = [], [], false
18
+ clean(text).each_line do |raw|
19
+ line = raw.strip
20
+ next if line.empty? || /\A[-\s]+\z/.match?(line)
21
+ next if /\A-+\s+-+\s+-+\s+-+\s+--TX---RX--\s+-+\z/.match?(line)
22
+ next if line == "* = value set by configuration; not autonegotiated."
23
+
24
+ if HEADER.match?(line)
25
+ issues << { code: :duplicate_interface_header } if header
26
+ header = true
27
+ elsif header && (match = ROW.match(line)) && match[2].to_i.positive?
28
+ name = "#{match[1].casecmp?("mgmt") ? "mgmt" : "port"}#{Integer(match[2], 10)}"
29
+ state = match[3].downcase
30
+ issues << { code: :unknown_interface_state, interface: name } unless %w[up down disabled].include?(state)
31
+ rows << { name: name, kind: :physical, admin_status: state == "disabled" ? :down : :unknown,
32
+ oper_status: { "up" => :up, "down" => :down, "disabled" => :down }.fetch(state, :unknown),
33
+ protocol_status: :unknown, protocol_applicable: nil }
34
+ else
35
+ issues << { code: :unrecognized_interface_line }
36
+ end
37
+ end
38
+ issues << { code: :interface_records_missing } if header && rows.empty?
39
+ build_result(rows, complete: complete, recognized: header, issues: issues, identity: [ :name ])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end