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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +22 -0
- data/README-zh.md +103 -0
- data/README.md +103 -0
- data/docs/architecture-zh.md +92 -0
- data/docs/release.md +13 -0
- data/examples/offline.rb +29 -0
- data/lib/wireless/ap_session.rb +119 -0
- data/lib/wireless/collector.rb +168 -0
- data/lib/wireless/configuration.rb +22 -0
- data/lib/wireless/coverage.rb +60 -0
- data/lib/wireless/fields.rb +131 -0
- data/lib/wireless/h3c/authentication_parser.rb +25 -0
- data/lib/wireless/h3c/command_output.rb +29 -0
- data/lib/wireless/h3c/configuration.rb +109 -0
- data/lib/wireless/h3c/parser.rb +13 -0
- data/lib/wireless/h3c/parsers/display_current_configuration.rb +246 -0
- data/lib/wireless/h3c/parsers/display_dot1x_connection.rb +13 -0
- data/lib/wireless/h3c/parsers/display_lldp_neighbor_information_verbose.rb +83 -0
- data/lib/wireless/h3c/parsers/display_mac_authentication_connection.rb +13 -0
- data/lib/wireless/h3c/parsers/display_wlan_ap_all_verbose.rb +80 -0
- data/lib/wireless/h3c/parsers/display_wlan_ap_model_all.rb +79 -0
- data/lib/wireless/h3c/parsers/display_wlan_bss_all_verbose.rb +41 -0
- data/lib/wireless/h3c/parsers/display_wlan_client_security_block_mac.rb +31 -0
- data/lib/wireless/h3c/parsers/display_wlan_client_verbose.rb +49 -0
- data/lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb +50 -0
- data/lib/wireless/h3c/profile.rb +45 -0
- data/lib/wireless/h3c/session_parser.rb +58 -0
- data/lib/wireless/h3c/verbose_parser.rb +82 -0
- data/lib/wireless/h3c_runtime.rb +18 -0
- data/lib/wireless/huawei_runtime.rb +65 -0
- data/lib/wireless/neighbor_parser.rb +25 -0
- data/lib/wireless/observation.rb +28 -0
- data/lib/wireless/parser_registry.rb +9 -0
- data/lib/wireless/projection.rb +16 -0
- data/lib/wireless/runtime_parser.rb +47 -0
- data/lib/wireless/snapshot.rb +37 -0
- data/lib/wireless/version.rb +6 -0
- data/lib/wireless.rb +18 -0
- 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,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
|