vxlan 0.2.1 → 0.2.3

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.
@@ -16,7 +16,7 @@ module Vxlan
16
16
  # 解析当前命令的记录,保留无法识别行的诊断。
17
17
  def parse_rows(text)
18
18
  layout = nil
19
- lines(text) do |line, number|
19
+ each_content_line(text) do |line, number|
20
20
  if line.match?(/\A(?:Port\s+VRF\s+Status\s+IP Address(?:\s+Speed\s+MTU)?|(?:Port|Interface)\s+Name\s+Status\s+Vlan\s+Duplex\s+Speed\s+Type|(?:Ethernet|Port-channel)\s+VLAN\s+Type\s+Mode\s+Status\s+Reason\s+Speed\s+(?:Port|Protocol)|Interface\s+Status\s+Description|Interface\s+Secondary VLAN\(Type\)\s+Status Reason|Port\s+Status Reason\s+MTU)\z/i)
21
21
  layout = line
22
22
  @header = true
@@ -26,7 +26,7 @@ module Vxlan
26
26
  words = line.split
27
27
  state = words.index { |word| %w[up down connected notconnect disabled admin-down].include?(word.downcase) }
28
28
  unless layout && state && INTERFACE.match?(words[0])
29
- reject(number)
29
+ unparsed_records(number)
30
30
  next
31
31
  end
32
32
  row = { interface: words[0], state: words[state].downcase, detail: words.drop(state + 1).join(" ") }
@@ -37,38 +37,38 @@ module Vxlan
37
37
  row.merge!(vrf: words[1], ip_address: words[3])
38
38
  row.merge!(speed: words[4], mtu: words[5].to_i) if words.size == 6
39
39
  else
40
- reject(number)
40
+ unparsed_records(number)
41
41
  next
42
42
  end
43
43
  when /\A(?:Ethernet|Port-channel)\s+VLAN/i
44
44
  unless state == 4 && words.size >= 8
45
- reject(number)
45
+ unparsed_records(number)
46
46
  next
47
47
  end
48
48
  row.merge!(vlan: words[1], type: words[2], mode: words[3], reason: words[5...-2].join(" "), speed: words[-2])
49
49
  row[layout.start_with?("Ethernet") ? :port_channel : :protocol] = words[-1]
50
50
  when /\AInterface\s+Status/i
51
51
  unless state == 1
52
- reject(number)
52
+ unparsed_records(number)
53
53
  next
54
54
  end
55
55
  row[:description] = words.drop(2).join(" ")
56
56
  when /\AInterface\s+Secondary/i
57
57
  unless state >= 2
58
- reject(number)
58
+ unparsed_records(number)
59
59
  next
60
60
  end
61
61
  row.merge!(secondary_vlan: words[1...state].join(" "), reason: words.drop(state + 1).join(" "))
62
62
  when /\APort\s+Status Reason/i
63
63
  unless state == 1 && words.size >= 4 && words.last.match?(/\A\d+\z/)
64
- reject(number)
64
+ unparsed_records(number)
65
65
  next
66
66
  end
67
67
  row.merge!(reason: words[2...-1].join(" "), mtu: words.last.to_i)
68
68
  else
69
69
  match = /\A\S+\s+(.*?)\s+(connected|notconnect|disabled|admin-down|up|down)\s+(\d+|routed|trunk|unassigned|--)\s+(full|half|a-full|a-half|auto|--)\s+(\S+)\s+(.+)\z/i.match(line)
70
70
  unless match
71
- reject(number)
71
+ unparsed_records(number)
72
72
  next
73
73
  end
