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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README-zh.md +37 -2
- data/README.md +34 -3
- data/docs/architecture-zh.md +4 -4
- data/lib/wireless/ap_session.rb +113 -52
- data/lib/wireless/collector.rb +49 -36
- data/lib/wireless/configuration.rb +0 -2
- data/lib/wireless/coverage.rb +15 -5
- data/lib/wireless/fields.rb +25 -20
- data/lib/wireless/h3c/{authentication_parser.rb → authentication.rb} +4 -4
- data/lib/wireless/h3c/command_output.rb +44 -3
- data/lib/wireless/h3c/configuration.rb +21 -5
- data/lib/wireless/h3c/document.rb +32 -0
- data/lib/wireless/h3c/parsers/display_current_configuration.rb +143 -69
- data/lib/wireless/h3c/parsers/display_dot1x_connection.rb +4 -2
- data/lib/wireless/h3c/parsers/display_lldp_neighbor_information_verbose.rb +34 -10
- data/lib/wireless/h3c/parsers/display_mac_authentication_connection.rb +4 -2
- data/lib/wireless/h3c/parsers/display_wlan_ap_all_verbose.rb +37 -11
- data/lib/wireless/h3c/parsers/display_wlan_ap_model_all.rb +5 -5
- data/lib/wireless/h3c/parsers/display_wlan_bss_all_verbose.rb +6 -6
- data/lib/wireless/h3c/parsers/display_wlan_client_security_block_mac.rb +2 -2
- data/lib/wireless/h3c/parsers/display_wlan_client_verbose.rb +14 -8
- data/lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb +2 -2
- data/lib/wireless/h3c/profile.rb +6 -3
- data/lib/wireless/h3c/record_parser.rb +138 -0
- data/lib/wireless/huawei/parser.rb +86 -0
- data/lib/wireless/huawei/parsers/display_ap_all.rb +14 -0
- data/lib/wireless/huawei/parsers/display_station_all.rb +14 -0
- data/lib/wireless/huawei/profile.rb +22 -0
- data/lib/wireless/observation.rb +2 -0
- data/lib/wireless/parser_registry.rb +2 -1
- data/lib/wireless/projection.rb +4 -0
- data/lib/wireless/runtime_parser.rb +16 -11
- data/lib/wireless/snapshot.rb +2 -1
- data/lib/wireless/terminal.rb +32 -0
- data/lib/wireless/version.rb +1 -1
- data/lib/wireless.rb +0 -2
- metadata +9 -20
- data/lib/wireless/h3c/session_parser.rb +0 -58
- data/lib/wireless/h3c/verbose_parser.rb +0 -82
- data/lib/wireless/h3c_runtime.rb +0 -18
- data/lib/wireless/huawei_runtime.rb +0 -65
|
@@ -16,12 +16,12 @@ module Wireless
|
|
|
16
16
|
row = line.strip
|
|
17
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
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.
|
|
19
|
+
records << { mac_address: Fields.mac_address(match[1]), reason: match[2] }
|
|
20
20
|
else
|
|
21
21
|
issues << { code: :unrecognized_line, line: index + 1 }
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
|
-
total =
|
|
24
|
+
total = reported_total(text, noun: "(?:blocked clients|clients)", issues: issues)
|
|
25
25
|
total = 0 if /\ANo blocked clients\s*$/i.match?(text.strip)
|
|
26
26
|
Fields.result(records, issues: issues, complete: complete, total: total, identity: [ :mac_address ])
|
|
27
27
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../
|
|
3
|
+
require_relative "../record_parser"
|
|
4
4
|
|
|
5
5
|
module Wireless
|
|
6
6
|
module H3c
|
|
7
7
|
module Parsers
|
|
8
8
|
# 解析客户端会话,区分相对 RSSI 与实测 dBm。
|
|
9
|
-
class DisplayWlanClientVerbose <
|
|
9
|
+
class DisplayWlanClientVerbose < RecordParser
|
|
10
10
|
MAPPING = COMMON_FIELDS.merge("mac address" => :client_mac, "ipv4 address" => :client_ip,
|
|
11
11
|
"ip address" => :client_ip, "ipv6 address" => :client_ipv6, "username" => :username,
|
|
12
12
|
"channel" => :channel, "frequency band" => :band, "state" => :operational_status, "status" => :operational_status,
|
|
13
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" => :
|
|
14
|
+
"sm power save" => :power_save, "qos mode" => :qos, "online time" => :online_seconds,
|
|
15
15
|
"authentication method" => :auth_method, "security mode" => :security, "akm mode" => :akm,
|
|
16
16
|
"cipher suite" => :cipher).freeze
|
|
17
17
|
DETAILS = [ "authorization car", "authorization acl id", "authorization user profile", "key derivation",
|
|
@@ -29,15 +29,21 @@ module Wireless
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
31
|
# 归一化成对客户端速率,区分相对 RSSI 与实测 dBm。
|
|
32
|
-
def
|
|
32
|
+
def assign_record_field(record, label, raw, issues, line)
|
|
33
33
|
case label
|
|
34
34
|
when "rx/tx rate"
|
|
35
|
-
|
|
35
|
+
parse_field(issues, line) do
|
|
36
|
+
rx, tx = Fields.rates_mbps(raw)
|
|
37
|
+
assign_value(record, :rx_rate_mbps, rx, issues, line)
|
|
38
|
+
assign_value(record, :tx_rate_mbps, tx, issues, line)
|
|
39
|
+
end
|
|
36
40
|
when "rssi"
|
|
37
|
-
|
|
41
|
+
parse_field(issues, line) do
|
|
38
42
|
signal = Fields.number(raw, unit: "dBm")
|
|
39
|
-
record
|
|
40
|
-
record
|
|
43
|
+
assign_value(record, :signal_dbm, signal&.negative? ? signal : nil, issues, line)
|
|
44
|
+
assign_value(record, :rssi, signal&.positive? ? signal.round : nil, issues, line)
|
|
45
|
+
record.delete(:signal_dbm) if record[:signal_dbm].nil?
|
|
46
|
+
record.delete(:rssi) if record[:rssi].nil?
|
|
41
47
|
end
|
|
42
48
|
else
|
|
43
49
|
super
|
|
@@ -41,8 +41,8 @@ module Wireless
|
|
|
41
41
|
month, day, hour, minute, second = match[6].split(/[- :]/).map(&:to_i)
|
|
42
42
|
raise ArgumentError, "Invalid conflict time" unless Date.valid_date?(2000, month, day) && hour < 24 && minute < 60 && second < 60
|
|
43
43
|
|
|
44
|
-
{ ip_address: Fields.
|
|
45
|
-
old_mac_address: Fields.
|
|
44
|
+
{ ip_address: Fields.ip_address(match[1]), new_mac_address: Fields.mac_address(match[2]), new_ap_id: Fields.integer(match[3]).to_s,
|
|
45
|
+
old_mac_address: Fields.mac_address(match[4]), old_ap_id: Fields.integer(match[5]).to_s, occurred_at: match[6] }
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
end
|
data/lib/wireless/h3c/profile.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Wireless
|
|
|
16
16
|
module H3c
|
|
17
17
|
# 声明 H3C 控制器及 AP 策略,共享回显仅解析一次后再投影维度。
|
|
18
18
|
class Profile < CtxParser::Profile
|
|
19
|
-
|
|
19
|
+
DECLARATIONS = CtxParser::Commands.new([
|
|
20
20
|
{ dimensions: [ :client_ip_conflicts ], command: "display wlan statistics client-ip-conflict", parser: Parsers::DisplayWlanStatisticsClientIpConflict },
|
|
21
21
|
{ dimensions: [ :ap_models ], command: "display wlan ap-model all", parser: Parsers::DisplayWlanApModelAll },
|
|
22
22
|
{ dimensions: %i[access_points radios], command: "display wlan ap all verbose", parser: Parsers::DisplayWlanApAllVerbose },
|
|
@@ -28,8 +28,11 @@ module Wireless
|
|
|
28
28
|
{ dimensions: [ :blocked_clients ], command: "display wlan client-security block-mac", parser: Parsers::DisplayWlanClientSecurityBlockMac },
|
|
29
29
|
{ dimensions: [ :lldp ], command: "display lldp neighbor-information verbose", parser: Parsers::DisplayLldpNeighborInformationVerbose }
|
|
30
30
|
])
|
|
31
|
-
COMMANDS =
|
|
32
|
-
PARSERS =
|
|
31
|
+
COMMANDS = DECLARATIONS.by_dimension
|
|
32
|
+
PARSERS = DECLARATIONS.parsers
|
|
33
|
+
|
|
34
|
+
# 本平台在当前控制器会话禁用分页。
|
|
35
|
+
def preparation_command = "screen-length disable"
|
|
33
36
|
|
|
34
37
|
private
|
|
35
38
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser"
|
|
4
|
+
|
|
5
|
+
module Wireless
|
|
6
|
+
module H3c
|
|
7
|
+
# 按字段标签划分 H3C 详细记录,并共享字段转换与诊断。
|
|
8
|
+
class RecordParser < Parser
|
|
9
|
+
COMMON_FIELDS = { "ap name" => :ap_name, "ap id" => :ap_id, "radio id" => :radio_id,
|
|
10
|
+
"ssid" => :ssid, "bssid" => :bssid, "vlan id" => :vlan, "service vlan id" => :vlan }.freeze
|
|
11
|
+
DETAILS = [].freeze
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# 各命令声明记录身份与字段映射,嵌套记录可覆盖此入口。
|
|
16
|
+
def parse_text(text, complete:)
|
|
17
|
+
issues, records = [], []
|
|
18
|
+
fields = read_fields(text, issues)
|
|
19
|
+
start_label = record_start_label(fields)
|
|
20
|
+
current = nil
|
|
21
|
+
fields.each do |label, raw, line|
|
|
22
|
+
if start_label ? label == start_label : self.class::MAPPING[label] == self.class::IDENTITY.first
|
|
23
|
+
current = {}
|
|
24
|
+
records << current
|
|
25
|
+
end
|
|
26
|
+
unless current
|
|
27
|
+
issues << { code: :missing_identity, line: line }
|
|
28
|
+
next
|
|
29
|
+
end
|
|
30
|
+
assign_record_field(current, label, raw, issues, line)
|
|
31
|
+
end
|
|
32
|
+
records.reject! do |record|
|
|
33
|
+
invalid = !valid_identity?(record)
|
|
34
|
+
issues << { code: :invalid_identity } if invalid
|
|
35
|
+
invalid
|
|
36
|
+
end
|
|
37
|
+
Fields.result(records, issues: issues, complete: complete, total: record_count(text, self.class::COUNT, issues), identity: self.class::IDENTITY)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 返回字段标签、原始值与来源行号,保留明确允许的续行。
|
|
41
|
+
def read_fields(text, issues)
|
|
42
|
+
text.lines.each_with_index.each_with_object([]) do |(line, index), fields|
|
|
43
|
+
row = line.strip
|
|
44
|
+
next if row.empty? || /\A[-=]+\z/.match?(row)
|
|
45
|
+
if (match = /\A([^:]+?)\s*:\s*(.*)\z/.match(row))
|
|
46
|
+
label = match[1].downcase.gsub(/\s+/, " ")
|
|
47
|
+
next if /\A(?:total|number of aps|connected|remaining|maximum|current number|(?:local|server|sync) ap licenses|(?:current|max|bound) bss number)\b/.match?(label)
|
|
48
|
+
fields << [ label, Fields.value(match[2]), index + 1, line[/\A\s*/].size ]
|
|
49
|
+
elsif (match = /\A:\s*(\S.*)\z/.match(row)) && fields.last&.first == "poe status"
|
|
50
|
+
fields.last[1] = [ fields.last[1], match[1] ].compact.join("\n")
|
|
51
|
+
elsif /\A(?:NSS\d+\s+[\d, ]+|TID \d+\s+(?:In|Out|Both)|[\d., ]+(?:Mbps)?)\z/.match?(row)
|
|
52
|
+
next
|
|
53
|
+
else
|
|
54
|
+
issues << { code: :unrecognized_line, line: index + 1 }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 更新当前记录前,先应用字段专用转换。
|
|
60
|
+
def assign_field(record, mapping, label, raw, issues, line, details:)
|
|
61
|
+
if record && (field = mapping[label])
|
|
62
|
+
parse_field(issues, line) do
|
|
63
|
+
# 优先使用运行带宽;80+80MHz 等歧义值保持原义,不截断为单个数值。
|
|
64
|
+
if field == :channel_width
|
|
65
|
+
label = "channel bandwidth" if label == "channel band-width"
|
|
66
|
+
assign_value(record[:details] ||= {}, label.tr(" ", "_").to_sym, raw, issues, line)
|
|
67
|
+
return if label == "channel bandwidth" && @field_assignments.fetch(record[:details]).key?(:operating_bandwidth)
|
|
68
|
+
end
|
|
69
|
+
assign_value(record, field, convert_field(field, raw, label: label), issues, line, identity: field == :channel_width ? label : field)
|
|
70
|
+
end
|
|
71
|
+
elsif details
|
|
72
|
+
value = label.end_with?("url") ? !raw.nil? && !raw.strip.empty? : raw
|
|
73
|
+
assign_value(details[:details] ||= {}, label.tr(" -", "__").to_sym, value, issues, line)
|
|
74
|
+
else
|
|
75
|
+
issues << { code: :unrecognized_field, line: line }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# 按声明的归一化规则转换已知字段。
|
|
80
|
+
def convert_field(field, raw, label:)
|
|
81
|
+
case field
|
|
82
|
+
when :mac_address, :client_mac, :bssid then Fields.mac_address(raw)
|
|
83
|
+
when :management_ip, :client_ip, :client_ipv6, :ip_address, :ipv6_address
|
|
84
|
+
address = Fields.ip_address(raw)
|
|
85
|
+
if address && ((label == "ipv4 address" && address.include?(":")) || (label == "ipv6 address" && !address.include?(":")))
|
|
86
|
+
raise ArgumentError, "Invalid wireless address family"
|
|
87
|
+
end
|
|
88
|
+
address
|
|
89
|
+
when :ap_id, :radio_id then Fields.integer(raw)&.to_s
|
|
90
|
+
when :client_count, :online_seconds, :uptime_seconds
|
|
91
|
+
%i[online_seconds uptime_seconds].include?(field) ? Fields.duration_seconds(raw) : Fields.integer(raw)
|
|
92
|
+
when :vlan, :initial_vlan_id, :authorized_vlan_id
|
|
93
|
+
vlan = Fields.integer(raw, max: 4094)
|
|
94
|
+
raise ArgumentError, "Invalid wireless VLAN" if vlan && vlan.zero?
|
|
95
|
+
vlan
|
|
96
|
+
when :channel then Fields.channel(raw)
|
|
97
|
+
when :channel_width then Fields.channel_width_mhz(raw)
|
|
98
|
+
when :band then Fields.frequency_band(raw)
|
|
99
|
+
when :noise_dbm then Fields.number(raw, unit: "dBm")&.then { |number| number if number.negative? }
|
|
100
|
+
when :utilization
|
|
101
|
+
number = Fields.number(raw, unit: "%")
|
|
102
|
+
raise ArgumentError, "Invalid wireless utilization" if number && !(0..100).cover?(number)
|
|
103
|
+
number&.round
|
|
104
|
+
else raw
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 解析非负计数,同时保留未知值。
|
|
109
|
+
def record_count(text, noun, issues)
|
|
110
|
+
reported_total(text, noun: noun, issues: issues)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 隔离单个格式错误字段,同时保留无关记录数据。
|
|
114
|
+
def parse_field(issues, line)
|
|
115
|
+
yield
|
|
116
|
+
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
117
|
+
issues << { code: :invalid_field, line: line }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 默认以身份字段开始新记录,命令可声明其他起始标签。
|
|
121
|
+
def record_start_label(fields)
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# 要求具备关联当前记录所需的稳定身份字段。
|
|
126
|
+
def valid_identity?(record)
|
|
127
|
+
value = record[self.class::IDENTITY.first]
|
|
128
|
+
value && !value.strip.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 将命令字段归一化到当前记录,并保留显式允许的详情。
|
|
132
|
+
def assign_record_field(record, label, raw, issues, line)
|
|
133
|
+
details = self.class::DETAILS.include?(label) ? record : nil
|
|
134
|
+
assign_field(record, self.class::MAPPING, label, raw, issues, line, details: details)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../fields"
|
|
4
|
+
require_relative "../terminal"
|
|
5
|
+
require "ctx_parser/parser"
|
|
6
|
+
|
|
7
|
+
module Wireless
|
|
8
|
+
module Huawei
|
|
9
|
+
# 仅支持有证据的 Huawei 表格格式,区分不支持维度与空清单。
|
|
10
|
+
class Parser < CtxParser::Parser
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# 标准解析边界负责参数校验,子类只选择对应命令的维度。
|
|
14
|
+
def parse_output(text, complete:)
|
|
15
|
+
name = self.class::DIMENSION
|
|
16
|
+
text = Fields.text(text)
|
|
17
|
+
if Terminal.unsupported?(text)
|
|
18
|
+
return CtxParser::Result.new(status: :unsupported, issues: [ { code: :unsupported_command } ])
|
|
19
|
+
end
|
|
20
|
+
if (issue = Terminal.issue(text))
|
|
21
|
+
return CtxParser::Result.new(status: :partial, issues: [ issue ])
|
|
22
|
+
end
|
|
23
|
+
records, issues = [], []
|
|
24
|
+
header = false
|
|
25
|
+
total = nil
|
|
26
|
+
text.lines.each_with_index do |line, index|
|
|
27
|
+
row = line.strip
|
|
28
|
+
next if row.empty? || /\A[-=]+\z/.match?(row)
|
|
29
|
+
if (match = /\ATotal\s*:\s*(\d+)\z/i.match(row))
|
|
30
|
+
reported = match[1].to_i
|
|
31
|
+
issues << { code: :conflicting_total, line: index + 1 } if total && total != reported
|
|
32
|
+
total ||= reported
|
|
33
|
+
elsif name == :access_points ? /\AID\s+MAC\s+Name\s+Group\s+IP\s+Type\s+State\s+STA\s+Uptime\b/i.match?(row) : /\ASTA MAC\s+AP ID\s+Ap name\s+Rf\/WLAN\b/i.match?(row)
|
|
34
|
+
header = true
|
|
35
|
+
elsif /\ATotal AP information:|\A(?:nor|fault|idle|stdby)\s*:/i.match?(row)
|
|
36
|
+
next
|
|
37
|
+
elsif !header
|
|
38
|
+
issues << { code: :missing_header, line: index + 1 }
|
|
39
|
+
else
|
|
40
|
+
begin
|
|
41
|
+
records << (name == :access_points ? ap(row.split) : client(row.split))
|
|
42
|
+
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
43
|
+
issues << { code: :invalid_row, line: index + 1 }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
issues << { code: :missing_header } unless header
|
|
48
|
+
result = Fields.result(records, issues: issues, complete: complete, total: total, identity: [ name == :access_points ? :name : :client_mac ])
|
|
49
|
+
result
|
|
50
|
+
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
51
|
+
CtxParser::Result.new(status: :failed, issues: [ { code: :invalid_output } ])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# IPv6 专用列不能接受合法但属于 IPv4 的地址。
|
|
57
|
+
def ipv6_address(raw)
|
|
58
|
+
address = Fields.ip_address(raw)
|
|
59
|
+
raise ArgumentError, "Invalid station IPv6 address" if address && !address.include?(":")
|
|
60
|
+
address
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 归一化控制器报告的单条 AP 记录。
|
|
64
|
+
def ap(columns)
|
|
65
|
+
raise ArgumentError, "Invalid AP row" unless columns.size == 9
|
|
66
|
+
id, mac, name, group, ip, model, state, clients, uptime = columns
|
|
67
|
+
{ ap_id: Fields.integer(id).to_s, mac_address: Fields.mac_address(mac), name: name, group_name: group,
|
|
68
|
+
management_ip: Fields.ip_address(Fields.value(ip)), model: model, operational_status: Fields.online_status(state),
|
|
69
|
+
client_count: Fields.integer(clients), uptime_seconds: Fields.duration_seconds(Fields.value(uptime)) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 归一化控制器报告的单条无线客户端记录。
|
|
73
|
+
def client(columns)
|
|
74
|
+
raise ArgumentError, "Invalid station row" unless columns.size == 15
|
|
75
|
+
mac, ap_id, ap_name, radio_wlan, band, protocol, rates, rssi, vlan, ip, ssid, state, ipv6, _ac_id, uptime = columns
|
|
76
|
+
raise ArgumentError, "Invalid station identity" unless /\A\d+\/\d+\z/.match?(radio_wlan)
|
|
77
|
+
rx, tx = Fields.rates_mbps(rates)
|
|
78
|
+
signal = Fields.number(Fields.value(rssi), unit: "dBm")
|
|
79
|
+
{ client_mac: Fields.mac_address(mac), ap_id: Fields.integer(ap_id).to_s, ap_name: ap_name, radio_id: radio_wlan.split("/").first,
|
|
80
|
+
band: Fields.frequency_band(band), protocol: protocol, rx_rate_mbps: rx, tx_rate_mbps: tx, signal_dbm: signal == 0 ? nil : signal,
|
|
81
|
+
vlan: Fields.integer(vlan, max: 4094), client_ip: Fields.ip_address(Fields.value(ip)), ssid: ssid, operational_status: state,
|
|
82
|
+
client_ipv6: ipv6_address(Fields.value(ipv6)), uptime_seconds: Fields.duration_seconds(Fields.value(uptime)) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/profile"
|
|
4
|
+
require_relative "parsers/display_ap_all"
|
|
5
|
+
require_relative "parsers/display_station_all"
|
|
6
|
+
|
|
7
|
+
module Wireless
|
|
8
|
+
module Huawei
|
|
9
|
+
# Huawei 查询通过相同命令声明和参数协议进入采集与解析。
|
|
10
|
+
class Profile < CtxParser::Profile
|
|
11
|
+
DECLARATIONS = CtxParser::Commands.new([
|
|
12
|
+
{ dimensions: [ :access_points ], command: "display ap all", parser: Parsers::DisplayApAll },
|
|
13
|
+
{ dimensions: [ :clients ], command: "display station all", parser: Parsers::DisplayStationAll }
|
|
14
|
+
])
|
|
15
|
+
COMMANDS = DECLARATIONS.by_dimension
|
|
16
|
+
PARSERS = DECLARATIONS.parsers
|
|
17
|
+
|
|
18
|
+
# 本平台使用临时会话分页设置。
|
|
19
|
+
def preparation_command = "screen-length 0 temporary"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/wireless/observation.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Wireless
|
|
|
7
7
|
Observation = Data.define(:name, :status, :records, :issues, :evidence, :started_at, :completed_at) do
|
|
8
8
|
# 冻结单个维度的记录、状态及诊断元数据。
|
|
9
9
|
def initialize(name:, status:, records: [], issues: [], evidence: [], started_at: nil, completed_at: nil)
|
|
10
|
+
raise ArgumentError, "Wireless observation name must be a symbol" unless name.is_a?(Symbol)
|
|
10
11
|
raise ArgumentError, "Invalid wireless observation status" unless %i[complete partial failed unsupported not_requested].include?(status)
|
|
11
12
|
raise ArgumentError, "Wireless records must be hashes" unless records.is_a?(Array) && records.all? { |record| record.is_a?(Hash) }
|
|
12
13
|
raise ArgumentError, "Unavailable wireless observation has records" if %i[failed unsupported not_requested].include?(status) && records.any?
|
|
@@ -24,5 +25,6 @@ module Wireless
|
|
|
24
25
|
def available? = complete? || partial?
|
|
25
26
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
26
27
|
def inspect = "#<#{self.class.name} name=#{name} status=#{status} records=#{records.size}>"
|
|
28
|
+
alias to_s inspect
|
|
27
29
|
end
|
|
28
30
|
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "ctx_parser/registry"
|
|
4
4
|
require_relative "h3c/profile"
|
|
5
|
+
require_relative "huawei/profile"
|
|
5
6
|
|
|
6
7
|
module Wireless
|
|
7
8
|
# 将 h3c_wireless 视为 H3C 无线平台策略的连接器别名。
|
|
8
|
-
ParserRegistry = CtxParser::Registry.new(profiles: { h3c: H3c::Profile }, aliases: { h3c_wireless: :h3c })
|
|
9
|
+
ParserRegistry = CtxParser::Registry.new(profiles: { h3c: H3c::Profile, huawei: Huawei::Profile }, aliases: { h3c_wireless: :h3c })
|
|
9
10
|
end
|
data/lib/wireless/projection.rb
CHANGED
|
@@ -12,5 +12,9 @@ module Wireless
|
|
|
12
12
|
|
|
13
13
|
# 判断所有请求的资产投影是否均已完成。
|
|
14
14
|
def complete? = dimensions.values.all? { |entry| entry.fetch(:status) == :complete }
|
|
15
|
+
|
|
16
|
+
# 展示应用范围,不在日志字符串中序列化应用详情。
|
|
17
|
+
def inspect = "#<#{self.class.name} dimensions=#{dimensions.size}>"
|
|
18
|
+
alias to_s inspect
|
|
15
19
|
end
|
|
16
20
|
end
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "parser_registry"
|
|
4
4
|
require_relative "fields"
|
|
5
|
-
require_relative "huawei_runtime"
|
|
6
5
|
require_relative "observation"
|
|
7
6
|
|
|
8
7
|
module Wireless
|
|
9
|
-
#
|
|
8
|
+
# 按平台解析无线观测,统一保留各维度的完整性与诊断。
|
|
10
9
|
class RuntimeParser
|
|
11
10
|
# 保留型号选择信息,供每次解析所选平台使用。
|
|
12
11
|
def initialize(model: nil)
|
|
13
|
-
|
|
12
|
+
raise ArgumentError, "Device model must be text" unless model.nil? || model.is_a?(String)
|
|
13
|
+
|
|
14
|
+
@model = model&.dup&.freeze
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
# 使用传入平台及保留的型号选择解析单个无线维度。
|
|
@@ -20,21 +21,25 @@ module Wireless
|
|
|
20
21
|
|
|
21
22
|
# 共享命令仅解析一次,再投影各请求维度。
|
|
22
23
|
def parse_many(names, content, platform: "h3c", complete: false)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return unavailable(names, :unsupported, :unsupported_command)
|
|
26
|
-
end
|
|
27
|
-
if platform.to_s == "huawei"
|
|
28
|
-
return names.to_h { |name| [ name, HuaweiRuntime.new.parse(name, text, complete: complete) ] }
|
|
24
|
+
unless names.is_a?(Array) && names.any? && names.all? { |name| name.is_a?(Symbol) } && names.uniq == names
|
|
25
|
+
raise ArgumentError, "Dimensions must be unique symbols"
|
|
29
26
|
end
|
|
27
|
+
raise ArgumentError, "Completeness must be boolean" unless [ true, false ].include?(complete)
|
|
28
|
+
raise ArgumentError, "Parser input must be text" unless content.is_a?(String)
|
|
30
29
|
|
|
31
30
|
profile = ParserRegistry.fetch(platform, model: @model)
|
|
31
|
+
begin
|
|
32
|
+
text = Fields.text(content, newlines: :preserve)
|
|
33
|
+
rescue ArgumentError
|
|
34
|
+
return unavailable(names, :failed, :invalid_output)
|
|
35
|
+
end
|
|
36
|
+
if Terminal.unsupported?(text)
|
|
37
|
+
return unavailable(names, :unsupported, :unsupported_command)
|
|
38
|
+
end
|
|
32
39
|
supported, unsupported = names.partition { |name| profile.commands.key?(name) }
|
|
33
40
|
results = supported.empty? ? {} : profile.parse_many(supported, text, complete: complete)
|
|
34
41
|
observations = results.to_h { |name, result| [ name, Observation.new(name: name, **result.to_h) ] }
|
|
35
42
|
observations.merge(unavailable(unsupported, :unsupported, :unsupported_platform_dimension))
|
|
36
|
-
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
37
|
-
unavailable(names, :failed, :invalid_output)
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
private
|
data/lib/wireless/snapshot.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Wireless
|
|
|
16
16
|
observed_at.is_a?(Time)
|
|
17
17
|
raise ArgumentError, "Invalid wireless snapshot"
|
|
18
18
|
end
|
|
19
|
-
super(target: CtxParser::Immutable.copy(target), dimensions: dimensions.
|
|
19
|
+
super(target: CtxParser::Immutable.copy(target), dimensions: dimensions.to_h { |name, value| [ name, value ] }.freeze,
|
|
20
20
|
observed_at: observed_at.dup.freeze)
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -33,5 +33,6 @@ module Wireless
|
|
|
33
33
|
|
|
34
34
|
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
35
35
|
def inspect = "#<#{self.class.name} complete=#{complete?}>"
|
|
36
|
+
alias to_s inspect
|
|
36
37
|
end
|
|
37
38
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/banner"
|
|
4
|
+
|
|
5
|
+
module Wireless
|
|
6
|
+
# 分页及终端编辑控制会破坏事实边界;正常换行和制表符仍是来源文本。
|
|
7
|
+
module Terminal
|
|
8
|
+
INVALID = /--[ \t]*More[ \t]*--|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/i
|
|
9
|
+
private_constant :INVALID
|
|
10
|
+
|
|
11
|
+
UNSUPPORTED = /(?:\A|(?<=[\r\n]))[ \t]*(?:%[ \t]*)?(?:Unrecognized command\b|Wrong parameter\b|Invalid (?:input|command)\b|Unknown command\b|Error:[^\r\n]*\bcommand\b)[^\r\n]*/i
|
|
12
|
+
|
|
13
|
+
private_constant :UNSUPPORTED
|
|
14
|
+
|
|
15
|
+
# 错误响应须独占来源行;横幅正文不是设备命令诊断。
|
|
16
|
+
def self.unsupported?(text)
|
|
17
|
+
banner = CtxParser::Banner.new
|
|
18
|
+
text.split(/\r\n?|\n/).any? { |line| !banner.consume(line.strip) && UNSUPPORTED.match?(line) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 在字段清理之前检测终端残留,避免将编辑后的文本接受为事实。
|
|
22
|
+
def self.invalid_output?(text) = INVALID.match?(text)
|
|
23
|
+
|
|
24
|
+
# 运行回显无可恢复的来源树,返回首个受损来源行的安全诊断。
|
|
25
|
+
def self.issue(text)
|
|
26
|
+
return unless invalid_output?(text)
|
|
27
|
+
|
|
28
|
+
line = text.split(/\r\n?|\n/).find_index { |source| invalid_output?(source) }
|
|
29
|
+
{ code: :invalid_terminal_output, line: line + 1 }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/wireless/version.rb
CHANGED
data/lib/wireless.rb
CHANGED
|
@@ -12,7 +12,5 @@ require_relative "wireless/projection"
|
|
|
12
12
|
require_relative "wireless/coverage"
|
|
13
13
|
require_relative "wireless/configuration"
|
|
14
14
|
require_relative "wireless/runtime_parser"
|
|
15
|
-
require_relative "wireless/h3c_runtime"
|
|
16
15
|
require_relative "wireless/neighbor_parser"
|
|
17
|
-
require_relative "wireless/ap_session"
|
|
18
16
|
require_relative "wireless/collector"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wireless-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -51,20 +51,6 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: net-connector
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0'
|
|
68
54
|
- !ruby/object:Gem::Dependency
|
|
69
55
|
name: time
|
|
70
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -98,9 +84,10 @@ files:
|
|
|
98
84
|
- lib/wireless/configuration.rb
|
|
99
85
|
- lib/wireless/coverage.rb
|
|
100
86
|
- lib/wireless/fields.rb
|
|
101
|
-
- lib/wireless/h3c/
|
|
87
|
+
- lib/wireless/h3c/authentication.rb
|
|
102
88
|
- lib/wireless/h3c/command_output.rb
|
|
103
89
|
- lib/wireless/h3c/configuration.rb
|
|
90
|
+
- lib/wireless/h3c/document.rb
|
|
104
91
|
- lib/wireless/h3c/parser.rb
|
|
105
92
|
- lib/wireless/h3c/parsers/display_current_configuration.rb
|
|
106
93
|
- lib/wireless/h3c/parsers/display_dot1x_connection.rb
|
|
@@ -113,16 +100,18 @@ files:
|
|
|
113
100
|
- lib/wireless/h3c/parsers/display_wlan_client_verbose.rb
|
|
114
101
|
- lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb
|
|
115
102
|
- lib/wireless/h3c/profile.rb
|
|
116
|
-
- lib/wireless/h3c/
|
|
117
|
-
- lib/wireless/
|
|
118
|
-
- lib/wireless/
|
|
119
|
-
- lib/wireless/
|
|
103
|
+
- lib/wireless/h3c/record_parser.rb
|
|
104
|
+
- lib/wireless/huawei/parser.rb
|
|
105
|
+
- lib/wireless/huawei/parsers/display_ap_all.rb
|
|
106
|
+
- lib/wireless/huawei/parsers/display_station_all.rb
|
|
107
|
+
- lib/wireless/huawei/profile.rb
|
|
120
108
|
- lib/wireless/neighbor_parser.rb
|
|
121
109
|
- lib/wireless/observation.rb
|
|
122
110
|
- lib/wireless/parser_registry.rb
|
|
123
111
|
- lib/wireless/projection.rb
|
|
124
112
|
- lib/wireless/runtime_parser.rb
|
|
125
113
|
- lib/wireless/snapshot.rb
|
|
114
|
+
- lib/wireless/terminal.rb
|
|
126
115
|
- lib/wireless/version.rb
|
|
127
116
|
homepage: https://github.com/gatework/wireless-rb
|
|
128
117
|
licenses:
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "verbose_parser"
|
|
4
|
-
|
|
5
|
-
module Wireless
|
|
6
|
-
module H3c
|
|
7
|
-
# 复用详细会话边界,各命令独立声明其字段结构。
|
|
8
|
-
class SessionParser < VerboseParser
|
|
9
|
-
COMMON_FIELDS = { "ap name" => :ap_name, "ap id" => :ap_id, "radio id" => :radio_id,
|
|
10
|
-
"ssid" => :ssid, "bssid" => :bssid, "vlan id" => :vlan, "service vlan id" => :vlan }.freeze
|
|
11
|
-
DETAILS = [].freeze
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
# 解释当前命令记录,保留不完整行诊断。
|
|
16
|
-
def parse_text(text, complete:)
|
|
17
|
-
issues, records = [], []
|
|
18
|
-
fields = tokens(text, issues)
|
|
19
|
-
start = start_label(fields)
|
|
20
|
-
current = nil
|
|
21
|
-
fields.each do |label, raw, line|
|
|
22
|
-
if start ? label == start : self.class::MAPPING[label] == self.class::IDENTITY.first
|
|
23
|
-
current = {}
|
|
24
|
-
records << current
|
|
25
|
-
end
|
|
26
|
-
unless current
|
|
27
|
-
issues << { code: :missing_identity, line: line }
|
|
28
|
-
next
|
|
29
|
-
end
|
|
30
|
-
assign_session_field(current, label, raw, issues, line)
|
|
31
|
-
end
|
|
32
|
-
records.reject! do |record|
|
|
33
|
-
invalid = !valid_identity?(record)
|
|
34
|
-
issues << { code: :invalid_identity } if invalid
|
|
35
|
-
invalid
|
|
36
|
-
end
|
|
37
|
-
Fields.result(records, issues: issues, complete: complete, total: count(text, self.class::COUNT), identity: self.class::IDENTITY)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# 返回当前命令格式中用于开始新记录的字段。
|
|
41
|
-
def start_label(fields)
|
|
42
|
-
nil
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# 要求具备关联当前记录所需的稳定身份字段。
|
|
46
|
-
def valid_identity?(record)
|
|
47
|
-
value = record[self.class::IDENTITY.first]
|
|
48
|
-
value && !value.strip.empty?
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# 将单个认证会话字段归一化到目标记录。
|
|
52
|
-
def assign_session_field(record, label, raw, issues, line)
|
|
53
|
-
details = self.class::DETAILS.include?(label) ? record : nil
|
|
54
|
-
assign_field(record, self.class::MAPPING, label, raw, issues, line, details: details)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|