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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/COUNTER_CONTRACT.md +33 -0
- data/LICENSE +22 -0
- data/README-zh.md +93 -0
- data/README.md +93 -0
- data/docs/architecture-zh.md +487 -0
- data/docs/release.md +13 -0
- data/examples/offline.rb +17 -0
- data/lib/ctx_checker/applicability.rb +187 -0
- data/lib/ctx_checker/checks/aggregation.rb +73 -0
- data/lib/ctx_checker/checks/alarms.rb +25 -0
- data/lib/ctx_checker/checks/base.rb +66 -0
- data/lib/ctx_checker/checks/clock.rb +30 -0
- data/lib/ctx_checker/checks/counters.rb +77 -0
- data/lib/ctx_checker/checks/device_counters.rb +32 -0
- data/lib/ctx_checker/checks/hardware.rb +45 -0
- data/lib/ctx_checker/checks/interfaces.rb +35 -0
- data/lib/ctx_checker/checks/ntp.rb +47 -0
- data/lib/ctx_checker/checks/optics.rb +107 -0
- data/lib/ctx_checker/checks/resources.rb +63 -0
- data/lib/ctx_checker/checks/routing.rb +86 -0
- data/lib/ctx_checker/checks/temperature.rb +53 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +56 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +153 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +110 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +41 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +140 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +107 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +164 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +96 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +65 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +36 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +44 -0
- data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +45 -0
- data/lib/ctx_checker/cisco_ios/parsers/system_values.rb +62 -0
- data/lib/ctx_checker/cisco_ios/profile.rb +39 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_bgp_ipv4_unicast_summary.rb +69 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_clock.rb +13 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +157 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_interface.rb +78 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +141 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +14 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +151 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +116 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +21 -0
- data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +89 -0
- data/lib/ctx_checker/cisco_nxos/profile.rb +35 -0
- data/lib/ctx_checker/collector.rb +175 -0
- data/lib/ctx_checker/evaluator.rb +49 -0
- data/lib/ctx_checker/failure.rb +24 -0
- data/lib/ctx_checker/finding.rb +19 -0
- data/lib/ctx_checker/h3c/parsers/counters_table.rb +56 -0
- data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +43 -0
- data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +44 -0
- data/lib/ctx_checker/h3c/parsers/display_clock.rb +72 -0
- data/lib/ctx_checker/h3c/parsers/display_counters_inbound_interface.rb +21 -0
- data/lib/ctx_checker/h3c/parsers/display_counters_outbound_interface.rb +21 -0
- data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +64 -0
- data/lib/ctx_checker/h3c/parsers/display_environment.rb +74 -0
- data/lib/ctx_checker/h3c/parsers/display_fan.rb +72 -0
- data/lib/ctx_checker/h3c/parsers/display_interface.rb +127 -0
- data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +72 -0
- data/lib/ctx_checker/h3c/parsers/display_memory.rb +132 -0
- data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +84 -0
- data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +47 -0
- data/lib/ctx_checker/h3c/parsers/display_power.rb +63 -0
- data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +109 -0
- data/lib/ctx_checker/h3c/parsers/system_values.rb +46 -0
- data/lib/ctx_checker/h3c/profile.rb +49 -0
- data/lib/ctx_checker/hillstone/parsers/show_clock.rb +68 -0
- data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +44 -0
- data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +114 -0
- data/lib/ctx_checker/hillstone/parsers/show_environment.rb +90 -0
- data/lib/ctx_checker/hillstone/parsers/show_interface.rb +82 -0
- data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +53 -0
- data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +22 -0
- data/lib/ctx_checker/hillstone/parsers/show_ip_ospf_neighbor.rb +21 -0
- data/lib/ctx_checker/hillstone/parsers/show_memory.rb +85 -0
- data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +110 -0
- data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +101 -0
- data/lib/ctx_checker/hillstone/profile.rb +40 -0
- data/lib/ctx_checker/observation.rb +165 -0
- data/lib/ctx_checker/observation_merger.rb +80 -0
- data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +50 -0
- data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +47 -0
- data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +68 -0
- data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +70 -0
- data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +52 -0
- data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +78 -0
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +66 -0
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +54 -0
- data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +142 -0
- data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +102 -0
- data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +54 -0
- data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +44 -0
- data/lib/ctx_checker/palo_alto/profile.rb +39 -0
- data/lib/ctx_checker/parser_registry.rb +43 -0
- data/lib/ctx_checker/parsers/base.rb +68 -0
- data/lib/ctx_checker/parsers/interface_names.rb +65 -0
- data/lib/ctx_checker/parsers/scoped_result.rb +35 -0
- data/lib/ctx_checker/policy.rb +104 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +48 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +46 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +22 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +71 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +22 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +44 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +47 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +48 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +34 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +53 -0
- data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +68 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +39 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +58 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +67 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +74 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +99 -0
- data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +86 -0
- data/lib/ctx_checker/radware_alteon/port_counter_parser.rb +34 -0
- data/lib/ctx_checker/radware_alteon/profile.rb +145 -0
- data/lib/ctx_checker/radware_alteon.rb +8 -0
- data/lib/ctx_checker/report.rb +112 -0
- data/lib/ctx_checker/runtime_parser.rb +234 -0
- data/lib/ctx_checker/snapshot.rb +44 -0
- data/lib/ctx_checker/system_policy.rb +70 -0
- data/lib/ctx_checker/system_record.rb +81 -0
- data/lib/ctx_checker/version.rb +6 -0
- data/lib/ctx_checker.rb +17 -0
- 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
|