wireless-rb 0.1.0 → 0.2.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README-zh.md +37 -2
  4. data/README.md +34 -3
  5. data/docs/architecture-zh.md +4 -4
  6. data/lib/wireless/ap_session.rb +113 -52
  7. data/lib/wireless/collector.rb +49 -36
  8. data/lib/wireless/configuration.rb +0 -2
  9. data/lib/wireless/coverage.rb +15 -5
  10. data/lib/wireless/fields.rb +25 -20
  11. data/lib/wireless/h3c/{authentication_parser.rb → authentication.rb} +4 -4
  12. data/lib/wireless/h3c/command_output.rb +44 -3
  13. data/lib/wireless/h3c/configuration.rb +21 -5
  14. data/lib/wireless/h3c/document.rb +32 -0
  15. data/lib/wireless/h3c/parsers/display_current_configuration.rb +143 -69
  16. data/lib/wireless/h3c/parsers/display_dot1x_connection.rb +4 -2
  17. data/lib/wireless/h3c/parsers/display_lldp_neighbor_information_verbose.rb +34 -10
  18. data/lib/wireless/h3c/parsers/display_mac_authentication_connection.rb +4 -2
  19. data/lib/wireless/h3c/parsers/display_wlan_ap_all_verbose.rb +37 -11
  20. data/lib/wireless/h3c/parsers/display_wlan_ap_model_all.rb +5 -5
  21. data/lib/wireless/h3c/parsers/display_wlan_bss_all_verbose.rb +6 -6
  22. data/lib/wireless/h3c/parsers/display_wlan_client_security_block_mac.rb +2 -2
  23. data/lib/wireless/h3c/parsers/display_wlan_client_verbose.rb +14 -8
  24. data/lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb +2 -2
  25. data/lib/wireless/h3c/profile.rb +6 -3
  26. data/lib/wireless/h3c/record_parser.rb +138 -0
  27. data/lib/wireless/huawei/parser.rb +86 -0
  28. data/lib/wireless/huawei/parsers/display_ap_all.rb +14 -0
  29. data/lib/wireless/huawei/parsers/display_station_all.rb +14 -0
  30. data/lib/wireless/huawei/profile.rb +22 -0
  31. data/lib/wireless/observation.rb +2 -0
  32. data/lib/wireless/parser_registry.rb +2 -1
  33. data/lib/wireless/projection.rb +4 -0
  34. data/lib/wireless/runtime_parser.rb +16 -11
  35. data/lib/wireless/snapshot.rb +2 -1
  36. data/lib/wireless/terminal.rb +32 -0
  37. data/lib/wireless/version.rb +1 -1
  38. data/lib/wireless.rb +0 -2
  39. metadata +9 -20
  40. data/lib/wireless/h3c/session_parser.rb +0 -58
  41. data/lib/wireless/h3c/verbose_parser.rb +0 -82
  42. data/lib/wireless/h3c_runtime.rb +0 -18
  43. data/lib/wireless/huawei_runtime.rb +0 -65
@@ -2,17 +2,21 @@
2
2
 
3
3
  require_relative "../command_output"
4
4
  require_relative "../configuration"
5
- require "ctx_parser/configuration_parser"
5
+ require_relative "../document"
6
+ require "ctx_parser/configuration"
6
7
 
7
8
  module Wireless
8
9
  module H3c
9
10
  module Parsers
10
11
  # 用缩进保留型号和 Radio 归属;相关命令不受支持时降低完整性。
11
- class DisplayCurrentConfiguration < CtxParser::ConfigurationParser
12
+ class DisplayCurrentConfiguration < CtxParser::Configuration
12
13
  include CommandOutput
13
14
 
14
15
  private
15
16
 
17
+ # 配置由 Document 标记受损行,仍保留无关事实和原始来源。
18
+ def terminal_issue(_text) = nil
19
+
16
20
  # 声明当前命令解析器所需的换行归一化方式。
17
21
  def output_newlines = :preserve
18
22
 
@@ -20,7 +24,9 @@ module Wireless
20
24
  def parse_text(text, complete:)
21
25
  @configuration = H3c::Configuration.new
22
26
  @issues = []
