wireless-rb 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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE +22 -0
  4. data/README-zh.md +103 -0
  5. data/README.md +103 -0
  6. data/docs/architecture-zh.md +92 -0
  7. data/docs/release.md +13 -0
  8. data/examples/offline.rb +29 -0
  9. data/lib/wireless/ap_session.rb +119 -0
  10. data/lib/wireless/collector.rb +168 -0
  11. data/lib/wireless/configuration.rb +22 -0
  12. data/lib/wireless/coverage.rb +60 -0
  13. data/lib/wireless/fields.rb +131 -0
  14. data/lib/wireless/h3c/authentication_parser.rb +25 -0
  15. data/lib/wireless/h3c/command_output.rb +29 -0
  16. data/lib/wireless/h3c/configuration.rb +109 -0
  17. data/lib/wireless/h3c/parser.rb +13 -0
  18. data/lib/wireless/h3c/parsers/display_current_configuration.rb +246 -0
  19. data/lib/wireless/h3c/parsers/display_dot1x_connection.rb +13 -0
  20. data/lib/wireless/h3c/parsers/display_lldp_neighbor_information_verbose.rb +83 -0
  21. data/lib/wireless/h3c/parsers/display_mac_authentication_connection.rb +13 -0
  22. data/lib/wireless/h3c/parsers/display_wlan_ap_all_verbose.rb +80 -0
  23. data/lib/wireless/h3c/parsers/display_wlan_ap_model_all.rb +79 -0
  24. data/lib/wireless/h3c/parsers/display_wlan_bss_all_verbose.rb +41 -0
  25. data/lib/wireless/h3c/parsers/display_wlan_client_security_block_mac.rb +31 -0
  26. data/lib/wireless/h3c/parsers/display_wlan_client_verbose.rb +49 -0
  27. data/lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb +50 -0
  28. data/lib/wireless/h3c/profile.rb +45 -0
  29. data/lib/wireless/h3c/session_parser.rb +58 -0
  30. data/lib/wireless/h3c/verbose_parser.rb +82 -0
  31. data/lib/wireless/h3c_runtime.rb +18 -0
  32. data/lib/wireless/huawei_runtime.rb +65 -0
  33. data/lib/wireless/neighbor_parser.rb +25 -0
  34. data/lib/wireless/observation.rb +28 -0
  35. data/lib/wireless/parser_registry.rb +9 -0
  36. data/lib/wireless/projection.rb +16 -0
  37. data/lib/wireless/runtime_parser.rb +47 -0
  38. data/lib/wireless/snapshot.rb +37 -0
  39. data/lib/wireless/version.rb +6 -0
  40. data/lib/wireless.rb +18 -0
  41. metadata +154 -0
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../command_output"
4
+ require_relative "../configuration"
5
+ require "ctx_parser/configuration_parser"
6
+
7
+ module Wireless
8
+ module H3c
9
+ module Parsers
10
+ # 用缩进保留型号和 Radio 归属;相关命令不受支持时降低完整性。
11
+ class DisplayCurrentConfiguration < CtxParser::ConfigurationParser
12
+ include CommandOutput
13
+
14
+ private
15
+
16
+ # 声明当前命令解析器所需的换行归一化方式。
17
+ def output_newlines = :preserve
18
+
19
+ # 仅构建一次来源块,再将解释结果关联到行状态。
20
+ def parse_text(text, complete:)
21
+ @configuration = H3c::Configuration.new
22
+ @issues = []
23
+ read_configuration(text, comments: nil, boundaries: /\A(?:#|return)\z/)
24
+ recognized = cursor.lines.any? { |line| line.text == "return" }
25
+ blocks.each { |block| recognized = parse_block(block) || recognized }
26
+ @issues << { code: :missing_completeness_evidence } unless complete
27
+ @issues << { code: :unrecognized_configuration } unless recognized
28
+ CtxParser::Result.new(status: @issues.empty? ? :complete : recognized ? :partial : :failed,
29
+ records: recognized ? [ @configuration.data ] : [], issues: @issues)
30
+ end
31
+
32
+ # 未知块独立拥有其后代,不能将其放入外层作用域解释。
33
+ def parse_block(block, owner = nil)
34
+ analyze(block) { parse_setting(block, owner) }
35
+ rescue ArgumentError, IPAddr::InvalidAddressError
36
+ @issues << { code: :invalid_configuration_line, line: block.line }
37
+ block.children.each { |child| unrecognized_block(child) }
38
+ false
39
+ end
40
+
41
+ # 在所属父级配置作用域内解释一项支持的设置。
42
+ def parse_setting(block, owner)
43
+ line = block.text
44
+ selected = owner ? nested_scope(line, owner) : root_scope(line)
45
+ if selected
46
+ scope(block, selected.path) do
47
+ block.children.each { |child| parse_block(child, selected) }
48
+ selected.finish
49
+ end
50
+ true
51
+ elsif !owner && (match = /\Asysname (\S+)\z/.match(line))
52
+ @configuration.data[:global_settings][:hostname] = match[1]
53
+ capture(block, match[1], [ :global_settings, :hostname ])
54
+ true
55
+ elsif owner
56
+ apply_block(block, owner)
57
+ elsif /\A(?:undo )?(?:wlan|dot1x|mac-authentication)\b/i.match?(line)
58
+ apply_block(block, @configuration.global)
59
+ true
60
+ else
61
+ ignore(block)
62
+ false
63
+ end
64
+ end
65
+
66
+ # 返回由顶层无线配置语句选定的型号作用域。
67
+ def root_scope(line)
68
+ if (match = /\Awlan (ap-group|service-template|ap) (\S+)(?: (.+))?\z/i.match(line))
69
+ kind = { "ap-group" => :ap_groups, "service-template" => :service_templates, "ap" => :access_points }.fetch(match[1].downcase)
70
+ name, suffix = match[2], match[3]
71
+ 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 "))
74
+ elsif (match = /\Awlan (global-configuration|virtual-ap-group(?: \S+)?|ap-license-group|vip-client-group)\z/i.match(line))
75
+ key = match[1].tr("-", "_").split.first.to_sym
76
+ @configuration.global(key, identity: line.split[2])
77
+ end
78
+ end
79
+
80
+ # 仅选择所属配置模型允许的作用域。
81
+ def nested_scope(line, owner)
82
+ case line
83
+ when /\Aap-model (.+)\z/ then owner.model(Regexp.last_match(1))
84
+ when /\Aradio (\d+|2\.4g|5g|6g)\z/i then owner.radio(Regexp.last_match(1))
85
+ when /\A(?:gigabitethernet|smartrate-ethernet|ethernet|ten-gigabitethernet) \d+\z/i
86
+ owner.ethernet(line)
87
+ end
88
+ end
89
+
90
+ # 标记已识别叶子语句,不将其嵌套未知语法提升到父对象。
91
+ def apply_block(block, owner)
92
+ unless apply(owner, block.text, @configuration.data)
93
+ unrecognized_block(block)
94
+ return false
95
+ end
96
+
97
+ if owner.kind == :global
98
+ if /\A(?:undo )?wlan (?:client-security )?static-(black|white)list\b/.match?(block.text)
99
+ key = block.text.include?("static-blacklist") ? :blacklist : :whitelist
100
+ entries = @configuration.data[key]
101
+ capture(block, entries.last, [ key, entries.size - 1 ])
102
+ else
103
+ capture(block, owner.record, owner.path)
104
+ end
105
+ end
106
+ true
107
+ end
108
+
109
+ # 逐来源行记录不支持语法,并保留完整来源块。
110
+ 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) }
114
+ end
115
+
116
+ # 报告未在预期作用域消费的相关子语句。
117
+ def unparsed_child(block) = unrecognized_block(block)
118
+
119
+ # 将已识别的无线设置应用到当前作用域记录。
120
+ def apply(scope, line, config)
121
+ owner, kind, record = scope.record, scope.kind, scope.settings
122
+ words = line.split
123
+ enabled = !line.start_with?("undo ")
124
+ case line
125
+ when /\A(?:undo )?region-code-lock enable\z/
126
+ return false unless kind == :global_configuration
127
+ record[:region_code_lock] = enabled
128
+ when /\Anas-id (\S+)\z/
129
+ return false unless kind == :global_configuration
130
+ record[:nas_id] = Regexp.last_match(1)
131
+ when /\A(?:undo )?rrop anti-bmc network (ipv4-and-ipv6-simple) enable\z/
132
+ return false unless kind == :ap_groups
133
+ (record[:rrop_anti_bmc] ||= {})[:network] = { mode: Regexp.last_match(1), enabled: enabled }
134
+ when /\Arrop anti-bmc default-action (deny|permit)\z/
135
+ return false unless kind == :ap_groups
136
+ (record[:rrop_anti_bmc] ||= {})[:default_action] = Regexp.last_match(1)
137
+ when /\Aserial-id (\S+)\z/
138
+ return false unless %i[access_points ap_groups].include?(kind)
139
+ kind == :ap_groups ? owner[:serial_ids] << words.last : record[:serial_id] = words.last
140
+ when /\Aap (\S+)\z/
141
+ return false unless kind == :ap_groups
142
+ owner[:ap_names] << words.last
143
+ when /\Avlan (\d+)\z/
144
+ vlan = Fields.integer(words.last, max: 4094)
145
+ kind == :ap_groups ? owner[:vlan_ids] << vlan : owner[:vlan_id] = vlan
146
+ when /\Assid (.+)\z/
147
+ return false unless kind == :service_templates
148
+ owner[:ssid] = Regexp.last_match(1)
149
+ when /\A(?:undo )?service-template enable\z/
150
+ return false unless kind == :service_templates
151
+ owner[:enabled] = enabled
152
+ when /\Aservice-template (\S+)(?: vlan (\d+))?\z/
153
+ return false unless kind == :radio
154
+ 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 ]
159
+ when /\Apreshared-key\s+.+\z/i
160
+ record[:has_preshared_key] = true
161
+ when /\Aakm mode (.+)\z/
162
+ record[:akm_modes] = Regexp.last_match(1).downcase.gsub("802.1x", "dot1x").split
163
+ when /\A(cipher-suite|security-ie) (.+)\z/
164
+ key = words.first == "cipher-suite" ? :cipher_suites : :security_ies
165
+ record[key] = words.drop(1).map(&:downcase)
166
+ when /\Aclient-security authentication-mode (.+)\z/
167
+ record[:client_security_authentication_mode] = Regexp.last_match(1).downcase
168
+ when /\Aclient-security (ignore-authentication|ignore-authorization)\z/
169
+ record[words.last.tr("-", "_").to_sym] = true
170
+ when /\A(dot1x|mac-authentication) domain (\S+)\z/
171
+ record["#{words.first.tr('-', '_')}_domain".to_sym] = words.last
172
+ when /\Aportal (enable method|domain|bas-ip|apply web-server|apply mac-trigger-server) (\S+)\z/
173
+ key, value = Regexp.last_match.captures
174
+ # 仅保留 Portal 是否存在;URL 与凭据不属于无线清单契约。
175
+ (record[:portal] ||= {})[key.tr(" -", "__").to_sym] = value.include?("://") ? true : value
176
+ when /\A(?:undo )?(dot1x|mac-authentication)\z/
177
+ record["#{Regexp.last_match(1).tr('-', '_')}_enabled".to_sym] = enabled
178
+ when /\Adot1x authentication-method (chap|eap|pap)\z/
179
+ record[:dot1x_authentication_method] = Regexp.last_match(1)
180
+ when /\Amac-authentication user-name-format fixed(?: account \S+)?(?: password (simple|cipher) \S+)?\z/
181
+ # 固定账号的明文和密文均不进入普通事实或捕获快照。
182
+ storage = Regexp.last_match(1)
183
+ record[:mac_authentication_user_name_format] = "fixed"
184
+ record[:mac_authentication_has_password] = !storage.nil?
185
+ 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)
188
+ when /\Adot1x timer (supp-timeout|tx-period) (\d+)\z/
189
+ (record[:dot1x_timers] ||= {})[words[2].tr("-", "_").to_sym] = Fields.integer(words.last)
190
+ when /\Awlan (?:client-security )?static-(black|white)list mac-address (\S+)(?: (.+))?\z/
191
+ kind, mac, reason = Regexp.last_match.captures
192
+ config[kind == "black" ? :blacklist : :whitelist] << { mac_address: Fields.mac(mac), reason: reason }
193
+ 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
195
+ when /\A(?:undo )?(beacon ssid-hide|bss transition-management enable)\z/
196
+ record[line.include?("ssid-hide") ? :ssid_hidden : :btm_enabled] = enabled
197
+ when /\A(client keep-alive|client-rate-limit|firmware-upgrade|radio) (enable|disable)\z/
198
+ key = { "client keep-alive" => :client_keep_alive, "client-rate-limit" => :client_rate_limit_enabled,
199
+ "firmware-upgrade" => :firmware_upgrade, "radio" => :enabled }.fetch(Regexp.last_match(1))
200
+ record[key] = words.last == "enable"
201
+ 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
+ key = { "client keep-alive interval" => :client_keep_alive_interval, "client max-count" => :client_max_count,
203
+ "client cache aging-time" => :client_cache_aging_time, "bss transition-management disassociation forced timer" => :btm_disassociation_timer,
204
+ "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)
206
+ 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)
211
+ 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)
215
+ 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 }
217
+ 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) }
219
+ when /\Aoption client (reject|reconnect) enable rssi (-?\d+(?:\.\d+)?)\z/
220
+ (record[:client_options] ||= {})[words[2].to_sym] = Fields.number(words.last)
221
+ when /\Asacp anti-sticky enable(?: rssi (-?\d+(?:\.\d+)?))? interval (\d+)\z/
222
+ signal, interval = Regexp.last_match.captures
223
+ record[:anti_sticky] = { enabled: true, rssi: Fields.number(signal), interval: Fields.integer(interval) }
224
+ when /\Apacket-filter name (\S+) (inbound|outbound)\z/
225
+ (record[:packet_filters] ||= {})[words.last.to_sym] = words[2]
226
+ when /\A(?:client forwarding-location|wlan image-load filepath|wlan fast-forwarding mode|led-mode) (\S+)\z/
227
+ key = { "client" => :forwarding_location, "wlan image-load" => :image_load_filepath,
228
+ "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
230
+ 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
239
+ else return false
240
+ end
241
+ true
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../authentication_parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # 解析 display dot1x connection 报告的会话。
9
+ class DisplayDot1xConnection < AuthenticationParser
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # AP LLDP 端点与控制器资产区分处理,并要求明确的零邻居证据。
9
+ class DisplayLldpNeighborInformationVerbose < Parser
10
+ DETAILS = [ "system description", "port description", "port vlan id", "vlan id", "maximum frame size",
11
+ "chassis type", "port type", "port id type", "management address type", "update time", "time to live",
12
+ "system capabilities supported", "system capabilities enabled", "management address interface type",
13
+ "management address interface id", "management address oid", "link aggregation supported", "link aggregation enabled",
14
+ "aggregation port id", "auto-negotiation supported", "auto-negotiation enabled", "opermau", "power port class",
15
+ "pse power supported", "pse power enabled", "pse pairs control ability", "power pairs", "port power classification",
16
+ "power type", "power source", "power priority", "pd requested power value", "pse allocated power value" ].freeze
17
+
18
+ private
19
+
20
+ # 解释当前命令记录,保留不完整行诊断。
21
+ def parse_text(text, complete:)
22
+ records, issues = [], []
23
+ current = nil
24
+ local = nil
25
+ description_indent = nil
26
+ flush = lambda do
27
+ if current && current.values_at(:local_interface, :remote_chassis_id, :remote_interface).all? { |value| value && !value.strip.empty? }
28
+ records << current
29
+ elsif current
30
+ issues << { code: :incomplete_neighbor }
31
+ end
32
+ current = nil
33
+ end
34
+ text.lines.each_with_index do |line, index|
35
+ row = line.strip
36
+ next if row.empty? || /\A[-=]+\z/.match?(row)
37
+ if description_indent && line[/\A\s*/].size > description_indent
38
+ current[:details][:system_description] += "\n#{row}"
39
+ next
40
+ end
41
+ description_indent = nil
42
+ if (match = /\ALLDP neighbor-information of port \d+\[(.+?)\]\s*:?\z/i.match(row))
43
+ flush.call
44
+ local = match[1]
45
+ elsif /\ALLDP neighbor index\s*:/i.match?(row)
46
+ flush.call
47
+ current = { local_interface: local }
48
+ elsif /\A(?:LLDP agent\s+[^:]+:|No (?:LLDP )?neighbors|(?:Total|Number of neighbors).*:\s*\d+)\s*\z/i.match?(row)
49
+ next
50
+ elsif (match = /\A([^:]+):\s*(.*?)\z/.match(row))
51
+ label, raw = match.captures
52
+ label = label.strip.downcase
53
+ current ||= { local_interface: local }
54
+ 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)
58
+ when "management address"
59
+ begin
60
+ current[:management_address] = Fields.ip(Fields.value(raw))
61
+ rescue IPAddr::InvalidAddressError, ArgumentError
62
+ issues << { code: :invalid_management_address, line: index + 1 }
63
+ end
64
+ when "port vlan id(pvid)" then (current[:details] ||= {})[:port_vlan_id] = raw
65
+ when *DETAILS
66
+ (current[:details] ||= {})[label.tr(" ", "_").to_sym] = raw
67
+ description_indent = line[/\A\s*/].size if label == "system description"
68
+ else issues << { code: :unrecognized_field, line: index + 1 }
69
+ end
70
+ else
71
+ issues << { code: :unrecognized_line, line: index + 1 }
72
+ end
73
+ end
74
+ flush.call
75
+ total = /(?:total(?: (?:LLDP )?neighbors)?|number of neighbors)[^\n:]*:\s*(\d+)\s*$/i.match(text)&.[](1)&.to_i
76
+ total ||= 0 if /\ANo (?:LLDP )?neighbors\s*$/i.match?(text.strip)
77
+ Fields.result(records, issues: issues, complete: complete, total: total,
78
+ identity: %i[local_interface remote_chassis_id remote_interface])
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../authentication_parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # 解析 display mac-authentication connection 报告的会话。
9
+ class DisplayMacAuthenticationConnection < AuthenticationParser
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../verbose_parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # 单条回显同时包含 AP 清单及其嵌套 Radio 清单。
9
+ class DisplayWlanApAllVerbose < VerboseParser
10
+ AP_FIELDS = { "ap id" => :ap_id, "ap group name" => :group_name, "state" => :operational_status,
11
+ "model" => :model, "mac address" => :mac_address, "ip address" => :management_ip,
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
14
+ RADIO_FIELDS = { "admin state" => :admin_status, "state" => :admin_status, "radio type" => :protocol,
15
+ "type" => :protocol, "channel" => :channel, "channel usage(%)" => :utilization,
16
+ "channel usage" => :utilization, "noise floor" => :noise_dbm, "channel bandwidth" => :channel_width,
17
+ "operating bandwidth" => :channel_width }.freeze
18
+ AP_DETAILS = [ "backup type", "ready-for-switchover", "system uptime", "region code", "region code lock",
19
+ "udp control port number", "udp data port number", "h/w version", "boot version", "usb state", "power level",
20
+ "power info", "poe status", "description", "priority", "echo interval", "echo count", "keepalive interval",
21
+ "discovery-response wait-time", "statistics report interval", "fragment size (data)", "fragment size (control)",
22
+ "mac type", "tunnel mode", "discovery type", "retransmission count", "retransmission interval", "firmware upgrade",
23
+ "sent control packets", "received control packets", "echo requests", "lost echo responses", "average echo delay",
24
+ "last reboot reason", "last reboot reason (ac check)", "latest ip address", "current ac ip", "tunnel down reason",
25
+ "connection count", "backup ipv4", "backup ipv6", "ctrl-tunnel encryption", "ctrl-tunnel encryption state",
26
+ "data-tunnel encryption", "data-tunnel encryption state", "led mode", "remote configuration", "energysaving level",
27
+ "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",
29
+ "secondary channel offset", "secondary channel mode", "short gi for 20mhz", "short gi for 40mhz",
30
+ "short gi for 80mhz", "short gi for 160mhz", "mimo", "green-energy-management", "a-msdu", "a-mpdu", "ldpc", "stbc",
31
+ "max power", "operational rate", "distance", "ani", "fragmentation threshold", "beacon interval", "protection threshold",
32
+ "long retry threshold", "short retry threshold", "frame aging time in cache", "protection mode", "mu-txbf", "su-txbf",
33
+ "continuous mode", "client dot11ax-only", "ofdma random access rus", "he ofdma random access rus", "channel width set",
34
+ "dl-ofdma", "ul-ofdma", "ul-mu-mimo", "bss-color", "twt negotiation", "radar-detect switch", "ht protection mode",
35
+ "atf", "preamble type", "operational vht-mcs set", "mandatory", "supported", "multicast", "operational ht mcs set",
36
+ "disabled", "operational he-mcs set" ].freeze
37
+
38
+ private
39
+
40
+ # 解释当前命令记录,保留不完整行诊断。
41
+ def parse_text(text, complete:)
42
+ access_points, radios, issues = [], [], []
43
+ ap = radio = nil
44
+ tokens(text, issues).each do |label, raw, line|
45
+ if label == "ap name"
46
+ ap = { name: raw }
47
+ access_points << ap
48
+ radio = nil
49
+ elsif (match = /\Aradio (\d+)\z/.match(label))
50
+ radio = { ap_name: ap&.fetch(:name), radio_id: match[1] }
51
+ radios << radio
52
+ else
53
+ owner = radio && RADIO_FIELDS.key?(label) ? radio : ap
54
+ mapping = owner.equal?(radio) && radio ? RADIO_FIELDS : AP_FIELDS
55
+ details = AP_DETAILS.include?(label) ? ap : RADIO_DETAILS.include?(label) ? radio : nil
56
+ assign_field(owner, mapping, label, raw, issues, line, details: details)
57
+ end
58
+ end
59
+ invalid = access_points.reject { |record| !record[:name].nil? && !record[:name].strip.empty? }
60
+ issues << { code: :invalid_identity } if invalid.any?
61
+ valid_aps = access_points - invalid
62
+ issues << { code: :duplicate_identity } if CtxParser::Records.new(valid_aps, identity: [ :name ]).duplicates?
63
+ radios.reject! do |record|
64
+ missing = record[:ap_name].nil? || record[:ap_name].strip.empty?
65
+ issues << { code: :invalid_identity } if missing
66
+ missing
67
+ end
68
+ total = count(text, "APs")
69
+ 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: {
73
+ 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])
75
+ })
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../verbose_parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # 型号能力与发现的 AP 资产及 Radio 分开保存。
9
+ class DisplayWlanApModelAll < VerboseParser
10
+ MODEL_FIELDS = { "alias" => :alias, "vendor name" => :vendor_name, "vendor id" => :vendor_id,
11
+ "license weight" => :license_weight, "license type" => :license_type, "radio count" => :radio_count, "type" => :type }.freeze
12
+ RADIO_FIELDS = { "mode" => :modes, "default mode" => :default_mode, "bss count" => :bss_count }.freeze
13
+ VERSION_FIELDS = { "software version" => :software_version, "default software version" => :default_software_version,
14
+ "image name" => :image_name }.freeze
15
+ INTEGER_FIELDS = %i[vendor_id license_weight license_type radio_count bss_count].freeze
16
+
17
+ private
18
+
19
+ # 解释当前命令记录,保留不完整行诊断。
20
+ def parse_text(text, complete:)
21
+ records, issues = [], []
22
+ lines = text.lines
23
+ model = section = nil
24
+ mapping = MODEL_FIELDS
25
+ tokens(text, issues).each do |label, raw, line|
26
+ safely(issues, line) do
27
+ if label == "ap model"
28
+ raise ArgumentError, "Missing AP model" unless raw
29
+ model = { model: raw, radios: [], versions: [] }
30
+ records << model
31
+ section, mapping = model, MODEL_FIELDS
32
+ elsif model && (match = /\Aradio (\d+)\z/.match(label)) && raw.nil?
33
+ section = { radio_id: Fields.integer(match[1]).to_s }
34
+ model[:radios] << section
35
+ mapping = RADIO_FIELDS
36
+ elsif model && label == "version support list" && raw.nil?
37
+ section, mapping = nil, VERSION_FIELDS
38
+ elsif model && (match = /\Ahardware version (.+)\z/.match(label)) && raw.nil?
39
+ section = { hardware_version: lines[line - 1].strip[/\AHardware version (.+?)\s*:/i, 1] }
40
+ model[:versions] << section
41
+ mapping = VERSION_FIELDS
42
+ elsif section && (field = mapping[label])
43
+ raise ArgumentError, "Duplicate model field" if section.key?(field)
44
+ section[field] = model_value(field, raw)
45
+ else
46
+ issues << { code: :unrecognized_field, line: line }
47
+ end
48
+ end
49
+ end
50
+ records.each { |record| validate_model(record, issues) }
51
+ Fields.result(records, issues: issues, complete: complete, total: count(text, "AP models"), identity: [ :model ])
52
+ end
53
+
54
+ # 根据声明字段类型归一化型号能力值。
55
+ def model_value(field, raw)
56
+ return Fields.integer(raw) if INTEGER_FIELDS.include?(field)
57
+ return raw&.split(/\s*,\s*/) if field == :modes
58
+
59
+ field == :type ? raw&.sub(/\A:\s*/, "") : raw
60
+ end
61
+
62
+ # 输出可作为权威事实的型号能力前,先校验其有效性。
63
+ def validate_model(record, issues)
64
+ radios = record[:radios]
65
+ if record[:radio_count].nil? || record[:radio_count] != radios.size ||
66
+ radios.any? { |radio| %i[modes default_mode bss_count].any? { |key| radio[key].nil? } }
67
+ issues << { code: :incomplete_model, model: record[:model] }
68
+ end
69
+ if record[:versions].any? { |version| VERSION_FIELDS.values.any? { |key| version[key].nil? } }
70
+ issues << { code: :incomplete_version, model: record[:model] }
71
+ end
72
+ [ [ radios, :radio_id ], [ record[:versions], :hardware_version ] ].each do |rows, key|
73
+ issues << { code: :duplicate_identity, model: record[:model] } if rows.map { |row| row[key] }.uniq.size != rows.size
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../session_parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # 同时接受以 BSSID 或 AP 名称开头的 BSS 块。
9
+ class DisplayWlanBssAllVerbose < SessionParser
10
+ MAPPING = COMMON_FIELDS.merge("service template" => :profile, "service template name" => :profile,
11
+ "state" => :operational_status, "client count" => :client_count, "number of clients" => :client_count,
12
+ "online clients" => :client_count).freeze
13
+ SECURITY = [ "security mode", "akm mode", "cipher suite", "bss id", "user authentication mode", "tx bssid", "ft status", "btm status", "mlo status" ].freeze
14
+ IDENTITY = [ :bssid ].freeze
15
+ COUNT = "BSSs?".freeze
16
+
17
+ private
18
+
19
+ # 返回当前命令格式中用于开始新记录的字段。
20
+ def start_label(fields)
21
+ first = fields.find { |label,| label == "bssid" || label == "ap name" }
22
+ "ap name" if first&.first == "ap name"
23
+ end
24
+
25
+ # 要求具备关联当前记录所需的稳定身份字段。
26
+ def valid_identity?(record)
27
+ super && record[:ap_name] && !record[:ap_name].strip.empty? && !record[:radio_id].nil?
28
+ end
29
+
30
+ # 保留 BSS 安全字段,其余字段委派给共享会话解析器。
31
+ def assign_session_field(record, label, raw, issues, line)
32
+ if SECURITY.include?(label)
33
+ (record[:security] ||= {})[label.tr(" -", "__").to_sym] = raw
34
+ else
35
+ super
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # 解析明确的阻断客户端记录及空列表证据。
9
+ class DisplayWlanClientSecurityBlockMac < Parser
10
+ private
11
+
12
+ # 解释当前命令记录,保留不完整行诊断。
13
+ def parse_text(text, complete:)
14
+ records, issues = [], []
15
+ text.lines.each_with_index do |line, index|
16
+ row = line.strip
17
+ next if row.empty? || /\A[-=]+\z/.match?(row) || /\A(?:No blocked clients|MAC address\b.*|(?:Client )?block list|Blocked clients|Total.*:\s*\d+)\z/i.match?(row)
18
+ if (match = /\A((?:[0-9a-f]{4}-){2}[0-9a-f]{4}|(?:[0-9a-f]{2}:){5}[0-9a-f]{2})\s*(.*?)\z/i.match(row))
19
+ records << { mac_address: Fields.mac(match[1]), reason: match[2] }
20
+ else
21
+ issues << { code: :unrecognized_line, line: index + 1 }
22
+ end
23
+ end
24
+ total = /Total[^\n:]*:\s*(\d+)/i.match(text)&.[](1)&.to_i
25
+ total = 0 if /\ANo blocked clients\s*$/i.match?(text.strip)
26
+ Fields.result(records, issues: issues, complete: complete, total: total, identity: [ :mac_address ])
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../session_parser"
4
+
5
+ module Wireless
6
+ module H3c
7
+ module Parsers
8
+ # 解析客户端会话,区分相对 RSSI 与实测 dBm。
9
+ class DisplayWlanClientVerbose < SessionParser
10
+ MAPPING = COMMON_FIELDS.merge("mac address" => :client_mac, "ipv4 address" => :client_ip,
11
+ "ip address" => :client_ip, "ipv6 address" => :client_ipv6, "username" => :username,
12
+ "channel" => :channel, "frequency band" => :band, "state" => :operational_status, "status" => :operational_status,
13
+ "wireless mode" => :protocol, "channel bandwidth" => :channel_width, "sm power save mode" => :power_save,
14
+ "sm power save" => :power_save, "qos mode" => :qos, "online time" => :uptime_seconds,
15
+ "authentication method" => :auth_method, "security mode" => :security, "akm mode" => :akm,
16
+ "cipher suite" => :cipher).freeze
17
+ DETAILS = [ "authorization car", "authorization acl id", "authorization user profile", "key derivation",
18
+ "forwarding policy name", "ft status", "sae pwe method", "authorization user group name", "authorization dynamic acl name",
19
+ "mlo status", "user authentication mode", "wpa3 status", "pmf status", "block ack", "supported vht-mcs set",
20
+ "supported ht mcs set", "supported rates", "supported he-mcs set", "sleep count", "short gi for 40mhz",
21
+ "short gi for 80mhz", "short gi for 160/80+80mhz", "stbc tx capability", "beamformee sts capability",
22
+ "number of sounding dimensions", "su beamformee capability", "mu beamformee capability", "5g 40and80mhz channel bandwidth",
23
+ "5g 160mhz channel bandwidth", "5g 80+80mhz channel bandwidth", "20/40 bss coexistence management",
24
+ "2.4g 40mhz channel bandwidth", "short gi for 20mhz", "stbc rx capability", "ldpc rx capability",
25
+ "ofdma random access rus", "twt scheduled", "listen interval", "aid", "roam status", "btm mode", "radio slice(last effecive)", "speed" ].freeze
26
+ IDENTITY = [ :client_mac ].freeze
27
+ COUNT = "clients".freeze
28
+
29
+ private
30
+
31
+ # 归一化成对客户端速率,区分相对 RSSI 与实测 dBm。
32
+ def assign_session_field(record, label, raw, issues, line)
33
+ case label
34
+ when "rx/tx rate"
35
+ safely(issues, line) { record[:rx_rate_mbps], record[:tx_rate_mbps] = Fields.rates(raw) }
36
+ when "rssi"
37
+ safely(issues, line) do
38
+ signal = Fields.number(raw, unit: "dBm")
39
+ record[:signal_dbm] = signal if signal&.negative?
40
+ record[:rssi] = signal.round if signal&.positive?
41
+ end
42
+ else
43
+ super
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end