ctx_checker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/COUNTER_CONTRACT.md +33 -0
  4. data/LICENSE +22 -0
  5. data/README-zh.md +93 -0
  6. data/README.md +93 -0
  7. data/docs/architecture-zh.md +487 -0
  8. data/docs/release.md +13 -0
  9. data/examples/offline.rb +17 -0
  10. data/lib/ctx_checker/applicability.rb +187 -0
  11. data/lib/ctx_checker/checks/aggregation.rb +73 -0
  12. data/lib/ctx_checker/checks/alarms.rb +25 -0
  13. data/lib/ctx_checker/checks/base.rb +66 -0
  14. data/lib/ctx_checker/checks/clock.rb +30 -0
  15. data/lib/ctx_checker/checks/counters.rb +77 -0
  16. data/lib/ctx_checker/checks/device_counters.rb +32 -0
  17. data/lib/ctx_checker/checks/hardware.rb +45 -0
  18. data/lib/ctx_checker/checks/interfaces.rb +35 -0
  19. data/lib/ctx_checker/checks/ntp.rb +47 -0
  20. data/lib/ctx_checker/checks/optics.rb +107 -0
  21. data/lib/ctx_checker/checks/resources.rb +63 -0
  22. data/lib/ctx_checker/checks/routing.rb +86 -0
  23. data/lib/ctx_checker/checks/temperature.rb +53 -0
  24. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +56 -0
  25. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +153 -0
  26. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +110 -0
  27. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +41 -0
  28. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +140 -0
  29. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +107 -0
  30. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +164 -0
  31. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +96 -0
  32. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +65 -0
  33. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +36 -0
  34. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +44 -0
  35. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +45 -0
  36. data/lib/ctx_checker/cisco_ios/parsers/system_values.rb +62 -0
  37. data/lib/ctx_checker/cisco_ios/profile.rb +39 -0
  38. data/lib/ctx_checker/cisco_nxos/parsers/show_bgp_ipv4_unicast_summary.rb +69 -0
  39. data/lib/ctx_checker/cisco_nxos/parsers/show_clock.rb +13 -0
  40. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +157 -0
  41. data/lib/ctx_checker/cisco_nxos/parsers/show_interface.rb +78 -0
  42. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +141 -0
  43. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +14 -0
  44. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +151 -0
  45. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +116 -0
  46. data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +21 -0
  47. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +89 -0
  48. data/lib/ctx_checker/cisco_nxos/profile.rb +35 -0
  49. data/lib/ctx_checker/collector.rb +175 -0
  50. data/lib/ctx_checker/evaluator.rb +49 -0
  51. data/lib/ctx_checker/failure.rb +24 -0
  52. data/lib/ctx_checker/finding.rb +19 -0
  53. data/lib/ctx_checker/h3c/parsers/counters_table.rb +56 -0
  54. data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +43 -0
  55. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +44 -0
  56. data/lib/ctx_checker/h3c/parsers/display_clock.rb +72 -0
  57. data/lib/ctx_checker/h3c/parsers/display_counters_inbound_interface.rb +21 -0
  58. data/lib/ctx_checker/h3c/parsers/display_counters_outbound_interface.rb +21 -0
  59. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +64 -0
  60. data/lib/ctx_checker/h3c/parsers/display_environment.rb +74 -0
  61. data/lib/ctx_checker/h3c/parsers/display_fan.rb +72 -0
  62. data/lib/ctx_checker/h3c/parsers/display_interface.rb +127 -0
  63. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +72 -0
  64. data/lib/ctx_checker/h3c/parsers/display_memory.rb +132 -0
  65. data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +84 -0
  66. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +47 -0
  67. data/lib/ctx_checker/h3c/parsers/display_power.rb +63 -0
  68. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +109 -0
  69. data/lib/ctx_checker/h3c/parsers/system_values.rb +46 -0
  70. data/lib/ctx_checker/h3c/profile.rb +49 -0
  71. data/lib/ctx_checker/hillstone/parsers/show_clock.rb +68 -0
  72. data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +44 -0
  73. data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +114 -0
  74. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +90 -0
  75. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +82 -0
  76. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +53 -0
  77. data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +22 -0
  78. data/lib/ctx_checker/hillstone/parsers/show_ip_ospf_neighbor.rb +21 -0
  79. data/lib/ctx_checker/hillstone/parsers/show_memory.rb +85 -0
  80. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +110 -0
  81. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +101 -0
  82. data/lib/ctx_checker/hillstone/profile.rb +40 -0
  83. data/lib/ctx_checker/observation.rb +165 -0
  84. data/lib/ctx_checker/observation_merger.rb +80 -0
  85. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +50 -0
  86. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +47 -0
  87. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +68 -0
  88. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +70 -0
  89. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +52 -0
  90. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +78 -0
  91. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +66 -0
  92. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +54 -0
  93. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +142 -0
  94. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +102 -0
  95. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +54 -0
  96. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +44 -0
  97. data/lib/ctx_checker/palo_alto/profile.rb +39 -0
  98. data/lib/ctx_checker/parser_registry.rb +43 -0
  99. data/lib/ctx_checker/parsers/base.rb +68 -0
  100. data/lib/ctx_checker/parsers/interface_names.rb +65 -0
  101. data/lib/ctx_checker/parsers/scoped_result.rb +35 -0
  102. data/lib/ctx_checker/policy.rb +104 -0
  103. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +48 -0
  104. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +46 -0
  105. data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +22 -0
  106. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +71 -0
  107. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +22 -0
  108. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +44 -0
  109. data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +47 -0
  110. data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +48 -0
  111. data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +34 -0
  112. data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +53 -0
  113. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +68 -0
  114. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +39 -0
  115. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +58 -0
  116. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +67 -0
  117. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +74 -0
  118. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +99 -0
  119. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +86 -0
  120. data/lib/ctx_checker/radware_alteon/port_counter_parser.rb +34 -0
  121. data/lib/ctx_checker/radware_alteon/profile.rb +145 -0
  122. data/lib/ctx_checker/radware_alteon.rb +8 -0
  123. data/lib/ctx_checker/report.rb +112 -0
  124. data/lib/ctx_checker/runtime_parser.rb +234 -0
  125. data/lib/ctx_checker/snapshot.rb +44 -0
  126. data/lib/ctx_checker/system_policy.rb +70 -0
  127. data/lib/ctx_checker/system_record.rb +81 -0
  128. data/lib/ctx_checker/version.rb +6 -0
  129. data/lib/ctx_checker.rb +17 -0
  130. metadata +271 -0
@@ -0,0 +1,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