23
- read_configuration(text, comments: nil, boundaries: /\A(?:#|return)\z/)
27
+ @setting_assignments = {}.compare_by_identity
28
+ read_configuration(text, comments: nil, boundaries: /\A(?:#|return)\z/, document_class: Document)
29
+ @issues.concat(document.issues)
24
30
  recognized = cursor.lines.any? { |line| line.text == "return" }
25
31
  blocks.each { |block| recognized = parse_block(block) || recognized }
26
32
  @issues << { code: :missing_completeness_evidence } unless complete
@@ -43,14 +49,12 @@ module Wireless
43
49
  line = block.text
44
50
  selected = owner ? nested_scope(line, owner) : root_scope(line)
45
51
  if selected
46
- scope(block, selected.path) do
47
- block.children.each { |child| parse_block(child, selected) }
48
- selected.finish
49
- end
52
+ parse_scope(block, selected)
50
53
  true
51
54
  elsif !owner && (match = /\Asysname (\S+)\z/.match(line))
52
- @configuration.data[:global_settings][:hostname] = match[1]
53
- capture(block, match[1], [ :global_settings, :hostname ])
55
+ settings = @configuration.data[:global_settings]
56
+ assign_setting(settings, :hostname, match[1], block)
57
+ capture(block, settings[:hostname], [ :global_settings, :hostname ]) if settings.key?(:hostname)
54
58
  true
55
59
  elsif owner
56
60
  apply_block(block, owner)
@@ -68,9 +72,12 @@ module Wireless
68
72
  if (match = /\Awlan (ap-group|service-template|ap) (\S+)(?: (.+))?\z/i.match(line))
69
73
  kind = { "ap-group" => :ap_groups, "service-template" => :service_templates, "ap" => :access_points }.fetch(match[1].downcase)
70
74
  name, suffix = match[2], match[3]
75
+ if kind == :ap_groups && suffix && !/\Atype \S+\z/i.match?(suffix)
76
+ raise ArgumentError, "Invalid AP group type"
77
+ end
71
78
  raise ArgumentError, "Invalid service template" if kind == :service_templates && suffix
72
- raise ArgumentError, "Missing AP model" if kind == :access_points && !suffix&.start_with?("model ")
73
- @configuration.entity(kind, name, model: suffix&.delete_prefix("model "), type: suffix&.delete_prefix("type "))
79
+ raise ArgumentError, "Missing AP model" if kind == :access_points && !/\Amodel \S+\z/i.match?(suffix.to_s)
80
+ @configuration.entity(kind, name, model: suffix&.sub(/\Amodel /i, ""), type: suffix&.sub(/\Atype /i, ""))
74
81
  elsif (match = /\Awlan (global-configuration|virtual-ap-group(?: \S+)?|ap-license-group|vip-client-group)\z/i.match(line))
75
82
  key = match[1].tr("-", "_").split.first.to_sym
76
83
  @configuration.global(key, identity: line.split[2])
@@ -89,7 +96,7 @@ module Wireless
89
96
 
90
97
  # 标记已识别叶子语句,不将其嵌套未知语法提升到父对象。
91
98
  def apply_block(block, owner)
92
- unless apply(owner, block.text, @configuration.data)
99
+ unless apply(owner, block, @configuration.data)
93
100
  unrecognized_block(block)
94
101
  return false
95
102
  end
@@ -108,134 +115,201 @@ module Wireless
108
115
 
109
116
  # 逐来源行记录不支持语法,并保留完整来源块。
110
117
  def unrecognized_block(block)
111
- cursor.mark(block.index, state: :unsupported)
112
- @issues << { code: :unrecognized_wireless_configuration, line: block.line }
113
- block.children.each { |child| unrecognized_block(child) }
118
+ pending = [ block ]
119
+ until pending.empty?
120
+ current = pending.pop
121
+ cursor.mark(current.index, state: :unsupported)
122
+ @issues << { code: :unrecognized_wireless_configuration, line: current.line }
123
+ pending.concat(current.children.reverse)
124
+ end
125
+ end
126
+
127
+ # 无关配置可忽略,未知父级内的无线声明不能被当作不存在。
128
+ def ignore(block)
129
+ pending = [ block ]
130
+ until pending.empty?
131
+ current = pending.pop
132
+ if /\A(?:undo )?wlan\b/i.match?(current.text)
133
+ unrecognized_block(current)
134
+ else
135
+ cursor.mark(current.index, state: :ignored)
136
+ pending.concat(current.children.reverse)
137
+ end
138
+ end
139
+ nil
114
140
  end
115
141
 
116
142
  # 报告未在预期作用域消费的相关子语句。
117
143
  def unparsed_child(block) = unrecognized_block(block)
118
144
 
145
+ # 每个声明块独立检查单值;重复进入的顺序配置仍保留各自来源快照。
146
+ def parse_scope(block, owner)
147
+ previous_assignments = @setting_assignments
148
+ @setting_assignments = {}.compare_by_identity
149
+ scope(block, owner.path) do
150
+ block.children.each { |child| parse_block(child, owner) }
151
+ if owner.kind == :service_templates && @setting_assignments.values.any? { |fields| fields.values.any? { |entry| entry[:conflicted] } }
152
+ owner.record[:authentication_type] = nil
153
+ else
154
+ owner.finish
155
+ end
156
+ owner.record
157
+ end
158
+ ensure
159
+ @setting_assignments = previous_assignments
160
+ end
161
+
162
+ # 等价重复不改变完整性;同块冲突删除事实,并使原始来源和后续声明失效。
163
+ def assign_setting(record, key, value, block)
164
+ assign_settings(record, key, { key => value }, block)
165
+ end
166
+
167
+ # 一条命令的关联字段共享身份,冲突时整体排除,不能留下残缺派生标志。
168
+ def assign_settings(record, identity, values, block)
169
+ fields = (@setting_assignments[record] ||= {})
170
+ previous = fields[identity]
171
+ if previous && (previous[:conflicted] || previous[:value] != values)
172
+ (previous[:value].keys | values.keys).each { |key| record.delete(key) }
173
+ cursor.mark(previous[:block].index, state: :invalid)
174
+ cursor.mark(block.index, state: :invalid)
175
+ @issues << { code: :conflicting_configuration_value, line: block.line, first_line: previous[:block].line }
176
+ previous[:conflicted] = true
177
+ else
178
+ fields[identity] ||= { value: values, block: block }
179
+ record.merge!(values)
180
+ end
181
+ end
182
+
183
+ # 未声明的可选 VLAN 保持未知,已声明值须在设备 VLAN 范围内。
184
+ def configured_vlan(raw)
185
+ vlan = Fields.integer(raw, max: 4094)
186
+ raise ArgumentError, "Invalid configured VLAN" if vlan && vlan.zero?
187
+ vlan
188
+ end
189
+
119
190
  # 将已识别的无线设置应用到当前作用域记录。
120
- def apply(scope, line, config)
191
+ def apply(scope, block, config)
192
+ line = block.text
121
193
  owner, kind, record = scope.record, scope.kind, scope.settings
122
194
  words = line.split
123
195
  enabled = !line.start_with?("undo ")
124
196
  case line
125
197
  when /\A(?:undo )?region-code-lock enable\z/
126
198
  return false unless kind == :global_configuration
127
- record[:region_code_lock] = enabled
199
+ assign_setting(record, :region_code_lock, enabled, block)
128
200
  when /\Anas-id (\S+)\z/
129
201
  return false unless kind == :global_configuration
130
- record[:nas_id] = Regexp.last_match(1)
202
+ assign_setting(record, :nas_id, Regexp.last_match(1), block)
131
203
  when /\A(?:undo )?rrop anti-bmc network (ipv4-and-ipv6-simple) enable\z/
132
204
  return false unless kind == :ap_groups
133
- (record[:rrop_anti_bmc] ||= {})[:network] = { mode: Regexp.last_match(1), enabled: enabled }
205
+ assign_setting(record[:rrop_anti_bmc] ||= {}, :network, { mode: Regexp.last_match(1), enabled: enabled }, block)
134
206
  when /\Arrop anti-bmc default-action (deny|permit)\z/
135
207
  return false unless kind == :ap_groups
136
- (record[:rrop_anti_bmc] ||= {})[:default_action] = Regexp.last_match(1)
208
+ assign_setting(record[:rrop_anti_bmc] ||= {}, :default_action, Regexp.last_match(1), block)
137
209
  when /\Aserial-id (\S+)\z/
138
210
  return false unless %i[access_points ap_groups].include?(kind)
139
- kind == :ap_groups ? owner[:serial_ids] << words.last : record[:serial_id] = words.last
211
+ kind == :ap_groups ? owner[:serial_ids] << words.last : assign_setting(record, :serial_id, words.last, block)
140
212
  when /\Aap (\S+)\z/
141
213
  return false unless kind == :ap_groups
142
214
  owner[:ap_names] << words.last
143
215
  when /\Avlan (\d+)\z/
144
- vlan = Fields.integer(words.last, max: 4094)
145
- kind == :ap_groups ? owner[:vlan_ids] << vlan : owner[:vlan_id] = vlan
216
+ vlan = configured_vlan(words.last)
217
+ kind == :ap_groups ? owner[:vlan_ids] << vlan : assign_setting(owner, :vlan_id, vlan, block)
146
218
  when /\Assid (.+)\z/
147
219
  return false unless kind == :service_templates
148
- owner[:ssid] = Regexp.last_match(1)
220
+ assign_setting(owner, :ssid, Regexp.last_match(1), block)
149
221
  when /\A(?:undo )?service-template enable\z/
150
222
  return false unless kind == :service_templates
151
- owner[:enabled] = enabled
223
+ assign_setting(owner, :enabled, enabled, block)
152
224
  when /\Aservice-template (\S+)(?: vlan (\d+))?\z/
153
225
  return false unless kind == :radio
154
226
  name, vlan = Regexp.last_match.captures
155
- binding = { name: name, vlan_id: Fields.integer(vlan, max: 4094) }
156
- previous = record[:service_templates].find { |entry| entry[:name] == name }
157
- raise ArgumentError, "Conflicting template binding" if previous && previous != binding
158
- record[:service_templates] |= [ binding ]
227
+ @configuration.bind_template(record, name, configured_vlan(vlan))
159
228
  when /\Apreshared-key\s+.+\z/i
160
- record[:has_preshared_key] = true
229
+ assign_setting(record, :has_preshared_key, true, block)
161
230
  when /\Aakm mode (.+)\z/
162
- record[:akm_modes] = Regexp.last_match(1).downcase.gsub("802.1x", "dot1x").split
231
+ assign_setting(record, :akm_modes, Regexp.last_match(1).downcase.gsub("802.1x", "dot1x").split, block)
163
232
  when /\A(cipher-suite|security-ie) (.+)\z/
164
233
  key = words.first == "cipher-suite" ? :cipher_suites : :security_ies
165
- record[key] = words.drop(1).map(&:downcase)
234
+ assign_setting(record, key, words.drop(1).map(&:downcase), block)
166
235
  when /\Aclient-security authentication-mode (.+)\z/
167
- record[:client_security_authentication_mode] = Regexp.last_match(1).downcase
236
+ mode = Regexp.last_match(1).downcase
237
+ unless %w[dot1x mac mac-and-dot1x dot1x-then-mac mac-then-dot1x oui-then-dot1x].include?(mode)
238
+ raise ArgumentError, "Invalid client authentication mode"
239
+ end
240
+ assign_setting(record, :client_security_authentication_mode, mode, block)
168
241
  when /\Aclient-security (ignore-authentication|ignore-authorization)\z/
169
- record[words.last.tr("-", "_").to_sym] = true
242
+ assign_setting(record, words.last.tr("-", "_").to_sym, true, block)
170
243
  when /\A(dot1x|mac-authentication) domain (\S+)\z/
171
- record["#{words.first.tr('-', '_')}_domain".to_sym] = words.last
244
+ assign_setting(record, "#{words.first.tr('-', '_')}_domain".to_sym, words.last, block)
172
245
  when /\Aportal (enable method|domain|bas-ip|apply web-server|apply mac-trigger-server) (\S+)\z/
173
246
  key, value = Regexp.last_match.captures
174
247
  # 仅保留 Portal 是否存在;URL 与凭据不属于无线清单契约。
175
- (record[:portal] ||= {})[key.tr(" -", "__").to_sym] = value.include?("://") ? true : value
248
+ assign_setting(record[:portal] ||= {}, key.tr(" -", "__").to_sym, value.include?("://") ? true : value, block)
176
249
  when /\A(?:undo )?(dot1x|mac-authentication)\z/
177
- record["#{Regexp.last_match(1).tr('-', '_')}_enabled".to_sym] = enabled
250
+ assign_setting(record, "#{Regexp.last_match(1).tr('-', '_')}_enabled".to_sym, enabled, block)
178
251
  when /\Adot1x authentication-method (chap|eap|pap)\z/
179
- record[:dot1x_authentication_method] = Regexp.last_match(1)
252
+ assign_setting(record, :dot1x_authentication_method, Regexp.last_match(1), block)
180
253
  when /\Amac-authentication user-name-format fixed(?: account \S+)?(?: password (simple|cipher) \S+)?\z/
181
254
  # 固定账号的明文和密文均不进入普通事实或捕获快照。
182
255
  storage = Regexp.last_match(1)
183
- record[:mac_authentication_user_name_format] = "fixed"
184
- record[:mac_authentication_has_password] = !storage.nil?
256
+ assign_settings(record, :mac_username_setting,
257
+ { mac_authentication_user_name_format: "fixed", mac_authentication_has_password: !storage.nil? }, block)
185
258
  when /\Amac-authentication user-name-format (mac-address(?: (?:with-hyphen|without-hyphen))?(?: (?:lowercase|uppercase))?)\z/
186
- record[:mac_authentication_user_name_format] = Regexp.last_match(1)
187
- record.delete(:mac_authentication_has_password)
259
+ assign_settings(record, :mac_username_setting, { mac_authentication_user_name_format: Regexp.last_match(1) }, block)
188
260
  when /\Adot1x timer (supp-timeout|tx-period) (\d+)\z/
189
- (record[:dot1x_timers] ||= {})[words[2].tr("-", "_").to_sym] = Fields.integer(words.last)
261
+ assign_setting(record[:dot1x_timers] ||= {}, words[2].tr("-", "_").to_sym, Fields.integer(words.last), block)
190
262
  when /\Awlan (?:client-security )?static-(black|white)list mac-address (\S+)(?: (.+))?\z/
191
263
  kind, mac, reason = Regexp.last_match.captures
192
- config[kind == "black" ? :blacklist : :whitelist] << { mac_address: Fields.mac(mac), reason: reason }
264
+ config[kind == "black" ? :blacklist : :whitelist] << { mac_address: Fields.mac_address(mac), reason: reason }
193
265
  when /\A(?:undo )?(?:wlan )?(auto-ap|auto-persistent|band-navigation|client inspect|radio-load-balance band-navigation) enable\z/
194
- record[Regexp.last_match(1).tr(" -", "__").to_sym] = enabled
266
+ assign_setting(record, Regexp.last_match(1).tr(" -", "__").to_sym, enabled, block)
195
267
  when /\A(?:undo )?(beacon ssid-hide|bss transition-management enable)\z/
196
- record[line.include?("ssid-hide") ? :ssid_hidden : :btm_enabled] = enabled
268
+ assign_setting(record, line.include?("ssid-hide") ? :ssid_hidden : :btm_enabled, enabled, block)
197
269
  when /\A(client keep-alive|client-rate-limit|firmware-upgrade|radio) (enable|disable)\z/
198
270
  key = { "client keep-alive" => :client_keep_alive, "client-rate-limit" => :client_rate_limit_enabled,
199
271
  "firmware-upgrade" => :firmware_upgrade, "radio" => :enabled }.fetch(Regexp.last_match(1))
200
- record[key] = words.last == "enable"
272
+ assign_setting(record, key, words.last == "enable", block)
201
273
  when /\A(client keep-alive interval|client max-count|client cache aging-time|bss transition-management disassociation forced timer|statistics-interval|priority|dot1x retry|wlan tcp mss) (\d+)\z/
202
274
  key = { "client keep-alive interval" => :client_keep_alive_interval, "client max-count" => :client_max_count,
203
275
  "client cache aging-time" => :client_cache_aging_time, "bss transition-management disassociation forced timer" => :btm_disassociation_timer,
204
276
  "statistics-interval" => :statistics_interval, "priority" => :priority, "dot1x retry" => :dot1x_retry, "wlan tcp mss" => :tcp_mss }.fetch(Regexp.last_match(1))
205
- record[key] = Fields.integer(words.last)
277
+ assign_setting(record, key, Fields.integer(words.last), block)
206
278
  when /\A(?:description|map-configuration) (.+)\z/
207
- record[words.first.tr("-", "_").to_sym] = words.drop(1).join(" ")
208
- when /\Abackup-ac ip (\S+)\z/ then record[:backup_ac_ip] = Fields.ip(words.last)
209
- when /\Achannel band-width (.+)\z/ then record[:channel_bandwidth] = Regexp.last_match(1)
210
- when /\Achannel auto unlock\z/ then record.merge!(channel: "auto", channel_number: nil, channel_unlocked: true)
279
+ assign_setting(record, words.first.tr("-", "_").to_sym, words.drop(1).join(" "), block)
280
+ when /\Abackup-ac ip (\S+)\z/ then assign_setting(record, :backup_ac_ip, Fields.ip_address(words.last), block)
281
+ when /\Achannel band-width (.+)\z/ then assign_setting(record, :channel_bandwidth, Regexp.last_match(1), block)
282
+ when /\Achannel auto unlock\z/
283
+ assign_settings(record, :channel_setting, { channel: "auto", channel_number: nil, channel_unlocked: true }, block)
211
284
  when /\Achannel (.+)\z/
212
- record[:channel] = Regexp.last_match(1)
213
- record[:channel_number] = Fields.channel(record[:channel])
214
- when /\Amax-power (\d+(?:\.\d+)?)\z/ then record[:max_power] = Fields.number(words.last)
285
+ channel = Regexp.last_match(1)
286
+ channel_number = Fields.channel(channel)
287
+ assign_settings(record, :channel_setting, { channel: channel, channel_number: channel_number }, block)
288
+ when /\Amax-power (\d+(?:\.\d+)?)\z/ then assign_setting(record, :max_power, Fields.number(words.last), block)
215
289
  when /\Aclient-rate-limit (inbound|outbound) mode (\S+) cir (\d+)\z/
216
- (record[:client_rate_limit] ||= {})[words[1].to_sym] = { mode: words[3], cir_bps: Fields.integer(words.last) * 1000 }
290
+ assign_setting(record[:client_rate_limit] ||= {}, words[1].to_sym, { mode: words[3], cir_bps: Fields.integer(words.last) * 1000 }, block)
217
291
  when /\Arate (mandatory|supported|disabled|multicast) (.+)\z/
218
- (record[:rates] ||= {})[words[1].to_sym] = words[1] == "multicast" ? words.drop(2).join(" ") : words.drop(2).map { |word| Fields.number(word) }
292
+ assign_setting(record[:rates] ||= {}, words[1].to_sym, words[1] == "multicast" ? words.drop(2).join(" ") : words.drop(2).map { |word| Fields.number(word) }, block)
219
293
  when /\Aoption client (reject|reconnect) enable rssi (-?\d+(?:\.\d+)?)\z/
220
- (record[:client_options] ||= {})[words[2].to_sym] = Fields.number(words.last)
294
+ assign_setting(record[:client_options] ||= {}, words[2].to_sym, Fields.number(words.last), block)
221
295
  when /\Asacp anti-sticky enable(?: rssi (-?\d+(?:\.\d+)?))? interval (\d+)\z/
222
296
  signal, interval = Regexp.last_match.captures
223
- record[:anti_sticky] = { enabled: true, rssi: Fields.number(signal), interval: Fields.integer(interval) }
297
+ assign_setting(record, :anti_sticky, { enabled: true, rssi: Fields.number(signal), interval: Fields.integer(interval) }, block)
224
298
  when /\Apacket-filter name (\S+) (inbound|outbound)\z/
225
- (record[:packet_filters] ||= {})[words.last.to_sym] = words[2]
299
+ assign_setting(record[:packet_filters] ||= {}, words.last.to_sym, words[2], block)
226
300
  when /\A(?:client forwarding-location|wlan image-load filepath|wlan fast-forwarding mode|led-mode) (\S+)\z/
227
301
  key = { "client" => :forwarding_location, "wlan image-load" => :image_load_filepath,
228
302
  "wlan fast-forwarding" => :fast_forwarding_mode, "led-mode" => :led_mode }
229
- record[key.fetch(words.first == "wlan" ? words.take(2).join(" ") : words.first)] = words.last
303
+ assign_setting(record, key.fetch(words.first == "wlan" ? words.take(2).join(" ") : words.first), words.last, block)
230
304
  when /\A(local|member) ip (\S+)\z/
231
- words.first == "local" ? record[:local_ip] = Fields.ip(words.last) : (record[:member_ips] ||= []) << Fields.ip(words.last)
232
- when /\Aclient-mac (\S+)\z/ then (record[:mac_addresses] ||= []) << Fields.mac(words.last)
233
- when "wlan tunnel-preempt enable" then record[:tunnel_preempt] = true
234
- when "broadcast-probe reply disable" then record[:broadcast_probe_reply] = false
235
- when "radar-detect disable" then record[:radar_detect_disabled] = true
236
- when "calibrate-channel self-decisive enable all" then record[:calibrate_channel] = true
237
- when "calibrate-power self-decisive enable all" then record[:calibrate_power] = true
238
- when "ap-license-synchronization enable" then record[:license_synchronization] = true
305
+ words.first == "local" ? assign_setting(record, :local_ip, Fields.ip_address(words.last), block) : (record[:member_ips] ||= []) << Fields.ip_address(words.last)
306
+ when /\Aclient-mac (\S+)\z/ then (record[:mac_addresses] ||= []) << Fields.mac_address(words.last)
307
+ when "wlan tunnel-preempt enable" then assign_setting(record, :tunnel_preempt, true, block)
308
+ when "broadcast-probe reply disable" then assign_setting(record, :broadcast_probe_reply, false, block)
309
+ when "radar-detect disable" then assign_setting(record, :radar_detect_disabled, true, block)
310
+ when "calibrate-channel self-decisive enable all" then assign_setting(record, :calibrate_channel, true, block)
311
+ when "calibrate-power self-decisive enable all" then assign_setting(record, :calibrate_power, true, block)
312
+ when "ap-license-synchronization enable" then assign_setting(record, :license_synchronization, true, block)
239
313
  else return false
240
314
  end
241
315
  true
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../authentication_parser"
3
+ require_relative "../record_parser"
4
+ require_relative "../authentication"
4
5
 
5
6
  module Wireless
6
7
  module H3c
7
8
  module Parsers
8
9
  # 解析 display dot1x connection 报告的会话。
9
- class DisplayDot1xConnection < AuthenticationParser
10
+ class DisplayDot1xConnection < RecordParser
11
+ include Authentication
10
12
  end
11
13
  end
12
14
  end
@@ -23,8 +23,25 @@ module Wireless
23
23
  current = nil
24
24
  local = nil
25
25
  description_indent = nil
26
+ description = description_line = nil
27
+ store_description = lambda do
28
+ if description
29
+ assign_value(current[:details] ||= {}, :system_description, description, issues, description_line)
30
+ description = description_indent = nil
31
+ end
32
+ end
33
+ continuation_lines = {}
26
34
  flush = lambda do
27
35
  if current && current.values_at(:local_interface, :remote_chassis_id, :remote_interface).all? { |value| value && !value.strip.empty? }
36
+ if /mac address/i.match?(current.dig(:details, :chassis_type).to_s)
37
+ begin
38
+ current[:remote_chassis_id] = Fields.mac_address(current[:remote_chassis_id])
39
+ rescue ArgumentError
40
+ issues << { code: :invalid_chassis_id }
41
+ current = nil
42
+ next
43
+ end
44
+ end
28
45
  records << current
29
46
  elsif current
30
47
  issues << { code: :incomplete_neighbor }
@@ -35,10 +52,11 @@ module Wireless
35
52
  row = line.strip
36
53
  next if row.empty? || /\A[-=]+\z/.match?(row)
37
54
  if description_indent && line[/\A\s*/].size > description_indent
38
- current[:details][:system_description] += "\n#{row}"
55
+ description << "\n" << row
56
+ continuation_lines[index + 1] = true
39
57
  next
40
58
  end
41
- description_indent = nil
59
+ store_description.call
42
60
  if (match = /\ALLDP neighbor-information of port \d+\[(.+?)\]\s*:?\z/i.match(row))
43
61
  flush.call
44
62
  local = match[1]
@@ -52,27 +70,33 @@ module Wireless
52
70
  label = label.strip.downcase
53
71
  current ||= { local_interface: local }
54
72
  case label
55
- when "chassis id" then current[:remote_chassis_id] = raw.downcase
56
- when "port id" then current[:remote_interface] = raw
57
- when "system name" then current[:remote_name] = Fields.value(raw)
73
+ when "chassis id" then assign_value(current, :remote_chassis_id, raw, issues, index + 1)
74
+ when "port id" then assign_value(current, :remote_interface, raw, issues, index + 1)
75
+ when "system name" then assign_value(current, :remote_name, Fields.value(raw), issues, index + 1)
58
76
  when "management address"
59
77
  begin
60
- current[:management_address] = Fields.ip(Fields.value(raw))
78
+ assign_value(current, :management_address, Fields.ip_address(Fields.value(raw)), issues, index + 1)
61
79
  rescue IPAddr::InvalidAddressError, ArgumentError
62
80
  issues << { code: :invalid_management_address, line: index + 1 }
63
81
  end
64
- when "port vlan id(pvid)" then (current[:details] ||= {})[:port_vlan_id] = raw
82
+ when "port vlan id(pvid)" then assign_value(current[:details] ||= {}, :port_vlan_id, raw, issues, index + 1)
65
83
  when *DETAILS
66
- (current[:details] ||= {})[label.tr(" ", "_").to_sym] = raw
67
- description_indent = line[/\A\s*/].size if label == "system description"
84
+ if label == "system description"
85
+ description = raw.dup
86
+ description_line = index + 1
87
+ description_indent = line[/\A\s*/].size
88
+ else
89
+ assign_value(current[:details] ||= {}, label.tr(" ", "_").to_sym, raw, issues, index + 1)
90
+ end
68
91
  else issues << { code: :unrecognized_field, line: index + 1 }
69
92
  end
70
93
  else
71
94
  issues << { code: :unrecognized_line, line: index + 1 }
72
95
  end
73
96
  end
97
+ store_description.call
74
98
  flush.call
75
- total = /(?:total(?: (?:LLDP )?neighbors)?|number of neighbors)[^\n:]*:\s*(\d+)\s*$/i.match(text)&.[](1)&.to_i
99
+ total = reported_total(text, noun: "(?:LLDP )?neighbors", issues: issues, continuation_lines: continuation_lines)
76
100
  total ||= 0 if /\ANo (?:LLDP )?neighbors\s*$/i.match?(text.strip)
77
101
  Fields.result(records, issues: issues, complete: complete, total: total,
78
102
  identity: %i[local_interface remote_chassis_id remote_interface])
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../authentication_parser"
3
+ require_relative "../record_parser"
4
+ require_relative "../authentication"
4
5
 
5
6
  module Wireless
6
7
  module H3c
7
8
  module Parsers
8
9
  # 解析 display mac-authentication connection 报告的会话。
9
- class DisplayMacAuthenticationConnection < AuthenticationParser
10
+ class DisplayMacAuthenticationConnection < RecordParser
11
+ include Authentication
10
12
  end
11
13
  end
12
14
  end
@@ -1,20 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../verbose_parser"
3
+ require_relative "../record_parser"
4
4
 
5
5
  module Wireless
6
6
  module H3c
7
7
  module Parsers
8
8
  # 单条回显同时包含 AP 清单及其嵌套 Radio 清单。
9
- class DisplayWlanApAllVerbose < VerboseParser
9
+ class DisplayWlanApAllVerbose < RecordParser
10
10
  AP_FIELDS = { "ap id" => :ap_id, "ap group name" => :group_name, "state" => :operational_status,
11
11
  "model" => :model, "mac address" => :mac_address, "ip address" => :management_ip,
12
12
  "serial id" => :serial, "s/w version" => :os_version, "client count" => :client_count,
13
- "number of clients" => :client_count, "online clients" => :client_count, "online time" => :uptime_seconds }.freeze
13
+ "number of clients" => :client_count, "online clients" => :client_count, "online time" => :online_seconds, "system uptime" => :uptime_seconds }.freeze
14
14
  RADIO_FIELDS = { "admin state" => :admin_status, "state" => :admin_status, "radio type" => :protocol,
15
15
  "type" => :protocol, "channel" => :channel, "channel usage(%)" => :utilization,
16
16
  "channel usage" => :utilization, "noise floor" => :noise_dbm, "channel bandwidth" => :channel_width,
17
- "operating bandwidth" => :channel_width }.freeze
17
+ "operating bandwidth" => :channel_width, "channel band-width" => :channel_width }.freeze
18
+ RADIO_SECTIONS = { "operational vht-mcs set" => :vht_mcs, "operational ht mcs set" => :ht_mcs,
19
+ "operational he-mcs set" => :he_mcs, "operational rate" => :operational_rates }.freeze
20
+ SECTION_FIELDS = %w[mandatory supported multicast disabled].freeze
18
21
  AP_DETAILS = [ "backup type", "ready-for-switchover", "system uptime", "region code", "region code lock",
19
22
  "udp control port number", "udp data port number", "h/w version", "boot version", "usb state", "power level",
20
23
  "power info", "poe status", "description", "priority", "echo interval", "echo count", "keepalive interval",
@@ -25,7 +28,7 @@ module Wireless
25
28
  "connection count", "backup ipv4", "backup ipv6", "ctrl-tunnel encryption", "ctrl-tunnel encryption state",
26
29
  "data-tunnel encryption", "data-tunnel encryption state", "led mode", "remote configuration", "energysaving level",
27
30
  "ap type", "map file", "forwarding mode", "ble mode", "capwap data-tunnel status" ].freeze
28
- RADIO_DETAILS = [ "basic bssid", "bssid", "antenna type", "client dot11ac-only", "client dot11n-only",
31
+ RADIO_DETAILS = [ "basic bssid", "base bssid", "bssid", "antenna type", "client dot11ac-only", "client dot11n-only",
29
32
  "secondary channel offset", "secondary channel mode", "short gi for 20mhz", "short gi for 40mhz",
30
33
  "short gi for 80mhz", "short gi for 160mhz", "mimo", "green-energy-management", "a-msdu", "a-mpdu", "ldpc", "stbc",
31
34
  "max power", "operational rate", "distance", "ani", "fragmentation threshold", "beacon interval", "protection threshold",
@@ -40,16 +43,35 @@ module Wireless
40
43
  # 解释当前命令记录,保留不完整行诊断。
41
44
  def parse_text(text, complete:)
42
45
  access_points, radios, issues = [], [], []
46
+ aps_with_radio_evidence = {}.compare_by_identity
43
47
  ap = radio = nil
44
- tokens(text, issues).each do |label, raw, line|
48
+ section = nil
49
+ section_indent = nil
50
+ read_fields(text, issues).each do |label, raw, line, indent|
45
51
  if label == "ap name"
46
52
  ap = { name: raw }
47
53
  access_points << ap
48
54
  radio = nil
55
+ section = nil
49
56
  elsif (match = /\Aradio (\d+)\z/.match(label))
50
57
  radio = { ap_name: ap&.fetch(:name), radio_id: match[1] }
51
58
  radios << radio
59
+ aps_with_radio_evidence[ap] = true if ap
60
+ section = nil
52
61
  else
62
+ if radio && RADIO_SECTIONS.key?(label)
63
+ section = RADIO_SECTIONS.fetch(label)
64
+ section_indent = indent
65
+ # 空标题本身不属于测量;其子标签在相应速率或 MCS 集合内拥有身份。
66
+ assign_value(radio[:details] ||= {}, label.tr(" -", "__").to_sym, raw, issues, line)
67
+ next
68
+ end
69
+ section = nil if section && indent <= section_indent
70
+ if radio && section && SECTION_FIELDS.include?(label)
71
+ details = radio[:details] ||= {}
72
+ assign_value(details[section] ||= {}, label.to_sym, raw, issues, line)
73
+ next
74
+ end
53
75
  owner = radio && RADIO_FIELDS.key?(label) ? radio : ap
54
76
  mapping = owner.equal?(radio) && radio ? RADIO_FIELDS : AP_FIELDS
55
77
  details = AP_DETAILS.include?(label) ? ap : RADIO_DETAILS.include?(label) ? radio : nil
@@ -65,13 +87,17 @@ module Wireless
65
87
  issues << { code: :invalid_identity } if missing
66
88
  missing
67
89
  end
68
- total = count(text, "APs")
90
+ total = record_count(text, "APs", issues)
69
91
  issues << { code: :count_mismatch, expected: total, parsed: valid_aps.size } if total && total != valid_aps.size
70
- # AP 总数证明共享命令的覆盖范围,不代表 Radio 数量。
71
- radio_total = total && total == valid_aps.size ? radios.size : nil
72
- CtxParser::ResultSet.new(results: {
92
+ # AP 总数只证明 AP 范围;未报告 Radio 的 AP 不能证明其 Radio 清单为空。
93
+ radio_issues = issues.dup
94
+ valid_aps.each do |record|
95
+ radio_issues << { code: :missing_radio_evidence, ap_name: record[:name] } unless aps_with_radio_evidence.key?(record)
96
+ end
97
+ radio_total = total == 0 && valid_aps.empty? ? 0 : nil
98
+ CtxParser::Results.new(by_dimension: {
73
99
  access_points: Fields.result(valid_aps, issues: issues.dup, complete: complete, total: total, identity: [ :name ]),
74
- radios: Fields.result(radios, issues: issues.dup, complete: complete, total: radio_total, identity: %i[ap_name radio_id])
100
+ radios: Fields.result(radios, issues: radio_issues, complete: complete, total: radio_total, identity: %i[ap_name radio_id])
75
101
  })
76
102
  end
77
103
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../verbose_parser"
3
+ require_relative "../record_parser"
4
4
 
5
5
  module Wireless
6
6
  module H3c
7
7
  module Parsers
8
8
  # 型号能力与发现的 AP 资产及 Radio 分开保存。
9
- class DisplayWlanApModelAll < VerboseParser
9
+ class DisplayWlanApModelAll < RecordParser
10
10
  MODEL_FIELDS = { "alias" => :alias, "vendor name" => :vendor_name, "vendor id" => :vendor_id,
11
11
  "license weight" => :license_weight, "license type" => :license_type, "radio count" => :radio_count, "type" => :type }.freeze
12
12
  RADIO_FIELDS = { "mode" => :modes, "default mode" => :default_mode, "bss count" => :bss_count }.freeze
@@ -22,8 +22,8 @@ module Wireless
22
22
  lines = text.lines
23
23
  model = section = nil
24
24
  mapping = MODEL_FIELDS
25
- tokens(text, issues).each do |label, raw, line|
26
- safely(issues, line) do
25
+ read_fields(text, issues).each do |label, raw, line|
26
+ parse_field(issues, line) do
27
27
  if label == "ap model"
28
28
  raise ArgumentError, "Missing AP model" unless raw
29
29
  model = { model: raw, radios: [], versions: [] }
@@ -48,7 +48,7 @@ module Wireless
48
48
  end
49
49
  end
50
50
  records.each { |record| validate_model(record, issues) }
51
- Fields.result(records, issues: issues, complete: complete, total: count(text, "AP models"), identity: [ :model ])
51
+ Fields.result(records, issues: issues, complete: complete, total: record_count(text, "AP models", issues), identity: [ :model ])
52
52
  end
53
53
 
54
54
  # 根据声明字段类型归一化型号能力值。
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../session_parser"
3
+ require_relative "../record_parser"
4
4
 
5
5
  module Wireless
6
6
  module H3c
7
7
  module Parsers
8
8
  # 同时接受以 BSSID 或 AP 名称开头的 BSS 块。
9
- class DisplayWlanBssAllVerbose < SessionParser
9
+ class DisplayWlanBssAllVerbose < RecordParser
10
10
  MAPPING = COMMON_FIELDS.merge("service template" => :profile, "service template name" => :profile,
11
11
  "state" => :operational_status, "client count" => :client_count, "number of clients" => :client_count,
12
12
  "online clients" => :client_count).freeze
@@ -17,7 +17,7 @@ module Wireless
17
17
  private
18
18
 
19
19
  # 返回当前命令格式中用于开始新记录的字段。
20
- def start_label(fields)
20
+ def record_start_label(fields)
21
21
  first = fields.find { |label,| label == "bssid" || label == "ap name" }
22
22
  "ap name" if first&.first == "ap name"
23
23
  end
@@ -27,10 +27,10 @@ module Wireless
27
27
  super && record[:ap_name] && !record[:ap_name].strip.empty? && !record[:radio_id].nil?
28
28
  end
29
29
 
30
- # 保留 BSS 安全字段,其余字段委派给共享会话解析器。
31
- def assign_session_field(record, label, raw, issues, line)
30
+ # 保留 BSS 安全字段,其余字段委派给共享记录解析器。
31
+ def assign_record_field(record, label, raw, issues, line)
32
32
  if SECURITY.include?(label)
33
- (record[:security] ||= {})[label.tr(" -", "__").to_sym] = raw
33
+ assign_value(record[:security] ||= {}, label.tr(" -", "__").to_sym, raw, issues, line)
34
34
  else
35
35
  super
36
36
  end