74
74
  row.merge!(name: match[1], state: match[2].downcase, vlan: match[3], duplex: match[4], speed: match[5], type: match[6],
@@ -19,7 +19,7 @@ module Vxlan
19
19
  # 解析命令表格行,保留数量及格式错误行诊断。
20
20
  def parse_rows(text)
21
21
  scope, totals = {}, []
22
- lines(text) do |line, number|
22
+ each_content_line(text) do |line, number|
23
23
  if HEADER.match?(line)
24
24
  @header = true
25
25
  elsif (match = /\AOSPF Process ID (\S+) VRF (\S+)\z/i.match(line))
@@ -31,15 +31,15 @@ module Vxlan
31
31
  row[:priority] = row[:priority].to_i
32
32
  unless ip_address?(row[:neighbor_id]) && ip_address?(row[:address]) && (0..255).cover?(row[:priority]) &&
33
33
  row[:state].match?(/\A(?:DOWN|ATTEMPT|INIT|2WAY|EXSTART|EXCHANGE|LOADING|FULL)\/(?:\s*(?:DR|BDR|DROTHER|-))\z/i)
34
- reject(number)
34
+ unparsed_records(number)
35
35
  next
36
36
  end
37
37
  @rows << row.merge(state: row[:state].downcase.delete(" "))
38
38
  else
39
- reject(number)
39
+ unparsed_records(number)
40
40
  end
41
41
  end
42
- totals.each { |count| mismatch(count, @rows.size) }
42
+ totals.each { |count| count_mismatch(count, @rows.size) }
43
43
  @empty = true if @rows.empty? && totals == [ 0 ]
44
44
  end
45
45
  end
@@ -13,22 +13,22 @@ module Vxlan
13
13
 
14
14
  # 解析当前命令的记录,保留无法识别行的诊断。
15
15
  def parse_rows(text)
16
- lines(text) do |line, number|
16
+ each_content_line(text) do |line, number|
17
17
  if line.match?(/\AVLAN\s+MAC Address\s+Type\s+age\s+Secure\s+NTFY\s+Ports\z/i)
18
18
  @header = true
19
19
  elsif (match = /\A(?:[*+GOCR~]\s+)?(\d+|-)\s+([\da-f]{4}(?:\.[\da-f]{4}){2})\s+(dynamic|static)\s+(\S+)\s+([TF])\s+([TF])\s+(.+)\z/i.match(line))
20
20
  unless match[1] == "-" || (1..4094).cover?(match[1].to_i)
21
- reject(number)
21
+ unparsed_records(number)
22
22
  next
23
23
  end
24
24
  @rows << { vlan_id: match[1] == "-" ? nil : match[1].to_i, mac: CtxParser::Values.mac_address(match[2]),
25
25
  type: match[3].downcase, age: match[4], secure: match[5].upcase == "T", notify: match[6].upcase == "T", destination: match[7],
26
26
  flags: line[/\A([*+GOCR~])\s+/, 1] }
27
27
  elsif !line.match?(/\A(?:Legend:|\* - primary entry, G - Gateway MAC, \(R\) - Routed MAC, O - Overlay MAC|age - seconds since last seen,\+ - primary entry using vPC Peer-Link,|\(T\) - True, \(F\) - False, C - ControlPlane MAC, ~ - vsan,?|\(NA\)- Not Applicable(?: A [–-] ESI Active Path, S [–-] ESI Standby Path)?)\z/i)
28
- reject(number)
28
+ unparsed_records(number)
29
29
  end
30
30
  end
31
- remove_duplicates(@rows, %i[vlan_id mac]) do |row|
31
+ exclude_ambiguous_records!(@rows, %i[vlan_id mac]) do |row|
32
32
  identity = row.values_at(:vlan_id, :mac)
33
33
  if row[:flags] == "G" && row[:type] == "static" && row[:destination].match?(/\A(?:sup-eth\d+\(R\)|(Lo\d+)\(R\)(?: \(\1\))?)\z/i)
34
34
  identity.concat([ :gateway, row[:destination] ])
@@ -17,13 +17,13 @@ module Vxlan
17
17
 
18
18
  # 解析命令表格行,保留数量及格式错误行诊断。
19
19
  def parse_rows(text)
20
- lines(text) do |line, number|
20
+ each_content_line(text) do |line, number|
21
21
  if HEADER.match?(line)
22
22
  @header = true
23
23
  elsif (match = ROW.match(line)) && ip_address?(match[2])
24
24
  @rows << FIELDS.zip(match.captures).to_h.merge(state: match[3].downcase)
25
25
  else
26
- reject(number)
26
+ unparsed_records(number)
27
27
  end
28
28
  end
29
29
  end
@@ -31,7 +31,7 @@ module Vxlan
31
31
  # 归一化平台值,同时保留未知或不支持的状态。
32
32
  def normalize(text)
33
33
  output = []
34
- super.each_line do |line|
34
+ super.each_line("\n") do |line|
35
35
  tail, previous = line.strip, output.last&.strip
36
36
  header = previous && previous.end_with?("Route") && tail == "r-Mac"
37
37
  mac = previous && tail.match?(/\A[\da-f]{4}\.[\da-f]{4}\z/i) && previous.match?(/\Anve\d+\s+\d+(?:\.\d+){3}\s+(?:Up|Down)\s+\S+\s+\S+\s+[\da-f]{4}\.\z/i)
@@ -41,7 +41,7 @@ module Vxlan
41
41
  output << line
42
42
  end
43
43
  end
44
- output.join
44
+ output.join("")
45
45
  end
46
46
  end
47
47
  end
@@ -17,19 +17,19 @@ module Vxlan
17
17
 
18
18
  # 解析命令表格行,保留数量及格式错误行诊断。
19
19
  def parse_rows(text)
20
- lines(text) do |line, number|
20
+ each_content_line(text) do |line, number|
21
21
  if HEADER.match?(line)
22
22
  @header = true
23
23
  elsif (match = ROW.match(line))
24
24
  row = FIELDS.zip(match.captures).to_h.compact
25
25
  row[:vni] = row[:vni].to_i
26
26
  unless (1..16_777_215).cover?(row[:vni]) && (!row[:flags] || %w[SA S-ND SU Xconn MS-IR HYB].include?(row[:flags]))
27
- reject(number)
27
+ unparsed_records(number)
28
28
  next
29
29
  end
30
30
  @rows << row.merge(state: row[:state].downcase, type: row[:type].downcase)
31
31
  elsif !line.gsub(/\s+/, " ").match?(/\A(?:Codes: CP - Control Plane DP - Data Plane|UC - Unconfigured SA - Suppress ARP|S-ND - Suppress ND|SU - Suppress Unknown Unicast|Xconn - Crossconnect|MS-IR - Multisite Ingress Replication|HYB - Hybrid IRB mode)\z/)
32
- reject(number)
32
+ unparsed_records(number)
33
33
  end
34
34
  end
35
35
  end
@@ -16,7 +16,7 @@ module Vxlan
16
16
  def parse_rows(text)
17
17
  totals = []
18
18
  previous = nil
19
- lines(text) do |line, number|
19
+ each_content_line(text) do |line, number|
20
20
  issue_count = @issues.size
21
21
  if line.match?(/\AGroup\s+Port-(?:Channel)?\s+Type\s+Protocol\s+Member Ports\z/i)
22
22
  @header = true
@@ -25,31 +25,31 @@ module Vxlan
25
25
  previous = nil
26
26
  next
27
27
  elsif (match = /\A(\d+)\s+(Po\d+)\(([^\s()]+)\)\s+(\S+)\s+(\S+)\s*(.*)\z/i.match(line))
28
- previous = { group: match[1].to_i, interface: match[2], flags: match[3], type: match[4], protocol: match[5], members: members(match[6]) }
28
+ previous = { group: match[1].to_i, interface: match[2], flags: match[3], type: match[4], protocol: match[5], members: parse_members(match[6]) }
29
29
  @rows << previous
30
30
  elsif line.match?(/\A(?:Eth(?:ernet)?\S+\([^)]+\)\s*)+\z/i) && previous
