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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE +22 -0
  4. data/README-zh.md +90 -0
  5. data/README.md +90 -0
  6. data/docs/architecture-zh.md +95 -0
  7. data/docs/release.md +13 -0
  8. data/examples/offline.rb +16 -0
  9. data/lib/vxlan/bgp_neighbors.rb +29 -0
  10. data/lib/vxlan/cisco_nxos/configuration.rb +84 -0
  11. data/lib/vxlan/cisco_nxos/configuration_document.rb +31 -0
  12. data/lib/vxlan/cisco_nxos/configuration_parsing/interfaces.rb +91 -0
  13. data/lib/vxlan/cisco_nxos/configuration_parsing/policies.rb +58 -0
  14. data/lib/vxlan/cisco_nxos/configuration_parsing/routing.rb +62 -0
  15. data/lib/vxlan/cisco_nxos/evpn_l2route.rb +118 -0
  16. data/lib/vxlan/cisco_nxos/parser.rb +105 -0
  17. data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +80 -0
  18. data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +86 -0
  19. data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +84 -0
  20. data/lib/vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +48 -0
  21. data/lib/vxlan/cisco_nxos/parsers/show_l2route_evpn_mac_all.rb +14 -0
  22. data/lib/vxlan/cisco_nxos/parsers/show_l2route_evpn_mac_ip_all.rb +14 -0
  23. data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +42 -0
  24. data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +49 -0
  25. data/lib/vxlan/cisco_nxos/parsers/show_nve_vni.rb +39 -0
  26. data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +49 -0
  27. data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +303 -0
  28. data/lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb +77 -0
  29. data/lib/vxlan/cisco_nxos/profile.rb +39 -0
  30. data/lib/vxlan/collector.rb +127 -0
  31. data/lib/vxlan/configuration.rb +20 -0
  32. data/lib/vxlan/configuration_review.rb +43 -0
  33. data/lib/vxlan/l2route_parser.rb +21 -0
  34. data/lib/vxlan/observation.rb +24 -0
  35. data/lib/vxlan/parser_registry.rb +9 -0
  36. data/lib/vxlan/prefix_list_references.rb +89 -0
  37. data/lib/vxlan/runtime_parser.rb +22 -0
  38. data/lib/vxlan/snapshot.rb +23 -0
  39. data/lib/vxlan/version.rb +6 -0
  40. data/lib/vxlan.rb +16 -0
  41. 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