wireless-rb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE +22 -0
  4. data/README-zh.md +103 -0
  5. data/README.md +103 -0
  6. data/docs/architecture-zh.md +92 -0
  7. data/docs/release.md +13 -0
  8. data/examples/offline.rb +29 -0
  9. data/lib/wireless/ap_session.rb +119 -0
  10. data/lib/wireless/collector.rb +168 -0
  11. data/lib/wireless/configuration.rb +22 -0
  12. data/lib/wireless/coverage.rb +60 -0
  13. data/lib/wireless/fields.rb +131 -0
  14. data/lib/wireless/h3c/authentication_parser.rb +25 -0
  15. data/lib/wireless/h3c/command_output.rb +29 -0
  16. data/lib/wireless/h3c/configuration.rb +109 -0
  17. data/lib/wireless/h3c/parser.rb +13 -0
  18. data/lib/wireless/h3c/parsers/display_current_configuration.rb +246 -0
  19. data/lib/wireless/h3c/parsers/display_dot1x_connection.rb +13 -0
  20. data/lib/wireless/h3c/parsers/display_lldp_neighbor_information_verbose.rb +83 -0
  21. data/lib/wireless/h3c/parsers/display_mac_authentication_connection.rb +13 -0
  22. data/lib/wireless/h3c/parsers/display_wlan_ap_all_verbose.rb +80 -0
  23. data/lib/wireless/h3c/parsers/display_wlan_ap_model_all.rb +79 -0
  24. data/lib/wireless/h3c/parsers/display_wlan_bss_all_verbose.rb +41 -0
  25. data/lib/wireless/h3c/parsers/display_wlan_client_security_block_mac.rb +31 -0
  26. data/lib/wireless/h3c/parsers/display_wlan_client_verbose.rb +49 -0
  27. data/lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb +50 -0
  28. data/lib/wireless/h3c/profile.rb +45 -0
  29. data/lib/wireless/h3c/session_parser.rb +58 -0
  30. data/lib/wireless/h3c/verbose_parser.rb +82 -0
  31. data/lib/wireless/h3c_runtime.rb +18 -0
  32. data/lib/wireless/huawei_runtime.rb +65 -0
  33. data/lib/wireless/neighbor_parser.rb +25 -0
  34. data/lib/wireless/observation.rb +28 -0
  35. data/lib/wireless/parser_registry.rb +9 -0
  36. data/lib/wireless/projection.rb +16 -0
  37. data/lib/wireless/runtime_parser.rb +47 -0
  38. data/lib/wireless/snapshot.rb +37 -0
  39. data/lib/wireless/version.rb +6 -0
  40. data/lib/wireless.rb +18 -0
  41. metadata +154 -0
@@ -0,0 +1,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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wireless
4
+ # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
+ VERSION = "0.1.0"
6
+ 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: []