31
- previous[:members].concat(members(line))
31
+ previous[:members].concat(parse_members(line))
32
32
  elsif (match = /\ANumber of port-channels\s*:\s*(\d+)\z/i.match(line))
33
33
  totals << match[1].to_i
34
34
  previous = nil
35
35
  elsif !line.match?(/\A(?:Flags:\s+D - Down\s+P - Up in port-channel \(members\)|I - Individual\s+H - Hot-standby \(LACP only\)|s - Suspended\s+r - Module-removed|b - BFD Session Wait|S - Switched\s+R - Routed|U - Up \(port-channel\)|p - Up in delay-lacp mode \(member\)|M - Not in use\. Min-links not met)\z/)
36
- reject(number)
36
+ unparsed_records(number)
37
37
  previous = nil
38
38
  end
39
39
  previous = nil if @issues.size > issue_count
40
40
  end
41
41
  entries = @rows.flat_map { |row| row[:members] }
42
- remove_duplicates(entries, nil) { |member| member[:interface].sub(/\AEth(?:ernet)?/i, "Eth") }
42
+ exclude_ambiguous_records!(entries, nil) { |member| member[:interface].sub(/\AEth(?:ernet)?/i, "Eth") }
43
43
  unambiguous = entries.each_with_object({}.compare_by_identity) { |member, index| index[member] = true }
44
44
  @rows.each do |row|
45
45
  row[:members].select! { |member| unambiguous.key?(member) }
46
46
  end
47
- totals.each { |total| mismatch(total, @rows.size) }
47
+ totals.each { |total| count_mismatch(total, @rows.size) }
48
48
  @empty = true if totals == [ 0 ] && @rows.empty?
49
49
  end
50
50
 
51
51
  # 将回显的聚合成员归一化为接口身份及状态标志。
52
- def members(text)
52
+ def parse_members(text)
53
53
  return [] if text.empty? || text == "--"
54
54
  text.split.filter_map do |word|
55
55
  match = /\A(Eth(?:ernet)?[\d\/]+)\(([^\s()]+)\)\z/i.match(word)
@@ -24,20 +24,23 @@ module Vxlan
24
24
 
25
25
  # 解析运行配置,不依赖传输或资产清单。
26
26
  def parse_output(text, complete:)
27
- configuration = CiscoNxos::Configuration.new
28
- @issues = configuration.issues
27
+ configuration_state = CiscoNxos::ConfigurationState.new
28
+ @issues = configuration_state.issues
29
29
  @conflicts = {}
30
30
  @policy_sequences = {}
31
+ @policy_sources = {}.compare_by_identity
31
32
  @sources = {}
33
+ @global_scopes = {}
32
34
  @setting_sources = {}.compare_by_identity
33
35
  @configuration_conflict_count = 0
34
36
  @feature_sources = []
35
37
  @disabled_feature_sources = []
36
- data = configuration.data
38
+ data = configuration_state.data
37
39
  read_configuration(text, comments: /\A!/, boundaries: /\Aend\z/, document_class: Document).each { |block| parse_block(block, data) }
38
40
  @issues.concat(document.issues)
41
+ finalize_policies(data)
39
42
  # 所有声明读取完毕后再关联模板与网段,避免配置先后顺序改变关联结果。
