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,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+ require "ctx_parser/result_set"
6
+
7
+ module CtxChecker
8
+ module CiscoIos
9
+ module Parsers
10
+ # 同一接口详细回显分别提供状态与计数器观测。
11
+ class ShowInterfaces < CtxChecker::Parsers::Base
12
+ PLATFORM = :cisco_ios
13
+ HEADER = /\A(\S+) is (administratively down|up|down|reset|deleted)(?:, line protocol is (up|down)(?: \([^)]*\))?)?(?:\s*\([^)]*\))?(?:\s*,\s*Autostate (?:Enabled|Disabled))?\s*\z/i
14
+ COUNTERS = { in_errors: /(?<!\S)(\d+) input errors?\b/i, out_errors: /(?<!\S)(\d+) output errors?\b/i,
15
+ crc_errors: /(?<!\S)(\d+) CRC(?:\/FCS)?\b/i, in_discards: /(?<!\S)(\d+) input discards?\b/i,
16
+ out_discards: /(?<!\S)(\d+) output discards?\b/i }.freeze
17
+ COUNTER_LABELS = { in_errors: /\binput errors?\b/i, out_errors: /\boutput errors?\b/i,
18
+ crc_errors: /\bCRC(?:\/FCS)?\b/i, in_discards: /\binput discards?\b/i, out_discards: /\boutput discards?\b/i }.freeze
19
+ AUXILIARY = /\A(?:Hardware[: ]|Description:|Internet address|MTU |reliability |Encapsulation |Keepalive |Full-duplex|Half-duplex|full-duplex|half-duplex|Auto-duplex|auto-duplex|Input flow-control|Output flow-control|ARP type:|Last input |Last clearing |Input queue:|Output queue:|Queueing strategy:|[\d ]+(?:second|minute) (?:input|output) rate|[\d ]+(?:packets input|packets output|bytes|no buffer|broadcasts|multicasts|runts|giants|watchdog|output buffer|lost carrier|unknown protocol drops|interface resets|carrier transitions|collisions|babbles|underruns)|Received |Output feature|L2 Switched:|L3 in Switched:|L3 out Switched:|Port mode is |Beacon is |Auto-Negotiation is |FEC mode is |Flowcontrol|Switchport monitor|EtherType is |Members in this channel:|Last link flapped |Last link change |Last input change |[\d ]+interface resets|Load-Interval |Input rate |Output rate |RX\z|TX\z|[\d ]+(?:unicast packets|input packets|output packets|jumbo packets|Rx pause|Tx pause|storm suppression)|Tunnel |tunnel |Transport |Destination |Source |DTR is |[\d ]+packets,|BW |Mode is |IP address is |Port-channel is |admin state is |[\d ]+bad etype drop|Last clearing of)/i
20
+
21
+ private
22
+
23
+ # 分别跟踪覆盖率;有效状态头不能证明计数器已采集。
24
+ def parse_output(text, complete:)
25
+ text = clean(text)
26
+ interfaces, counters, state_issues, counter_issues = [], [], [], []
27
+ current = nil
28
+ text.each_line.with_index(1) do |source, number|
29
+ line = source.strip
30
+ next if line.empty?
31
+
32
+ if (match = interface_header(line))
33
+ current = interface_record(match)
34
+ interfaces << current
35
+ elsif (match = /\A(\S+) is /.match(line)) && CtxChecker::Parsers::InterfaceNames.kind(normalize_name(match[1])) != :unknown
36
+ current = nil
37
+ state_issues << { code: :unparsed_interface_header, line: number }
38
+ elsif current && (match = /\Aadmin state is (up|down)\b/i.match(line))
39
+ current[:admin_status] = match[1].downcase.to_sym
40
+ elsif current && (match = /\ALine protocol is (up|down)\b/i.match(line))
41
+ current[:protocol_status] = match[1].downcase.to_sym
42
+ elsif current && (match = /\ALast clearing of "show interfaces?" counters\s+(.+)\z/i.match(line))
43
+ # 相对清零时长不能证明两个采样间的计数器连续性。
44
+ current[:discontinuity] = nil
45
+ elsif current && /\ADescription:/i.match?(line)
46
+ next
47
+ elsif current && counter_line?(line)
48
+ found = counter_patterns.flat_map do |metric, pattern|
49
+ source = counter_source(line, metric)
50
+ values = source.scan(pattern).flatten
51
+ if values.size != source.scan(counter_labels.fetch(metric)).size
52
+ counter_issues << { code: :invalid_counter_value, metric: metric, line: number }
53
+ end
54
+ values.map { |value| { interface: current[:name], metric: metric, value: Integer(value, 10), discontinuity: nil } }
55
+ end
56
+ output_drops = line.scan(/\bTotal output drops:\s*(\d+)(?=\s*(?:;|\z))/i).flatten
57
+ output_drops.each do |value|
58
+ # IOS-XE 此处可能显示字节数或包数,取决于平台配置。
59
+ found << { interface: current[:name], metric: :out_discards, value: Integer(value, 10), discontinuity: nil,
60
+ unit: :unknown, scope: :queue }
61
+ end
62
+ if output_drops.size != line.scan(/\bTotal output drops:/i).size
63
+ counter_issues << { code: :invalid_counter_value, metric: :out_discards, line: number }
64
+ end
65
+ if (match = /\AInput queue:\s*\d+\/\d+\/(\d+)(?:\/\d+)?\s+\(size\/max\/drops(?:\/flushes)?\)/i.match(line))
66
+ found << { interface: current[:name], metric: :in_discards, value: Integer(match[1], 10), discontinuity: nil,
67
+ unit: :packets, scope: :queue }
68
+ elsif line.match?(/\AInput queue:.*drops/i)
69
+ counter_issues << { code: :invalid_counter_value, metric: :in_discards, line: number }
70
+ end
71
+ counters.concat(found)
72
+ counter_issues << { code: :unparsed_counter, line: number } if found.empty?
73
+ elsif current && interface_detail(current, line)
74
+ next
75
+ elsif current && auxiliary?(line)
76
+ next
77
+ else
78
+ state_issues << { code: :unparsed_row, line: number }
79
+ end
80
+ end
81
+ discontinuities = interfaces.to_h { |record| [ record[:name], record.delete(:discontinuity) ] }
82
+ counters.each { |record| record[:discontinuity] = discontinuities[record[:interface]] }
83
+ counters_by_interface = counters.group_by { |counter| counter[:interface] }
84
+ interfaces.select { |record| record[:kind] == :physical }.each do |record|
85
+ counters.concat(complete_counter_coverage(record, counters_by_interface.fetch(record[:name], []), counter_issues))
86
+ end
87
+ CtxParser::ResultSet.new(results: {
88
+ interfaces: build_result(interfaces, complete: complete, recognized: interfaces.any?, issues: state_issues, identity: [ :name ]),
89
+ counters: build_result(counters, complete: complete, recognized: counters.any?, issues: state_issues + counter_issues,
90
+ identity: %i[interface metric])
91
+ })
92
+ end
93
+
94
+ # 按计数器标签匹配,不误取总包数,并保留格式错误的指标行。
95
+ def counter_line?(line)
96
+ line.match?(/\b(?:input errors?|output errors?|CRC(?:\/FCS)?|input discards?|output discards?|Total output drops)\b/i) ||
97
+ line.match?(/\AInput queue:.*drops/i)
98
+ end
99
+
100
+ # 将接口别名归一化为平台标准身份。
101
+ def normalize_name(name) = CtxChecker::Parsers::InterfaceNames.normalize(name, platform: self.class::PLATFORM)
102
+
103
+ # 平台语法覆盖独立于公共计数器提取逻辑。
104
+ def interface_header(line) = HEADER.match(line)
105
+ # 将平台特有的详细行留给子类扩展点处理。
106
+ def interface_detail(_record, _line) = false
107
+ # 识别说明性回显,不将其作为数据记录。
108
+ def auxiliary?(line)
109
+ AUXILIARY.match?(line) || /\AOutput \d+ broadcasts \(\d+ (?:IP )?multicasts\)\z/i.match?(line) ||
110
+ /\AUnknown, Unknown, link type is \S+, media type is \S+\z/i.match?(line) || /\AFec is \S+\z/i.match?(line)
111
+ end
112
+ # 声明当前平台支持的接口错误与丢弃计数模式。
113
+ def counter_patterns = COUNTERS
114
+ # 将平台计数器标签映射到归一化指标名称。
115
+ def counter_labels = COUNTER_LABELS
116
+ # 标记这些计数测量来自哪个命令视图。
117
+ def counter_source(line, _metric) = line
118
+
119
+ # 物理端口健康需要明确的错误包与 CRC 测量证据。
120
+ def complete_counter_coverage(record, counters, issues)
121
+ missing = %i[in_errors out_errors crc_errors] - counters.map { |counter| counter[:metric] }
122
+ issues << { code: :counter_fields_missing, interface: record[:name], metrics: missing } if missing.any?
123
+ []
124
+ end
125
+
126
+ # 复用接口头字段及子接口身份,不虚构协议状态。
127
+ def interface_record(match)
128
+ name = normalize_name(match[1])
129
+ record = { name: name, kind: CtxChecker::Parsers::InterfaceNames.kind(name),
130
+ admin_status: match[2].casecmp?("administratively down") ? :down : :unknown,
131
+ oper_status: match[2].casecmp?("up") ? :up : :down,
132
+ protocol_status: match[3]&.downcase&.to_sym || :unknown, parent: nil }
133
+ record[:admin_status] = :up if self.class::PLATFORM == :cisco_ios && record[:admin_status] != :down
134
+ record[:parent] = name.split(".", 2).first if record[:kind] == :subinterface
135
+ record
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+
6
+ module CtxChecker
7
+ module CiscoIos
8
+ module Parsers
9
+ # IOS DOM 按指标分表展示端口,其阈值顺序不同于 NX-OS。
10
+ class ShowInterfacesTransceiverDetail < CtxChecker::Parsers::Base
11
+ NUMBER = /(?:[+-]?\d+(?:\.\d+)?|N\/A|NA|--)/i
12
+ MARKERS = { "++" => :high_alarm, "+" => :high_warning, "--" => :low_alarm, "-" => :low_warning }.freeze
13
+
14
+ private
15
+
16
+ # 解析 show interfaces transceiver detail,保留不完整或不支持的证据。
17
+ def parse_output(text, complete:)
18
+ text = clean(text)
19
+ rows, issues, ports = [], [], []
20
+ metric, header, lane_column = nil, false, false
21
+ text.each_line.with_index(1) do |source, number|
22
+ line = source.strip
23
+ next if line.empty? || line.match?(/\A[-= ]+\z/)
24
+
25
+ if line.match?(/\bTransmit Power\b/i)
26
+ metric, header = :tx_power, false
27
+ elsif line.match?(/\bReceive Power\b/i)
28
+ metric, header = :rx_power, false
29
+ elsif line.match?(/\A(?:Temperature|Voltage|Current)\s+Threshold/i)
30
+ metric, header = :other, false
31
+ elsif line.match?(/\APort\s+(?:Lane\s+)?\(/i)
32
+ header = true
33
+ lane_column = line.match?(/\APort\s+Lane\s/i)
34
+ elsif interface_row?(line)
35
+ name = CtxChecker::Parsers::InterfaceNames.normalize(line.split.first, platform: :cisco_ios)
36
+ ports << name
37
+ if metric && header
38
+ next if metric == :other
39
+
40
+ row = measurement(line, metric, lane_column)
41
+ row ? rows << row : issues << { code: :unparsed_optical_measurement, line: number }
42
+ else
43
+ issues << { code: :missing_optical_header, line: number }
44
+ end
45
+ elsif auxiliary?(line)
46
+ next
47
+ else
48
+ issues << { code: :unparsed_row, line: number }
49
+ end
50
+ end
51
+ identities = rows.map { |row| row.values_at(:interface, :lane) }
52
+ identities.concat((ports.uniq - rows.map { |row| row[:interface] }).map { |name| [ name, nil ] })
53
+ identities.uniq.each do |name, lane|
54
+ %i[tx_power rx_power].each do |expected|
55
+ next if rows.any? { |row| row.values_at(:interface, :lane, :metric) == [ name, lane, expected ] }
56
+
57
+ issues << { code: :optical_measurement_missing, interface: name, lane: lane, metric: expected }
58
+ end
59
+ end
60
+ build_result(rows, complete: complete, recognized: rows.any?, issues: issues, identity: %i[interface lane metric])
61
+ end
62
+
63
+ # 解析支持的传感器测量值及回显单位。
64
+ def measurement(line, metric, lane_column)
65
+ # 括号内 A2D 读数属于独立校准证据,不作为阈值。
66
+ normalized = line.gsub(/\(\s*#{NUMBER}\s*\)/i, "")
67
+ columns = normalized.split
68
+ name = CtxChecker::Parsers::InterfaceNames.normalize(columns.shift, platform: :cisco_ios)
69
+ lane = lane_column ? columns.shift : nil
70
+ lane = nil if lane && /\AN\/?A\z/i.match?(lane)
71
+ return if lane && !lane.match?(/\A\d+\z/)
72
+
73
+ current = columns.shift
74
+ marker = columns.shift if MARKERS.key?(columns.first)
75
+ return unless current&.match?(/\A#{NUMBER}\z/) && columns.size == 4 && columns.all? { |value| value.match?(/\A#{NUMBER}\z/) }
76
+
77
+ high_alarm, high_warning, low_warning, low_alarm = columns.map { |value| decimal(value) }
78
+ { interface: name, lane: lane && Integer(lane, 10), metric: metric, value: decimal(current), unit: :dbm,
79
+ low_alarm: low_alarm, low_warning: low_warning, high_warning: high_warning, high_alarm: high_alarm,
80
+ alarm: marker ? MARKERS.fetch(marker) : :normal, present: true, dom_supported: true }
81
+ end
82
+
83
+ # 区分接口数据行与表格标题。
84
+ def interface_row?(line)
85
+ name = CtxChecker::Parsers::InterfaceNames.normalize(line.split.first, platform: :cisco_ios)
86
+ CtxChecker::Parsers::InterfaceNames.kind(name) == :physical
87
+ end
88
+
89
+ # 解析有限小数,不将占位符转换为零。
90
+ def decimal(value)
91
+ return unless value.match?(/\A[+-]?\d+(?:\.\d+)?\z/)
92
+
93
+ parsed = Float(value)
94
+ return parsed if parsed.finite?
95
+
96
+ @issues << { code: :invalid_optical_value }
97
+ nil
98
+ end
99
+
100
+ # 识别说明性回显,不将其作为数据记录。
101
+ def auxiliary?(line)
102
+ line.match?(/\A(?:mA: |\+\+\s*:|A2D readouts |The threshold values |(?:Optical\s+)?High Alarm\s+High Warn\s+Low Warn\s+Low Alarm\z|Optical\z|ITU Channel |Transceiver is |Transceiver monitoring is |Wavelength |(?:Temperature|Voltage|Current)\s+Threshold)/i)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require "ctx_parser/values"
5
+
6
+ module CtxChecker
7
+ module CiscoIos
8
+ module Parsers
9
+ # 仅以 BGP 汇总表声明的最后一列作为权威会话状态。
10
+ class ShowIpBgpSummary < CtxChecker::Parsers::Base
11
+ ADDRESS_FAMILY = "ipv4_unicast".freeze
12
+ HEADER = /\ANeighbor\s+V\s+AS\s+MsgRcvd\s+MsgSent\s+TblVer\s+InQ\s+OutQ\s+Up\/Down\s+State\/PfxRcd\z/i
13
+ STATES = { "idle" => :idle, "active" => :active, "connect" => :connect, "opensent" => :opensent,
14
+ "openconfirm" => :openconfirm, "established" => :established, "shutdown" => :shutdown,
15
+ "idle (admin)" => :shutdown }.freeze
16
+
17
+ private
18
+
19
+ # 使用作用域与行身份,避免不同地址族或 VRF 相互覆盖。
20
+ def parse_output(text, complete:)
21
+ text = clean(text).gsub(/State\/\s*\n\s*PfxRcd/, "State/PfxRcd")
22
+ rows, issues = [], []
23
+ scope = { vrf: "default", afi_safi: self.class::ADDRESS_FAMILY }
24
+ sections = []
25
+ section = { header: false, empty: false, count: 0, scope_fields: [], scope: scope }
26
+ sections << section
27
+ pending = nil
28
+ text.each_line.with_index(1) do |source, number|
29
+ line = source.strip
30
+ next if line.empty? || line.match?(/\A[-= ]+\z/)
31
+
32
+ if HEADER.match?(line)
33
+ section[:header] = true
34
+ elsif (change = parse_scope(line, scope, issues, number))
35
+ issues << { code: :truncated_peer } if pending
36
+ pending = nil
37
+ next_scope, declared_fields = change
38
+ if section[:header] || section[:empty] || section[:count].positive? || (section[:scope_fields] & declared_fields).any?
39
+ section = { header: false, empty: false, count: 0, scope_fields: [] }
40
+ sections << section
41
+ end
42
+ section[:scope_fields] |= declared_fields
43
+ scope = next_scope
44
+ section[:scope] = scope
45
+ elsif line.match?(/\A(?:No (?:BGP )?neighbors(?: found)?\.?|0 (?:BGP )?neighbors)\z/i)
46
+ section[:empty] = true
47
+ elsif parse_statistics(line, section, scope, issues, number)
48
+ next
49
+ elsif CtxParser::Values.valid_host?(line)
50
+ issues << { code: :unparsed_row, line: number - 1 } if pending
51
+ pending = line
52
+ elsif peer_row?(line) || (pending && line.match?(/\A4\s/))
53
+ line = "#{pending} #{line}" if pending
54
+ pending = nil
55
+ row = parse_peer(line, scope)
56
+ if row
57
+ rows << row
58
+ section[:count] += 1
59
+ issues << { code: :missing_neighbor_header, line: number } unless section[:header]
60
+ else
61
+ issues << { code: :unparsed_peer, line: number }
62
+ end
63
+ elsif auxiliary?(line)
64
+ next
65
+ else
66
+ issues << { code: :unparsed_row, line: number }
67
+ end
68
+ end
69
+ issues << { code: :truncated_peer } if pending
70
+ sections.each { |entry| validate_section(entry, issues) }
71
+ recognized = sections.any? { |entry| section_empty?(entry) || entry[:header] && entry[:count].positive? }
72
+ scopes = sections.filter_map do |entry|
73
+ entry[:scope] if section_empty?(entry) || entry[:header] && entry[:count].positive?
74
+ end
75
+ build_result(rows, complete: complete, recognized: recognized, issues: issues,
76
+ identity: %i[vrf afi_safi peer], covered_scopes: scopes)
77
+ end
78
+
79
+ # 识别协议对等体记录,排除摘要和表头。
80
+ def peer_row?(line) = CtxParser::Values.valid_host?(line.split.first.to_s)
81
+
82
+ # 解析单个路由作用域,不合并无关的 VRF 证据。
83
+ def parse_scope(line, scope, issues, number)
84
+ if (match = /\ABGP summary information for VRF (\S+), address family (.+)\z/i.match(line))
85
+ scope = scope.merge(vrf: match[1])
86
+ family = match[2]
87
+ declared_fields = %i[vrf afi_safi]
88
+ elsif (match = /\A(?:For address family:|Address family:)\s*(.+)\z/i.match(line))
89
+ family = match[1]
90
+ declared_fields = [ :afi_safi ]
91
+ elsif (match = /\A(?:BGP VRF|VRF:)\s+(\S+?)(?:,.*)?\z/i.match(line))
92
+ return [ scope.merge(vrf: match[1]), [ :vrf ] ]
93
+ elsif line.match?(/\A(?:BGP summary information\b|BGP VRF\b|VRF:|For address family:|Address family:)/i)
94
+ issues << { code: :invalid_protocol_scope, line: number }
95
+ return [ { vrf: nil, afi_safi: nil }, %i[vrf afi_safi] ]
96
+ else
97
+ return
98
+ end
99
+ normalized = address_family(family)
100
+ issues << { code: :unknown_address_family, line: number } unless normalized
101
+ [ scope.merge(afi_safi: normalized), declared_fields ]
102
+ end
103
+
104
+ # 允许平台扩展读取统计信息,但不放宽对等体行校验。
105
+ def parse_statistics(_line, _section, _scope, _issues, _number) = false
106
+
107
+ # 未采集到邻居记录时,仅明确空表声明足以证明为空。
108
+ def section_empty?(section) = section[:empty]
109
+
110
+ # 每张表都必须独立核对记录行与空表声明。
111
+ def validate_section(section, issues)
112
+ issues << { code: :contradictory_empty } if section_empty?(section) && section[:count].positive?
113
+ if (section[:header] || section[:scope_fields].any?) && section[:count].zero? && !section_empty?(section)
114
+ issues << { code: :neighbor_count_unavailable }
115
+ end
116
+ end
117
+
118
+ # 仅归一化支持的地址族;未知作用域保留为 nil。
119
+ def address_family(value)
120
+ { "ipv4 unicast" => "ipv4_unicast", "ipv6 unicast" => "ipv6_unicast",
121
+ "l2vpn evpn" => "l2vpn_evpn" }[value.downcase.split.join(" ")]
122
+ end
123
+
124
+ # 归一化单个路由对等体及其协议状态。
125
+ def parse_peer(line, scope)
126
+ match = /\A(\S+)\s+4\s+(\d+(?:\.\d+)?)\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\S+\s+(.+)\z/.match(line)
127
+ return unless match && CtxParser::Values.valid_host?(match[1])
128
+
129
+ remote_as = as_number(match[2])
130
+ return unless remote_as
131
+
132
+ state, prefixes = peer_state(match[3])
133
+ return unless state
134
+
135
+ scope.merge(peer: CtxParser::Values.host(match[1]), state: state, received_prefixes: prefixes, remote_as: remote_as)
136
+ end
137
+
138
+ # 仅在不存在平台附加标记时,末列数字才能证明会话已建立。
139
+ def peer_state(text)
140
+ raw_state = text.downcase
141
+ prefixes = Integer(raw_state, 10) if raw_state.match?(/\A\d+\z/)
142
+ [ prefixes ? :established : STATES[raw_state], prefixes ]
143
+ end
144
+
145
+ # 解析支持展示格式的 BGP 自治系统号。
146
+ def as_number(text)
147
+ parts = text.split(".").map { |value| Integer(value, 10) }
148
+ return if parts.size == 2 && parts.any? { |value| value > 65_535 }
149
+
150
+ value = parts.size == 2 ? parts[0] * 65_536 + parts[1] : parts.first
151
+ value if (1..4_294_967_295).cover?(value)
152
+ end
153
+
154
+ # 识别说明性回显,不将其作为数据记录。
155
+ def auxiliary?(line)
156
+ return true if /\A\d+\/\d+ BGP path\/bestpath attribute entries using \d+ bytes of memory\z/i.match?(line)
157
+ return true if /\A\d+ (?:BGP extended community entries using \d+ bytes of memory|received paths for inbound soft reconfiguration|networks peaked at .+)\z/i.match?(line)
158
+
159
+ line.match?(/\A(?:BGP router identifier |BGP table version |[\d ]+(?:network entries|path entries|BGP path attribute entries|BGP AS-PATH entries|BGP community entries|BGP route-map cache entries|BGP filter-list cache entries|paths using)|BGP using |BGP activity |BGP attribute entries |BGP community entries |BGP AS path entries |Neighbor sessions:|Memory used by BGP |BGP scan interval |Scan interval )/i)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "../../parsers/interface_names"
5
+ require "ctx_parser/values"
6
+
7
+ module CtxChecker
8
+ module CiscoIos
9
+ module Parsers
10
+ # OSPF 身份包含进程、VRF、接口和路由器 ID;未知作用域保留为 nil。
11
+ class ShowIpOspfNeighbor < CtxChecker::Parsers::Base
12
+ PLATFORM = :cisco_ios
13
+ STATES = { "FULL" => :full, "2WAY" => :two_way, "2-WAY" => :two_way, "DOWN" => :down,
14
+ "ATTEMPT" => :attempt, "INIT" => :init, "EXSTART" => :exstart, "EXCHANGE" => :exchange,
15
+ "LOADING" => :loading }.freeze
16
+
17
+ private
18
+
19
+ # 解析 show ip ospf neighbor,保留不完整或不支持的证据。
20
+ def parse_output(text, complete:)
21
+ text = clean(text)
22
+ rows, issues = [], []
23
+ scope = { vrf: "default", process_id: nil, area: nil }
24
+ sections = []
25
+ section = { header: false, empty: false, count: 0, totals: [], declared: false, scope: scope }
26
+ sections << section
27
+ text.each_line.with_index(1) do |source, number|
28
+ line = source.strip
29
+ next if line.empty? || line.match?(/\A[-= ]+\z/)
30
+
31
+ if line.match?(/\ANeighbor ID\s+Pri\s+State\s+(?:Dead Time|Up Time)\s+Address\s+Interface\z/i)
32
+ section[:header] = true
33
+ elsif (match = /\AOSPF\s+Process\s+ID\s+(\S+)\s+VRF\s+(\S+)\z/i.match(line))
34
+ scope = { vrf: match[2], process_id: match[1], area: nil }
35
+ section = { header: false, empty: false, count: 0, totals: [], declared: true, scope: scope }
36
+ sections << section
37
+ elsif (match = /\A(?:Process ID|OSPF Process(?!\s+ID\b))\s*:?\s*(\S+)(?:,?\s+VRF\s+(\S+))?\z/i.match(line))
38
+ scope = { vrf: match[2] || "default", process_id: match[1], area: nil }
39
+ section = { header: false, empty: false, count: 0, totals: [], declared: true, scope: scope }
40
+ sections << section
41
+ elsif line.match?(/\A(?:OSPF\s+Process\b|Process ID\b)/i)
42
+ issues << { code: :invalid_protocol_scope, line: number }
43
+ scope = { vrf: nil, process_id: nil, area: nil }
44
+ section = { header: false, empty: false, count: 0, totals: [], declared: true, scope: scope }
45
+ sections << section
46
+ elsif (match = /\ATotal number of neighbors:\s*(\d+)\z/i.match(line))
47
+ section[:totals] << Integer(match[1], 10)
48
+ elsif line.match?(/\A(?:No (?:OSPF )?neighbors(?: found)?\.?|0 (?:OSPF )?neighbors)\z/i)
49
+ section[:empty] = true
50
+ elsif (row = parse_neighbor(line, scope))
51
+ rows << row
52
+ section[:count] += 1
53
+ else
54
+ issues << { code: :unparsed_row, line: number }
55
+ end
56
+ end
57
+ sections.each do |entry|
58
+ entry[:totals].each do |total|
59
+ issues << { code: :count_mismatch, expected: total, observed: entry[:count] } unless total == entry[:count]
60
+ end
61
+ issues << { code: :contradictory_empty } if entry[:empty] && entry[:count].positive?
62
+ if entry[:count].positive? && !entry[:header]
63
+ issues << { code: :missing_neighbor_header }
64
+ elsif (entry[:header] || entry[:declared]) && entry[:count].zero? && !entry[:empty] && !entry[:totals].include?(0)
65
+ issues << { code: :neighbor_count_unavailable }
66
+ end
67
+ end
68
+ recognized = sections.any? { |entry| entry[:empty] || entry[:totals].include?(0) || entry[:header] && entry[:count].positive? }
69
+ scopes = sections.filter_map do |entry|
70
+ value = entry[:scope].slice(:vrf, :process_id)
71
+ value if value.values.none?(&:nil?) && (entry[:empty] || entry[:totals].include?(0) || entry[:header] && entry[:count].positive?)
72
+ end
73
+ build_result(rows, complete: complete, recognized: recognized, issues: issues,
74
+ identity: %i[vrf process_id interface router_id], covered_scopes: scopes)
75
+ end
76
+
77
+ # 解析协议邻居行的状态及稳定身份。
78
+ def parse_neighbor(line, scope)
79
+ match = /\A(\S+)\s+(\d+)\s+(FULL|2WAY|2-WAY|DOWN|ATTEMPT|INIT|EXSTART|EXCHANGE|LOADING)\/(?:\s*)(DR|BDR|DROTHER|-)\s+\S+\s+(\S+)\s+(\S+)\z/i.match(line)
80
+ return unless match && (0..255).cover?(Integer(match[2], 10)) &&
81
+ ipv4_address?(match[1]) && ipv4_address?(match[5])
82
+
83
+ scope.merge(interface: CtxChecker::Parsers::InterfaceNames.normalize(match[6], platform: self.class::PLATFORM),
84
+ router_id: CtxParser::Values.host(match[1]), neighbor_address: CtxParser::Values.host(match[5]),
85
+ state: STATES.fetch(match[3].upcase), network_type: :unknown, local_role: :unknown,
86
+ neighbor_role: match[4] == "-" ? :unknown : match[4].downcase.to_sym)
87
+ end
88
+
89
+ # OSPFv2 路由器 ID 与接口地址必须是 IPv4 主机地址。
90
+ def ipv4_address?(text)
91
+ text.match?(/\A\d+(?:\.\d+){3}\z/) && CtxParser::Values.valid_host?(text)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module CiscoIos
7
+ module Parsers
8
+ # IP 协议栈计数为累计值,与 ASIC 和接口计数区分处理。
9
+ class ShowIpTraffic < CtxChecker::Parsers::Base
10
+ METRICS = {
11
+ "format errors" => [ "format_errors", :error ], "checksum errors" => [ "checksum_errors", :error ],
12
+ "bad hop count" => [ "bad_ttl", :error ], "unknown protocol" => [ "unknown_protocol", :error ],
13
+ "not a gateway" => [ "not_a_gateway", :discard ], "security failures" => [ "security_failures", :policy ],
14
+ "bad options" => [ "bad_options", :error ], "encapsulation failed" => [ "encapsulation_failed", :discard ],
15
+ "unresolved" => [ "unresolved", :discard ], "no adjacency" => [ "no_adjacency", :discard ],
16
+ "no route" => [ "no_route", :discard ], "unicast RPF" => [ "unicast_rpf", :policy ],
17
+ "forced drop" => [ "forced_drop", :discard ], "options denied" => [ "options_denied", :policy ],
18
+ "source IP address zero" => [ "source_address_zero", :discard ],
19
+ "ignored" => [ "options_ignored", :policy ], "invalid hole" => [ "invalid_hole", :error ],
20
+ "unsupported-addr" => [ "unsupported_address", :discard ],
21
+ "timeouts" => [ "reassembly_timeouts", :error ], "couldn't reassemble" => [ "reassembly_failed", :error ],
22
+ "couldn't fragment" => [ "fragmentation_failed", :error ]
23
+ }.freeze
24
+ INFORMATIONAL = /\A\d+ (?:total|local destination|with options|end|nop|basic security|loose source route|timestamp|extended security|record route|stream ID|strict source route|alert|cipso|ump|other|reassembled|fragmented|fragments|received|sent|generated|forwarded)\z/i
25
+ REQUIRED_METRICS = %w[format_errors checksum_errors bad_ttl unknown_protocol encapsulation_failed no_route].freeze
26
+
27
+ private
28
+
29
+ # 解析 show ip traffic,保留不完整或不支持的证据。
30
+ def parse_output(text, complete:)
31
+ records, issues = [], []
32
+ active = false
33
+ recognized = false
34
+ clean(text).each_line.with_index(1) do |source, number|
35
+ line = source.strip
36
+ next if line.empty?
37
+ if line == "IP statistics:"
38
+ active = recognized = true
39
+ next
40
+ end
41
+ if /\A\S+ statistics:/.match?(line)
42
+ active = false
43
+ next
44
+ end
45
+ next unless active
46
+
47
+ line.sub(/\A(?:Rcvd|Opts|Frags|Bcast|Mcast|Sent|Drop):\s*/, "").split(/,\s*/).each do |cell|
48
+ match = /\A(\d+) (.+)\z/.match(cell)
49
+ definition = match && METRICS[match[2]]
50
+ if definition
51
+ records << { component: "ipv4/control_plane", metric: definition[0], category: definition[1],
52
+ value: Integer(match[1], 10), discontinuity: nil }
53
+ elsif !INFORMATIONAL.match?(cell)
54
+ issues << { code: :unparsed_counter, line: number }
55
+ end
56
+ end
57
+ end
58
+ missing = REQUIRED_METRICS - records.map { |record| record[:metric] }
59
+ issues << { code: :counter_fields_missing, count: missing.size } if recognized && missing.any?
60
+ build_result(records, complete: complete, recognized: recognized, issues: issues, identity: %i[component metric])
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+
5
+ module CtxChecker
6
+ module CiscoIos
7
+ module Parsers
8
+ # 分别保留 IOS 各内存分配池,并使用精确字节数核算。
9
+ class ShowMemoryStatistics < CtxChecker::Parsers::Base
10
+ private
11
+
12
+ # 解析 show memory statistics,保留不完整或不支持的证据。
13
+ def parse_output(text, complete:)
14
+ records, issues = [], []
15
+ clean(text).each_line.with_index(1) do |source, number|
16
+ line = source.strip
17
+ next if line.empty? || /\AHead\s+Total\(b\)/.match?(line) || /\ATracekey\s*:\s*[\da-f#]+\z/i.match?(line)
18
+
19
+ match = /\A(.+?)\s+Pool\s+Total:\s*(\d+)\s+Used:\s*(\d+)\s+Free:\s*(\d+)\z/i.match(line) ||
20
+ /\A(.+?)\s+[0-9a-f]+\s+(\d+)\s+(\d+)\s+(\d+)\s+\d+\s+\d+\z/i.match(line)
21
+ unless match
22
+ issues << { code: :unparsed_row, line: number }
23
+ next
24
+ end
25
+ total, used, free = match.captures.drop(1).map { |value| Integer(value, 10) }
26
+ consistent = total.positive? && used + free == total
27
+ issues << { code: :inconsistent_memory_totals, line: number } unless consistent
28
+ records << { component: match[1], scope: "ios_memory_pool", total_bytes: total, used_bytes: used,
29
+ free_bytes: free, available_bytes: nil, utilization: consistent ? used.fdiv(total) * 100 : nil, basis: :used_total }
30
+ end
31
+ build_result(records, complete: complete, recognized: records.any?, issues: issues, identity: %i[component scope])
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../parsers/base"
4
+ require_relative "system_values"
5
+
6
+ module CtxChecker
7
+ module CiscoIos
8
+ module Parsers
9
+ # IOS 显式报告系统同步状态,并使用毫秒表示偏移。
10
+ class ShowNtpStatus < CtxChecker::Parsers::Base
11
+ include SystemValues
12
+ AUXILIARY = /\A(?:nominal freq|reference time|root dispersion|loopfilter state|system poll interval|ntp uptime|system time|leap time|leap direction|root delay|time resolution)/i
13
+
14
+ private
15
+
16
+ # 解析 show ntp status,保留不完整或不支持的证据。
17
+ def parse_output(text, complete:)
18
+ records, issues = [], []
19
+ clean(text).each_line.with_index(1) do |source, number|
20
+ line = source.strip
21
+ next if line.empty?
22
+
23
+ if (match = /\AClock is (synchronized|unsynchronized), stratum (\d+)(?:, reference assoc id \d+)?(?:, reference is (\S+))?\z/i.match(line))
24
+ stratum = Integer(match[2], 10)
25
+ issues << { code: :invalid_ntp_stratum, line: number } unless stratum.between?(0, 16)
26
+ records << { component: "system", synchronized: match[1].casecmp?("synchronized"),
27
+ stratum: stratum.between?(0, 16) ? stratum : nil, reference: match[3], peer: nil, offset_ms: nil, leap: nil }
28
+ elsif (match = /\Aclock offset is (#{NUMBER}) msec(?:,|\z)/i.match(line)) && records.one?
29
+ issues << { code: :duplicate_ntp_offset, line: number } if records.first[:offset_ms]
30
+ records.first[:offset_ms] = checked_number(match[1], issues: issues, component: "system")
31
+ elsif /\A%?NTP is not enabled\.?\z/i.match?(line)
32
+ records << { component: "system", synchronized: false, stratum: nil, reference: nil, peer: nil, offset_ms: nil, leap: nil }
33
+ elsif AUXILIARY.match?(line)
34
+ next
35
+ else
36
+ issues << unparsed(number)
37
+ end
38
+ end
39
+ build_result(records, complete: complete, recognized: records.any?, issues: issues, identity: [ :component ])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end