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,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require_relative "../parser"
|
|
5
|
+
|
|
6
|
+
module Wireless
|
|
7
|
+
module H3c
|
|
8
|
+
module Parsers
|
|
9
|
+
# 解析冲突事件,不虚构设备时间戳的年份或时区。
|
|
10
|
+
class DisplayWlanStatisticsClientIpConflict < Parser
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# 解释当前命令记录,保留不完整行诊断。
|
|
14
|
+
def parse_text(text, complete:)
|
|
15
|
+
records, issues = [], []
|
|
16
|
+
header = false
|
|
17
|
+
text.lines.each_with_index do |line, index|
|
|
18
|
+
row = line.strip
|
|
19
|
+
next if row.empty? || /\A[-=]+\z/.match?(row)
|
|
20
|
+
if /\AIP\s+New-MAC\/APID\s+Old-MAC\/APID\s+Time\z/i.match?(row)
|
|
21
|
+
header = true
|
|
22
|
+
next
|
|
23
|
+
end
|
|
24
|
+
begin
|
|
25
|
+
records << parse_row(row)
|
|
26
|
+
rescue ArgumentError, IPAddr::Error
|
|
27
|
+
issues << { code: :invalid_row, line: index + 1 }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
# 本命令没有总数尾行;采集完整且只有表头时表示空表。
|
|
31
|
+
total = 0 if header && records.empty? && issues.empty?
|
|
32
|
+
Fields.result(records, issues: issues, complete: complete, total: total,
|
|
33
|
+
identity: %i[ip_address new_mac_address new_ap_id old_mac_address old_ap_id occurred_at])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 按当前命令的身份与字段布局解析单条表格记录。
|
|
37
|
+
def parse_row(row)
|
|
38
|
+
match = /\A(\S+)\s+(\S+)\/(\d+)\s+(\S+)\/(\d+)\s+(\d{2}-\d{2} \d{2}:\d{2}:\d{2})\z/.match(row)
|
|
39
|
+
raise ArgumentError, "Invalid conflict row" unless match
|
|
40
|
+
|
|
41
|
+
month, day, hour, minute, second = match[6].split(/[- :]/).map(&:to_i)
|
|
42
|
+
raise ArgumentError, "Invalid conflict time" unless Date.valid_date?(2000, month, day) && hour < 24 && minute < 60 && second < 60
|
|
43
|
+
|
|
44
|
+
{ ip_address: Fields.ip(match[1]), new_mac_address: Fields.mac(match[2]), new_ap_id: Fields.integer(match[3]).to_s,
|
|
45
|
+
old_mac_address: Fields.mac(match[4]), old_ap_id: Fields.integer(match[5]).to_s, occurred_at: match[6] }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/profile"
|
|
4
|
+
require_relative "parsers/display_wlan_ap_all_verbose"
|
|
5
|
+
require_relative "parsers/display_wlan_bss_all_verbose"
|
|
6
|
+
require_relative "parsers/display_wlan_client_verbose"
|
|
7
|
+
require_relative "parsers/display_current_configuration"
|
|
8
|
+
require_relative "parsers/display_dot1x_connection"
|
|
9
|
+
require_relative "parsers/display_mac_authentication_connection"
|
|
10
|
+
require_relative "parsers/display_wlan_client_security_block_mac"
|
|
11
|
+
require_relative "parsers/display_lldp_neighbor_information_verbose"
|
|
12
|
+
require_relative "parsers/display_wlan_statistics_client_ip_conflict"
|
|
13
|
+
require_relative "parsers/display_wlan_ap_model_all"
|
|
14
|
+
|
|
15
|
+
module Wireless
|
|
16
|
+
module H3c
|
|
17
|
+
# 声明 H3C 控制器及 AP 策略,共享回显仅解析一次后再投影维度。
|
|
18
|
+
class Profile < CtxParser::Profile
|
|
19
|
+
CATALOG = CtxParser::CommandCatalog.new([
|
|
20
|
+
{ dimensions: [ :client_ip_conflicts ], command: "display wlan statistics client-ip-conflict", parser: Parsers::DisplayWlanStatisticsClientIpConflict },
|
|
21
|
+
{ dimensions: [ :ap_models ], command: "display wlan ap-model all", parser: Parsers::DisplayWlanApModelAll },
|
|
22
|
+
{ dimensions: %i[access_points radios], command: "display wlan ap all verbose", parser: Parsers::DisplayWlanApAllVerbose },
|
|
23
|
+
{ dimensions: [ :bsses ], command: "display wlan bss all verbose", parser: Parsers::DisplayWlanBssAllVerbose },
|
|
24
|
+
{ dimensions: [ :clients ], command: "display wlan client verbose", parser: Parsers::DisplayWlanClientVerbose },
|
|
25
|
+
{ dimensions: %i[configuration ap_groups service_templates], command: "display current-configuration", parser: Parsers::DisplayCurrentConfiguration },
|
|
26
|
+
{ dimensions: [ :dot1x_connections ], command: "display dot1x connection", parser: Parsers::DisplayDot1xConnection },
|
|
27
|
+
{ dimensions: [ :mab_connections ], command: "display mac-authentication connection", parser: Parsers::DisplayMacAuthenticationConnection },
|
|
28
|
+
{ dimensions: [ :blocked_clients ], command: "display wlan client-security block-mac", parser: Parsers::DisplayWlanClientSecurityBlockMac },
|
|
29
|
+
{ dimensions: [ :lldp ], command: "display lldp neighbor-information verbose", parser: Parsers::DisplayLldpNeighborInformationVerbose }
|
|
30
|
+
])
|
|
31
|
+
COMMANDS = CATALOG.commands
|
|
32
|
+
PARSERS = CATALOG.parsers
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# 从共享命令结果中提取请求维度。
|
|
37
|
+
def project(name, result)
|
|
38
|
+
return super unless %i[ap_groups service_templates].include?(name)
|
|
39
|
+
|
|
40
|
+
records = result.records.first&.fetch(name) || []
|
|
41
|
+
result.with(records: records)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser"
|
|
4
|
+
|
|
5
|
+
module Wireless
|
|
6
|
+
module H3c
|
|
7
|
+
# 为 H3C 详细命令回显共享词元切分与类型化字段处理。
|
|
8
|
+
class VerboseParser < Parser
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# 切分含引号的命令值,不将其作为 Ruby 代码解释。
|
|
12
|
+
def tokens(text, issues)
|
|
13
|
+
text.lines.each_with_index.each_with_object([]) do |(line, index), fields|
|
|
14
|
+
row = line.strip
|
|
15
|
+
next if row.empty? || /\A[-=]+\z/.match?(row)
|
|
16
|
+
if (match = /\A([^:]+?)\s*:\s*(.*)\z/.match(row))
|
|
17
|
+
label = match[1].downcase.gsub(/\s+/, " ")
|
|
18
|
+
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)
|
|
19
|
+
fields << [ label, Fields.value(match[2]), index + 1 ]
|
|
20
|
+
elsif (match = /\A:\s*(\S.*)\z/.match(row)) && fields.last&.first == "poe status"
|
|
21
|
+
fields.last[1] = [ fields.last[1], match[1] ].compact.join("\n")
|
|
22
|
+
elsif /\A(?:NSS\d+\s+[\d, ]+|TID \d+\s+(?:In|Out|Both)|[\d., ]+(?:Mbps)?)\z/.match?(row)
|
|
23
|
+
next
|
|
24
|
+
else
|
|
25
|
+
issues << { code: :unrecognized_line, line: index + 1 }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 更新当前记录前,先应用字段专用转换。
|
|
31
|
+
def assign_field(record, mapping, label, raw, issues, line, details:)
|
|
32
|
+
if record && (field = mapping[label])
|
|
33
|
+
safely(issues, line) do
|
|
34
|
+
# 优先使用运行带宽;80+80MHz 等歧义值保持原义,不截断为单个数值。
|
|
35
|
+
if field == :channel_width
|
|
36
|
+
(record[:details] ||= {})[label.tr(" ", "_").to_sym] = raw
|
|
37
|
+
return if label == "channel bandwidth" && record[:details].key?(:operating_bandwidth)
|
|
38
|
+
end
|
|
39
|
+
record[field] = convert(field, raw)
|
|
40
|
+
end
|
|
41
|
+
elsif details
|
|
42
|
+
(details[:details] ||= {})[label.tr(" -", "__").to_sym] = label.end_with?("url") ? !raw.nil? && !raw.strip.empty? : raw
|
|
43
|
+
else
|
|
44
|
+
issues << { code: :unrecognized_field, line: line }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 按声明的归一化规则转换已知字段。
|
|
49
|
+
def convert(field, raw)
|
|
50
|
+
case field
|
|
51
|
+
when :mac_address, :client_mac, :bssid then Fields.mac(raw)
|
|
52
|
+
when :management_ip, :client_ip, :client_ipv6, :ip_address, :ipv6_address then Fields.ip(raw)
|
|
53
|
+
when :ap_id, :radio_id then Fields.integer(raw)&.to_s
|
|
54
|
+
when :client_count, :online_seconds, :uptime_seconds
|
|
55
|
+
%i[online_seconds uptime_seconds].include?(field) ? Fields.duration(raw) : Fields.integer(raw)
|
|
56
|
+
when :vlan, :initial_vlan_id, :authorized_vlan_id then Fields.integer(raw, max: 4094)
|
|
57
|
+
when :channel then Fields.channel(raw)
|
|
58
|
+
when :channel_width then Fields.width(raw)
|
|
59
|
+
when :band then Fields.band(raw)
|
|
60
|
+
when :noise_dbm then Fields.number(raw, unit: "dBm")&.then { |number| number if number.negative? }
|
|
61
|
+
when :utilization
|
|
62
|
+
number = Fields.number(raw, unit: "%")
|
|
63
|
+
raise ArgumentError, "Invalid wireless utilization" if number && !(0..100).cover?(number)
|
|
64
|
+
number&.round
|
|
65
|
+
else raw
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 解析非负计数,同时保留未知值。
|
|
70
|
+
def count(text, noun)
|
|
71
|
+
/(?:Total(?: number of)?\s+(?:#{noun})|Total|Current BSS number)\s*:\s*(\d+)\s*$/i.match(text)&.[](1)&.to_i
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 隔离单个格式错误字段,同时保留无关记录数据。
|
|
75
|
+
def safely(issues, line)
|
|
76
|
+
yield
|
|
77
|
+
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
78
|
+
issues << { code: :invalid_field, line: line }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "runtime_parser"
|
|
4
|
+
|
|
5
|
+
module Wireless
|
|
6
|
+
# 保留 H3C 公共入口,具体 CLI 语法由独立命令类负责。
|
|
7
|
+
class H3cRuntime
|
|
8
|
+
# 绑定 H3C 运行解析器,并保留既有公共入口。
|
|
9
|
+
def initialize(model: nil)
|
|
10
|
+
@parser = RuntimeParser.new(model: model)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# 将单个 H3C 运行维度委派给共享无线解析入口。
|
|
14
|
+
def parse(name, text, complete:)
|
|
15
|
+
@parser.parse(name, text, platform: :h3c, complete: complete)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fields"
|
|
4
|
+
require_relative "observation"
|
|
5
|
+
|
|
6
|
+
module Wireless
|
|
7
|
+
# 仅支持有证据的 Huawei 表格格式,区分不支持维度与空清单。
|
|
8
|
+
class HuaweiRuntime
|
|
9
|
+
# 解析支持的 Huawei AP 或客户端汇总,其余维度标记为不支持。
|
|
10
|
+
def parse(name, text, complete:)
|
|
11
|
+
unless %i[access_points clients].include?(name)
|
|
12
|
+
return Observation.new(name: name, status: :unsupported, issues: [ { code: :unsupported_platform_dimension } ])
|
|
13
|
+
end
|
|
14
|
+
records, issues = [], []
|
|
15
|
+
header = false
|
|
16
|
+
total = nil
|
|
17
|
+
text.lines.each_with_index do |line, index|
|
|
18
|
+
row = line.strip
|
|
19
|
+
next if row.empty? || /\A[-=]+\z/.match?(row)
|
|
20
|
+
if (match = /\ATotal\s*:\s*(\d+)\z/i.match(row))
|
|
21
|
+
total = match[1].to_i
|
|
22
|
+
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)
|
|
23
|
+
header = true
|
|
24
|
+
elsif /\ATotal AP information:|\A(?:nor|fault|idle|stdby)\s*:/i.match?(row)
|
|
25
|
+
next
|
|
26
|
+
elsif !header
|
|
27
|
+
issues << { code: :missing_header, line: index + 1 }
|
|
28
|
+
else
|
|
29
|
+
begin
|
|
30
|
+
records << (name == :access_points ? ap(row.split) : client(row.split))
|
|
31
|
+
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
32
|
+
issues << { code: :invalid_row, line: index + 1 }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
issues << { code: :missing_header } unless header
|
|
37
|
+
result = Fields.result(records, issues: issues, complete: complete, total: total, identity: [ name == :access_points ? :name : :client_mac ])
|
|
38
|
+
Observation.new(name: name, **result.to_h)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# 归一化控制器报告的单条 AP 记录。
|
|
44
|
+
def ap(columns)
|
|
45
|
+
raise ArgumentError, "Invalid AP row" unless columns.size == 9
|
|
46
|
+
id, mac, name, group, ip, model, state, clients, uptime = columns
|
|
47
|
+
{ ap_id: Fields.integer(id).to_s, mac_address: Fields.mac(mac), name: name, group_name: group,
|
|
48
|
+
management_ip: Fields.ip(Fields.value(ip)), model: model, operational_status: Fields.online_status(state),
|
|
49
|
+
client_count: Fields.integer(clients), uptime_seconds: Fields.duration(Fields.value(uptime)) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 归一化控制器报告的单条无线客户端记录。
|
|
53
|
+
def client(columns)
|
|
54
|
+
raise ArgumentError, "Invalid station row" unless columns.size == 15
|
|
55
|
+
mac, ap_id, ap_name, radio_wlan, band, protocol, rates, rssi, vlan, ip, ssid, state, ipv6, _ac_id, uptime = columns
|
|
56
|
+
raise ArgumentError, "Invalid station identity" unless /\A\d+\/\d+\z/.match?(radio_wlan)
|
|
57
|
+
rx, tx = Fields.rates(rates)
|
|
58
|
+
signal = Fields.number(Fields.value(rssi), unit: "dBm")
|
|
59
|
+
{ client_mac: Fields.mac(mac), ap_id: Fields.integer(ap_id).to_s, ap_name: ap_name, radio_id: radio_wlan.split("/").first,
|
|
60
|
+
band: Fields.band(band), protocol: protocol, rx_rate_mbps: rx, tx_rate_mbps: tx, signal_dbm: signal == 0 ? nil : signal,
|
|
61
|
+
vlan: Fields.integer(vlan, max: 4094), client_ip: Fields.ip(Fields.value(ip)), ssid: ssid, operational_status: state,
|
|
62
|
+
client_ipv6: Fields.ip(Fields.value(ipv6)), uptime_seconds: Fields.duration(Fields.value(uptime)) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser_registry"
|
|
4
|
+
require_relative "observation"
|
|
5
|
+
|
|
6
|
+
module Wireless
|
|
7
|
+
# AP 身份由采集上下文提供;LLDP 解析器只读取响应报告的端点。
|
|
8
|
+
class NeighborParser
|
|
9
|
+
# 按 AP 型号选择邻居回显解析器。
|
|
10
|
+
def initialize(platform: :h3c, model: nil)
|
|
11
|
+
@profile = ParserRegistry.fetch(platform, model: model)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# 解析 LLDP 端点,并关联采集上下文提供的 AP 身份。
|
|
15
|
+
def parse(content, ap_name:, complete: false)
|
|
16
|
+
raise ArgumentError, "Missing AP name" unless ap_name.is_a?(String) && !ap_name.strip.empty?
|
|
17
|
+
|
|
18
|
+
result = @profile.parse(:lldp, content, complete: complete)
|
|
19
|
+
records = result.records.map { |record| record.merge(ap_name: ap_name) }
|
|
20
|
+
Observation.new(name: :lldp, **result.to_h.merge(records: records))
|
|
21
|
+
rescue ArgumentError
|
|
22
|
+
Observation.new(name: :lldp, status: :failed, issues: [ { code: :invalid_output } ])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
|
|
5
|
+
module Wireless
|
|
6
|
+
# 保存单个维度的事实与安全诊断;默认检查不展示记录正文。
|
|
7
|
+
Observation = Data.define(:name, :status, :records, :issues, :evidence, :started_at, :completed_at) do
|
|
8
|
+
# 冻结单个维度的记录、状态及诊断元数据。
|
|
9
|
+
def initialize(name:, status:, records: [], issues: [], evidence: [], started_at: nil, completed_at: nil)
|
|
10
|
+
raise ArgumentError, "Invalid wireless observation status" unless %i[complete partial failed unsupported not_requested].include?(status)
|
|
11
|
+
raise ArgumentError, "Wireless records must be hashes" unless records.is_a?(Array) && records.all? { |record| record.is_a?(Hash) }
|
|
12
|
+
raise ArgumentError, "Unavailable wireless observation has records" if %i[failed unsupported not_requested].include?(status) && records.any?
|
|
13
|
+
|
|
14
|
+
super(name: name, status: status, records: CtxParser::Immutable.copy(records),
|
|
15
|
+
issues: CtxParser::Immutable.copy(issues), evidence: CtxParser::Immutable.copy(evidence),
|
|
16
|
+
started_at: started_at&.dup&.freeze, completed_at: completed_at&.dup&.freeze)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 判断所有已选事实是否都有完整证据支持。
|
|
20
|
+
def complete? = status == :complete
|
|
21
|
+
# 判断证据不完整时是否仍保留可用事实。
|
|
22
|
+
def partial? = status == :partial
|
|
23
|
+
# 判断当前观测是否包含完整或部分可用证据。
|
|
24
|
+
def available? = complete? || partial?
|
|
25
|
+
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
26
|
+
def inspect = "#<#{self.class.name} name=#{name} status=#{status} records=#{records.size}>"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/registry"
|
|
4
|
+
require_relative "h3c/profile"
|
|
5
|
+
|
|
6
|
+
module Wireless
|
|
7
|
+
# 将 h3c_wireless 视为 H3C 无线平台策略的连接器别名。
|
|
8
|
+
ParserRegistry = CtxParser::Registry.new(profiles: { h3c: H3c::Profile }, aliases: { h3c_wireless: :h3c })
|
|
9
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
|
|
5
|
+
module Wireless
|
|
6
|
+
# 资产应用具有独立结果;父对象缺失不使已采集事实失效。
|
|
7
|
+
Projection = Data.define(:dimensions) do
|
|
8
|
+
# 冻结各维度的资产应用结果。
|
|
9
|
+
def initialize(dimensions: {})
|
|
10
|
+
super(dimensions: CtxParser::Immutable.copy(dimensions))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# 判断所有请求的资产投影是否均已完成。
|
|
14
|
+
def complete? = dimensions.values.all? { |entry| entry.fetch(:status) == :complete }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser_registry"
|
|
4
|
+
require_relative "fields"
|
|
5
|
+
require_relative "huawei_runtime"
|
|
6
|
+
require_relative "observation"
|
|
7
|
+
|
|
8
|
+
module Wireless
|
|
9
|
+
# 将平台解析结果映射为无线维度,同时保留 Huawei 解析入口。
|
|
10
|
+
class RuntimeParser
|
|
11
|
+
# 保留型号选择信息,供每次解析所选平台使用。
|
|
12
|
+
def initialize(model: nil)
|
|
13
|
+
@model = model
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 使用传入平台及保留的型号选择解析单个无线维度。
|
|
17
|
+
def parse(name, content, platform: "h3c", complete: false)
|
|
18
|
+
parse_many([ name ], content, platform: platform, complete: complete).fetch(name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 共享命令仅解析一次,再投影各请求维度。
|
|
22
|
+
def parse_many(names, content, platform: "h3c", complete: false)
|
|
23
|
+
text = Fields.text(content)
|
|
24
|
+
if H3c::Parser::UNSUPPORTED.match?(text)
|
|
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) ] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
profile = ParserRegistry.fetch(platform, model: @model)
|
|
32
|
+
supported, unsupported = names.partition { |name| profile.commands.key?(name) }
|
|
33
|
+
results = supported.empty? ? {} : profile.parse_many(supported, text, complete: complete)
|
|
34
|
+
observations = results.to_h { |name, result| [ name, Observation.new(name: name, **result.to_h) ] }
|
|
35
|
+
observations.merge(unavailable(unsupported, :unsupported, :unsupported_platform_dimension))
|
|
36
|
+
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
37
|
+
unavailable(names, :failed, :invalid_output)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# 构建明确不可用的观测,不虚构成功的空结果。
|
|
43
|
+
def unavailable(names, status, code)
|
|
44
|
+
names.to_h { |name| [ name, Observation.new(name: name, status: status, issues: [ { code: code } ]) ] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/immutable"
|
|
4
|
+
require_relative "observation"
|
|
5
|
+
|
|
6
|
+
module Wireless
|
|
7
|
+
# 携带调用方提供的设备身份,不引入独立资产模型。
|
|
8
|
+
Snapshot = Data.define(:target, :dimensions, :observed_at) do
|
|
9
|
+
DIMENSIONS = %i[access_points radios bsses clients configuration ap_groups service_templates
|
|
10
|
+
dot1x_connections mab_connections blocked_clients client_ip_conflicts ap_models lldp collection].freeze
|
|
11
|
+
|
|
12
|
+
# 冻结设备身份、观测结果与采样时间。
|
|
13
|
+
def initialize(target:, dimensions:, observed_at: Time.now.utc)
|
|
14
|
+
unless target.is_a?(Hash) && dimensions.is_a?(Hash) && dimensions.any? &&
|
|
15
|
+
dimensions.all? { |name, value| DIMENSIONS.include?(name) && value.is_a?(Observation) && value.name == name } &&
|
|
16
|
+
observed_at.is_a?(Time)
|
|
17
|
+
raise ArgumentError, "Invalid wireless snapshot"
|
|
18
|
+
end
|
|
19
|
+
super(target: CtxParser::Immutable.copy(target), dimensions: dimensions.dup.freeze,
|
|
20
|
+
observed_at: observed_at.dup.freeze)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 返回所选观测,或明确的未请求占位结果。
|
|
24
|
+
def dimension(name) = dimensions.fetch(name) { Observation.new(name: name, status: :not_requested) }
|
|
25
|
+
# 返回请求观测维度的记录。
|
|
26
|
+
def records(name) = dimension(name).records
|
|
27
|
+
|
|
28
|
+
# 判断所有已选事实是否都有完整证据支持。
|
|
29
|
+
def complete?
|
|
30
|
+
selected = dimensions.values.reject { |value| value.status == :not_requested }
|
|
31
|
+
selected.any? && selected.all?(&:complete?)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
35
|
+
def inspect = "#<#{self.class.name} complete=#{complete?}>"
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/wireless.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "wireless/version"
|
|
4
|
+
|
|
5
|
+
module Wireless
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative "wireless/fields"
|
|
9
|
+
require_relative "wireless/observation"
|
|
10
|
+
require_relative "wireless/snapshot"
|
|
11
|
+
require_relative "wireless/projection"
|
|
12
|
+
require_relative "wireless/coverage"
|
|
13
|
+
require_relative "wireless/configuration"
|
|
14
|
+
require_relative "wireless/runtime_parser"
|
|
15
|
+
require_relative "wireless/h3c_runtime"
|
|
16
|
+
require_relative "wireless/neighbor_parser"
|
|
17
|
+
require_relative "wireless/ap_session"
|
|
18
|
+
require_relative "wireless/collector"
|
metadata
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wireless-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Gatework contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ctx_parser
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ctx_driver
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: date
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: time
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: Wireless configuration, access point, radio, client and neighbor observations
|
|
83
|
+
with independent command and nested-session failure handling.
|
|
84
|
+
executables: []
|
|
85
|
+
extensions: []
|
|
86
|
+
extra_rdoc_files: []
|
|
87
|
+
files:
|
|
88
|
+
- CHANGELOG.md
|
|
89
|
+
- LICENSE
|
|
90
|
+
- README-zh.md
|
|
91
|
+
- README.md
|
|
92
|
+
- docs/architecture-zh.md
|
|
93
|
+
- docs/release.md
|
|
94
|
+
- examples/offline.rb
|
|
95
|
+
- lib/wireless.rb
|
|
96
|
+
- lib/wireless/ap_session.rb
|
|
97
|
+
- lib/wireless/collector.rb
|
|
98
|
+
- lib/wireless/configuration.rb
|
|
99
|
+
- lib/wireless/coverage.rb
|
|
100
|
+
- lib/wireless/fields.rb
|
|
101
|
+
- lib/wireless/h3c/authentication_parser.rb
|
|
102
|
+
- lib/wireless/h3c/command_output.rb
|
|
103
|
+
- lib/wireless/h3c/configuration.rb
|
|
104
|
+
- lib/wireless/h3c/parser.rb
|
|
105
|
+
- lib/wireless/h3c/parsers/display_current_configuration.rb
|
|
106
|
+
- lib/wireless/h3c/parsers/display_dot1x_connection.rb
|
|
107
|
+
- lib/wireless/h3c/parsers/display_lldp_neighbor_information_verbose.rb
|
|
108
|
+
- lib/wireless/h3c/parsers/display_mac_authentication_connection.rb
|
|
109
|
+
- lib/wireless/h3c/parsers/display_wlan_ap_all_verbose.rb
|
|
110
|
+
- lib/wireless/h3c/parsers/display_wlan_ap_model_all.rb
|
|
111
|
+
- lib/wireless/h3c/parsers/display_wlan_bss_all_verbose.rb
|
|
112
|
+
- lib/wireless/h3c/parsers/display_wlan_client_security_block_mac.rb
|
|
113
|
+
- lib/wireless/h3c/parsers/display_wlan_client_verbose.rb
|
|
114
|
+
- lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb
|
|
115
|
+
- lib/wireless/h3c/profile.rb
|
|
116
|
+
- lib/wireless/h3c/session_parser.rb
|
|
117
|
+
- lib/wireless/h3c/verbose_parser.rb
|
|
118
|
+
- lib/wireless/h3c_runtime.rb
|
|
119
|
+
- lib/wireless/huawei_runtime.rb
|
|
120
|
+
- lib/wireless/neighbor_parser.rb
|
|
121
|
+
- lib/wireless/observation.rb
|
|
122
|
+
- lib/wireless/parser_registry.rb
|
|
123
|
+
- lib/wireless/projection.rb
|
|
124
|
+
- lib/wireless/runtime_parser.rb
|
|
125
|
+
- lib/wireless/snapshot.rb
|
|
126
|
+
- lib/wireless/version.rb
|
|
127
|
+
homepage: https://github.com/gatework/wireless-rb
|
|
128
|
+
licenses:
|
|
129
|
+
- MIT
|
|
130
|
+
metadata:
|
|
131
|
+
source_code_uri: https://github.com/gatework/wireless-rb
|
|
132
|
+
bug_tracker_uri: https://github.com/gatework/wireless-rb/issues
|
|
133
|
+
changelog_uri: https://github.com/gatework/wireless-rb/blob/main/CHANGELOG.md
|
|
134
|
+
documentation_uri: https://github.com/gatework/wireless-rb#readme
|
|
135
|
+
allowed_push_host: https://rubygems.org
|
|
136
|
+
rubygems_mfa_required: 'true'
|
|
137
|
+
rdoc_options: []
|
|
138
|
+
require_paths:
|
|
139
|
+
- lib
|
|
140
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: 4.0.6
|
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - ">="
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: '0'
|
|
150
|
+
requirements: []
|
|
151
|
+
rubygems_version: 4.0.16
|
|
152
|
+
specification_version: 4
|
|
153
|
+
summary: Parse and collect wireless controller inventories.
|
|
154
|
+
test_files: []
|