40
- configuration.finish
43
+ configuration_state.finalize_facts
41
44
  @feature_sources.each { |block, feature| capture(block, feature, [ :features, data[:features].index(feature) ]) }
42
45
  @disabled_feature_sources.each { |block, feature| capture(block, feature, [ :disabled_features, data[:disabled_features].index(feature) ]) }
43
46
  data[:config_digest] = Digest::SHA256.hexdigest(@cursor.lines.map(&:raw).reject { |line| line.strip.empty? || line.lstrip.start_with?("!") }.map(&:rstrip).join("\n"))
@@ -59,9 +62,8 @@ module Vxlan
59
62
  def parse_setting(block, data)
60
63
  case block.text
61
64
  when /\Ahostname (\S+)\z/
62
- issue(block, :duplicate_hostname) if data[:hostname]
63
- data[:hostname] = $1
64
- capture(block, data[:hostname], [ :hostname ])
65
+ name = $1
66
+ store_global_scope(data, :hostname, block, empty: nil, duplicate_code: :duplicate_hostname) { name }
65
67
  when /\Afeature (.+)\z/
66
68
  data[:features] << $1
67
69
  @feature_sources << [ block, $1 ]
@@ -71,14 +73,13 @@ module Vxlan
71
73
  when /\Ainterface breakout module (\d+) port (\d+) map (\d+g-\d+x)\z/
72
74
  module_id, port, mapping = integer($1, 1..65_535), integer($2, 1..65_535), $3
73
75
  key = "#{module_id}/#{port}"
74
- store(data[:breakouts], key, block, path: [ :breakouts, key ]) { { module_id: module_id, port: port, map: mapping } }
76
+ store_unique_fact(data[:breakouts], key, block, path: [ :breakouts, key ]) { { module_id: module_id, port: port, map: mapping } }
75
77
  when /\Ainterface (\S+)\z/
76
78
  name = $1.sub(/\Avlan/i, "Vlan")
77
79
  if name.match?(/\Anve\d+\z/i)
78
- issue(block, :multiple_nve_interfaces) if data[:nve][:interface]
79
- data[:nve] = scope(block, [ :nve ]) { nve(block).merge(interface: name) }
80
+ store_global_scope(data, :nve, block, empty: { members: {} }, duplicate_code: :multiple_nve_interfaces) { nve(block).merge(interface: name) }
80
81
  else
81
- store(data[:interfaces], name, block, path: [ :interfaces, name ]) { interface(block) }
82
+ store_unique_fact(data[:interfaces], name, block, path: [ :interfaces, name ]) { interface(block) }
82
83
  settings = data[:interfaces][name]
83
84
  # VRF 歧义不能等同于默认 VRF;排除此接口,阻止跨接口借址和网段推导。
84
85
  if settings && @setting_sources.fetch(settings, {}).key?(:vrf) && !settings.key?(:vrf)
@@ -97,7 +98,7 @@ module Vxlan
97
98
 
98
99
  mappings = block.children.select { |child| child.text.start_with?("vn-segment") }
99
100
  ids.uniq.each do |id|
100
- store(data[:vlan_vnis], id, block, path: [ :vlan_vnis, id ]) do
101
+ store_unique_fact(data[:vlan_vnis], id, block, path: [ :vlan_vnis, id ]) do
101
102
  raise ArgumentError if mappings.size > 1
102
103
  vni = mappings.first && analyze(mappings.first) { integer(mappings.first.text.delete_prefix("vn-segment "), 1..16_777_215) }
103
104
  raise ArgumentError if vni && ids.uniq.size != 1
@@ -105,32 +106,35 @@ module Vxlan
105
106
  end
106
107
  end
107
108
  block.children.each { |child| ignore(child) unless mappings.include?(child) }
108
- when /\Avrf context (\S+)\z/ then store(data[:vrfs], $1, block, path: [ :vrfs, $1 ]) { vrf(block) }
109
+ when /\Avrf context (\S+)\z/ then store_unique_fact(data[:vrfs], $1, block, path: [ :vrfs, $1 ]) { vrf(block) }
109
110
  when "evpn"
110
111
  analyze_children(block) do |child|
111
112
  if (match = /\Avni (\d+) (l[23])\z/i.match(child.text))
112
113
  id = integer(match[1], 1..16_777_215)
113
- store(data[:evpn], id, child, path: [ :evpn, id ]) { vrf(child).merge(type: match[2].downcase) }
114
+ store_unique_fact(data[:evpn], id, child, path: [ :evpn, id ]) { vrf(child, address_families: false).merge(type: match[2].downcase) }
114
115
  else
115
116
  issue(child)
116
117
  end
117
118
  end
118
119
  when /\Avpc domain (\d+)\z/
