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
|
@@ -15,8 +15,6 @@ module Wireless
|
|
|
15
15
|
def parse(content, complete: false)
|
|
16
16
|
result = @profile.parse(:configuration, content, complete: complete)
|
|
17
17
|
Observation.new(name: :configuration, **result.to_h)
|
|
18
|
-
rescue ArgumentError
|
|
19
|
-
Observation.new(name: :configuration, status: :failed, issues: [ { code: :invalid_output } ])
|
|
20
18
|
end
|
|
21
19
|
end
|
|
22
20
|
end
|
data/lib/wireless/coverage.rb
CHANGED
|
@@ -7,24 +7,34 @@ module Wireless
|
|
|
7
7
|
class Coverage
|
|
8
8
|
# 绑定观测、投影结果及请求的新鲜度时间窗。
|
|
9
9
|
def initialize(observations:, projection:, dimensions:, now:, stale_after:)
|
|
10
|
+
unless now.is_a?(Time) && stale_after.is_a?(Numeric) && stale_after.real? && stale_after.finite? && stale_after >= 0
|
|
11
|
+
raise ArgumentError, "Coverage requires a clock time and a finite nonnegative freshness window"
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
@observations, @projection, @dimensions = observations, projection || {}, dimensions
|
|
11
|
-
@now, @
|
|
15
|
+
@now, @stale_after_seconds = now, stale_after
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
# 历史资产清单不声明 CLI 完整性,保持既有纯 Netdisco 视图兼容。
|
|
15
19
|
def to_h
|
|
16
20
|
return { "state" => "untracked", "source" => "inventory", "dimensions" => {} } unless @observations
|
|
17
21
|
|
|
18
|
-
dimensions = @dimensions.to_h { |name| [ name.to_s,
|
|
22
|
+
dimensions = @dimensions.to_h { |name| [ name.to_s, dimension_coverage(name.to_s) ] }
|
|
19
23
|
states = dimensions.values.map { |entry| entry.fetch("state") }
|
|
20
|
-
state = states.
|
|
24
|
+
state = if states.empty? || states.include?("unknown")
|
|
25
|
+
"unknown"
|
|
26
|
+
elsif states.all?("complete")
|
|
27
|
+
"complete"
|
|
28
|
+
else
|
|
29
|
+
"stale"
|
|
30
|
+
end
|
|
21
31
|
{ "state" => state, "source" => "cli", "dimensions" => dimensions }
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
private
|
|
25
35
|
|
|
26
36
|
# 新记录不能刷新旧命令或失败命令的时效;投影受阻不算完整覆盖。
|
|
27
|
-
def
|
|
37
|
+
def dimension_coverage(name)
|
|
28
38
|
stored = @observations.fetch(name, {})
|
|
29
39
|
latest = stored.fetch("latest", {})
|
|
30
40
|
projection = @projection.fetch(name, {})
|
|
@@ -35,7 +45,7 @@ module Wireless
|
|
|
35
45
|
[ "unknown", latest.empty? ? "not_requested" : "collection_#{latest['status']}" ]
|
|
36
46
|
elsif !at || at > @now
|
|
37
47
|
[ "unknown", "observation_time_unknown" ]
|
|
38
|
-
elsif @now - at > @
|
|
48
|
+
elsif @now - at > @stale_after_seconds
|
|
39
49
|
[ "stale", "evidence_stale" ]
|
|
40
50
|
elsif @projection.key?(name) && projected_at != at
|
|
41
51
|
[ "unknown", "projection_time_unconfirmed" ]
|
data/lib/wireless/fields.rb
CHANGED
|
@@ -27,15 +27,15 @@ module Wireless
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# 归一化 MAC 地址,拒绝格式错误的输入。
|
|
30
|
-
def
|
|
31
|
-
CtxParser::Values.
|
|
30
|
+
def mac_address(raw)
|
|
31
|
+
CtxParser::Values.mac_address(raw)
|
|
32
32
|
rescue ArgumentError
|
|
33
33
|
raise ArgumentError, "Invalid wireless MAC"
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# 归一化 IP 主机地址,拒绝无效无线身份数据。
|
|
37
|
-
def
|
|
38
|
-
CtxParser::Values.
|
|
37
|
+
def ip_address(raw)
|
|
38
|
+
CtxParser::Values.ip_address(raw)
|
|
39
39
|
rescue IPAddr::Error
|
|
40
40
|
raise
|
|
41
41
|
rescue ArgumentError
|
|
@@ -56,11 +56,15 @@ module Wireless
|
|
|
56
56
|
return if raw.nil?
|
|
57
57
|
match = /\A(-?\d+(?:\.\d+)?)\s*(?:#{Regexp.escape(unit.to_s)})?\z/i.match(raw.to_s)
|
|
58
58
|
raise ArgumentError, "Invalid wireless number" unless match
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
measurement = Float(match[1])
|
|
61
|
+
raise ArgumentError, "Invalid wireless number" unless measurement.finite?
|
|
62
|
+
|
|
63
|
+
measurement
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
# 将支持的时长格式转换为秒,并校验各时间分量。
|
|
63
|
-
def
|
|
67
|
+
def duration_seconds(raw)
|
|
64
68
|
return if raw.nil?
|
|
65
69
|
if /\A\d+:\d{2}:\d{2}:\d{2}\z/.match?(raw)
|
|
66
70
|
days, hours, minutes, seconds = raw.split(":").map(&:to_i)
|
|
@@ -74,12 +78,17 @@ module Wireless
|
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
# 将成对收发速率转换为每秒兆比特。
|
|
77
|
-
def
|
|
81
|
+
def rates_mbps(raw)
|
|
78
82
|
return [ nil, nil ] if raw.nil?
|
|
79
83
|
match = /\A(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)\s*([kmgt]?bps)?\z/i.match(raw)
|
|
80
84
|
raise ArgumentError, "Invalid wireless rates" unless match
|
|
81
85
|
multiplier = { "bps" => 0.000001, "kbps" => 0.001, "mbps" => 1, "gbps" => 1000, "tbps" => 1_000_000 }.fetch((match[3] || "Mbps").downcase)
|
|
82
|
-
[ match[1], match[2] ].map
|
|
86
|
+
[ match[1], match[2] ].map do |value|
|
|
87
|
+
rate = Float(value) * multiplier
|
|
88
|
+
raise ArgumentError, "Invalid wireless rates" unless rate.finite?
|
|
89
|
+
|
|
90
|
+
rate
|
|
91
|
+
end
|
|
83
92
|
end
|
|
84
93
|
|
|
85
94
|
# 解析明确无线信道;自动选择仍保持未知。
|
|
@@ -92,22 +101,18 @@ module Wireless
|
|
|
92
101
|
end
|
|
93
102
|
|
|
94
103
|
# 将支持的 MHz 回显归一化为正数信道宽度。
|
|
95
|
-
def
|
|
104
|
+
def channel_width_mhz(raw)
|
|
96
105
|
match = /\A(\d+)\s*(?:MHz)?\z/i.match(raw.to_s)
|
|
97
106
|
match[1].to_i if match && match[1].to_i.positive?
|
|
98
107
|
end
|
|
99
108
|
|
|
100
|
-
#
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
when 1..14 then "2.4GHz"
|
|
108
|
-
when 15..199 then "5GHz"
|
|
109
|
-
when 200..233 then "6GHz"
|
|
110
|
-
end
|
|
109
|
+
# 信道编号在不同频段重叠,频段须由明确标签或专属 PHY 证据确定。
|
|
110
|
+
def frequency_band(hint)
|
|
111
|
+
bands = []
|
|
112
|
+
bands << "6GHz" if /(?<![\d.])6\s*g(?:hz)?\b/i.match?(hint.to_s)
|
|
113
|
+
bands << "5GHz" if /(?<![\d.])5\s*g(?:hz)?\b/i.match?(hint.to_s)
|
|
114
|
+
bands << "2.4GHz" if /(?<![\d.])2\.4\s*g(?:hz)?\b|802\.11[bg](?:\b|\/)/i.match?(hint.to_s)
|
|
115
|
+
bands.first if bands.one?
|
|
111
116
|
end
|
|
112
117
|
|
|
113
118
|
# 归一化已知在线、离线标签,同时保留未识别状态。
|
|
@@ -1,12 +1,12 @@
|
|
|
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
|
-
#
|
|
8
|
-
|
|
9
|
-
MAPPING = COMMON_FIELDS.except("vlan id", "service vlan id").merge(
|
|
7
|
+
# 认证命令共享字段映射、详情及复合会话身份。
|
|
8
|
+
module Authentication
|
|
9
|
+
MAPPING = RecordParser::COMMON_FIELDS.except("vlan id", "service vlan id").merge(
|
|
10
10
|
"mac address" => :mac_address, "user mac address" => :mac_address, "username" => :username, "user name" => :username,
|
|
11
11
|
"access interface" => :interface, "interface" => :interface, "ip address" => :ip_address,
|
|
12
12
|
"ipv4 address" => :ip_address, "ipv6 address" => :ipv6_address, "authentication domain" => :domain, "domain" => :domain,
|
|
@@ -1,27 +1,68 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../fields"
|
|
4
|
+
require_relative "../terminal"
|
|
4
5
|
|
|
5
6
|
module Wireless
|
|
6
7
|
module H3c
|
|
7
8
|
# 复用 H3C 输入校验;各命令实现 parse_text,配置解析保留原始换行。
|
|
8
9
|
module CommandOutput
|
|
9
|
-
UNSUPPORTED = /(?:Unrecognized command|Wrong parameter|Invalid (?:input|command)|Unknown command|Error:[^\r\n]*command)/i
|
|
10
|
-
|
|
11
10
|
private
|
|
12
11
|
|
|
13
12
|
# 解释当前命令回显,保留不支持或格式错误的证据。
|
|
14
13
|
def parse_output(text, complete:)
|
|
15
14
|
text = Fields.text(text, newlines: output_newlines)
|
|
16
|
-
if
|
|
15
|
+
if Terminal.unsupported?(text)
|
|
17
16
|
return CtxParser::Result.new(status: :unsupported, issues: [ { code: :unsupported_command } ])
|
|
18
17
|
end
|
|
19
18
|
|
|
19
|
+
if (issue = terminal_issue(text))
|
|
20
|
+
return CtxParser::Result.new(status: :partial, issues: [ issue ])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@field_assignments = {}.compare_by_identity
|
|
20
24
|
parse_text(text, complete: complete)
|
|
21
25
|
rescue ArgumentError, IPAddr::InvalidAddressError
|
|
22
26
|
CtxParser::Result.new(status: :failed, issues: [ { code: :invalid_output } ])
|
|
23
27
|
end
|
|
24
28
|
|
|
29
|
+
# 校验每条总数声明,不能用第一条匹配掩盖矛盾或损坏的尾行。
|
|
30
|
+
def reported_total(text, noun:, issues:, continuation_lines: nil)
|
|
31
|
+
prefix = /\A(?:Total\b|Number of (?:#{noun})\b|Current BSS number\b)/i
|
|
32
|
+
totals = []
|
|
33
|
+
text.each_line.with_index do |line, index|
|
|
34
|
+
row = line.strip
|
|
35
|
+
next if continuation_lines&.key?(index + 1)
|
|
36
|
+
next unless prefix.match?(row)
|
|
37
|
+
if (match = /\A(?:Total(?: number of)?(?:\s+(?:#{noun}))?|Number of (?:#{noun})|Current BSS number)\s*:\s*(\d+)\s*\z/i.match(row))
|
|
38
|
+
totals << match[1].to_i
|
|
39
|
+
else
|
|
40
|
+
issues << { code: :invalid_total, line: index + 1 }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
issues << { code: :conflicting_total } if totals.uniq.size > 1
|
|
44
|
+
totals.first
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 同一字段允许等价重复;发生歧义后删除事实,后续声明不能恢复它。
|
|
48
|
+
def assign_value(record, field, value, issues, line, identity: field)
|
|
49
|
+
assignments = (@field_assignments[record] ||= {})
|
|
50
|
+
if (previous = assignments[identity])
|
|
51
|
+
if previous[:conflicted] || previous[:value] != value
|
|
52
|
+
record.delete(field)
|
|
53
|
+
issues << { code: :conflicting_field, line: line, first_line: previous[:line] }
|
|
54
|
+
previous[:conflicted] = true
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
assignments[identity] = { value: value, line: line }
|
|
59
|
+
end
|
|
60
|
+
record[field] = value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 运行回显受损时不能继续信任任何字段。
|
|
64
|
+
def terminal_issue(text) = Terminal.issue(text)
|
|
65
|
+
|
|
25
66
|
# 声明当前命令解析器所需的换行归一化方式。
|
|
26
67
|
def output_newlines = :lf
|
|
27
68
|
end
|
|
@@ -23,6 +23,7 @@ module Wireless
|
|
|
23
23
|
raise ArgumentError, "Group radio requires a model"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
identity = identity.downcase
|
|
26
27
|
collection = identity.end_with?("g") ? :band_defaults : :radios
|
|
27
28
|
keys = [ collection, identity ]
|
|
28
29
|
keys.unshift(:configuration) if kind == :ap_groups
|
|
@@ -37,7 +38,7 @@ module Wireless
|
|
|
37
38
|
|
|
38
39
|
# 汇总已解析记录,并报告未解决或不完整的来源状态。
|
|
39
40
|
def finish
|
|
40
|
-
record[:authentication_type] =
|
|
41
|
+
record[:authentication_type] = authentication_type if kind == :service_templates
|
|
41
42
|
record
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -51,29 +52,44 @@ module Wireless
|
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
# 解析实际生效的认证方法及外部服务器引用。
|
|
54
|
-
def
|
|
55
|
+
def authentication_type
|
|
55
56
|
mode = settings[:client_security_authentication_mode]
|
|
56
|
-
return
|
|
57
|
-
return "
|
|
57
|
+
return mode == "mac" ? "mab" : mode if mode
|
|
58
|
+
return "dot1x" if settings[:akm_modes].include?("dot1x")
|
|
58
59
|
return "psk" if settings[:akm_modes].include?("psk") || settings[:has_preshared_key]
|
|
59
60
|
return "portal" if settings[:portal]&.key?(:enable_method)
|
|
60
61
|
nil
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
# 使用模板名索引绑定,保持数组输出并避免每条声明扫描既有绑定。
|
|
66
|
+
def bind_template(record, name, vlan_id)
|
|
67
|
+
bindings = (@template_bindings[record] ||= {})
|
|
68
|
+
binding = { name: name, vlan_id: vlan_id }
|
|
69
|
+
previous = bindings[name]
|
|
70
|
+
raise ArgumentError, "Conflicting template binding" if previous && previous != binding
|
|
71
|
+
return if previous
|
|
72
|
+
|
|
73
|
+
bindings[name] = binding
|
|
74
|
+
record[:service_templates] << binding
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
attr_reader :data
|
|
65
78
|
|
|
66
79
|
# 初始化无线实体集合及配置诊断。
|
|
67
80
|
def initialize
|
|
81
|
+
@entity_names = Hash.new { |hash, kind| hash[kind] = {} }
|
|
82
|
+
@template_bindings = {}.compare_by_identity
|
|
68
83
|
@data = { access_points: {}, ap_groups: [], service_templates: [], global_settings: {}, blacklist: [], whitelist: [] }
|
|
69
84
|
end
|
|
70
85
|
|
|
71
86
|
# 使用实体集合作为身份索引,首条重复记录仅供部分结果保留。
|
|
72
87
|
def entity(kind, name, model: nil, type: nil)
|
|
73
88
|
collection = data.fetch(kind)
|
|
74
|
-
duplicate = kind
|
|
89
|
+
duplicate = @entity_names[kind].key?(name)
|
|
75
90
|
raise ArgumentError, "Duplicate wireless configuration identity" if duplicate
|
|
76
91
|
|
|
92
|
+
@entity_names[kind][name] = true
|
|
77
93
|
record = case kind
|
|
78
94
|
when :ap_groups
|
|
79
95
|
{ name: name, ap_names: [], serial_ids: [], vlan_ids: [], model_profiles: {}, configuration: type ? { type: type } : {} }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/document"
|
|
4
|
+
require "ctx_parser/banner"
|
|
5
|
+
require_relative "../terminal"
|
|
6
|
+
|
|
7
|
+
module Wireless
|
|
8
|
+
module H3c
|
|
9
|
+
# 横幅仅含文本;分页及终端控制残留使配置证据不完整。
|
|
10
|
+
class Document < CtxParser::Document
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
# 先隔离横幅,再分类影响完整性的终端残留。
|
|
14
|
+
def classify(line)
|
|
15
|
+
@banner ||= CtxParser::Banner.new
|
|
16
|
+
return :ignored if @banner.consume(line.text)
|
|
17
|
+
|
|
18
|
+
if Terminal.invalid_output?(line.raw)
|
|
19
|
+
issues << { code: :incomplete_configuration, line: line.number }
|
|
20
|
+
return :ignored
|
|
21
|
+
end
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 未闭合横幅不能成为完整配置证据。
|
|
26
|
+
def finish
|
|
27
|
+
issues.each { |issue| cursor.mark(issue.fetch(:line) - 1, state: :invalid) }
|
|
28
|
+
issues << { code: :incomplete_banner } if @banner&.open?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|