vxlan 0.2.0 → 0.2.2
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 +21 -0
- data/README-zh.md +9 -1
- data/README.md +9 -1
- 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 +31 -22
- data/lib/vxlan/cisco_nxos/parser.rb +7 -7
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +5 -5
- 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 +2 -2
- 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 +46 -35
- data/lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb +9 -9
- 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 +10 -9
|
@@ -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
|
|
@@ -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).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,7 +149,7 @@ 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"
|
|
@@ -210,8 +214,8 @@ module Vxlan
|
|
|
210
214
|
values = { peer_keepalive: {}, virtual_peer_link: {}, peer_gateway: false, peer_switch: false, layer3_peer_router: false }
|
|
211
215
|
analyze_children(block) do |child|
|
|
212
216
|
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,
|
|
217
|
+
when /\Apeer-keepalive (.+)\z/ then assign(values, :peer_keepalive, parse_keyword_pairs($1, %w[destination source vrf]), child)
|
|
218
|
+
when /\Avirtual peer-link (.+)\z/ then assign(values, :virtual_peer_link, parse_keyword_pairs($1, %w[destination source dscp]), child)
|
|
215
219
|
when "peer-gateway" then values[:peer_gateway] = true
|
|
216
220
|
when "peer-switch" then values[:peer_switch] = true
|
|
217
221
|
when "layer3 peer-router" then values[:layer3_peer_router] = true
|
|
@@ -224,12 +228,12 @@ module Vxlan
|
|
|
224
228
|
end
|
|
225
229
|
|
|
226
230
|
# 提取支持的键值配置对。
|
|
227
|
-
def
|
|
231
|
+
def parse_keyword_pairs(text, allowed)
|
|
228
232
|
words = text.split
|
|
229
233
|
raise ArgumentError unless words.size.even? && (words.each_slice(2).map(&:first) - allowed).empty?
|
|
230
234
|
values = words.each_slice(2).to_h.transform_keys(&:to_sym)
|
|
231
235
|
raise ArgumentError unless values.size == words.size / 2
|
|
232
|
-
%i[source destination].each { |key| values[key] =
|
|
236
|
+
%i[source destination].each { |key| values[key] = ip_address(values[key]) if values[key] }
|
|
233
237
|
integer(values[:dscp], 0..63) if values[:dscp]
|
|
234
238
|
values
|
|
235
239
|
end
|
|
@@ -238,7 +242,7 @@ module Vxlan
|
|
|
238
242
|
def static_route(block)
|
|
239
243
|
words = block.text.split
|
|
240
244
|
raise ArgumentError if words.size < 4
|
|
241
|
-
{ prefix:
|
|
245
|
+
{ prefix: ipv4_prefix(words[2]), next_hop: words[3], options: words.drop(4).join(" ") }
|
|
242
246
|
end
|
|
243
247
|
|
|
244
248
|
# 解析十进制整数,并校验声明的边界。
|
|
@@ -247,13 +251,13 @@ module Vxlan
|
|
|
247
251
|
CtxParser::Values.integer(value, min: range.min, max: range.max)
|
|
248
252
|
end
|
|
249
253
|
|
|
250
|
-
#
|
|
251
|
-
def
|
|
254
|
+
# 校验并归一化 IPv4 或 IPv6 主机地址,不接受网络前缀。
|
|
255
|
+
def ip_address(value)
|
|
252
256
|
CtxParser::Values.ip_address(value, allow_zone: true)
|
|
253
257
|
end
|
|
254
258
|
|
|
255
259
|
# 校验网络前缀,不静默修正错误语法。
|
|
256
|
-
def
|
|
260
|
+
def ipv4_prefix(value, preserve_host_bits: false) = CtxParser::Values.ipv4_prefix(value, preserve_host_bits: preserve_host_bits)
|
|
257
261
|
|
|
258
262
|
# 单值配置可重复确认,冲突后移除歧义字段并使两端来源失效。
|
|
259
263
|
def assign(values, key, value, block)
|
|
@@ -276,15 +280,22 @@ module Vxlan
|
|
|
276
280
|
issue(block, :conflicting_configuration_value)
|
|
277
281
|
end
|
|
278
282
|
|
|
279
|
-
#
|
|
280
|
-
def
|
|
283
|
+
# 全局单例共享唯一身份规则,歧义后恢复空形状供下游安全读取。
|
|
284
|
+
def store_global_scope(data, key, block, empty:, duplicate_code:)
|
|
285
|
+
store_unique_fact(@global_scopes, key, block, path: [ key ], duplicate_code: duplicate_code) { yield }
|
|
286
|
+
ensure
|
|
287
|
+
data[key] = @global_scopes.fetch(key, empty)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# 保存唯一身份的事实及来源;重复身份整体排除,后续声明不能恢复。
|
|
291
|
+
def store_unique_fact(hash, key, block, path:, duplicate_code: :duplicate_identity)
|
|
281
292
|
identity = [ hash.object_id, key ]
|
|
282
293
|
if hash.key?(key) || @conflicts[identity]
|
|
283
294
|
@configuration_conflict_count += 1
|
|
284
295
|
@conflicts[identity] = true
|
|
285
296
|
hash.delete(key)
|
|
286
297
|
@cursor.mark(@sources[identity].index, state: :invalid) if @sources[identity]
|
|
287
|
-
issue(block,
|
|
298
|
+
issue(block, duplicate_code)
|
|
288
299
|
return
|
|
289
300
|
end
|
|
290
301
|
@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
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 条目及策略引用。
|
|
@@ -5,18 +5,28 @@ module Vxlan
|
|
|
5
5
|
module Syntax
|
|
6
6
|
# 路由作用域保留邻居、模板、VRF 与地址族的归属。
|
|
7
7
|
module Routing
|
|
8
|
+
BGP_LEAF = /\A(?:no )?(?:remote-as|inherit peer|update-source|route-map|next-hop-self|route-reflector-client|advertise-pip|advertise l2vpn evpn|send-community|retain route-target)\b/
|
|
9
|
+
private_constant :BGP_LEAF
|
|
10
|
+
|
|
8
11
|
private
|
|
9
12
|
|
|
10
13
|
# 解析支持的路由作用域及协议关系。
|
|
11
|
-
def routing(block, protocol, context = :process)
|
|
14
|
+
def routing(block, protocol, context = :process, family: nil, parent: nil)
|
|
12
15
|
values = { vrfs: {}, neighbors: {}, templates: {}, address_families: {}, redistribute: [] }
|
|
13
16
|
analyze_children(block) do |child|
|
|
17
|
+
next issue(child) if protocol != :bgp && BGP_LEAF.match?(child.text)
|
|
18
|
+
next issue(child) unless routing_leaf?(child.text, protocol, context, family, parent)
|
|
19
|
+
|
|
14
20
|
case child.text
|
|
15
21
|
when /\Avrf (\S+)\z/ then routing_scope(values, :vrfs, $1, child, protocol, context)
|
|
16
22
|
when /\Atemplate peer (\S+)\z/ then routing_scope(values, :templates, $1, child, protocol, context)
|
|
17
23
|
when /\Aneighbor (\S+)\z/ then routing_scope(values, :neighbors, $1, child, protocol, context)
|
|
18
24
|
when /\Aaddress-family (.+)\z/ then routing_scope(values, :address_families, $1, child, protocol, context)
|
|
19
|
-
when /\Arouter-id (\S+)\z/
|
|
25
|
+
when /\Arouter-id (\S+)\z/
|
|
26
|
+
router_id = ip_address($1)
|
|
27
|
+
raise ArgumentError unless IPAddr.new(router_id).ipv4?
|
|
28
|
+
|
|
29
|
+
assign(values, :router_id, router_id, child)
|
|
20
30
|
when /\Aremote-as (\S+)\z/
|
|
21
31
|
value = $1
|
|
22
32
|
integer(value, 1..4_294_967_295) unless %w[internal external].include?(value)
|
|
@@ -42,9 +52,12 @@ module Vxlan
|
|
|
42
52
|
assign(values, :extended_community, enabled, child) if kind
|
|
43
53
|
when /\Anetwork (\S+)(?: route-map (\S+))?\z/
|
|
44
54
|
subnet, route_map = $1, $2
|
|
45
|
-
|
|
55
|
+
unless protocol == :bgp && context == :address_families && %i[process vrfs].include?(parent) &&
|
|
56
|
+
/\Aipv[46] (?:unicast|multicast)\z/.match?(family)
|
|
57
|
+
next issue(child)
|
|
58
|
+
end
|
|
46
59
|
|
|
47
|
-
(values[:networks] ||= []) << { prefix: network_prefix(subnet), route_map: route_map }.compact
|
|
60
|
+
(values[:networks] ||= []) << { prefix: network_prefix(subnet, family: family.split.first), route_map: route_map }.compact
|
|
48
61
|
when /\Aredistribute (\S+)(?: (?!route-map\b)(\S+))?(?: route-map (\S+))?\z/
|
|
49
62
|
values[:redistribute] << { protocol: $1, process: $2, route_map: $3 }.compact
|
|
50
63
|
when /\Aretain route-target (all|route-map \S+)\z/ then assign(values, :retain_route_target, $1, child)
|
|
@@ -56,18 +69,44 @@ module Vxlan
|
|
|
56
69
|
values
|
|
57
70
|
end
|
|
58
71
|
|
|
72
|
+
# 已建模叶子必须位于其使用方作用域,不能只因文字可解析就接受事实。
|
|
73
|
+
def routing_leaf?(text, protocol, context, family, parent)
|
|
74
|
+
process_family = context == :address_families && %i[process vrfs].include?(parent)
|
|
75
|
+
case text
|
|
76
|
+
when /\A(?:remote-as|inherit peer|update-source)\b/
|
|
77
|
+
protocol == :bgp && %i[neighbors templates].include?(context)
|
|
78
|
+
when /\Aroute-map\b/
|
|
79
|
+
protocol == :bgp && context == :address_families && %i[neighbors templates].include?(parent)
|
|
80
|
+
when /\Aredistribute\b/
|
|
81
|
+
if protocol == :ospf
|
|
82
|
+
%i[process vrfs].include?(context)
|
|
83
|
+
else
|
|
84
|
+
process_family && /\Aipv[46] (?:unicast|multicast)\z/.match?(family)
|
|
85
|
+
end
|
|
86
|
+
when /\Aretain route-target\b/
|
|
87
|
+
protocol == :bgp && process_family && family == "l2vpn evpn"
|
|
88
|
+
when /\Arouter-id\b/
|
|
89
|
+
%i[process vrfs].include?(context)
|
|
90
|
+
else true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
59
94
|
# 只进入当前事实模型可表达的子作用域,阻止任意递归及跨协议误归属。
|
|
60
95
|
def routing_scope(values, kind, name, block, protocol, context)
|
|
61
96
|
supported = case kind
|
|
62
97
|
when :vrfs then context == :process
|
|
63
98
|
when :templates then protocol == :bgp && context == :process
|
|
64
99
|
when :neighbors then protocol == :bgp && %i[process vrfs].include?(context)
|
|
65
|
-
when :address_families
|
|
100
|
+
when :address_families
|
|
101
|
+
protocol == :bgp && %i[process vrfs neighbors templates].include?(context) &&
|
|
102
|
+
/\A(?:ipv[46] (?:unicast|multicast)|vpnv[46] unicast|l2vpn evpn)\z/.match?(name)
|
|
66
103
|
end
|
|
67
104
|
return issue(block) unless supported
|
|
68
105
|
|
|
69
106
|
conflicts_before = @configuration_conflict_count
|
|
70
|
-
|
|
107
|
+
store_unique_fact(values[kind], name, block, path: @source_path + [ kind, name ]) do
|
|
108
|
+
routing(block, protocol, kind, family: kind == :address_families ? name : nil, parent: context)
|
|
109
|
+
end
|
|
71
110
|
# 邻居及模板的子地址族冲突也使继承身份失效;保留同进程其他邻居。
|
|
72
111
|
if protocol == :bgp && %i[neighbors templates].include?(kind) && @configuration_conflict_count > conflicts_before
|
|
73
112
|
exclude_conflicted_scope(values[kind], name, block)
|
|
@@ -8,7 +8,9 @@ module Vxlan
|
|
|
8
8
|
private_constant :INVALID
|
|
9
9
|
|
|
10
10
|
# 检查原始正文,避免 strip 或字段解析先吞掉控制字符。
|
|
11
|
-
def self.invalid_output?(text
|
|
11
|
+
def self.invalid_output?(text, allow_cr_newlines: false)
|
|
12
|
+
INVALID.match?(text) || (!allow_cr_newlines && /\r(?!\n)/.match?(text))
|
|
13
|
+
end
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
data/lib/vxlan/collector.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Vxlan
|
|
|
34
34
|
begin
|
|
35
35
|
connector.open do |session|
|
|
36
36
|
reader = CtxDriver::Reader.new(session: session, clock: @clock)
|
|
37
|
-
if (code =
|
|
37
|
+
if (code = prepare_terminal(reader))
|
|
38
38
|
dimensions.each { |name| observations[name] = failure(name, code) }
|
|
39
39
|
observations[:collection] = failure(:collection, code)
|
|
40
40
|
next
|
|
@@ -45,7 +45,7 @@ module Vxlan
|
|
|
45
45
|
observations[name] = Observation.new(name: name, status: :complete, completed_at: @clock.call,
|
|
46
46
|
issues: [ { code: :vpc_not_configured } ])
|
|
47
47
|
else
|
|
48
|
-
observations[name] =
|
|
48
|
+
observations[name] = collect_observation(reader, name, config, profile)
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
session_finished = true
|
|
@@ -72,7 +72,7 @@ module Vxlan
|
|
|
72
72
|
private
|
|
73
73
|
|
|
74
74
|
# 采集设备回显前,禁用分页并归一化终端宽度。
|
|
75
|
-
def
|
|
75
|
+
def prepare_terminal(reader)
|
|
76
76
|
PREPARATION_COMMANDS.each do |command|
|
|
77
77
|
capture = reader.read(command)
|
|
78
78
|
return capture.failure.fetch(:code) unless capture.success?
|
|
@@ -81,7 +81,7 @@ module Vxlan
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# 解析失败限定在单条查询内;Reader 负责传输与回执校验。
|
|
84
|
-
def
|
|
84
|
+
def collect_observation(reader, name, configuration, profile)
|
|
85
85
|
command = profile.command(name)
|
|
86
86
|
capture = reader.read(command)
|
|
87
87
|
metadata = { started_at: capture.started_at, completed_at: capture.completed_at, evidence: capture.evidence }
|