119
- issue(block, :duplicate_vpc_domain) if data[:vpc_domain]
120
- id = integer($1, 1..1000)
121
- data[:vpc_domain] = scope(block, [ :vpc_domain ]) { vpc(block).merge(id: id) }
120
+ identifier = $1
121
+ store_global_scope(data, :vpc_domain, block, empty: nil, duplicate_code: :duplicate_vpc_domain) do
122
+ id = integer(identifier, 1..1000)
123
+ vpc(block).merge(id: id)
124
+ end
122
125
  when /\Arouter bgp (\d+)\z/
123
- asn = integer($1, 1..4_294_967_295)
124
- issue(block, :duplicate_bgp_process) if data[:bgp].any?
125
- data[:bgp] = scope(block, [ :bgp ]) { routing(block, :bgp).merge(asn: asn) }
126
- when /\Arouter ospf (\S+)\z/ then store(data[:ospf], $1, block, path: [ :ospf, $1 ]) { routing(block, :ospf) }
126
+ identifier = $1
127
+ store_global_scope(data, :bgp, block, empty: {}, duplicate_code: :duplicate_bgp_process) do
128
+ asn = integer(identifier, 1..4_294_967_295)
129
+ routing(block, :bgp).merge(asn: asn)
130
+ end
131
+ when /\Arouter ospf (\S+)\z/ then store_unique_fact(data[:ospf], $1, block, path: [ :ospf, $1 ]) { routing(block, :ospf) }
127
132
  when /\Aip prefix-list / then prefix_list(block, data[:prefix_lists], family: "ipv4")
128
133
  when /\Aipv6 prefix-list / then prefix_list(block, data[:ipv6_prefix_lists], family: "ipv6")
129
134
  when /\Aroute-map (\S+) (permit|deny) (\d+)\z/
130
135
  name, action, sequence = $1, $2, integer($3, 1..65_535)
131
136
  entries = (data[:route_maps][name] ||= [])
132
- issue(block, :duplicate_route_map_entry) if register_policy_sequence(:route_maps, name, sequence)
133
- entries << scope(block, [ :route_maps, name, entries.size ]) { route_map(block).merge(action: action, sequence: sequence) }
137
+ policy_entry(:route_maps, name, sequence, entries, block) { route_map(block).merge(action: action, sequence: sequence) }
134
138
  when /\Aip access-list (\S+)\z/
135
139
  name = $1
136
140
  entries = block.children.filter_map do |child|
@@ -145,14 +149,16 @@ module Vxlan
145
149
  end
146
150
  end
147
151
  end
148
- store(data[:acls], name, block, path: [ :acls, name ]) { entries }
152
+ store_unique_fact(data[:acls], name, block, path: [ :acls, name ]) { entries }
149
153
  when /\Aip route /
150
154
  data[:static_routes] << scope(block, [ :static_routes, data[:static_routes].size ]) { static_route(block) }
151
155
  when "nv overlay evpn", "no nv overlay evpn"
152
156
  assign(data, :nv_overlay_evpn, block.text == "nv overlay evpn", block)
153
157
  capture(block, data[:nv_overlay_evpn], [ :nv_overlay_evpn ]) if data.key?(:nv_overlay_evpn)
154
158
  when /\Afabric forwarding anycast-gateway-mac (\S+)\z/
155
- assign(data, :anycast_mac, $1, block)
159
+ mac = $1
160
+ CtxParser::Values.mac_address(mac)
161
+ assign(data, :anycast_mac, mac, block)
156
162
  capture(block, data[:anycast_mac], [ :anycast_mac ]) if data.key?(:anycast_mac)
157
163
  when /\A(?:interface|vlan|vrf|router|vpc|evpn|feature|ip prefix-list|ipv6 prefix-list|route-map)\b/ then issue(block)
158
164
  when /\A(?:%|ERROR:|Invalid command|--More--)/i then issue(block, :invalid_command_output)
@@ -162,7 +168,7 @@ module Vxlan
162
168
  end
163
169
 
164
170
  # 解析 VRF 定义及其 VNI 或 route-target 关联。
165
- def vrf(block)
171
+ def vrf(block, address_families: true)
166
172
  values = { route_targets: [], routes: [] }
167
173
  analyze_children(block) do |child|
168
174
  case child.text
@@ -171,6 +177,8 @@ module Vxlan
171
177
  when /\Aroute-target / then values[:route_targets] << route_target(child)
172
178
  when /\Aaddress-family (.+)\z/
173
179
  family = $1
180
+ next issue(child) unless address_families && %w[ipv4\ unicast ipv6\ unicast].include?(family)
181
+
174
182
  targets_start = values[:route_targets].size
175
183
  exports_start = values.fetch(:exports, []).size
176
184
  analyze_children(child) do |setting|
@@ -210,8 +218,8 @@ module Vxlan
210
218
  values = { peer_keepalive: {}, virtual_peer_link: {}, peer_gateway: false, peer_switch: false, layer3_peer_router: false }
211
219
  analyze_children(block) do |child|
212
220
  case child.text
