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,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# NX-OS 的 IPv4 软件协议栈标签不同于 IOS,需显式保留作用域。
|
|
9
|
+
class ShowIpTraffic < CtxChecker::Parsers::Base
|
|
10
|
+
METRICS = {
|
|
11
|
+
"bad checksum" => [ "checksum_errors", :error ], "packet too small" => [ "packet_too_small", :error ],
|
|
12
|
+
"bad version" => [ "bad_version", :error ], "bad header length" => [ "bad_header_length", :error ],
|
|
13
|
+
"bad packet length" => [ "bad_packet_length", :error ], "bad destination" => [ "bad_destination", :error ],
|
|
14
|
+
"bad ttl" => [ "bad_ttl", :error ], "could not forward" => [ "could_not_forward", :discard ],
|
|
15
|
+
"no buffer dropped" => [ "no_buffer", :discard ], "bad encapsulation" => [ "encapsulation_failed", :discard ],
|
|
16
|
+
"no route" => [ "no_route", :discard ], "non-existent protocol" => [ "unknown_protocol", :error ]
|
|
17
|
+
}.freeze
|
|
18
|
+
OPTIONAL_METRICS = {
|
|
19
|
+
"stateful restart recovery" => [ "stateful_restart_recovery", :informational ],
|
|
20
|
+
"mbuf pull up fail" => [ "mbuf_pullup_failed", :discard ],
|
|
21
|
+
"bad options" => [ "bad_options", :error ], "bad context id" => [ "bad_context_id", :error ],
|
|
22
|
+
"bad gw mac" => [ "bad_gateway_mac", :error ], "rpf drops" => [ "rpf_drops", :policy ],
|
|
23
|
+
"vinci migration packets" => [ "vinci_migration_packets", :informational ],
|
|
24
|
+
"total packet snooped" => [ "packets_snooped", :informational ],
|
|
25
|
+
"total packet on down svi" => [ "packets_on_down_svi", :informational ],
|
|
26
|
+
"ingress option processing failed" => [ "ingress_option_processing_failed", :discard ],
|
|
27
|
+
"nat inside drop" => [ "nat_inside_drops", :discard ], "nat outside drop" => [ "nat_outside_drops", :discard ],
|
|
28
|
+
"ingress mforward failed" => [ "ingress_mforward_failed", :discard ],
|
|
29
|
+
"ingress lisp drop" => [ "ingress_lisp_drops", :discard ],
|
|
30
|
+
"ingress lisp decap drop" => [ "ingress_lisp_decap_drops", :discard ],
|
|
31
|
+
"ingress lisp encap drop" => [ "ingress_lisp_encap_drops", :discard ],
|
|
32
|
+
"ingress lisp encap" => [ "ingress_lisp_encapsulated", :informational ],
|
|
33
|
+
"ingress mfwd copy drop" => [ "ingress_mfwd_copy_drops", :discard ],
|
|
34
|
+
"ingress ra/reass drop" => [ "ingress_ra_reass_drops", :discard ],
|
|
35
|
+
"ingress icmp redirect processing drop" => [ "ingress_icmp_redirect_processing_drops", :discard ],
|
|
36
|
+
"ingress drop (ifmgr init)" => [ "ingress_ifmgr_init_drops", :discard ],
|
|
37
|
+
"ingress drop (invalid filter)" => [ "ingress_invalid_filter_drops", :discard ],
|
|
38
|
+
"ingress drop (invalid l2 msg)" => [ "ingress_invalid_l2_message_drops", :discard ]
|
|
39
|
+
}.freeze
|
|
40
|
+
ERROR_METRICS = METRICS.merge(OPTIONAL_METRICS).freeze
|
|
41
|
+
ERROR_LABELS = Regexp.union((ERROR_METRICS.keys - [ "bad gw mac" ]).sort_by { |label| -label.size })
|
|
42
|
+
ERROR_CELL = /\G\s*(?:,\s*)?(?:(#{ERROR_LABELS.source})\s*:\s*|(bad gw mac)\s+)(\d+)(?=\s|,|\z)/i
|
|
43
|
+
ACL_METRICS = { "ingress" => "acl_ingress_drops", "egress" => "acl_egress_drops",
|
|
44
|
+
"directed broadcast" => "acl_directed_broadcast_drops" }.freeze
|
|
45
|
+
FRAGMENT_METRICS = {
|
|
46
|
+
"fragments dropped" => [ "fragments_dropped", :discard ],
|
|
47
|
+
"fragments timed out" => [ "fragments_timed_out", :discard ]
|
|
48
|
+
}.freeze
|
|
49
|
+
FRAGMENT_CONTEXT = [ "fragments received", "fragments sent", "fragments created", "packets with df", "packets reassembled" ].freeze
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# 解析 show ip traffic,保留不完整或不支持的证据。
|
|
54
|
+
def parse_output(text, complete:)
|
|
55
|
+
records, issues = [], []
|
|
56
|
+
recognized = false
|
|
57
|
+
section = nil
|
|
58
|
+
fragment_sections, acl_sections = 0, 0
|
|
59
|
+
ipv4 = false
|
|
60
|
+
clean(text).each_line.with_index(1) do |source, number|
|
|
61
|
+
line = source.strip
|
|
62
|
+
next if line.empty?
|
|
63
|
+
if /\AIP Software Processed Traffic Statistics\z/i.match?(line)
|
|
64
|
+
ipv4 = recognized = true
|
|
65
|
+
section = nil
|
|
66
|
+
elsif /\A(?:ICMP|TCP|UDP|IPv6)\s/i.match?(line)
|
|
67
|
+
ipv4 = false
|
|
68
|
+
section = nil
|
|
69
|
+
elsif ipv4 && /\AErrors:\z/i.match?(line)
|
|
70
|
+
section = :errors
|
|
71
|
+
elsif ipv4 && /\AFragmentation\/reassembly:\z/i.match?(line)
|
|
72
|
+
section = :fragments
|
|
73
|
+
fragment_sections += 1
|
|
74
|
+
elsif ipv4 && /\AACL Filter Drops\s*:\z/i.match?(line)
|
|
75
|
+
section = :acl
|
|
76
|
+
acl_sections += 1
|
|
77
|
+
elsif ipv4 && /\AFragments created per protocol\z/i.match?(line)
|
|
78
|
+
section = :fragment_protocols
|
|
79
|
+
elsif /\A(?:Multicast|IP output|IP input|Rcvd|Sent|Input|Output|Statistics last reset)(?:\s|:)/i.match?(line)
|
|
80
|
+
section = nil
|
|
81
|
+
elsif section == :errors
|
|
82
|
+
parse_error_counters(line, number, records, issues)
|
|
83
|
+
elsif section == :acl
|
|
84
|
+
parse_acl_counter(line, number, records, issues)
|
|
85
|
+
elsif section == :fragment_protocols
|
|
86
|
+
issues << { code: :unparsed_counter, line: number } unless /\A\d+\s*:\s*\d+\z/.match?(line)
|
|
87
|
+
elsif section == :fragments
|
|
88
|
+
line.delete_suffix(",").split(/,\s*/).each do |cell|
|
|
89
|
+
match = /\A([^:]+):\s*(\d+)\z/.match(cell)
|
|
90
|
+
label = match && match[1].downcase
|
|
91
|
+
definition = FRAGMENT_METRICS[label]
|
|
92
|
+
if definition
|
|
93
|
+
records << counter(definition, match[2])
|
|
94
|
+
elsif !FRAGMENT_CONTEXT.include?(label)
|
|
95
|
+
issues << { code: :unparsed_counter, line: number }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
missing = METRICS.values.map(&:first) - records.map { |record| record[:metric] }
|
|
101
|
+
issues << { code: :counter_fields_missing, count: missing.size } if recognized && missing.any?
|
|
102
|
+
if fragment_sections.positive?
|
|
103
|
+
missing = FRAGMENT_METRICS.values.map(&:first) - records.map { |record| record[:metric] }
|
|
104
|
+
issues << { code: :fragment_counter_fields_missing, count: missing.size } if missing.any?
|
|
105
|
+
issues << { code: :duplicate_counter_section, scope: "fragmentation" } if fragment_sections > 1
|
|
106
|
+
end
|
|
107
|
+
if acl_sections.positive?
|
|
108
|
+
missing = ACL_METRICS.values - records.map { |record| record[:metric] }
|
|
109
|
+
issues << { code: :acl_counter_fields_missing, count: missing.size } if missing.any?
|
|
110
|
+
issues << { code: :duplicate_counter_section, scope: "acl" } if acl_sections > 1
|
|
111
|
+
end
|
|
112
|
+
build_result(records, complete: complete, recognized: recognized, issues: issues, identity: %i[component metric])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# NX-OS 的部分相邻错误字段不以逗号分隔,且某个标签可能重复出现。
|
|
116
|
+
def parse_error_counters(line, number, records, issues)
|
|
117
|
+
position = 0
|
|
118
|
+
until /\A\s*,?\s*\z/.match?(line[position..])
|
|
119
|
+
match = ERROR_CELL.match(line, position)
|
|
120
|
+
unless match
|
|
121
|
+
issues << { code: :unparsed_counter, line: number }
|
|
122
|
+
break
|
|
123
|
+
end
|
|
124
|
+
definition = ERROR_METRICS.fetch((match[1] || match[2]).downcase)
|
|
125
|
+
record = counter(definition, match[3])
|
|
126
|
+
# 仅允许此类已知重复 NX-OS 展示字段合并相同值。
|
|
127
|
+
repeated = record[:metric] == "ingress_option_processing_failed" && records.include?(record)
|
|
128
|
+
records << record unless repeated
|
|
129
|
+
position = match.end(0)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ACL 过滤属于主动策略,不能作为通用硬件错误总数。
|
|
134
|
+
def parse_acl_counter(line, number, records, issues)
|
|
135
|
+
match = /\A(Ingress|Egress|Directed Broadcast)\s+-\s+(\d+)\z/i.match(line)
|
|
136
|
+
if match
|
|
137
|
+
records << counter([ ACL_METRICS.fetch(match[1].downcase), :policy ], match[2])
|
|
138
|
+
else
|
|
139
|
+
issues << { code: :unparsed_counter, line: number }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# 提取支持的累计计数器,不强制转换未知值。
|
|
144
|
+
def counter(definition, value)
|
|
145
|
+
{ component: "ipv4/control_plane", metric: definition[0], category: definition[1],
|
|
146
|
+
value: Integer(value, 10), discontinuity: nil }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
|
|
6
|
+
module CtxChecker
|
|
7
|
+
module CiscoNxos
|
|
8
|
+
module Parsers
|
|
9
|
+
# 所选 NTP 对等体用于证明同步,与报告 CFS 的 NX-OS show ntp status 区分。
|
|
10
|
+
class ShowNtpPeerStatus < CtxChecker::Parsers::Base
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# 解析 show ntp peer status,保留不完整或不支持的证据。
|
|
14
|
+
def parse_output(text, complete:)
|
|
15
|
+
peers, issues, totals = [], [], []
|
|
16
|
+
vrf_column = nil
|
|
17
|
+
each_ntp_line(clean(text), issues) do |line, number|
|
|
18
|
+
next if line.empty? || /\A-+\z/.match?(line)
|
|
19
|
+
|
|
20
|
+
if (match = /\ATotal peers\s*:\s*(\d+)\z/i.match(line))
|
|
21
|
+
totals << Integer(match[1], 10)
|
|
22
|
+
elsif /\A(?:\*\s*-\s*selected for sync|\-\s*-\s*peer mode)/i.match?(line)
|
|
23
|
+
next
|
|
24
|
+
elsif (match = /\Aremote\s+local\s+st\s+poll\s+reach\s+delay(?:\s+(vrf))?\z/i.match(line))
|
|
25
|
+
issues << { code: :duplicate_ntp_header, line: number } unless vrf_column.nil?
|
|
26
|
+
vrf_column = !match[1].nil?
|
|
27
|
+
else
|
|
28
|
+
peer = peer_row(line, number, vrf_column, issues)
|
|
29
|
+
peers << peer if peer
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
issues << { code: :ntp_peer_count_mismatch } unless totals.one? && totals.first == peers.size
|
|
33
|
+
issues << { code: :duplicate_identity } unless peers.map { |peer| peer.values_at(:peer, :vrf) }.uniq.size == peers.size
|
|
34
|
+
selected = peers.select { |peer| peer[:selected] }
|
|
35
|
+
issues << { code: :conflicting_ntp_selection } if selected.size > 1
|
|
36
|
+
peer = selected.one? ? selected.first : nil
|
|
37
|
+
issues << { code: :invalid_ntp_selection } if peer && (!peer[:reachable] || !peer[:stratum].between?(1, 14))
|
|
38
|
+
issues << { code: :local_ntp_reference } if peer && peer[:peer] == "127.127.1.0"
|
|
39
|
+
recognized = totals.any? || peers.any?
|
|
40
|
+
synchronized = issues.empty? && (complete || peer) ? !!peer : nil
|
|
41
|
+
records = recognized ? [ { component: "system", synchronized: synchronized,
|
|
42
|
+
stratum: peer && peer[:stratum] < 16 ? peer[:stratum] + 1 : nil, reference: peer&.fetch(:peer),
|
|
43
|
+
peer: peer&.fetch(:peer), offset_ms: nil, leap: nil } ] : []
|
|
44
|
+
build_result(records, complete: complete, recognized: recognized, issues: issues, identity: [ :component ])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 旧版本省略 VRF,新版本可能紧接 delay 后输出 VRF。
|
|
48
|
+
def peer_row(line, number, vrf_column, issues)
|
|
49
|
+
match = /\A([*+\-=])?\s*(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+([0-7]+)\s+(.+)\z/.match(line)
|
|
50
|
+
unless match
|
|
51
|
+
issues << { code: :unparsed_row, line: number }
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
if vrf_column.nil?
|
|
55
|
+
issues << { code: :missing_ntp_header, line: number }
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
remote, local = match.values_at(2, 3)
|
|
60
|
+
raise ArgumentError if [ remote, local ].any? { |address| address.include?("/") || address.include?("%") }
|
|
61
|
+
remote = IPAddr.new(remote).native.to_s
|
|
62
|
+
IPAddr.new(local)
|
|
63
|
+
stratum, reach = Integer(match[4], 10), Integer(match[6], 8)
|
|
64
|
+
raise ArgumentError unless stratum.between?(0, 16) && reach <= 255
|
|
65
|
+
|
|
66
|
+
delay = if vrf_column
|
|
67
|
+
/\A(\d+(?:\.\d+)?)(?:\s+(\S+)|([^\d.\s]\S*))?\z/.match(match[7])
|
|
68
|
+
else
|
|
69
|
+
/\A(\d+(?:\.\d+)?)\z/.match(match[7])
|
|
70
|
+
end
|
|
71
|
+
raise ArgumentError unless delay && Float(delay[1]).finite?
|
|
72
|
+
vrf = vrf_column ? delay[2] || delay[3] : nil
|
|
73
|
+
raise ArgumentError if vrf_column && delay[3] && /\A[eE][+-]?\d+\z/.match?(delay[3])
|
|
74
|
+
|
|
75
|
+
{ peer: remote, vrf: vrf, selected: match[1] == "*", stratum: stratum, reachable: reach.positive? }
|
|
76
|
+
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
77
|
+
issues << { code: :invalid_ntp_peer, line: number }
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 仅拼接文档明确的续行;未完成的记录不能吞并新的作用域。
|
|
82
|
+
def each_ntp_line(source, issues)
|
|
83
|
+
pending = nil
|
|
84
|
+
source.each_line.with_index(1) do |raw, number|
|
|
85
|
+
line = raw.strip
|
|
86
|
+
next if line.empty?
|
|
87
|
+
|
|
88
|
+
if pending
|
|
89
|
+
kind, prefix, first_line = pending
|
|
90
|
+
continuation = kind == :header ? /\Ast\s+poll\s+reach\s+delay(?:\s+vrf)?\z/i : /\A\d+\s+\d+\s+[0-7]+\s+.+\z/
|
|
91
|
+
pending = nil
|
|
92
|
+
if continuation.match?(line)
|
|
93
|
+
yield "#{prefix} #{line}", first_line
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
issues << { code: kind == :header ? :truncated_ntp_header : :truncated_ntp_peer, line: first_line }
|
|
97
|
+
end
|
|
98
|
+
kind = if /\Aremote\s+local\z/i.match?(line)
|
|
99
|
+
:header
|
|
100
|
+
elsif /\A(?:[*+\-=]\s*)?[0-9a-f:.]+\s+[0-9a-f:.]+\z/i.match?(line)
|
|
101
|
+
:peer
|
|
102
|
+
end
|
|
103
|
+
if kind
|
|
104
|
+
pending = [ kind, line, number ]
|
|
105
|
+
else
|
|
106
|
+
yield line, number
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
if pending
|
|
110
|
+
issues << { code: pending[0] == :header ? :truncated_ntp_header : :truncated_ntp_peer, line: pending[2] }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../cisco_ios/parsers/show_etherchannel_summary"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# NX-OS 使用相同的聚合组与成员标志,另增加接口类型列。
|
|
9
|
+
class ShowPortChannelSummary < CiscoIos::Parsers::ShowEtherchannelSummary
|
|
10
|
+
PLATFORM = :cisco_nxos
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 部分终端会将下一表格行附加在占满终端宽度的分隔线后。
|
|
15
|
+
def parse_output(text, complete:)
|
|
16
|
+
super(clean(text).gsub(/^([ \t]*-{3,})(?=Group\s+Port-|\d+\s+(?:Po|port-channel)\d+\()/i, "\\1\n"), complete: complete)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../parsers/base"
|
|
4
|
+
require_relative "../../cisco_ios/parsers/system_values"
|
|
5
|
+
require "ctx_parser/result_set"
|
|
6
|
+
|
|
7
|
+
module CtxChecker
|
|
8
|
+
module CiscoNxos
|
|
9
|
+
module Parsers
|
|
10
|
+
# CPU 使用率为 100 减空闲率;Linux 已用内存可能包含可回收缓存。
|
|
11
|
+
class ShowSystemResources < CtxChecker::Parsers::Base
|
|
12
|
+
include CtxChecker::CiscoIos::Parsers::SystemValues
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# 解析 show system resources,保留不完整或不支持的证据。
|
|
17
|
+
def parse_output(text, complete:)
|
|
18
|
+
cpu, memory, cpu_issues, memory_issues = [], [], [], []
|
|
19
|
+
caches, statuses = [], []
|
|
20
|
+
kernel_cache = {}
|
|
21
|
+
clean(text).each_line.with_index(1) do |source, number|
|
|
22
|
+
line = source.strip
|
|
23
|
+
next if line.empty?
|
|
24
|
+
|
|
25
|
+
if (match = /\ACPU(?:\s*(\d+))? states\s*:\s*(#{NUMBER})% user,\s*(#{NUMBER})% kernel,\s*(#{NUMBER})% idle\z/i.match(line))
|
|
26
|
+
values = match.captures.drop(1).map { |value| percentage(value) }
|
|
27
|
+
valid = values.all? && (values.sum - 100).abs <= 0.2
|
|
28
|
+
cpu_issues << { code: :invalid_cpu_value, line: number } unless valid
|
|
29
|
+
cpu << { component: match[1] ? "cpu #{match[1]}" : "processor", scope: "system", window_seconds: 1,
|
|
30
|
+
utilization: valid ? (100 - values.last).round(3) : nil, interrupt_utilization: nil }
|
|
31
|
+
elsif (match = /\AMemory usage:\s*(\d+)K total,\s*(\d+)K used,\s*(\d+)K free\z/i.match(line))
|
|
32
|
+
total, used, free = match.captures.map { |value| Integer(value, 10) * 1024 }
|
|
33
|
+
valid = total.positive? && used + free == total
|
|
34
|
+
memory_issues << { code: :inconsistent_memory_totals, line: number } unless valid
|
|
35
|
+
memory << { component: "system", scope: "system", total_bytes: total, used_bytes: used, free_bytes: free,
|
|
36
|
+
available_bytes: nil, utilization: valid ? used.fdiv(total) * 100 : nil, basis: :used_including_cache, status: nil }
|
|
37
|
+
elsif (match = /\A(\d+)K buffers,\s*(\d+)K cache\z/i.match(line))
|
|
38
|
+
caches << match.captures.sum { |value| Integer(value, 10) * 1024 }
|
|
39
|
+
elsif (match = /\AKernel (buffers|cached)\s*:\s*(\d+)K Used\z/i.match(line))
|
|
40
|
+
key = match[1].downcase
|
|
41
|
+
memory_issues << { code: :duplicate_memory_cache, line: number } if kernel_cache.key?(key)
|
|
42
|
+
kernel_cache[key] = Integer(match[2], 10) * 1024
|
|
43
|
+
elsif (match = /\ACurrent memory status:\s*(.+)\z/i.match(line))
|
|
44
|
+
state = match[1].casecmp?("severe") ? :critical : component_status(match[1])
|
|
45
|
+
memory_issues << { code: :unknown_memory_status, line: number } if state == :unknown
|
|
46
|
+
statuses << state
|
|
47
|
+
elsif /\A(?:Load average:|Processes\s*:|Kernel vmalloc:\s*\d+K total,\s*\d+K free\z)/i.match?(line)
|
|
48
|
+
next
|
|
49
|
+
elsif /\ACPU/i.match?(line)
|
|
50
|
+
cpu_issues << unparsed(number)
|
|
51
|
+
elsif /\A(?:Memory|Kernel|Current memory|\d+K buffers)/i.match?(line)
|
|
52
|
+
memory_issues << unparsed(number)
|
|
53
|
+
else
|
|
54
|
+
cpu_issues << unparsed(number)
|
|
55
|
+
memory_issues << unparsed(number)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
memory_issues << { code: :incomplete_memory_cache } if kernel_cache.size == 1
|
|
59
|
+
duplicate_cache = memory_issues.any? { |issue| issue[:code] == :duplicate_memory_cache }
|
|
60
|
+
caches << kernel_cache.values.sum if kernel_cache.size == 2 && !duplicate_cache
|
|
61
|
+
memory_issues << { code: :duplicate_memory_cache } if caches.size > 1
|
|
62
|
+
memory_issues << { code: :duplicate_memory_status } if statuses.size > 1
|
|
63
|
+
if memory.empty? && statuses.any?
|
|
64
|
+
memory_issues << { code: :memory_fields_missing }
|
|
65
|
+
memory << { component: "system", scope: "system", total_bytes: nil, used_bytes: nil, free_bytes: nil,
|
|
66
|
+
available_bytes: nil, utilization: nil, basis: :unknown, status: nil }
|
|
67
|
+
end
|
|
68
|
+
memory.each do |record|
|
|
69
|
+
record[:status] = most_severe_status(statuses) if statuses.any?
|
|
70
|
+
next unless caches.one? && record[:utilization] && !duplicate_cache
|
|
71
|
+
|
|
72
|
+
if caches.first <= record[:used_bytes]
|
|
73
|
+
record[:basis] = :used_minus_cache
|
|
74
|
+
record[:used_bytes] -= caches.first
|
|
75
|
+
record[:available_bytes] = record[:total_bytes] - record[:used_bytes]
|
|
76
|
+
record[:utilization] = record[:used_bytes].fdiv(record[:total_bytes]) * 100
|
|
77
|
+
else
|
|
78
|
+
memory_issues << { code: :inconsistent_memory_cache }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
CtxParser::ResultSet.new(results: {
|
|
82
|
+
cpu: build_result(cpu, complete: complete, recognized: cpu.any?, issues: cpu_issues, identity: %i[component scope window_seconds]),
|
|
83
|
+
memory: build_result(memory, complete: complete, recognized: memory.any?, issues: memory_issues, identity: %i[component scope])
|
|
84
|
+
})
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/profile"
|
|
4
|
+
require_relative "parsers/show_interface"
|
|
5
|
+
require_relative "parsers/show_port_channel_summary"
|
|
6
|
+
require_relative "parsers/show_interface_transceiver_details"
|
|
7
|
+
require_relative "parsers/show_bgp_ipv4_unicast_summary"
|
|
8
|
+
require_relative "parsers/show_system_resources"
|
|
9
|
+
require_relative "parsers/show_environment"
|
|
10
|
+
require_relative "parsers/show_clock"
|
|
11
|
+
require_relative "parsers/show_ntp_peer_status"
|
|
12
|
+
require_relative "parsers/show_ip_traffic"
|
|
13
|
+
require_relative "parsers/show_ip_ospf_neighbors"
|
|
14
|
+
|
|
15
|
+
module CtxChecker
|
|
16
|
+
module CiscoNxos
|
|
17
|
+
# NX-OS 命令选择保留完整接口详情,并明确 IPv4 BGP 作用域。
|
|
18
|
+
class Profile < CtxParser::Profile
|
|
19
|
+
CATALOG = CtxParser::CommandCatalog.new([
|
|
20
|
+
{ dimensions: %i[interfaces counters], command: "show interface", parser: Parsers::ShowInterface },
|
|
21
|
+
{ dimensions: [ :aggregation ], command: "show port-channel summary", parser: Parsers::ShowPortChannelSummary },
|
|
22
|
+
{ dimensions: [ :optics ], command: "show interface transceiver details", parser: Parsers::ShowInterfaceTransceiverDetails },
|
|
23
|
+
{ dimensions: [ :bgp ], command: "show bgp ipv4 unicast summary", parser: Parsers::ShowBgpIpv4UnicastSummary },
|
|
24
|
+
{ dimensions: [ :ospf ], command: "show ip ospf neighbors", parser: Parsers::ShowIpOspfNeighbors },
|
|
25
|
+
{ dimensions: %i[cpu memory], command: "show system resources", parser: Parsers::ShowSystemResources },
|
|
26
|
+
{ dimensions: %i[temperature environment power], command: "show environment", parser: Parsers::ShowEnvironment },
|
|
27
|
+
{ dimensions: [ :clock ], command: "show clock", parser: Parsers::ShowClock },
|
|
28
|
+
{ dimensions: [ :ntp ], command: "show ntp peer-status", parser: Parsers::ShowNtpPeerStatus },
|
|
29
|
+
{ dimensions: [ :device_counters ], command: "show ip traffic", parser: Parsers::ShowIpTraffic }
|
|
30
|
+
])
|
|
31
|
+
COMMANDS = CATALOG.commands
|
|
32
|
+
PARSERS = CATALOG.parsers
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "ctx_driver"
|
|
5
|
+
require_relative "runtime_parser"
|
|
6
|
+
require_relative "snapshot"
|
|
7
|
+
require_relative "failure"
|
|
8
|
+
require_relative "applicability"
|
|
9
|
+
|
|
10
|
+
module CtxChecker
|
|
11
|
+
# 按声明目录顺序查询设备,并明确连接资源的归属。
|
|
12
|
+
class Collector
|
|
13
|
+
DEFAULT_DIMENSIONS = RuntimeParser::DIMENSIONS
|
|
14
|
+
|
|
15
|
+
# 注入为顺序命令观测生成时间戳的时钟。
|
|
16
|
+
def initialize(clock: -> { Time.now.utc })
|
|
17
|
+
@clock = clock
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 连接的建立与清理由传入连接器的块生命周期管理。
|
|
21
|
+
def collect(target:, connector:, dimensions: DEFAULT_DIMENSIONS, applicability: nil)
|
|
22
|
+
parser, identity = prepare(target, dimensions)
|
|
23
|
+
if connector.respond_to?(:platform) && ParserRegistry.canonical(connector.platform) != parser.platform
|
|
24
|
+
raise ArgumentError, "Connector platform differs from the target"
|
|
25
|
+
end
|
|
26
|
+
observations = inapplicable_observations(parser, dimensions, applicability)
|
|
27
|
+
dimensions = dimensions - observations.keys
|
|
28
|
+
return Snapshot.new(target: identity, observed_at: @clock.call, observations: observations) if dimensions.empty?
|
|
29
|
+
|
|
30
|
+
issues = []
|
|
31
|
+
stage = :connect
|
|
32
|
+
begin
|
|
33
|
+
connector.open do |session|
|
|
34
|
+
stage = :collect
|
|
35
|
+
collect_into(session, parser, dimensions, observations)
|
|
36
|
+
stage = :close
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError => error
|
|
39
|
+
raise unless CtxDriver::Failure.connection_error?(error, connector)
|
|
40
|
+
|
|
41
|
+
code = stage == :close ? :session_close_failed : CtxDriver::Failure.code(error, :connection_failed)
|
|
42
|
+
issues << { code: code, stage: stage }
|
|
43
|
+
mark_unavailable(parser, dimensions, observations, code: code, stage: stage, error: error)
|
|
44
|
+
end
|
|
45
|
+
reconcile_applicability(observations, applicability)
|
|
46
|
+
Snapshot.new(target: identity, observed_at: @clock.call, observations: observations, issues: issues)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 借用已打开的 net-connector 会话,不负责建立或关闭连接。
|
|
50
|
+
def collect_session(target:, session:, dimensions: DEFAULT_DIMENSIONS, applicability: nil)
|
|
51
|
+
parser, identity = prepare(target, dimensions)
|
|
52
|
+
observations = inapplicable_observations(parser, dimensions, applicability)
|
|
53
|
+
dimensions = dimensions - observations.keys
|
|
54
|
+
collect_into(session, parser, dimensions, observations) if dimensions.any?
|
|
55
|
+
reconcile_applicability(observations, applicability)
|
|
56
|
+
Snapshot.new(target: identity, observed_at: @clock.call, observations: observations)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# 配置证据可排除未启用的可选功能,但不改变未知运行证据的状态。
|
|
62
|
+
def inapplicable_observations(parser, dimensions, applicability)
|
|
63
|
+
return {} unless applicability
|
|
64
|
+
unless applicability.is_a?(Applicability) && applicability.platform == parser.platform
|
|
65
|
+
raise ArgumentError, "Applicability must belong to the target platform"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
dimensions.select { |name| applicability.not_applicable?(name) }.to_h do |name|
|
|
69
|
+
[ name, Observation.new(name: name, status: :not_applicable,
|
|
70
|
+
issues: [ { code: :feature_not_configured } ], evidence: applicability.evidence) ]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 已配置功能与运行回显禁用相互矛盾,两者不能静默覆盖。
|
|
75
|
+
def reconcile_applicability(observations, applicability)
|
|
76
|
+
return unless applicability
|
|
77
|
+
|
|
78
|
+
observations.each do |name, observation|
|
|
79
|
+
next unless observation.status == :not_applicable && applicability.state(name) == :configured
|
|
80
|
+
|
|
81
|
+
observations[name] = Observation.new(name: name, status: :partial,
|
|
82
|
+
issues: [ *observation.issues, { code: :configuration_runtime_conflict } ],
|
|
83
|
+
evidence: observation.evidence + applicability.evidence)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 发送任何设备命令前,先校验维度选择与目标身份。
|
|
88
|
+
def prepare(target, dimensions)
|
|
89
|
+
RuntimeParser.validate_dimensions!(dimensions)
|
|
90
|
+
raise ArgumentError, "Target must be a hash" unless target.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
parser = RuntimeParser.new(platform: target.fetch(:platform), model: target[:model], software_version: target[:software_version])
|
|
93
|
+
parser.discovery_dimensions(dimensions)
|
|
94
|
+
# 打开连接器前校验身份,避免将输入错误误报为连接故障。
|
|
95
|
+
snapshot = Snapshot.new(target: target.merge(platform: parser.platform), observed_at: @clock.call, observations: {})
|
|
96
|
+
[ parser, snapshot.target ]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 采集发现命令及其依赖命令,保留各自独立的失败结果。
|
|
100
|
+
def collect_into(session, parser, dimensions, observations)
|
|
101
|
+
reader = CtxDriver::Reader.new(session: session, clock: @clock)
|
|
102
|
+
fragments = Hash.new { |hash, key| hash[key] = [] }
|
|
103
|
+
dependencies = parser.discovery_dimensions(dimensions)
|
|
104
|
+
dependency_commands = dependencies.empty? ? [] : parser.queries(dependencies).keys
|
|
105
|
+
discovery_queries = parser.queries((dimensions + dependencies).uniq).select { |command, _names| dependency_commands.include?(command) }
|
|
106
|
+
collect_queries(reader, parser, discovery_queries, fragments)
|
|
107
|
+
discoveries = dependencies.to_h { |name| [ name, parser.combine(name, fragments.fetch(name)) ] }
|
|
108
|
+
begin
|
|
109
|
+
parser = parser.expand(discoveries, dimensions: dimensions)
|
|
110
|
+
rescue StandardError => error
|
|
111
|
+
expansion_error = error
|
|
112
|
+
end
|
|
113
|
+
queries = parser.queries(dimensions).reject { |command, _names| discovery_queries.key?(command) }
|
|
114
|
+
collect_queries(reader, parser, queries, fragments)
|
|
115
|
+
dimensions.each do |name|
|
|
116
|
+
observations[name] = if parser.profile.commands.key?(name)
|
|
117
|
+
parser.combine(name, fragments.fetch(name))
|
|
118
|
+
else
|
|
119
|
+
parser.unavailable_observation(name, discoveries: discoveries, error: expansion_error)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 发现阶段与扩展采集阶段共用单条顺序命令边界。
|
|
125
|
+
def collect_queries(reader, parser, queries, fragments)
|
|
126
|
+
queries.each do |command, names|
|
|
127
|
+
read(reader, parser, command, names).each { |name, observation| fragments[name] << observation }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 每条命令独立承担失败;单个脚本失败不能丢弃此前事实。
|
|
132
|
+
def read(reader, parser, command, names)
|
|
133
|
+
capture = reader.read(command)
|
|
134
|
+
evidence = capture.evidence
|
|
135
|
+
unless capture.success?
|
|
136
|
+
return names.to_h do |name|
|
|
137
|
+
[ name, Failure.observation(name, **capture.failure, command: command, evidence: evidence, skipped: capture.skipped?) ]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
begin
|
|
142
|
+
parser.parse_many(names, capture.body, complete: true, command: command).transform_values do |observation|
|
|
143
|
+
records = if %i[counters device_counters].include?(observation.name)
|
|
144
|
+
observation.records.map { |record| record.merge(sampled_at: evidence.first[:observed_at]) }
|
|
145
|
+
else
|
|
146
|
+
observation.records
|
|
147
|
+
end
|
|
148
|
+
source_status = observation.evidence.first&.fetch(:status, nil) || observation.status
|
|
149
|
+
Observation.new(name: observation.name, status: observation.status, records: records,
|
|
150
|
+
covered_scopes: observation.covered_scopes,
|
|
151
|
+
issues: observation.issues, evidence: evidence.map { |entry| entry.merge(status: source_status) })
|
|
152
|
+
end
|
|
153
|
+
rescue StandardError => error
|
|
154
|
+
failure = CtxDriver::Failure.details(error, fallback: :command_processing_failed, stage: :parse)
|
|
155
|
+
names.to_h { |name| [ name, Failure.observation(name, **failure, command: command, evidence: evidence) ] }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 连接建立失败时仍标明各条计划查询,不覆盖已完成事实。
|
|
160
|
+
def mark_unavailable(parser, dimensions, observations, **failure)
|
|
161
|
+
dimensions.each do |name|
|
|
162
|
+
next if observations.key?(name)
|
|
163
|
+
|
|
164
|
+
observations[name] = if parser.profile.commands.key?(name)
|
|
165
|
+
sources = parser.profile.commands_for(name).map do |command|
|
|
166
|
+
Failure.observation(name, **failure, command: command, skipped: true)
|
|
167
|
+
end
|
|
168
|
+
parser.combine(name, sources)
|
|
169
|
+
else
|
|
170
|
+
parser.unavailable_observation(name, error: failure[:error], code: failure[:code], stage: failure[:stage])
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "policy"
|
|
4
|
+
require_relative "report"
|
|
5
|
+
require_relative "checks/interfaces"
|
|
6
|
+
require_relative "checks/counters"
|
|
7
|
+
require_relative "checks/aggregation"
|
|
8
|
+
require_relative "checks/optics"
|
|
9
|
+
require_relative "checks/routing"
|
|
10
|
+
require_relative "checks/resources"
|
|
11
|
+
require_relative "checks/temperature"
|
|
12
|
+
require_relative "checks/hardware"
|
|
13
|
+
require_relative "checks/clock"
|
|
14
|
+
require_relative "checks/ntp"
|
|
15
|
+
require_relative "checks/alarms"
|
|
16
|
+
require_relative "checks/device_counters"
|
|
17
|
+
|
|
18
|
+
module CtxChecker
|
|
19
|
+
# 基于不可变当前采样及可选的前次采样,组合厂商无关的检查。
|
|
20
|
+
class Evaluator
|
|
21
|
+
CHECKS = { interfaces: Checks::Interfaces, counters: Checks::Counters, aggregation: Checks::Aggregation,
|
|
22
|
+
optics: Checks::Optics, bgp: Checks::Routing, ospf: Checks::Routing,
|
|
23
|
+
cpu: Checks::Resources, memory: Checks::Resources, temperature: Checks::Temperature,
|
|
24
|
+
environment: Checks::Hardware, power: Checks::Hardware, clock: Checks::Clock,
|
|
25
|
+
ntp: Checks::Ntp, alarms: Checks::Alarms, device_counters: Checks::DeviceCounters }.freeze
|
|
26
|
+
|
|
27
|
+
# 绑定归一化证据评估使用的策略。
|
|
28
|
+
def initialize(policy: Policy.new)
|
|
29
|
+
raise ArgumentError, "Evaluator requires a Policy" unless policy.is_a?(Policy)
|
|
30
|
+
@policy = policy
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 评估所有适用检查,分别返回健康结果与证据覆盖率。
|
|
35
|
+
def call(snapshot, previous: nil)
|
|
36
|
+
raise ArgumentError, "Evaluator requires a Snapshot" unless snapshot.is_a?(Snapshot)
|
|
37
|
+
if previous && (!snapshot.same_target?(previous) || previous.observed_at >= snapshot.observed_at)
|
|
38
|
+
raise ArgumentError, "Previous sample must belong to the same target and precede the current sample"
|
|
39
|
+
end
|
|
40
|
+
findings = CHECKS.flat_map do |name, checker|
|
|
41
|
+
checker.new(snapshot: snapshot, policy: @policy, previous: previous).call(snapshot.observation(name))
|
|
42
|
+
end
|
|
43
|
+
snapshot.issues.each do |issue|
|
|
44
|
+
findings << Finding.new(id: "collection.#{issue[:code]}", dimension: :collection, status: :unknown, reason: issue[:code].to_sym)
|
|
45
|
+
end
|
|
46
|
+
Report.new(snapshot: snapshot, findings: findings)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "observation"
|
|
4
|
+
|
|
5
|
+
module CtxChecker
|
|
6
|
+
# 运行失败仅展示阶段与安全异常类型,不包含异常消息或原始回显。
|
|
7
|
+
module Failure
|
|
8
|
+
# 构建单个失败维度,保留安全错误元数据及可追溯的命令证据。
|
|
9
|
+
def self.observation(name, code:, stage:, command:, error: nil, error_type: nil, evidence: [], skipped: false)
|
|
10
|
+
issue = { code: code, stage: stage, command: command, skipped: skipped }
|
|
11
|
+
issue[:error_type] = error ? self.error_type(error) : error_type if error || error_type
|
|
12
|
+
entries = evidence.empty? ? [ {} ] : evidence
|
|
13
|
+
Observation.new(name: name, status: :failed, issues: [ issue ],
|
|
14
|
+
evidence: entries.map { |entry| entry.merge(command: command, status: :failed) })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 匿名异常仍使用安全类别,不输出对象检查内容。
|
|
18
|
+
def self.error_type(error)
|
|
19
|
+
name = error.class.name
|
|
20
|
+
name.is_a?(String) && Observation::ERROR_TYPE_PATTERN.match?(name) ? name : "StandardError"
|
|
21
|
+
end
|
|
22
|
+
private_class_method :error_type
|
|
23
|
+
end
|
|
24
|
+
end
|