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,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require_relative "../../parsers/base"
|
|
5
|
+
require "ctx_parser/values"
|
|
6
|
+
|
|
7
|
+
module CtxChecker
|
|
8
|
+
module Hillstone
|
|
9
|
+
module Parsers
|
|
10
|
+
# 上次同步尝试与已配置对等体不能证明当前系统已同步。
|
|
11
|
+
class ShowNtpStatus < CtxChecker::Parsers::Base
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 逐对等体解析,同时保留系统同步证据缺失的状态。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
source = clean(text)
|
|
17
|
+
rows, clients, versions, settings = [], [], [], []
|
|
18
|
+
source.each_line.with_index(1) do |raw, number|
|
|
19
|
+
line = raw.strip
|
|
20
|
+
next if line.empty?
|
|
21
|
+
|
|
22
|
+
if (match = /\Antp client is (enabled|disabled), authentication is (enabled|disabled)\z/i.match(line))
|
|
23
|
+
clients << match[1].downcase
|
|
24
|
+
elsif (match = /\Antp version is ([1-4])\z/i.match(line))
|
|
25
|
+
versions << match[1]
|
|
26
|
+
elsif /\Antp query-interval is \d+(?: minute(?:s)?)?, max-adjustment time is \d+(?: seconds?)?\z/i.match?(line)
|
|
27
|
+
settings << line
|
|
28
|
+
elsif (match = /\Antp server (\S+?)(?:, (.+))?\z/i.match(line))
|
|
29
|
+
row = parse_peer(match[1], match[2], number)
|
|
30
|
+
rows << row if row
|
|
31
|
+
else
|
|
32
|
+
@issues << { code: :unrecognized_ntp_line, line: number }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
@issues << { code: :duplicate_ntp_field, field: :version } if versions.size > 1
|
|
36
|
+
@issues << { code: :duplicate_ntp_field, field: :settings } if settings.size > 1
|
|
37
|
+
@issues << { code: :missing_ntp_status } if clients.empty?
|
|
38
|
+
@issues << { code: :duplicate_ntp_field, field: :client } if clients.size > 1
|
|
39
|
+
synchronized = false if clients == [ "disabled" ]
|
|
40
|
+
@issues << { code: :ntp_system_status_unavailable } unless synchronized == false
|
|
41
|
+
rows.unshift(component: "system", synchronized: synchronized) unless clients.empty?
|
|
42
|
+
build_result(rows, complete: complete, recognized: clients.any?, identity: [ :component ])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 设备提供虚拟路由器时,将其纳入对等体身份。
|
|
46
|
+
def parse_peer(host, options, number)
|
|
47
|
+
unless valid_peer?(host)
|
|
48
|
+
@issues << { code: :invalid_ntp_peer, line: number }
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
fields = {}
|
|
52
|
+
Array(options&.split(/,\s*/)).each do |option|
|
|
53
|
+
key, value = peer_option(option, number)
|
|
54
|
+
next unless key
|
|
55
|
+
if fields.key?(key)
|
|
56
|
+
@issues << { code: :duplicate_ntp_field, field: key, line: number }
|
|
57
|
+
fields[key] = nil
|
|
58
|
+
else
|
|
59
|
+
fields[key] = value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
peer = CtxParser::Values.valid_host?(host) ? CtxParser::Values.host(host) : host.downcase
|
|
63
|
+
{ component: "peer/#{fields[:vrouter] || 'unspecified'}/#{peer}", peer: peer, synchronized: nil,
|
|
64
|
+
last_sync_status: fields[:last_sync_status] || :unknown, last_sync_at: nil }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 未知选项作为诊断保留,不能误判为同步证据。
|
|
68
|
+
def peer_option(option, number)
|
|
69
|
+
if (match = /\Avrouter (\S+)\z/.match(option))
|
|
70
|
+
[ :vrouter, match[1] ]
|
|
71
|
+
elsif (match = /\Akey (\d+)\z/.match(option))
|
|
72
|
+
[ :key, match[1] ]
|
|
73
|
+
elsif option == "prefer"
|
|
74
|
+
[ :prefer, true ]
|
|
75
|
+
elsif (match = /\Alast sync: (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) \(([^)]+)\)\z/.match(option))
|
|
76
|
+
[ :last_sync_status, sync_attempt(match, number) ]
|
|
77
|
+
else
|
|
78
|
+
@issues << { code: :unrecognized_ntp_field, line: number }
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 本地时间戳在确定偏移前保留为不可用于偏差评估的证据。
|
|
84
|
+
def sync_attempt(match, number)
|
|
85
|
+
Date.iso8601(match[1])
|
|
86
|
+
unless match[2].split(":").map(&:to_i).zip([ 24, 60, 60 ]).all? { |value, limit| value < limit }
|
|
87
|
+
raise ArgumentError, "Invalid local clock"
|
|
88
|
+
end
|
|
89
|
+
@issues << { code: :ntp_last_sync_offset_unknown, line: number }
|
|
90
|
+
return :failed if match[3] == "Error"
|
|
91
|
+
return :succeeded if match[3] == "Success"
|
|
92
|
+
|
|
93
|
+
@issues << { code: :unknown_ntp_last_sync_status, line: number }
|
|
94
|
+
:unknown
|
|
95
|
+
rescue ArgumentError
|
|
96
|
+
@issues << { code: :invalid_ntp_last_sync_time, line: number }
|
|
97
|
+
:unknown
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 服务器名称仅接受主机名或主机地址,不接受任意 CLI 片段。
|
|
101
|
+
def valid_peer?(text)
|
|
102
|
+
return true if CtxParser::Values.valid_host?(text)
|
|
103
|
+
return false if text.match?(/\A[\d.]+\z/) || text.length > 253
|
|
104
|
+
|
|
105
|
+
text.delete_suffix(".").split(".", -1).all? { |label| /\A[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\z/.match?(label) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module Hillstone
|
|
7
|
+
module Parsers
|
|
8
|
+
# 光模块读数保留模块自身的正常、警告或告警范围。
|
|
9
|
+
class ShowTransceiver < CtxChecker::Parsers::Base
|
|
10
|
+
NUMBER = /[+-]?\d+(?:\.\d+)?/
|
|
11
|
+
HEADER = /\ATransceiver status information is shown as below:\z/i
|
|
12
|
+
BLOCK = /\A=+\s+((?:x?ethernet)\d+(?:\/\d+)+)\s+=+\z/i
|
|
13
|
+
METADATA = /\A(?:SFP(?:\+\/XFP)? information:|Part Number:|Serial Number:|Transmission media:|Module temperature(?: (?:normal|warning|alarm) range)?:|Tx supply voltage(?: (?:normal|warning|alarm) range)?:|Tx bias current(?: (?:normal|warning|alarm) range)?:|Module Wavelength:|single module max transmit distance:|OM[123] max transmit distance:|OM4 or copper max transmit distance:)/i
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
# 逐模块隔离,避免未知块将测量值错误关联到前一模块。
|
|
18
|
+
def parse_output(text, complete:)
|
|
19
|
+
rows, issues, blocks, current, header = [], [], [], nil, false
|
|
20
|
+
clean(text).each_line do |raw|
|
|
21
|
+
line = raw.strip
|
|
22
|
+
next if line.empty? || /\A=+\z/.match?(line)
|
|
23
|
+
|
|
24
|
+
if HEADER.match?(line)
|
|
25
|
+
issues << { code: :duplicate_optics_header } if header
|
|
26
|
+
header = true
|
|
27
|
+
elsif (match = BLOCK.match(line))
|
|
28
|
+
current = { name: CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :hillstone), lines: [] }
|
|
29
|
+
blocks << current
|
|
30
|
+
elsif line.start_with?("=")
|
|
31
|
+
current = nil
|
|
32
|
+
issues << { code: :unrecognized_optics_block }
|
|
33
|
+
elsif current
|
|
34
|
+
current[:lines] << line
|
|
35
|
+
else
|
|
36
|
+
issues << { code: :unrecognized_optics_line }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
blocks.each { |block| rows.concat(measurements(block, issues)) }
|
|
40
|
+
issues << { code: :optics_header_missing } unless header
|
|
41
|
+
issues << { code: :optics_measurements_missing } if blocks.empty?
|
|
42
|
+
build_result(rows, complete: complete, recognized: header || blocks.any?, issues: issues,
|
|
43
|
+
identity: %i[interface lane metric])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 缺失或重复测量保持不可用,不替换为零。
|
|
47
|
+
def measurements(block, issues)
|
|
48
|
+
name = block[:name]
|
|
49
|
+
values, limits, seen = {}, {}, Hash.new(0)
|
|
50
|
+
block[:lines].each do |line|
|
|
51
|
+
if (match = /\A(Tx|Rx) optical power:\s*(#{NUMBER}|N\/A|--)\s*dBm\z/i.match(line))
|
|
52
|
+
metric = metric(match[1])
|
|
53
|
+
set_value(values, metric, number(match[2]), seen, issues, name)
|
|
54
|
+
elsif (match = /\A(Tx|Rx) optical power (normal|warning|alarm) range:\s*\[\s*(#{NUMBER})\s*[,,]\s*(#{NUMBER})\s*\]\z/i.match(line))
|
|
55
|
+
metric = metric(match[1])
|
|
56
|
+
level = match[2].casecmp?("alarm") ? :alarm : :warning
|
|
57
|
+
low, high = number(match[3]), number(match[4])
|
|
58
|
+
limits[metric] ||= {}
|
|
59
|
+
%i[low high].zip([ low, high ]).each do |bound, value|
|
|
60
|
+
key = "#{bound}_#{level}".to_sym
|
|
61
|
+
set_value(limits[metric], key, value, seen, issues, name, identity: [ metric, key ])
|
|
62
|
+
end
|
|
63
|
+
elsif !METADATA.match?(line)
|
|
64
|
+
issues << { code: :unrecognized_optics_field, interface: name }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
%i[tx_power rx_power].map do |metric|
|
|
68
|
+
issues << { code: :optics_measurement_missing, interface: name, metric: metric } unless seen[metric].positive?
|
|
69
|
+
{ interface: name, lane: nil, metric: metric, value: values[metric], unit: :dbm,
|
|
70
|
+
low_alarm: nil, high_alarm: nil, low_warning: nil, high_warning: nil,
|
|
71
|
+
alarm: :unknown, present: true, dom_supported: values.key?(metric) ? true : nil }.merge(limits.fetch(metric, {}))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 即使数值相同,重复测量也使该指标失效。
|
|
76
|
+
def set_value(values, key, value, seen, issues, name, identity: key)
|
|
77
|
+
seen[identity] += 1
|
|
78
|
+
if seen[identity] > 1
|
|
79
|
+
values[key] = nil
|
|
80
|
+
issues << { code: :duplicate_optics_value, interface: name }
|
|
81
|
+
else
|
|
82
|
+
values[key] = value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# 标准测量名称独立于厂商标签。
|
|
87
|
+
def metric(direction)
|
|
88
|
+
direction.casecmp?("Tx") ? :tx_power : :rx_power
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 解析有限读数,明确保留 DOM 值不可用的状态。
|
|
92
|
+
def number(value)
|
|
93
|
+
return unless /\A#{NUMBER}\z/.match?(value)
|
|
94
|
+
|
|
95
|
+
parsed = Float(value)
|
|
96
|
+
parsed if parsed.finite?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/profile"
|
|
4
|
+
require_relative "parsers/show_interface"
|
|
5
|
+
require_relative "parsers/show_interface_counter"
|
|
6
|
+
require_relative "parsers/show_transceiver"
|
|
7
|
+
require_relative "parsers/show_ip_bgp_summary"
|
|
8
|
+
require_relative "parsers/show_ip_ospf_neighbor"
|
|
9
|
+
require_relative "parsers/show_cpu"
|
|
10
|
+
require_relative "parsers/show_memory"
|
|
11
|
+
require_relative "parsers/show_environment"
|
|
12
|
+
require_relative "parsers/show_clock"
|
|
13
|
+
require_relative "parsers/show_ntp_status"
|
|
14
|
+
require_relative "parsers/show_cpu_cntr"
|
|
15
|
+
|
|
16
|
+
module CtxChecker
|
|
17
|
+
module Hillstone
|
|
18
|
+
# StoneOS 命令已在 SG-6000-A3700 验证;未覆盖作用域明确保留不可用。
|
|
19
|
+
class Profile < CtxParser::Profile
|
|
20
|
+
CATALOG = CtxParser::CommandCatalog.new([
|
|
21
|
+
{ dimensions: %i[interfaces aggregation], command: "show interface", parser: Parsers::ShowInterface },
|
|
22
|
+
{ dimensions: [ :counters ], command: "show interface counter", parser: Parsers::ShowInterfaceCounter },
|
|
23
|
+
{ dimensions: [ :optics ], command: "show transceiver", parser: Parsers::ShowTransceiver },
|
|
24
|
+
{ dimensions: [ :bgp ], command: "show ip bgp summary", parser: Parsers::ShowIpBgpSummary },
|
|
25
|
+
{ dimensions: [ :ospf ], command: "show ip ospf neighbor", parser: Parsers::ShowIpOspfNeighbor },
|
|
26
|
+
{ dimensions: [ :cpu ], command: "show cpu", parser: Parsers::ShowCpu },
|
|
27
|
+
{ dimensions: [ :memory ], command: "show memory", parser: Parsers::ShowMemory },
|
|
28
|
+
{ dimensions: %i[temperature environment power], command: "show environment", parser: Parsers::ShowEnvironment },
|
|
29
|
+
{ dimensions: [ :clock ], command: "show clock", parser: Parsers::ShowClock },
|
|
30
|
+
{ dimensions: [ :ntp ], command: "show ntp status", parser: Parsers::ShowNtpStatus },
|
|
31
|
+
{ dimensions: [ :device_counters ], command: "show cpu-cntr", parser: Parsers::ShowCpuCntr }
|
|
32
|
+
])
|
|
33
|
+
COMMANDS = CATALOG.commands
|
|
34
|
+
PARSERS = CATALOG.parsers
|
|
35
|
+
|
|
36
|
+
# 在线接口丢弃表不提供错误包或 CRC 测量。
|
|
37
|
+
def counter_metrics = %i[in_errors out_errors crc_errors].freeze
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "system_record"
|
|
6
|
+
|
|
7
|
+
module CtxChecker
|
|
8
|
+
# 提供经过校验的不可变运行事实,与后续健康评估相互独立。
|
|
9
|
+
class Observation
|
|
10
|
+
NAMES = (%i[interfaces counters aggregation optics bgp ospf] + SystemRecord::FIELDS.keys).freeze
|
|
11
|
+
STATUSES = %i[complete partial failed unsupported not_requested not_applicable].freeze
|
|
12
|
+
METRICS = %i[in_errors out_errors in_discards out_discards crc_errors stomped_crc_errors out_carrier_errors].freeze
|
|
13
|
+
COUNTER_UNITS = %i[packets bytes events unknown].freeze
|
|
14
|
+
COUNTER_SCOPES = %i[interface mac cpu logical queue unknown].freeze
|
|
15
|
+
OPTICAL_THRESHOLDS = %i[low_alarm low_warning high_warning high_alarm].freeze
|
|
16
|
+
FIELDS = CtxParser::Immutable.copy({
|
|
17
|
+
interfaces: %i[name kind admin_status oper_status protocol_status protocol_applicable parent],
|
|
18
|
+
counters: %i[interface metric value unit scope discontinuity sampled_at source_command],
|
|
19
|
+
aggregation: %i[group member member_status oper_status],
|
|
20
|
+
optics: %i[interface lane metric value unit low_alarm low_warning high_warning high_alarm alarm present dom_supported applicable],
|
|
21
|
+
bgp: %i[vrf afi_safi peer state received_prefixes remote_as],
|
|
22
|
+
ospf: %i[vrf process_id area interface router_id neighbor_address state network_type local_role neighbor_role], **SystemRecord::FIELDS
|
|
23
|
+
}.transform_values { |fields| fields + [ :source_complete ] })
|
|
24
|
+
IDENTITIES = CtxParser::Immutable.copy(interfaces: %i[name], counters: %i[interface metric],
|
|
25
|
+
aggregation: %i[group member], optics: %i[interface lane metric], bgp: %i[vrf afi_safi peer],
|
|
26
|
+
ospf: %i[vrf process_id interface router_id neighbor_address], **SystemRecord::IDENTITIES)
|
|
27
|
+
REQUIRED = CtxParser::Immutable.copy(interfaces: %i[name], counters: %i[interface metric],
|
|
28
|
+
aggregation: %i[group], optics: %i[interface metric], bgp: %i[peer], ospf: %i[router_id], **SystemRecord::REQUIRED)
|
|
29
|
+
ISSUE_FIELDS = %i[code line lines field count skipped interface metric metrics lane section expected observed group stage component scope window_seconds command error_type].freeze
|
|
30
|
+
ERROR_TYPE_PATTERN = /\A(?:[A-Z][A-Za-z0-9_]*::)*[A-Z][A-Za-z0-9_]*\z/
|
|
31
|
+
ENUMS = CtxParser::Immutable.copy(
|
|
32
|
+
kind: %i[physical aggregate bridge redundant svi loopback tunnel subinterface unknown],
|
|
33
|
+
admin_status: %i[up down unknown], oper_status: %i[up down unknown], protocol_status: %i[up down unknown],
|
|
34
|
+
member_status: %i[active standby down suspended unselected unknown], unit: %i[dbm],
|
|
35
|
+
alarm: %i[normal high_alarm high_warning low_warning low_alarm unknown],
|
|
36
|
+
network_type: %i[broadcast nbma point_to_point point_to_multipoint virtual_link unknown],
|
|
37
|
+
local_role: %i[dr bdr drother unknown], neighbor_role: %i[dr bdr drother unknown])
|
|
38
|
+
SCOPE_FIELDS = CtxParser::Immutable.copy(bgp: %i[vrf afi_safi], ospf: %i[vrf process_id interface])
|
|
39
|
+
attr_reader :name, :status, :records, :issues, :evidence, :covered_scopes
|
|
40
|
+
|
|
41
|
+
# 冻结单个维度的记录、状态及诊断元数据。
|
|
42
|
+
def initialize(name:, status:, records: [], issues: [], evidence: [], covered_scopes: [])
|
|
43
|
+
raise ArgumentError, "Unknown observation dimension" unless NAMES.include?(name)
|
|
44
|
+
raise ArgumentError, "Invalid observation status" unless STATUSES.include?(status)
|
|
45
|
+
raise ArgumentError, "Records must be an array" unless records.is_a?(Array)
|
|
46
|
+
if %i[failed unsupported not_requested not_applicable].include?(status) && records.any?
|
|
47
|
+
raise ArgumentError, "Unavailable observations cannot contain records"
|
|
48
|
+
end
|
|
49
|
+
@name, @status = name, status
|
|
50
|
+
validate_scopes!(covered_scopes)
|
|
51
|
+
normalized = records.map { |record| validate_record(record) }
|
|
52
|
+
identities = normalized.map { |record| identity(record) }
|
|
53
|
+
raise ArgumentError, "Duplicate observation identity" unless identities.uniq.size == identities.size
|
|
54
|
+
unless issues.is_a?(Array) && issues.all? { |issue| self.class.valid_issue?(issue) }
|
|
55
|
+
raise ArgumentError, "Observation issues require safe codes"
|
|
56
|
+
end
|
|
57
|
+
unless evidence.is_a?(Array) && evidence.all? { |entry| entry.is_a?(Hash) && (entry.keys - %i[command sha256 bytes reference observed_at status]).empty? }
|
|
58
|
+
raise ArgumentError, "Invalid observation evidence"
|
|
59
|
+
end
|
|
60
|
+
evidence.each do |entry|
|
|
61
|
+
raise ArgumentError, "Invalid command evidence status" if entry.key?(:status) && !%i[complete partial failed unsupported].include?(entry[:status])
|
|
62
|
+
raise ArgumentError, "Invalid evidence command" if entry.key?(:command) && !text?(entry[:command])
|
|
63
|
+
raise ArgumentError, "Invalid evidence digest" if entry.key?(:sha256) && !/\A[0-9a-f]{64}\z/.match?(entry[:sha256].to_s)
|
|
64
|
+
raise ArgumentError, "Invalid evidence byte count" if entry.key?(:bytes) && !(entry[:bytes].is_a?(Integer) && entry[:bytes] >= 0)
|
|
65
|
+
raise ArgumentError, "Invalid evidence reference" if entry.key?(:reference) && !text?(entry[:reference])
|
|
66
|
+
if entry.key?(:observed_at)
|
|
67
|
+
raise ArgumentError, "Invalid evidence time" unless entry[:observed_at].is_a?(String)
|
|
68
|
+
Time.iso8601(entry[:observed_at])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
@records, @issues, @evidence = [ normalized, issues, evidence ].map { |value| CtxParser::Immutable.copy(value) }
|
|
72
|
+
@covered_scopes = CtxParser::Immutable.copy(covered_scopes)
|
|
73
|
+
@observed_at = evidence.filter_map { |entry| Time.iso8601(entry[:observed_at]).getutc if entry[:observed_at] }.max&.freeze
|
|
74
|
+
freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# 判断所有已选事实是否都有完整证据支持。
|
|
78
|
+
def complete? = status == :complete
|
|
79
|
+
# 返回当前观测关联的归一化时间戳。
|
|
80
|
+
def observed_at = @observed_at
|
|
81
|
+
# 返回用于关联同一资源多次采样的稳定字段。
|
|
82
|
+
def identity(record) = record.values_at(*IDENTITIES.fetch(name))
|
|
83
|
+
# 未增加作用域证明时,保持旧快照和计数器指纹稳定。
|
|
84
|
+
def to_h
|
|
85
|
+
value = { name: name, status: status, records: records, issues: issues, evidence: evidence }
|
|
86
|
+
covered_scopes.empty? ? value : value.merge(covered_scopes: covered_scopes)
|
|
87
|
+
end
|
|
88
|
+
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
89
|
+
def inspect = "#<#{self.class} name=#{name} status=#{status} records=#{records.size}>"
|
|
90
|
+
|
|
91
|
+
# 命令失败与快照失败共用安全诊断结构。
|
|
92
|
+
def self.valid_issue?(issue)
|
|
93
|
+
return false unless issue.is_a?(Hash) && (issue.keys - ISSUE_FIELDS).empty?
|
|
94
|
+
|
|
95
|
+
code = issue[:code]
|
|
96
|
+
return false unless (code.is_a?(Symbol) || code.is_a?(String)) && /\A[a-z][a-z0-9_]*\z/.match?(code.to_s)
|
|
97
|
+
|
|
98
|
+
!issue.key?(:error_type) || issue[:error_type].is_a?(String) && ERROR_TYPE_PATTERN.match?(issue[:error_type])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# 完整协议表仅覆盖其明确的 VRF、地址族或 OSPF 进程。
|
|
104
|
+
def validate_scopes!(scopes)
|
|
105
|
+
unless scopes.is_a?(Array) && scopes.uniq == scopes
|
|
106
|
+
raise ArgumentError, "Covered scopes must be a unique array"
|
|
107
|
+
end
|
|
108
|
+
return if scopes.empty?
|
|
109
|
+
|
|
110
|
+
fields = SCOPE_FIELDS[name]
|
|
111
|
+
required = name == :ospf ? %i[vrf process_id] : fields
|
|
112
|
+
unless fields && %i[complete partial].include?(status) && scopes.all? do |scope|
|
|
113
|
+
scope.is_a?(Hash) && (scope.keys - fields).empty? && (required - scope.keys).empty? &&
|
|
114
|
+
scope.all? { |key, value| text?(value) || key == :process_id && value.is_a?(Integer) && value >= 0 }
|
|
115
|
+
end
|
|
116
|
+
raise ArgumentError, "Covered scopes require explicit protocol identities"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 测量缺失保留 nil;身份字段不能静默消失。
|
|
121
|
+
def validate_record(record)
|
|
122
|
+
unless record.is_a?(Hash) && (record.keys - FIELDS.fetch(name)).empty?
|
|
123
|
+
raise ArgumentError, "Unsupported observation record fields"
|
|
124
|
+
end
|
|
125
|
+
row = FIELDS.fetch(name).to_h { |key| [ key, record[key] ] }
|
|
126
|
+
raise ArgumentError, "Missing observation identity" unless REQUIRED.fetch(name).all? { |key| !row[key].nil? }
|
|
127
|
+
row.each do |key, value|
|
|
128
|
+
next if value.nil?
|
|
129
|
+
valid = if key == :source_complete
|
|
130
|
+
value == true || value == false
|
|
131
|
+
elsif SystemRecord::FIELDS.key?(name)
|
|
132
|
+
SystemRecord.valid?(name, key, value)
|
|
133
|
+
elsif key == :sampled_at
|
|
134
|
+
SystemRecord.timestamp?(value)
|
|
135
|
+
elsif name == :counters && key == :unit
|
|
136
|
+
COUNTER_UNITS.include?(value)
|
|
137
|
+
elsif name == :counters && key == :scope
|
|
138
|
+
COUNTER_SCOPES.include?(value)
|
|
139
|
+
elsif ENUMS.key?(key)
|
|
140
|
+
ENUMS.fetch(key).include?(value)
|
|
141
|
+
elsif key == :metric
|
|
142
|
+
(name == :optics ? %i[tx_power rx_power] : METRICS).include?(value)
|
|
143
|
+
elsif key == :state
|
|
144
|
+
states = name == :bgp ? %i[established idle active connect opensent openconfirm shutdown unknown] : %i[full two_way down attempt init exstart exchange loading unknown]
|
|
145
|
+
states.include?(value)
|
|
146
|
+
elsif %i[value low_alarm low_warning high_warning high_alarm].include?(key)
|
|
147
|
+
name == :counters ? value.is_a?(Integer) && value >= 0 : value.is_a?(Numeric) && value.real? && value.finite?
|
|
148
|
+
elsif %i[received_prefixes remote_as].include?(key)
|
|
149
|
+
value.is_a?(Integer) && value >= 0
|
|
150
|
+
elsif %i[present dom_supported applicable protocol_applicable].include?(key)
|
|
151
|
+
value == true || value == false
|
|
152
|
+
elsif key == :lane || key == :discontinuity || key == :process_id
|
|
153
|
+
value.is_a?(Integer) && value >= 0 || text?(value)
|
|
154
|
+
else
|
|
155
|
+
text?(value)
|
|
156
|
+
end
|
|
157
|
+
raise ArgumentError, "Invalid #{name} field #{key}" unless valid
|
|
158
|
+
end
|
|
159
|
+
row
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# 校验非空文本,拒绝将含控制字符的值作为标识。
|
|
163
|
+
def text?(value) = value.is_a?(String) && !value.strip.empty? && !/[\x00-\x1f\x7f]/.match?(value)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "observation"
|
|
4
|
+
require "ctx_parser/records"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
# 组合独立命令证据,不拼接不相容的 CLI 语法正文。
|
|
8
|
+
class ObservationMerger
|
|
9
|
+
# 归并已声明的命令证据,不丢失来源失败或身份歧义。
|
|
10
|
+
def self.call(name, observations, commands:, required_metrics: [])
|
|
11
|
+
unless observations.is_a?(Array) && observations.all? { |observation| observation.is_a?(Observation) && observation.name == name }
|
|
12
|
+
raise ArgumentError, "Command observations must share a dimension"
|
|
13
|
+
end
|
|
14
|
+
sources = observations.map do |observation|
|
|
15
|
+
source = observation.evidence.map { |entry| entry[:command] }.compact.uniq
|
|
16
|
+
raise ArgumentError, "Observation must identify one declared source command" unless source.one? && commands.include?(source.first)
|
|
17
|
+
source.first
|
|
18
|
+
end
|
|
19
|
+
raise ArgumentError, "Duplicate source command" unless sources.uniq == sources
|
|
20
|
+
|
|
21
|
+
if observations.any? && observations.all? { |observation| observation.status == :not_applicable } && (commands - sources).empty?
|
|
22
|
+
return Observation.new(name: name, status: :not_applicable,
|
|
23
|
+
issues: observations.flat_map(&:issues).uniq, evidence: observations.flat_map(&:evidence))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
issues = observations.zip(sources).flat_map do |observation, command|
|
|
27
|
+
observation.issues.reject { |issue| issue[:code] == :incomplete_command_set }.map { |issue| issue.merge(command: command) }
|
|
28
|
+
end
|
|
29
|
+
if observations.any? { |observation| observation.status == :partial && observation.issues.empty? }
|
|
30
|
+
issues << { code: :incomplete_command_output }
|
|
31
|
+
end
|
|
32
|
+
missing = commands - sources
|
|
33
|
+
issues << { code: :incomplete_command_set, count: missing.size } if missing.any?
|
|
34
|
+
records = observations.flat_map do |observation|
|
|
35
|
+
observation.records.map do |record|
|
|
36
|
+
if %i[counters device_counters].include?(name) && record[:sampled_at].nil? && observation.observed_at
|
|
37
|
+
record.merge(sampled_at: observation.observed_at.iso8601(6))
|
|
38
|
+
else
|
|
39
|
+
record
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
grouped = CtxParser::Records.new(records, identity: Observation::IDENTITIES.fetch(name))
|
|
44
|
+
covered_scopes = observations.flat_map(&:covered_scopes).uniq
|
|
45
|
+
if grouped.duplicates?
|
|
46
|
+
issues << { code: :duplicate_identity, count: grouped.duplicate_identity_count }
|
|
47
|
+
records = grouped.unambiguous.dup
|
|
48
|
+
# 隔离的对等体实际已被观察到,因此移除后不能再据其作用域证明缺失。
|
|
49
|
+
ambiguous = grouped.first_occurrences - grouped.unambiguous
|
|
50
|
+
covered_scopes = covered_scopes.reject do |scope|
|
|
51
|
+
ambiguous.any? { |row| scope.all? { |key, value| row[key].nil? || row[key] == value } }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
if name == :counters && required_metrics.any?
|
|
55
|
+
# 补齐缺失指标的未知占位,不把独立方向查询失败解释为零错误。
|
|
56
|
+
records.group_by { |row| row[:interface] }.each do |interface, rows|
|
|
57
|
+
absent = required_metrics - rows.map { |row| row[:metric] }
|
|
58
|
+
next if absent.empty?
|
|
59
|
+
issues << { code: :counter_metrics_missing, interface: interface, metrics: absent }
|
|
60
|
+
records.concat(absent.map { |metric| { interface: interface, metric: metric, value: nil } })
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
unavailable = observations.all? { |observation| %i[failed unsupported].include?(observation.status) }
|
|
64
|
+
# 有成功事实时降为部分完整,而非让同级脚本失败抹除整份维度结果。
|
|
65
|
+
status = if unavailable && records.empty?
|
|
66
|
+
observations.any? && observations.all? { |observation| observation.status == :unsupported } && missing.empty? ? :unsupported : :failed
|
|
67
|
+
elsif issues.any? || observations.any? { |observation| %i[failed unsupported].include?(observation.status) }
|
|
68
|
+
:partial
|
|
69
|
+
else
|
|
70
|
+
:complete
|
|
71
|
+
end
|
|
72
|
+
if name == :aggregation && status == :complete && records.empty?
|
|
73
|
+
status = :not_applicable
|
|
74
|
+
issues << { code: :aggregation_not_configured }
|
|
75
|
+
end
|
|
76
|
+
Observation.new(name: name, status: status, records: records, issues: issues.uniq,
|
|
77
|
+
covered_scopes: covered_scopes, evidence: observations.flat_map(&:evidence))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../../parsers/base"
|
|
6
|
+
|
|
7
|
+
module CtxChecker
|
|
8
|
+
module PaloAlto
|
|
9
|
+
module Parsers
|
|
10
|
+
# 命名本地时区缺少明确偏移,不能用于安全判断 UTC 时钟偏差。
|
|
11
|
+
class ShowClock < CtxChecker::Parsers::Base
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 解析 show clock,保留不完整或不支持的证据。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
source = clean(text)
|
|
17
|
+
rows, issues = [], []
|
|
18
|
+
source.each_line.with_index(1) do |raw, number|
|
|
19
|
+
next if raw.strip.empty?
|
|
20
|
+
match = /\A(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+([A-Z][a-z]{2})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\d{4})\z/.match(raw.strip)
|
|
21
|
+
unless match
|
|
22
|
+
issues << { code: :invalid_clock_output, line: number }
|
|
23
|
+
next
|
|
24
|
+
end
|
|
25
|
+
zone = %w[UTC GMT Z].include?(match[4]) ? "+0000" : match[4]
|
|
26
|
+
timestamp = clock_time(match, zone)
|
|
27
|
+
issues << { code: :clock_offset_unknown, line: number } unless timestamp
|
|
28
|
+
rows << { component: "system", time: timestamp, source: "system_clock", synchronized: nil }
|
|
29
|
+
end
|
|
30
|
+
build_result(rows, complete: complete, recognized: rows.any?, issues: issues, identity: [ :component ])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 解析设备时钟,不虚构未知时区。
|
|
34
|
+
def clock_time(match, zone)
|
|
35
|
+
return unless /\A[+-]\d{2}:?\d{2}\z/.match?(zone)
|
|
36
|
+
month = Date::ABBR_MONTHNAMES.index(match[1])
|
|
37
|
+
year, day = Integer(match[5], 10), Integer(match[2], 10)
|
|
38
|
+
return unless month && Date.valid_date?(year, month, day)
|
|
39
|
+
hours, minutes, seconds = match[3].split(":").map { |part| Integer(part, 10) }
|
|
40
|
+
return unless hours < 24 && minutes < 60 && seconds < 60
|
|
41
|
+
offset = zone.delete(":")
|
|
42
|
+
return unless offset[1, 2].to_i < 24 && offset[3, 2].to_i < 60
|
|
43
|
+
Time.strptime("#{match[1]} #{day} #{match[3]} #{offset} #{year}", "%b %d %H:%M:%S %z %Y").iso8601
|
|
44
|
+
rescue ArgumentError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module PaloAlto
|
|
7
|
+
module Parsers
|
|
8
|
+
# 数值自数据平面重启后累计;CLI 速率采样由外部调用方管理。
|
|
9
|
+
class ShowCounterGlobal < CtxChecker::Parsers::Base
|
|
10
|
+
CATEGORIES = { "info" => :informational, "warn" => :error, "error" => :error, "drop" => :discard }.freeze
|
|
11
|
+
# 安全策略执行与设备转发故障区分处理。
|
|
12
|
+
METRIC_CATEGORIES = { "flow_policy_deny" => :policy }.freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# 解析 show counter global,保留不完整或不支持的证据。
|
|
17
|
+
def parse_output(text, complete:)
|
|
18
|
+
source = clean(text)
|
|
19
|
+
rows, totals, issues = [], [], []
|
|
20
|
+
title, header = false, false
|
|
21
|
+
source.each_line.with_index(1) do |raw, number|
|
|
22
|
+
line = raw.strip
|
|
23
|
+
if line == "Global counters:"
|
|
24
|
+
title = true
|
|
25
|
+
elsif line.match?(/\Aname\s+value\s+rate\s+severity\s+category\s+aspect\s+description\z/)
|
|
26
|
+
header = true
|
|
27
|
+
elsif (match = /\ATotal counters shown:\s*(\d+)\z/.match(line))
|
|
28
|
+
totals << Integer(match[1], 10)
|
|
29
|
+
elsif line.empty? || line.match?(/\A-+\z/) || line.match?(/\AElapsed time since last sampling:\s*\d+(?:\.\d+)? seconds\z/)
|
|
30
|
+
next
|
|
31
|
+
elsif header && (match = /\A([a-zA-Z][a-zA-Z0-9_]*)\s+(\d+)\s+\d+\s+(info|warn|error|drop)\s+(\S+)\s+(\S+)\s+.+\z/.match(line))
|
|
32
|
+
rows << { component: "dataplane/#{match[4]}/#{match[5]}", metric: match[1], category: METRIC_CATEGORIES.fetch(match[1], CATEGORIES.fetch(match[3])),
|
|
33
|
+
value: Integer(match[2], 10), discontinuity: nil }
|
|
34
|
+
else
|
|
35
|
+
issues << { code: :unrecognized_counter_row, line: number }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
if !totals.one? || totals.first != rows.size
|
|
39
|
+
issues << { code: :counter_total_mismatch, observed: rows.size, expected: totals.one? ? totals.first : nil }
|
|
40
|
+
end
|
|
41
|
+
recognized = title && header && (rows.any? || totals == [ 0 ])
|
|
42
|
+
build_result(rows, complete: complete, recognized: recognized, issues: issues, identity: %i[component metric])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module PaloAlto
|
|
7
|
+
module Parsers
|
|
8
|
+
# CPU 接收错误属于 L2 至 L4 错误,不能替代 MAC CRC 错误。
|
|
9
|
+
class ShowCounterInterfaceAll < CtxChecker::Parsers::Base
|
|
10
|
+
METRICS = {
|
|
11
|
+
"receive errors" => :in_errors, "transmit errors" => :out_errors,
|
|
12
|
+
"receive discarded" => :in_discards, "receive packets dropped" => :in_discards,
|
|
13
|
+
"transmit packets dropped" => :out_discards
|
|
14
|
+
}.freeze
|
|
15
|
+
DETAIL = /\A(?:bytes (?:received|transmitted)|packets (?:received|transmitted|dropped(?: by flow state check)?|routed to different zone)|receive incoming errors|forwarding errors|no route|arp not found|neighbor (?:not found|info pending)|mac not found|land attacks|ping-of-death attacks|teardrop attacks|[im]ac spoof attacks|ip spoof attacks|ICMP fragment|layer2 (?:encapsulated|decapsulated) packets|multicast packets received|(?:tcp|udp|sctp|other) cps)\s+\d+\z/i
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# 解析 show counter interface all,保留不完整或不支持的证据。
|
|
20
|
+
def parse_output(text, complete:)
|
|
21
|
+
text = clean(text)
|
|
22
|
+
rows, issues, interfaces = [], [], {}
|
|
23
|
+
text.split(/(?=^\s*Interface:)/i).each do |block|
|
|
24
|
+
next if block.strip.empty?
|
|
25
|
+
match = /\A\s*Interface:\s*([^\n]+)\n/i.match(block)
|
|
26
|
+
unless match
|
|
27
|
+
lines = block.lines.map(&:strip).reject { |line| line.empty? || line.match?(/\A-+\z/) }
|
|
28
|
+
issues << { code: :unrecognized_counter_block } unless lines == [ "Hardware interface counters:" ]
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
name = match[1].strip.casecmp?("Management Interface") ? "management" : match[1].strip.downcase
|
|
32
|
+
interfaces[name] = true
|
|
33
|
+
kind = CtxChecker::Parsers::InterfaceNames.kind(name)
|
|
34
|
+
wanted = kind == :physical ? :hardware : :logical
|
|
35
|
+
section, values = nil, []
|
|
36
|
+
block.lines.drop(1).each do |raw|
|
|
37
|
+
line = raw.strip
|
|
38
|
+
if (heading = /\A(Hardware|Logical) interface counters(?: read from CPU)?:\z/i.match(line))
|
|
39
|
+
section = heading[1].downcase.to_sym
|
|
40
|
+
elsif line.match?(/\A(?:Detailed )?Physical port counters read from MAC:\z/i)
|
|
41
|
+
section = :mac
|
|
42
|
+
elsif line.empty? || line.match?(/\A-+\z/)
|
|
43
|
+
next
|
|
44
|
+
elsif section == wanted && (metric = /\A(.+?)\s+(\d+)\z/.match(line)) && METRICS.key?(metric[1].downcase)
|
|
45
|
+
values << { interface: name, metric: METRICS.fetch(metric[1].downcase), value: Integer(metric[2], 10), discontinuity: nil,
|
|
46
|
+
unit: :packets, scope: section == :hardware ? :cpu : :logical }
|
|
47
|
+
elsif section == wanted && !DETAIL.match?(line)
|
|
48
|
+
issues << { code: :unrecognized_counter_field, interface: name }
|
|
49
|
+
elsif section.nil?
|
|
50
|
+
issues << { code: :counter_section_missing, interface: name }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
rows.concat(values)
|
|
54
|
+
end
|
|
55
|
+
# 同一接口可能同时出现在硬件与逻辑清单中,须先确定其标准层级,
|
|
56
|
+
# 再补齐缺失指标,不能按每个展示块重复生成。
|
|
57
|
+
by_interface = rows.group_by { |row| row[:interface] }
|
|
58
|
+
interfaces.each_key do |name|
|
|
59
|
+
missing = %i[in_errors out_errors in_discards out_discards crc_errors] - by_interface.fetch(name, []).map { |row| row[:metric] }
|
|
60
|
+
missing.each { |metric| rows << { interface: name, metric: metric, value: nil, discontinuity: nil } }
|
|
61
|
+
issues << { code: :missing_counter_metrics, interface: name, metrics: missing } if missing.any?
|
|
62
|
+
end
|
|
63
|
+
build_result(rows, complete: complete, recognized: interfaces.any?, issues: issues, identity: %i[interface metric])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|