213
- when /\Apeer-keepalive (.+)\z/ then assign(values, :peer_keepalive, pairs($1, %w[destination source vrf]), child)
214
- when /\Avirtual peer-link (.+)\z/ then assign(values, :virtual_peer_link, pairs($1, %w[destination source dscp]), child)
221
+ when /\Apeer-keepalive (.+)\z/ then assign(values, :peer_keepalive, parse_keyword_pairs($1, %w[destination source vrf]), child)
222
+ when /\Avirtual peer-link (.+)\z/ then assign(values, :virtual_peer_link, parse_keyword_pairs($1, %w[destination source dscp]), child)
215
223
  when "peer-gateway" then values[:peer_gateway] = true
216
224
  when "peer-switch" then values[:peer_switch] = true
217
225
  when "layer3 peer-router" then values[:layer3_peer_router] = true
@@ -224,12 +232,12 @@ module Vxlan
224
232
  end
225
233
 
226
234
  # 提取支持的键值配置对。
227
- def pairs(text, allowed)
235
+ def parse_keyword_pairs(text, allowed)
228
236
  words = text.split
229
237
  raise ArgumentError unless words.size.even? && (words.each_slice(2).map(&:first) - allowed).empty?
230
238
  values = words.each_slice(2).to_h.transform_keys(&:to_sym)
231
239
  raise ArgumentError unless values.size == words.size / 2
232
- %i[source destination].each { |key| values[key] = address(values[key]) if values[key] }
240
+ %i[source destination].each { |key| values[key] = ip_address(values[key]) if values[key] }
233
241
  integer(values[:dscp], 0..63) if values[:dscp]
234
242
  values
235
243
  end
@@ -238,7 +246,29 @@ module Vxlan
238
246
  def static_route(block)
239
247
  words = block.text.split
240
248
  raise ArgumentError if words.size < 4
241
- { prefix: prefix(words[2]), next_hop: words[3], options: words.drop(4).join(" ") }
249
+ validate_static_next_hop(words.drop(3))
250
+
251
+ { prefix: ipv4_prefix(words[2]), next_hop: words[3], options: words.drop(4).join(" ") }
252
+ end
253
+
254
+ # IP route 的下一跳是 IPv4 或出接口;接口分词形式不能误当作地址拒绝。
255
+ def validate_static_next_hop(words)
256
+ hop = words.first
257
+ interface = /\A(?:ethernet\d+(?:\/\d+){1,2}(?:\.\d+)?|port-channel\d+(?:\.\d+)?|vlan\d+|mgmt\d+|null0)\z/i
258
+ offset = if interface.match?(hop)
259
+ 1
260
+ elsif /\A(?:ethernet|port-channel|vlan|mgmt|null)\z/i.match?(hop)
261
+ raise ArgumentError unless words[1] && interface.match?(hop + words[1])
262
+
263
+ 2
264
+ else
265
+ 0
266
+ end
267
+ hop = words[offset]
268
+ # 出接口之后可直接跟已有选项;其余词须先作为 IPv4 下一跳校验。
269
+ return if offset.positive? && (hop.nil? || /\A(?:name|tag|track|vrf|\d+)\z/.match?(hop))
270
+
271
+ raise ArgumentError unless IPAddr.new(ip_address(hop)).ipv4?
242
272
  end
243
273
 
244
274
  # 解析十进制整数,并校验声明的边界。
@@ -247,13 +277,13 @@ module Vxlan
247
277
  CtxParser::Values.integer(value, min: range.min, max: range.max)
248
278
  end
249
279
 
250
- # 校验接口地址,同时保留网络前缀。
251
- def address(value)
280
+ # 校验并归一化 IPv4 或 IPv6 主机地址,不接受网络前缀。
281
+ def ip_address(value)
252
282
  CtxParser::Values.ip_address(value, allow_zone: true)
253
283
  end
254
284
 
255
285
  # 校验网络前缀,不静默修正错误语法。
256
- def prefix(value, preserve_host_bits: false) = CtxParser::Values.ipv4_prefix(value, preserve_host_bits: preserve_host_bits)
286
+ def ipv4_prefix(value, preserve_host_bits: false) = CtxParser::Values.ipv4_prefix(value, preserve_host_bits: preserve_host_bits)
257
287
 
258
288
  # 单值配置可重复确认,冲突后移除歧义字段并使两端来源失效。
259
289
  def assign(values, key, value, block)
@@ -276,15 +306,22 @@ module Vxlan
276
306
  issue(block, :conflicting_configuration_value)
277
307
  end
278
308
 
279
- # 保存已解析配置,同时保留其来源。
280
- def store(hash, key, block, path:)
309
+ # 全局单例共享唯一身份规则,歧义后恢复空形状供下游安全读取。
310
+ def store_global_scope(data, key, block, empty:, duplicate_code:)
311
+ store_unique_fact(@global_scopes, key, block, path: [ key ], duplicate_code: duplicate_code) { yield }
312
+ ensure
313
+ data[key] = @global_scopes.fetch(key, empty)
314
+ end
315
+
316
+ # 保存唯一身份的事实及来源;重复身份整体排除,后续声明不能恢复。
317
+ def store_unique_fact(hash, key, block, path:, duplicate_code: :duplicate_identity)
281
318
  identity = [ hash.object_id, key ]
