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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README-zh.md +8 -0
- data/README.md +8 -0
- data/docs/architecture-zh.md +1 -1
- data/lib/vxlan/bgp_neighbors.rb +4 -1
- data/lib/vxlan/cisco_nxos/configuration.rb +3 -77
- data/lib/vxlan/cisco_nxos/configuration_state.rb +87 -0
- data/lib/vxlan/cisco_nxos/document.rb +1 -1
- data/lib/vxlan/cisco_nxos/evpn_l2route.rb +32 -23
- data/lib/vxlan/cisco_nxos/parser.rb +9 -9
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +7 -7
- data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +7 -7
- data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +8 -8
- data/lib/vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +4 -4
- data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +4 -4
- data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +4 -4
- data/lib/vxlan/cisco_nxos/parsers/show_nve_vni.rb +3 -3
- data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +7 -7
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +74 -37
- data/lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb +10 -10
- data/lib/vxlan/cisco_nxos/syntax/interfaces.rb +3 -3
- data/lib/vxlan/cisco_nxos/syntax/policies.rb +35 -13
- data/lib/vxlan/cisco_nxos/syntax/routing.rb +45 -6
- data/lib/vxlan/cisco_nxos/terminal.rb +3 -1
- data/lib/vxlan/collector.rb +4 -4
- data/lib/vxlan/configuration_review.rb +13 -2
- data/lib/vxlan/prefix_list_references.rb +3 -3
- data/lib/vxlan/version.rb +1 -1
- metadata +2 -1
|
@@ -16,7 +16,7 @@ module Vxlan
|
|
|
16
16
|
# 解析当前命令的记录,保留无法识别行的诊断。
|
|
17
17
|
def parse_rows(text)
|
|
18
18
|
layout = nil
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
unparsed_records(number)
|
|
35
35
|
next
|
|
36
36
|
end
|
|
37
37
|
@rows << row.merge(state: row[:state].downcase.delete(" "))
|
|
38
38
|
else
|
|
39
|
-
|
|
39
|
+
unparsed_records(number)
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
|
-
totals.each { |count|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
unparsed_records(number)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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|
|
|
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
|
|
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
|
-
|
|
28
|
-
@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 =
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
data
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
214
|
-
when /\Avirtual peer-link (.+)\z/ then assign(values, :virtual_peer_link,
|
|
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
|
|
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] =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
unparsed_records(number) if values[:attributes].key?(label)
|
|
55
55
|
values[:attributes][label] = value
|
|
56
56
|
else
|
|
57
|
-
|
|
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
|
-
|
|
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/) ?
|
|
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|
|
|
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] <<
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
28
|
+
# 用固定槽位排除冲突序号,避免反复扫描或移动整个策略数组。
|
|
29
|
+
def policy_entry(collection, name, sequence, entries, block)
|
|
29
30
|
key = [ collection, name, sequence ]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 条目及策略引用。
|