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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README-zh.md +37 -2
  4. data/README.md +34 -3
  5. data/docs/architecture-zh.md +4 -4
  6. data/lib/wireless/ap_session.rb +113 -52
  7. data/lib/wireless/collector.rb +49 -36
  8. data/lib/wireless/configuration.rb +0 -2
  9. data/lib/wireless/coverage.rb +15 -5
  10. data/lib/wireless/fields.rb +25 -20
  11. data/lib/wireless/h3c/{authentication_parser.rb → authentication.rb} +4 -4
  12. data/lib/wireless/h3c/command_output.rb +44 -3
  13. data/lib/wireless/h3c/configuration.rb +21 -5
  14. data/lib/wireless/h3c/document.rb +32 -0
  15. data/lib/wireless/h3c/parsers/display_current_configuration.rb +143 -69
  16. data/lib/wireless/h3c/parsers/display_dot1x_connection.rb +4 -2
  17. data/lib/wireless/h3c/parsers/display_lldp_neighbor_information_verbose.rb +34 -10
  18. data/lib/wireless/h3c/parsers/display_mac_authentication_connection.rb +4 -2
  19. data/lib/wireless/h3c/parsers/display_wlan_ap_all_verbose.rb +37 -11
  20. data/lib/wireless/h3c/parsers/display_wlan_ap_model_all.rb +5 -5
  21. data/lib/wireless/h3c/parsers/display_wlan_bss_all_verbose.rb +6 -6
  22. data/lib/wireless/h3c/parsers/display_wlan_client_security_block_mac.rb +2 -2
  23. data/lib/wireless/h3c/parsers/display_wlan_client_verbose.rb +14 -8
  24. data/lib/wireless/h3c/parsers/display_wlan_statistics_client_ip_conflict.rb +2 -2
  25. data/lib/wireless/h3c/profile.rb +6 -3
  26. data/lib/wireless/h3c/record_parser.rb +138 -0
  27. data/lib/wireless/huawei/parser.rb +86 -0
  28. data/lib/wireless/huawei/parsers/display_ap_all.rb +14 -0
  29. data/lib/wireless/huawei/parsers/display_station_all.rb +14 -0
  30. data/lib/wireless/huawei/profile.rb +22 -0
  31. data/lib/wireless/observation.rb +2 -0
  32. data/lib/wireless/parser_registry.rb +2 -1
  33. data/lib/wireless/projection.rb +4 -0
  34. data/lib/wireless/runtime_parser.rb +16 -11
  35. data/lib/wireless/snapshot.rb +2 -1
  36. data/lib/wireless/terminal.rb +32 -0
  37. data/lib/wireless/version.rb +1 -1
  38. data/lib/wireless.rb +0 -2
  39. metadata +9 -20
  40. data/lib/wireless/h3c/session_parser.rb +0 -58
  41. data/lib/wireless/h3c/verbose_parser.rb +0 -82
  42. data/lib/wireless/h3c_runtime.rb +0 -18
  43. 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
@@ -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, @stale_after = now, stale_after
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, dimension(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.all?("complete") ? "complete" : states.include?("unknown") ? "unknown" : "stale"
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 dimension(name)
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 > @stale_after
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" ]
@@ -27,15 +27,15 @@ module Wireless
27
27
  end
28
28
 
29
29
  # 归一化 MAC 地址,拒绝格式错误的输入。
30
- def mac(raw)
31
- CtxParser::Values.mac(raw)
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 ip(raw)
38
- CtxParser::Values.host(raw)
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
- Float(match[1])
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 duration(raw)
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 rates(raw)
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 { |value| Float(value) * multiplier }
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 width(raw)
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 band(hint, channel: nil)
102
- return "6GHz" if /6\s*g(?:hz)?/i.match?(hint.to_s)
103
- return "5GHz" if /5\s*g(?:hz)?/i.match?(hint.to_s)
104
- return "2.4GHz" if /2\.?4\s*g(?:hz)?|802\.11[bg](?:\b|\/)/i.match?(hint.to_s)
105
- return unless channel
106
- case channel
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 "session_parser"
3
+ require_relative "record_parser"
4
4
 
5
5
  module Wireless
6
6
  module H3c
7
- # 认证命令共享字段语法及复合会话身份。
8
- class AuthenticationParser < SessionParser
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 UNSUPPORTED.match?(text)
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] = authentication if kind == :service_templates
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 authentication
55
+ def authentication_type
55
56
  mode = settings[:client_security_authentication_mode]
56
- return "dot1x" if mode == "dot1x" || settings[:akm_modes].include?("dot1x") || settings[:dot1x_domain]
57
- return "mab" if mode == "mac" || settings[:mac_authentication_domain]
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 == :access_points ? collection.key?(name) : collection.any? { |record| record[:name] == name }
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