282
319
  if hash.key?(key) || @conflicts[identity]
283
320
  @configuration_conflict_count += 1
284
321
  @conflicts[identity] = true
285
322
  hash.delete(key)
286
323
  @cursor.mark(@sources[identity].index, state: :invalid) if @sources[identity]
287
- issue(block, :duplicate_identity)
324
+ issue(block, duplicate_code)
288
325
  return
289
326
  end
290
327
  @sources[identity] = block
@@ -14,7 +14,7 @@ module Vxlan
14
14
  values, section = { attributes: {}, peer_links: [], members: [] }, nil
15
15
  labels = { "vPC domain id" => :domain_id, "Peer status" => :peer_status, "vPC keep-alive status" => :keepalive_status,
16
16
  "Configuration consistency status" => :consistency_status, "vPC role" => :role }
17
- lines(text) do |line, number|
17
+ each_content_line(text) do |line, number|
18
18
  if line.match?(/\Aid\s+Port\s+Status\s+Active vlans\z/i)
19
19
  section = :peer_links
20
20
  elsif line.match?(/\Aid\s+Port\s+Status\s+Consistency\s+Reason\s+Active vlans\z/i)
@@ -25,14 +25,14 @@ module Vxlan
25
25
  if section == :members
26
26
  member = /\A(\S+)\s+(.+?)\s{2,}([\d, .-]+)\z/.match(match[4])
27
27
  unless member
28
- reject(number)
28
+ unparsed_records(number)
29
29
  next
30
30
  end
31
31
  row.merge!(consistency: member[1], reason: member[2], active_vlans_summary: member[3])
32
32
  elsif match[4].match?(/\A[\d, .-]+\z/)
33
- row[:active_vlans_summary] = rest.join
33
+ row[:active_vlans_summary] = rest.join("")
34
34
  else
35
- reject(number)
35
+ unparsed_records(number)
36
36
  next
37
37
  end
38
38
  values[section] << row
@@ -46,18 +46,18 @@ module Vxlan
46
46
  if key
47
47
  @header = true
48
48
  if values.key?(key) || value.empty? || (key == :domain_id && !(value.match?(/\A\d+\z/) && (1..1000).cover?(value.to_i)))
49
- reject(number)
49
+ unparsed_records(number)
50
50
  else
51
51
  values[key] = key == :domain_id ? value.to_i : value
52
52
  end
53
53
  elsif label.match?(/\A(?:Per-vlan consistency status|Type-2 consistency status|Number of vPCs configured|Peer Gateway|Dual-active excluded VLANs|Graceful Consistency Check|Auto-recovery status|Delay-restore(?: SVI| Orphan-port)? status|Operational Layer3 Peer-router|Virtual-peerlink mode)\z/) && !value.empty?
54
- reject(number) if values[:attributes].key?(label)
54
+ unparsed_records(number) if values[:attributes].key?(label)
55
55
  values[:attributes][label] = value
56
56
  else
57
- reject(number)
57
+ unparsed_records(number)
58
58
  end
59
59
  elsif !line.match?(/\A(?:vPC Peer-link status|vPC status|Legend:|\(\*\) - local vPC is down, forwarding via vPC peer-link|Please check "show vpc consistency-parameters vpc <vpc-num>" for the|consistency reason of down vpc and for type-2 consistency reasons for|any vpc\.)\z/i)
60
- reject(number)
60
+ unparsed_records(number)
61
61
  end
62
62
  end
63
63
  return unless @header
@@ -66,9 +66,9 @@ module Vxlan
66
66
  values.values_at(:peer_links, :members).flatten.each { |row| row[:active_vlans_truncated] = row[:active_vlans_summary].include?("...") }
67
67
  count = values[:attributes]["Number of vPCs configured"]
68
68
  if count
69
- count.match?(/\A\d+\z/) ? mismatch(count.to_i, values[:members].size) : @issues << { code: :invalid_vpc_count }
69
+ count.match?(/\A\d+\z/) ? count_mismatch(count.to_i, values[:members].size) : @issues << { code: :invalid_vpc_count }
70
70
  end
71
- %i[peer_links members].each { |key| remove_duplicates(values[key], [ :id ]) }
71
+ %i[peer_links members].each { |key| exclude_ambiguous_records!(values[key], [ :id ]) }
72
72
  @rows << values
73
73
  end
74
74
  end
@@ -12,7 +12,7 @@ module Vxlan
12
12
  values = { addresses: [], acl_bindings: {}, anycast: false }
13
13
  analyze_children(block) do |child|
14
14
  case child.text
15
- when /\Aip address (\S+)(?: secondary)?\z/ then values[:addresses] << prefix($1, preserve_host_bits: true)
15
+ when /\Aip address (\S+)(?: secondary)?\z/ then values[:addresses] << ipv4_prefix($1, preserve_host_bits: true)
16
16
  when /\Aip unnumbered (loopback\d+)\z/i then assign(values, :unnumbered_interface, $1, child)
