vxlan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +22 -0
- data/README-zh.md +90 -0
- data/README.md +90 -0
- data/docs/architecture-zh.md +95 -0
- data/docs/release.md +13 -0
- data/examples/offline.rb +16 -0
- data/lib/vxlan/bgp_neighbors.rb +29 -0
- data/lib/vxlan/cisco_nxos/configuration.rb +84 -0
- data/lib/vxlan/cisco_nxos/configuration_document.rb +31 -0
- data/lib/vxlan/cisco_nxos/configuration_parsing/interfaces.rb +91 -0
- data/lib/vxlan/cisco_nxos/configuration_parsing/policies.rb +58 -0
- data/lib/vxlan/cisco_nxos/configuration_parsing/routing.rb +62 -0
- data/lib/vxlan/cisco_nxos/evpn_l2route.rb +118 -0
- data/lib/vxlan/cisco_nxos/parser.rb +105 -0
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +80 -0
- data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +86 -0
- data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +84 -0
- data/lib/vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +48 -0
- data/lib/vxlan/cisco_nxos/parsers/show_l2route_evpn_mac_all.rb +14 -0
- data/lib/vxlan/cisco_nxos/parsers/show_l2route_evpn_mac_ip_all.rb +14 -0
- data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +42 -0
- data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +49 -0
- data/lib/vxlan/cisco_nxos/parsers/show_nve_vni.rb +39 -0
- data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +49 -0
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +303 -0
- data/lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb +77 -0
- data/lib/vxlan/cisco_nxos/profile.rb +39 -0
- data/lib/vxlan/collector.rb +127 -0
- data/lib/vxlan/configuration.rb +20 -0
- data/lib/vxlan/configuration_review.rb +43 -0
- data/lib/vxlan/l2route_parser.rb +21 -0
- data/lib/vxlan/observation.rb +24 -0
- data/lib/vxlan/parser_registry.rb +9 -0
- data/lib/vxlan/prefix_list_references.rb +89 -0
- data/lib/vxlan/runtime_parser.rb +22 -0
- data/lib/vxlan/snapshot.rb +23 -0
- data/lib/vxlan/version.rb +6 -0
- data/lib/vxlan.rb +16 -0
- metadata +140 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../evpn_l2route"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# 解析 show l2route evpn mac-ip all 的 CLI 回显。
|
|
9
|
+
class ShowL2routeEvpnMacIpAll < EvpnL2route
|
|
10
|
+
TYPE = :mac_ip
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# 解析 MAC 表记录,将网关条目保留为独立身份。
|
|
9
|
+
class ShowMacAddressTable < Parser
|
|
10
|
+
EMPTY = "(?:MAC addresses|entries)".freeze
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# 解析当前命令的记录,保留无法识别行的诊断。
|
|
15
|
+
def parse_rows(text)
|
|
16
|
+
lines(text) do |line, number|
|
|
17
|
+
if line.match?(/\AVLAN\s+MAC Address\s+Type\s+age\s+Secure\s+NTFY\s+Ports\z/i)
|
|
18
|
+
@header = true
|
|
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
|
+
unless match[1] == "-" || (1..4094).cover?(match[1].to_i)
|
|
21
|
+
reject(number)
|
|
22
|
+
next
|
|
23
|
+
end
|
|
24
|
+
@rows << { vlan_id: match[1] == "-" ? nil : match[1].to_i, mac: CtxParser::Values.mac(match[2]),
|
|
25
|
+
type: match[3].downcase, age: match[4], secure: match[5].upcase == "T", notify: match[6].upcase == "T", destination: match[7],
|
|
26
|
+
flags: line[/\A([*+GOCR~])\s+/, 1] }
|
|
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)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
remove_duplicates(@rows, %i[vlan_id mac]) do |row|
|
|
32
|
+
identity = row.values_at(:vlan_id, :mac)
|
|
33
|
+
if row[:flags] == "G" && row[:type] == "static" && row[:destination].match?(/\A(?:sup-eth\d+\(R\)|(Lo\d+)\(R\)(?: \(\1\))?)\z/i)
|
|
34
|
+
identity.concat([ :gateway, row[:destination] ])
|
|
35
|
+
end
|
|
36
|
+
identity
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# 解析 NVE 端点及路由器 MAC,支持终端换行记录。
|
|
9
|
+
class ShowNvePeers < Parser
|
|
10
|
+
EMPTY = "peers"
|
|
11
|
+
IDENTITY = %i[interface peer_ip].freeze
|
|
12
|
+
HEADER = /\AInterface\s+Peer-IP\s+State\s+LearnType\s+Uptime\s+Router-Mac\z/i
|
|
13
|
+
ROW = /\A(nve\d+)\s+(\d+(?:\.\d+){3})\s+(Up|Down)\s+(\S+)\s+(\S+)\s+([\da-f]{4}(?:\.[\da-f]{4}){2})\z/i
|
|
14
|
+
FIELDS = %i[interface peer_ip state learn_type uptime router_mac].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# 解析命令表格行,保留数量及格式错误行诊断。
|
|
19
|
+
def parse_rows(text)
|
|
20
|
+
lines(text) do |line, number|
|
|
21
|
+
if HEADER.match?(line)
|
|
22
|
+
@header = true
|
|
23
|
+
elsif (match = ROW.match(line)) && valid_host?(match[2])
|
|
24
|
+
@rows << FIELDS.zip(match.captures).to_h.merge(state: match[3].downcase)
|
|
25
|
+
else
|
|
26
|
+
reject(number)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 归一化平台值,同时保留未知或不支持的状态。
|
|
32
|
+
def normalize(text)
|
|
33
|
+
output = []
|
|
34
|
+
super.each_line do |line|
|
|
35
|
+
tail, previous = line.strip, output.last&.strip
|
|
36
|
+
header = previous && previous.end_with?("Route") && tail == "r-Mac"
|
|
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)
|
|
38
|
+
if header || mac
|
|
39
|
+
output[-1] = output.last.rstrip + tail + "\n"
|
|
40
|
+
else
|
|
41
|
+
output << line
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
output.join
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# 从 show nve vni 解析 VNI 身份、关联关系及 NX-OS 标志。
|
|
9
|
+
class ShowNveVni < Parser
|
|
10
|
+
EMPTY = "VNIs"
|
|
11
|
+
IDENTITY = %i[interface vni].freeze
|
|
12
|
+
HEADER = /\AInterface\s+VNI\s+Multicast-group\s+State\s+Mode\s+Type\s+\[BD\/VRF\](?:\s+Flags)?\z/i
|
|
13
|
+
ROW = /\A(nve\d+)\s+(\d+)\s+(\S+)\s+(Up|Down)\s+(\S+)\s+(L[23])\s+\[([^\]]+)\](?:\s+(\S+))?\z/i
|
|
14
|
+
FIELDS = %i[interface vni multicast_group state mode type association flags].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# 解析命令表格行,保留数量及格式错误行诊断。
|
|
19
|
+
def parse_rows(text)
|
|
20
|
+
lines(text) do |line, number|
|
|
21
|
+
if HEADER.match?(line)
|
|
22
|
+
@header = true
|
|
23
|
+
elsif (match = ROW.match(line))
|
|
24
|
+
row = FIELDS.zip(match.captures).to_h.compact
|
|
25
|
+
row[:vni] = row[:vni].to_i
|
|
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)
|
|
28
|
+
next
|
|
29
|
+
end
|
|
30
|
+
@rows << row.merge(state: row[:state].downcase, type: row[:type].downcase)
|
|
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)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# 解析 port-channel 汇总,保留成员状态与明确数量证据。
|
|
9
|
+
class ShowPortChannelSummary < Parser
|
|
10
|
+
EMPTY = "port-channels".freeze
|
|
11
|
+
IDENTITY = %i[group].freeze
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# 解析当前命令的记录,保留无法识别行的诊断。
|
|
16
|
+
def parse_rows(text)
|
|
17
|
+
totals = []
|
|
18
|
+
lines(text) do |line, number|
|
|
19
|
+
if line.match?(/\AGroup\s+Port-(?:Channel)?\s+Type\s+Protocol\s+Member Ports\z/i)
|
|
20
|
+
@header = true
|
|
21
|
+
elsif line == "Channel" && @header
|
|
22
|
+
next
|
|
23
|
+
elsif (match = /\A(\d+)\s+(Po\d+)\(([^\s()]+)\)\s+(\S+)\s+(\S+)\s*(.*)\z/i.match(line))
|
|
24
|
+
@rows << { group: match[1].to_i, interface: match[2], flags: match[3], type: match[4], protocol: match[5], members: members(match[6]) }
|
|
25
|
+
elsif line.match?(/\A(?:Eth(?:ernet)?\S+\([^)]+\)\s*)+\z/i) && @rows.any?
|
|
26
|
+
@rows.last[:members].concat(members(line))
|
|
27
|
+
elsif (match = /\ANumber of port-channels\s*:\s*(\d+)\z/i.match(line))
|
|
28
|
+
totals << match[1].to_i
|
|
29
|
+
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/)
|
|
30
|
+
reject(number)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
totals.each { |total| mismatch(total, @rows.size) }
|
|
34
|
+
@empty = true if totals == [ 0 ] && @rows.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 将回显的聚合成员归一化为接口身份及状态标志。
|
|
38
|
+
def members(text)
|
|
39
|
+
return [] if text.empty? || text == "--"
|
|
40
|
+
text.split.filter_map do |word|
|
|
41
|
+
match = /\A(Eth(?:ernet)?[\d\/]+)\(([^\s()]+)\)\z/i.match(word)
|
|
42
|
+
@issues << { code: :invalid_port_channel_member } unless match
|
|
43
|
+
{ interface: match[1], flags: match[2] } if match
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
require_relative "../configuration_parsing/interfaces"
|
|
6
|
+
require_relative "../configuration_parsing/routing"
|
|
7
|
+
require_relative "../configuration_parsing/policies"
|
|
8
|
+
require_relative "../configuration_document"
|
|
9
|
+
require_relative "../configuration"
|
|
10
|
+
require_relative "../../configuration_review"
|
|
11
|
+
require "ctx_parser/configuration_parser"
|
|
12
|
+
require "ctx_parser/values"
|
|
13
|
+
|
|
14
|
+
module Vxlan
|
|
15
|
+
module CiscoNxos
|
|
16
|
+
module Parsers
|
|
17
|
+
# 按 NX-OS 缩进确定作用域;相关语法不受支持时保留部分结果。
|
|
18
|
+
class ShowRunningConfig < CtxParser::ConfigurationParser
|
|
19
|
+
include ConfigurationParsing::Interfaces
|
|
20
|
+
include ConfigurationParsing::Routing
|
|
21
|
+
include ConfigurationParsing::Policies
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# 解析运行配置,不依赖传输或资产清单。
|
|
26
|
+
def parse_output(text, complete:)
|
|
27
|
+
configuration = CiscoNxos::Configuration.new
|
|
28
|
+
@issues = configuration.issues
|
|
29
|
+
@conflicts = {}
|
|
30
|
+
@sources = {}
|
|
31
|
+
@feature_sources = []
|
|
32
|
+
@disabled_feature_sources = []
|
|
33
|
+
data = configuration.data
|
|
34
|
+
read_configuration(text, comments: /\A!/, boundaries: /\Aend\z/, document_class: ConfigurationDocument).each { |block| parse_block(block, data) }
|
|
35
|
+
@issues.concat(document.issues)
|
|
36
|
+
# 所有声明读取完毕后再关联模板与网段,避免配置先后顺序改变关联结果。
|
|
37
|
+
configuration.finish
|
|
38
|
+
@feature_sources.each { |block, feature| capture(block, feature, [ :features, data[:features].index(feature) ]) }
|
|
39
|
+
@disabled_feature_sources.each { |block, feature| capture(block, feature, [ :disabled_features, data[:disabled_features].index(feature) ]) }
|
|
40
|
+
data[:config_digest] = Digest::SHA256.hexdigest(@cursor.lines.map(&:raw).reject { |line| line.strip.empty? || line.lstrip.start_with?("!") }.map(&:rstrip).join("\n"))
|
|
41
|
+
@issues << { code: :output_incomplete } unless complete
|
|
42
|
+
status = @issues.empty? ? :complete : :partial
|
|
43
|
+
data[:review_status] = status
|
|
44
|
+
data[:review_findings] = Vxlan::ConfigurationReview.new(data).call(complete: status == :complete)
|
|
45
|
+
CtxParser::Result.new(status: status, records: [ data ], issues: @issues)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 分派单个配置块,同时保留来源作用域。
|
|
49
|
+
def parse_block(block, data)
|
|
50
|
+
analyze(block) { parse_setting(block, data) }
|
|
51
|
+
rescue ArgumentError, IPAddr::Error
|
|
52
|
+
issue(block, :invalid_configuration_value)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 在所属父级配置作用域内解释一项支持的设置。
|
|
56
|
+
def parse_setting(block, data)
|
|
57
|
+
case block.text
|
|
58
|
+
when /\Ahostname (\S+)\z/
|
|
59
|
+
issue(block, :duplicate_hostname) if data[:hostname]
|
|
60
|
+
data[:hostname] = $1
|
|
61
|
+
capture(block, data[:hostname], [ :hostname ])
|
|
62
|
+
when /\Afeature (.+)\z/
|
|
63
|
+
data[:features] << $1
|
|
64
|
+
@feature_sources << [ block, $1 ]
|
|
65
|
+
when /\Ano feature (.+)\z/
|
|
66
|
+
data[:disabled_features] << $1
|
|
67
|
+
@disabled_feature_sources << [ block, $1 ]
|
|
68
|
+
when /\Ainterface breakout module (\d+) port (\d+) map (\d+g-\d+x)\z/
|
|
69
|
+
module_id, port, mapping = integer($1, 1..65_535), integer($2, 1..65_535), $3
|
|
70
|
+
key = "#{module_id}/#{port}"
|
|
71
|
+
store(data[:breakouts], key, block, path: [ :breakouts, key ]) { { module_id: module_id, port: port, map: mapping } }
|
|
72
|
+
when /\Ainterface (\S+)\z/
|
|
73
|
+
name = $1.sub(/\Avlan/i, "Vlan")
|
|
74
|
+
if name.match?(/\Anve\d+\z/i)
|
|
75
|
+
issue(block, :multiple_nve_interfaces) if data[:nve][:interface]
|
|
76
|
+
data[:nve] = scope(block, [ :nve ]) { nve(block).merge(interface: name) }
|
|
77
|
+
else
|
|
78
|
+
store(data[:interfaces], name, block, path: [ :interfaces, name ]) { interface(block) }
|
|
79
|
+
end
|
|
80
|
+
when /\Avlan ([\d,-]+)\z/
|
|
81
|
+
ids = $1.split(",", -1).flat_map do |part|
|
|
82
|
+
raise ArgumentError unless /\A\d+(?:-\d+)?\z/.match?(part)
|
|
83
|
+
first, last = part.split("-").map { |value| integer(value, 1..4094) }
|
|
84
|
+
raise ArgumentError if last && last < first
|
|
85
|
+
(first..(last || first)).to_a
|
|
86
|
+
end
|
|
87
|
+
# NX-OS 先输出 VLAN 汇总,再输出各 VLAN 的独立配置块。
|
|
88
|
+
return if block.children.empty?
|
|
89
|
+
|
|
90
|
+
mappings = block.children.select { |child| child.text.start_with?("vn-segment") }
|
|
91
|
+
ids.uniq.each do |id|
|
|
92
|
+
store(data[:vlan_vnis], id, block, path: [ :vlan_vnis, id ]) do
|
|
93
|
+
raise ArgumentError if mappings.size > 1
|
|
94
|
+
vni = mappings.first && analyze(mappings.first) { integer(mappings.first.text.delete_prefix("vn-segment "), 1..16_777_215) }
|
|
95
|
+
raise ArgumentError if vni && ids.uniq.size != 1
|
|
96
|
+
vni
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
block.children.each { |child| ignore(child) unless mappings.include?(child) }
|
|
100
|
+
when /\Avrf context (\S+)\z/ then store(data[:vrfs], $1, block, path: [ :vrfs, $1 ]) { vrf(block) }
|
|
101
|
+
when "evpn"
|
|
102
|
+
analyze_children(block) do |child|
|
|
103
|
+
if (match = /\Avni (\d+) (l[23])\z/i.match(child.text))
|
|
104
|
+
id = integer(match[1], 1..16_777_215)
|
|
105
|
+
store(data[:evpn], id, child, path: [ :evpn, id ]) { vrf(child).merge(type: match[2].downcase) }
|
|
106
|
+
else
|
|
107
|
+
issue(child)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
when /\Avpc domain (\d+)\z/
|
|
111
|
+
issue(block, :duplicate_vpc_domain) if data[:vpc_domain]
|
|
112
|
+
id = integer($1, 1..1000)
|
|
113
|
+
data[:vpc_domain] = scope(block, [ :vpc_domain ]) { vpc(block).merge(id: id) }
|
|
114
|
+
when /\Arouter bgp (\d+)\z/
|
|
115
|
+
asn = integer($1, 1..4_294_967_295)
|
|
116
|
+
issue(block, :duplicate_bgp_process) if data[:bgp].any?
|
|
117
|
+
data[:bgp] = scope(block, [ :bgp ]) { routing(block, :bgp).merge(asn: asn) }
|
|
118
|
+
when /\Arouter ospf (\S+)\z/ then store(data[:ospf], $1, block, path: [ :ospf, $1 ]) { routing(block, :ospf) }
|
|
119
|
+
when /\Aip prefix-list / then prefix_list(block, data[:prefix_lists], family: "ipv4")
|
|
120
|
+
when /\Aipv6 prefix-list / then prefix_list(block, data[:ipv6_prefix_lists], family: "ipv6")
|
|
121
|
+
when /\Aroute-map (\S+) (permit|deny) (\d+)\z/
|
|
122
|
+
name, action, sequence = $1, $2, integer($3, 1..65_535)
|
|
123
|
+
entries = (data[:route_maps][name] ||= [])
|
|
124
|
+
issue(block, :duplicate_route_map_entry) if entries.any? { |entry| entry[:sequence] == sequence }
|
|
125
|
+
entries << scope(block, [ :route_maps, name, entries.size ]) { route_map(block).merge(action: action, sequence: sequence) }
|
|
126
|
+
when /\Aip access-list (\S+)\z/
|
|
127
|
+
name = $1
|
|
128
|
+
entries = block.children.filter_map do |child|
|
|
129
|
+
analyze(child) do
|
|
130
|
+
next ignore(child) if /\A(?:(?:\d+ )?remark\b|statistics per-entry\z)/.match?(child.text)
|
|
131
|
+
match = /\A(\d+) (permit|deny) (.+)\z/.match(child.text)
|
|
132
|
+
if match
|
|
133
|
+
{ sequence: integer(match[1], 1..4_294_967_295), action: match[2], expression: match[3] }
|
|
134
|
+
else
|
|
135
|
+
issue(child)
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
store(data[:acls], name, block, path: [ :acls, name ]) { entries }
|
|
141
|
+
when /\Aip route /
|
|
142
|
+
data[:static_routes] << scope(block, [ :static_routes, data[:static_routes].size ]) { static_route(block) }
|
|
143
|
+
when "nv overlay evpn", "no nv overlay evpn"
|
|
144
|
+
data[:nv_overlay_evpn] = block.text == "nv overlay evpn"
|
|
145
|
+
capture(block, data[:nv_overlay_evpn], [ :nv_overlay_evpn ])
|
|
146
|
+
when /\Afabric forwarding anycast-gateway-mac (\S+)\z/
|
|
147
|
+
data[:anycast_mac] = $1
|
|
148
|
+
capture(block, data[:anycast_mac], [ :anycast_mac ])
|
|
149
|
+
when /\A(?:interface|vlan|vrf|router|vpc|evpn|feature|ip prefix-list|ipv6 prefix-list|route-map)\b/ then issue(block)
|
|
150
|
+
when /\A(?:%|ERROR:|Invalid command|--More--)/i then issue(block, :invalid_command_output)
|
|
151
|
+
else
|
|
152
|
+
check_unhandled(block)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# 解析 VRF 定义及其 VNI 或 route-target 关联。
|
|
157
|
+
def vrf(block)
|
|
158
|
+
values = { route_targets: [], routes: [] }
|
|
159
|
+
analyze_children(block) do |child|
|
|
160
|
+
case child.text
|
|
161
|
+
when /\Avni (\d+)\z/ then values[:vni] = integer($1, 1..16_777_215)
|
|
162
|
+
when /\Ard (\S+)\z/ then values[:rd] = $1
|
|
163
|
+
when /\Aroute-target / then values[:route_targets] << route_target(child)
|
|
164
|
+
when /\Aaddress-family (.+)\z/
|
|
165
|
+
family = $1
|
|
166
|
+
targets_start = values[:route_targets].size
|
|
167
|
+
exports_start = values.fetch(:exports, []).size
|
|
168
|
+
analyze_children(child) do |setting|
|
|
169
|
+
case setting.text
|
|
170
|
+
when /\Aroute-target / then values[:route_targets] << route_target(setting).merge(address_family: family)
|
|
171
|
+
when /\Aexport vrf default(?: (\d+))? map (\S+)( allow-vpn)?\z/
|
|
172
|
+
limit, route_map, allow_vpn = $1, $2, !$3.nil?
|
|
173
|
+
(values[:exports] ||= []) << { vrf: "default", prefix_limit: limit && integer(limit, 1..2_147_483_647),
|
|
174
|
+
route_map: route_map, allow_vpn: allow_vpn, address_family: family }
|
|
175
|
+
else issue(setting)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
values[:route_targets].drop(targets_start).each_with_index do |target, index|
|
|
179
|
+
capture(child, target, @source_path + [ :route_targets, targets_start + index ])
|
|
180
|
+
end
|
|
181
|
+
values.fetch(:exports, []).drop(exports_start).each_with_index do |export, index|
|
|
182
|
+
capture(child, export, @source_path + [ :exports, exports_start + index ])
|
|
183
|
+
end
|
|
184
|
+
when /\Aip route /
|
|
185
|
+
values[:routes] << scope(child, @source_path + [ :routes, values[:routes].size ]) { static_route(child) }
|
|
186
|
+
when /\Adescription / then ignore(child)
|
|
187
|
+
else issue(child)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
values
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# 归一化 route-target 方向及地址族作用域。
|
|
194
|
+
def route_target(block)
|
|
195
|
+
match = /\Aroute-target (import|export|both) (\S+)( evpn)?\z/.match(block.text)
|
|
196
|
+
raise ArgumentError unless match
|
|
197
|
+
{ direction: match[1], target: match[2], evpn: !match[3].nil? }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# 解析 vPC 域配置,不推断运行可用性。
|
|
201
|
+
def vpc(block)
|
|
202
|
+
values = { peer_keepalive: {}, virtual_peer_link: {}, peer_gateway: false, peer_switch: false, layer3_peer_router: false }
|
|
203
|
+
analyze_children(block) do |child|
|
|
204
|
+
case child.text
|
|
205
|
+
when /\Apeer-keepalive (.+)\z/ then values[:peer_keepalive] = pairs($1, %w[destination source vrf])
|
|
206
|
+
when /\Avirtual peer-link (.+)\z/ then values[:virtual_peer_link] = pairs($1, %w[destination source dscp])
|
|
207
|
+
when "peer-gateway" then values[:peer_gateway] = true
|
|
208
|
+
when "peer-switch" then values[:peer_switch] = true
|
|
209
|
+
when "layer3 peer-router" then values[:layer3_peer_router] = true
|
|
210
|
+
when "fast-convergence" then ignore(child)
|
|
211
|
+
when /\A(?:role priority|system-priority|auto-recovery|delay restore|ip arp synchronize)\b/ then ignore(child)
|
|
212
|
+
else issue(child)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
values
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# 提取支持的键值配置对。
|
|
219
|
+
def pairs(text, allowed)
|
|
220
|
+
words = text.split
|
|
221
|
+
raise ArgumentError unless words.size.even? && (words.each_slice(2).map(&:first) - allowed).empty?
|
|
222
|
+
values = words.each_slice(2).to_h.transform_keys(&:to_sym)
|
|
223
|
+
raise ArgumentError unless values.size == words.size / 2
|
|
224
|
+
%i[source destination].each { |key| values[key] = address(values[key]) if values[key] }
|
|
225
|
+
integer(values[:dscp], 0..63) if values[:dscp]
|
|
226
|
+
values
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# 归一化静态路由及可选下一跳属性。
|
|
230
|
+
def static_route(block)
|
|
231
|
+
words = block.text.split
|
|
232
|
+
raise ArgumentError if words.size < 4
|
|
233
|
+
{ prefix: prefix(words[2]), next_hop: words[3], options: words.drop(4).join(" ") }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# 解析十进制整数,并校验声明的边界。
|
|
237
|
+
def integer(value, range)
|
|
238
|
+
raise ArgumentError unless value.is_a?(String)
|
|
239
|
+
CtxParser::Values.integer(value, min: range.min, max: range.max)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# 校验接口地址,同时保留网络前缀。
|
|
243
|
+
def address(value)
|
|
244
|
+
CtxParser::Values.host(value, allow_zone: true)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# 校验网络前缀,不静默修正错误语法。
|
|
248
|
+
def prefix(value, host: false) = CtxParser::Values.ipv4_prefix(value, host: host)
|
|
249
|
+
|
|
250
|
+
# 保存已解析配置,同时保留其来源。
|
|
251
|
+
def store(hash, key, block, path:)
|
|
252
|
+
identity = [ hash.object_id, key ]
|
|
253
|
+
if hash.key?(key) || @conflicts[identity]
|
|
254
|
+
@conflicts[identity] = true
|
|
255
|
+
hash.delete(key)
|
|
256
|
+
@cursor.mark(@sources[identity].index, state: :invalid) if @sources[identity]
|
|
257
|
+
issue(block, :duplicate_identity)
|
|
258
|
+
return
|
|
259
|
+
end
|
|
260
|
+
@sources[identity] = block
|
|
261
|
+
hash[key] = scope(block, path) { yield }
|
|
262
|
+
rescue ArgumentError, IPAddr::Error
|
|
263
|
+
@conflicts[identity] = true
|
|
264
|
+
hash.delete(key)
|
|
265
|
+
raise
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# 忽略无关系统设置,但不将位置错误的已建模语法视为完整。
|
|
269
|
+
def check_unhandled(block)
|
|
270
|
+
if /\A(?:no )?(?:hostname|interface|vlan|vrf|router|vpc|evpn|feature|ip address|ip router ospf|ip access-group|ip prefix-list|ipv6 prefix-list|ip access-list|route-map|vn-segment|member vni|route-target|channel-group|fabric forwarding|mtu|ip unnumbered|ip ospf network|switchport mode|switchport access vlan|ip forward|ipv6 address)\b/.match?(block.text)
|
|
271
|
+
issue(block)
|
|
272
|
+
else
|
|
273
|
+
@cursor.mark(block.index, state: :ignored)
|
|
274
|
+
end
|
|
275
|
+
block.children.each { |child| check_unhandled(child) }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# 忽略已知叶子语句时,不能隐藏其嵌套配置作用域。
|
|
279
|
+
def ignore(block)
|
|
280
|
+
@cursor.mark(block.index, state: :ignored)
|
|
281
|
+
pending = block.children.dup
|
|
282
|
+
until pending.empty?
|
|
283
|
+
child = pending.pop
|
|
284
|
+
issue(child)
|
|
285
|
+
pending.concat(child.children)
|
|
286
|
+
end
|
|
287
|
+
nil
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# 将稳定诊断代码关联到对应来源行。
|
|
291
|
+
def issue(block, code = :unsupported_configuration)
|
|
292
|
+
@issues << { code: code, line: block.line }
|
|
293
|
+
source = code == :invalid_configuration_value ? (@invalid_source || block) : block
|
|
294
|
+
state = code == :unsupported_configuration ? :unsupported : :invalid
|
|
295
|
+
@cursor.mark(source.index, state: state)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# 已知叶子语句不能静默容纳另一条已建模配置命令。
|
|
299
|
+
def unparsed_child(block) = check_unhandled(block)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module CiscoNxos
|
|
7
|
+
module Parsers
|
|
8
|
+
# 保留 vPC 域属性、peer-link 证据及成员状态。
|
|
9
|
+
class ShowVpcBrief < Parser
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# 解析当前命令的记录,保留无法识别行的诊断。
|
|
13
|
+
def parse_rows(text)
|
|
14
|
+
values, section = { attributes: {}, peer_links: [], members: [] }, nil
|
|
15
|
+
labels = { "vPC domain id" => :domain_id, "Peer status" => :peer_status, "vPC keep-alive status" => :keepalive_status,
|
|
16
|
+
"Configuration consistency status" => :consistency_status, "vPC role" => :role }
|
|
17
|
+
lines(text) do |line, number|
|
|
18
|
+
if line.match?(/\Aid\s+Port\s+Status\s+Active vlans\z/i)
|
|
19
|
+
section = :peer_links
|
|
20
|
+
elsif line.match?(/\Aid\s+Port\s+Status\s+Consistency\s+Reason\s+Active vlans\z/i)
|
|
21
|
+
section = :members
|
|
22
|
+
elsif (match = /\A(\d+)\s+(Po\d+)\s+(up|down)\s+(.+)\z/i.match(line)) && section
|
|
23
|
+
row = { id: match[1].to_i, interface: match[2], state: match[3].downcase }
|
|
24
|
+
rest = match[4].split(/\s{2,}/)
|
|
25
|
+
if section == :members
|
|
26
|
+
member = /\A(\S+)\s+(.+?)\s{2,}([\d, .-]+)\z/.match(match[4])
|
|
27
|
+
unless member
|
|
28
|
+
reject(number)
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
row.merge!(consistency: member[1], reason: member[2], active_vlans_summary: member[3])
|
|
32
|
+
elsif match[4].match?(/\A[\d, .-]+\z/)
|
|
33
|
+
row[:active_vlans_summary] = rest.join
|
|
34
|
+
else
|
|
35
|
+
reject(number)
|
|
36
|
+
next
|
|
37
|
+
end
|
|
38
|
+
values[section] << row
|
|
39
|
+
elsif section && values[section].any? && line.match?(/\A[\d,.-]+\z/)
|
|
40
|
+
values[section].last[:active_vlans_summary] += line
|
|
41
|
+
elsif line == "Legend:"
|
|
42
|
+
next
|
|
43
|
+
elsif line.include?(":")
|
|
44
|
+
label, value = line.split(":", 2).map(&:strip)
|
|
45
|
+
key = labels[label]
|
|
46
|
+
if key
|
|
47
|
+
@header = true
|
|
48
|
+
if values.key?(key) || value.empty? || (key == :domain_id && !(value.match?(/\A\d+\z/) && (1..1000).cover?(value.to_i)))
|
|
49
|
+
reject(number)
|
|
50
|
+
else
|
|
51
|
+
values[key] = key == :domain_id ? value.to_i : value
|
|
52
|
+
end
|
|
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)
|
|
55
|
+
values[:attributes][label] = value
|
|
56
|
+
else
|
|
57
|
+
reject(number)
|
|
58
|
+
end
|
|
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)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
return unless @header
|
|
64
|
+
missing = %i[domain_id peer_status keepalive_status consistency_status] - values.keys
|
|
65
|
+
@issues << { code: :missing_vpc_fields, fields: missing } if missing.any?
|
|
66
|
+
values.values_at(:peer_links, :members).flatten.each { |row| row[:active_vlans_truncated] = row[:active_vlans_summary].include?("...") }
|
|
67
|
+
count = values[:attributes]["Number of vPCs configured"]
|
|
68
|
+
if count
|
|
69
|
+
count.match?(/\A\d+\z/) ? mismatch(count.to_i, values[:members].size) : @issues << { code: :invalid_vpc_count }
|
|
70
|
+
end
|
|
71
|
+
%i[peer_links members].each { |key| remove_duplicates(values[key], [ :id ]) }
|
|
72
|
+
@rows << values
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ctx_parser/profile"
|
|
4
|
+
require_relative "parsers/show_running_config"
|
|
5
|
+
require_relative "parsers/show_cdp_neighbors_detail"
|
|
6
|
+
require_relative "parsers/show_nve_peers"
|
|
7
|
+
require_relative "parsers/show_nve_vni"
|
|
8
|
+
require_relative "parsers/show_bgp_l2vpn_evpn_summary"
|
|
9
|
+
require_relative "parsers/show_ip_ospf_neighbors"
|
|
10
|
+
require_relative "parsers/show_vpc_brief"
|
|
11
|
+
require_relative "parsers/show_interface_brief"
|
|
12
|
+
require_relative "parsers/show_port_channel_summary"
|
|
13
|
+
require_relative "parsers/show_mac_address_table"
|
|
14
|
+
require_relative "parsers/show_l2route_evpn_mac_all"
|
|
15
|
+
require_relative "parsers/show_l2route_evpn_mac_ip_all"
|
|
16
|
+
|
|
17
|
+
module Vxlan
|
|
18
|
+
module CiscoNxos
|
|
19
|
+
# 声明 Cisco N9K NX-OS 命令,并通过 MODEL_PARSERS 逐命令覆盖型号差异。
|
|
20
|
+
class Profile < CtxParser::Profile
|
|
21
|
+
CATALOG = CtxParser::CommandCatalog.new([
|
|
22
|
+
{ dimensions: [ :configuration ], command: "show running-config", parser: Parsers::ShowRunningConfig },
|
|
23
|
+
{ dimensions: [ :cdp_neighbors ], command: "show cdp neighbors detail", parser: Parsers::ShowCdpNeighborsDetail },
|
|
24
|
+
{ dimensions: [ :nve_peers ], command: "show nve peers", parser: Parsers::ShowNvePeers },
|
|
25
|
+
{ dimensions: [ :nve_vnis ], command: "show nve vni", parser: Parsers::ShowNveVni },
|
|
26
|
+
{ dimensions: [ :bgp_neighbors ], command: "show bgp l2vpn evpn summary", parser: Parsers::ShowBgpL2vpnEvpnSummary },
|
|
27
|
+
{ dimensions: [ :ospf_neighbors ], command: "show ip ospf neighbors", parser: Parsers::ShowIpOspfNeighbors },
|
|
28
|
+
{ dimensions: [ :vpc ], command: "show vpc brief", parser: Parsers::ShowVpcBrief },
|
|
29
|
+
{ dimensions: [ :interfaces ], command: "show interface brief", parser: Parsers::ShowInterfaceBrief },
|
|
30
|
+
{ dimensions: [ :port_channels ], command: "show port-channel summary", parser: Parsers::ShowPortChannelSummary },
|
|
31
|
+
{ dimensions: [ :mac_addresses ], command: "show mac address-table", parser: Parsers::ShowMacAddressTable },
|
|
32
|
+
{ dimensions: [ :l2route_macs ], command: "show l2route evpn mac all", parser: Parsers::ShowL2routeEvpnMacAll },
|
|
33
|
+
{ dimensions: [ :l2route_mac_ips ], command: "show l2route evpn mac-ip all", parser: Parsers::ShowL2routeEvpnMacIpAll }
|
|
34
|
+
])
|
|
35
|
+
COMMANDS = CATALOG.commands
|
|
36
|
+
PARSERS = CATALOG.parsers
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|