17
17
  when /\Avrf member (\S+)\z/ then assign(values, :vrf, $1, child)
18
18
  when /\Aip router ospf (\S+) area (\S+)\z/ then assign(values, :ospf, "#{$1} area #{$2}", child)
@@ -48,7 +48,7 @@ module Vxlan
48
48
  analyze_children(child) { |setting| nve_member(setting, member) }
49
49
  raise ArgumentError if member[:replication] == "bgp" && member[:multicast_group]
50
50
 
51
- store(values[:members], id, child, path: @source_path + [ :members, id ]) { member }
51
+ store_unique_fact(values[:members], id, child, path: @source_path + [ :members, id ]) { member }
52
52
  when /\Adescription (.+)\z/ then assign(values, :description, $1, child)
53
53
  when /\Aglobal mcast-group (\S+)\z/ then assign(values, :multicast_group, multicast_address($1), child)
54
54
  when /\Asource-interface (\S+)\z/ then assign(values, :source_interface, $1, child)
@@ -80,7 +80,7 @@ module Vxlan
80
80
 
81
81
  # 校验 VXLAN 复制使用的组播地址。
82
82
  def multicast_address(value)
83
- group = address(value)
83
+ group = ip_address(value)
84
84
  raise ArgumentError unless IPAddr.new("224.0.0.0/4").include?(group)
85
85
 
86
86
  group
@@ -12,24 +12,46 @@ module Vxlan
12
12
  return ignore(block) if /\A(?:ip|ipv6) prefix-list \S+ description .+\z/.match?(block.text)
13
13
  match = /\A(?:ip|ipv6) prefix-list (\S+) seq (\d+) (permit|deny) (\S+)(?: ge (\d+))?(?: le (\d+))?\z/.match(block.text)
14
14
  raise ArgumentError unless match
15
- subnet = network_prefix(match[4], family: family)
16
- length, width = IPAddr.new(subnet).prefix, family == "ipv6" ? 128 : 32
17
- ge, le = match[5]&.to_i, match[6]&.to_i
18
- raise ArgumentError if (ge && !(length..width).cover?(ge)) || (le && !((ge || length)..width).cover?(le))
19
- entries = (lists[match[1]] ||= [])
20
15
  sequence = integer(match[2], 1..4_294_967_295)
21
16
  collection = family == "ipv6" ? :ipv6_prefix_lists : :prefix_lists
22
- raise ArgumentError if register_policy_sequence(collection, match[1], sequence)
23
- entries << { sequence: sequence, action: match[3], prefix: subnet, ge: ge, le: le }
24
- capture(block, entries.last, [ collection, match[1], entries.size - 1 ])
17
+ entries = (lists[match[1]] ||= [])
18
+ policy_entry(collection, match[1], sequence, entries, block) do
19
+ subnet = network_prefix(match[4], family: family)
20
+ length, width = IPAddr.new(subnet).prefix, family == "ipv6" ? 128 : 32
21
+ ge, le = match[5]&.to_i, match[6]&.to_i
22
+ raise ArgumentError if (ge && !(length..width).cover?(ge)) || (le && !((ge || length)..width).cover?(le))
23
+
24
+ { sequence: sequence, action: match[3], prefix: subnet, ge: ge, le: le }
25
+ end
25
26
  end
26
27
 
27
- # 单次解析用索引识别重复序号,避免长策略逐条扫描已有条目。
28
- def register_policy_sequence(collection, name, sequence)
28
+ # 用固定槽位排除冲突序号,避免反复扫描或移动整个策略数组。
29
+ def policy_entry(collection, name, sequence, entries, block)
29
30
  key = [ collection, name, sequence ]
30
- duplicate = @policy_sequences.key?(key)
31
- @policy_sequences[key] = true
32
- duplicate
31
+ if (previous = @policy_sequences[key])
32
+ entries[previous.fetch(:index)] = nil
33
+ @cursor.mark(previous.fetch(:block).index, state: :invalid)
34
+ code = collection == :route_maps ? :duplicate_route_map_entry : :invalid_configuration_value
35
+ return issue(block, code)
36
+ end
37
+ @policy_sequences[key] = { index: entries.size, block: block }
38
+ entries << nil
39
+ entry = yield
40
+ entries[-1] = entry
41
+ @policy_sources[entry] = block
42
+ entry
43
+ end
44
+
45
+ # 冲突处理完毕后按最终索引捕获来源,路径与公开结果保持一致。
46
+ def finalize_policies(data)
47
+ %i[prefix_lists ipv6_prefix_lists route_maps].each do |collection|
48
+ data.fetch(collection).each do |name, entries|
49
+ entries.compact!
50
+ entries.each_with_index do |entry, index|
51
+ capture(@policy_sources.fetch(entry), entry, [ collection, name, index ])
52
+ end
53
+ end
54
+ end
33
55
  end
34
56
 
35
57
  # 解析有序 route-map 条目及策略引用。