netstack 0.1.1 → 0.2.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 (279) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -3
  3. data/{LICENSE.txt → LICENSE} +7 -6
  4. data/NOTICE +25 -0
  5. data/README.md +58 -20
  6. data/docs/acceptance.md +92 -0
  7. data/docs/backups.md +100 -0
  8. data/docs/configuration.md +198 -0
  9. data/docs/connector.md +187 -0
  10. data/docs/ddi.md +142 -0
  11. data/docs/deployment.md +47 -0
  12. data/docs/foundations.md +45 -0
  13. data/docs/implementation.md +68 -0
  14. data/docs/inventory.md +42 -0
  15. data/docs/monitoring.md +227 -0
  16. data/docs/motor_integration.md +96 -0
  17. data/docs/performance.md +39 -0
  18. data/docs/release.md +20 -0
  19. data/docs/vxlan.md +91 -0
  20. data/docs/wireless.md +81 -0
  21. data/lib/netstack/backups/artifact.rb +23 -0
  22. data/lib/netstack/backups/diff.rb +15 -0
  23. data/lib/netstack/backups/repository.rb +77 -0
  24. data/lib/netstack/backups/request.rb +78 -0
  25. data/lib/netstack/circuits/business_calendar.rb +51 -0
  26. data/lib/netstack/circuits/capacity_review.rb +191 -0
  27. data/lib/netstack/circuits/endpoint.rb +25 -0
  28. data/lib/netstack/circuits/monitoring_plan.rb +212 -0
  29. data/lib/netstack/configuration/artifact.rb +85 -0
  30. data/lib/netstack/configuration/artifact_resolver.rb +30 -0
  31. data/lib/netstack/configuration/bootstrap/payload.rb +67 -0
  32. data/lib/netstack/configuration/bootstrap/validator.rb +224 -0
  33. data/lib/netstack/configuration/catalog.rb +60 -0
  34. data/lib/netstack/configuration/generator.rb +87 -0
  35. data/lib/netstack/configuration/input_contract.rb +82 -0
  36. data/lib/netstack/configuration/payload.rb +168 -0
  37. data/lib/netstack/configuration/reader.rb +124 -0
  38. data/lib/netstack/configuration/support.rb +30 -0
  39. data/lib/netstack/configuration/system_configuration.rb +56 -0
  40. data/lib/netstack/configuration/template_manager.rb +174 -0
  41. data/lib/netstack/configuration/template_store.rb +40 -0
  42. data/lib/netstack/configuration/validator.rb +566 -0
  43. data/lib/netstack/connector/authentication.rb +77 -0
  44. data/lib/netstack/connector/authentication_error.rb +11 -0
  45. data/lib/netstack/connector/backup.rb +19 -0
  46. data/lib/netstack/connector/backup_busy.rb +11 -0
  47. data/lib/netstack/connector/backup_persistence_error.rb +32 -0
  48. data/lib/netstack/connector/command.rb +63 -0
  49. data/lib/netstack/connector/command_result.rb +24 -0
  50. data/lib/netstack/connector/command_timeout.rb +11 -0
  51. data/lib/netstack/connector/configuration.rb +138 -0
  52. data/lib/netstack/connector/connection_closed.rb +11 -0
  53. data/lib/netstack/connector/connection_error.rb +11 -0
  54. data/lib/netstack/connector/device.rb +284 -0
  55. data/lib/netstack/connector/device_error.rb +11 -0
  56. data/lib/netstack/connector/dialogue.rb +55 -0
  57. data/lib/netstack/connector/error.rb +44 -0
  58. data/lib/netstack/connector/error_metadata.rb +58 -0
  59. data/lib/netstack/connector/event.rb +24 -0
  60. data/lib/netstack/connector/execution.rb +110 -0
  61. data/lib/netstack/connector/interaction.rb +55 -0
  62. data/lib/netstack/connector/internal_error.rb +11 -0
  63. data/lib/netstack/connector/known_hosts.rb +128 -0
  64. data/lib/netstack/connector/local_backup.rb +63 -0
  65. data/lib/netstack/connector/log/event.rb +65 -0
  66. data/lib/netstack/connector/log/formatter.rb +18 -0
  67. data/lib/netstack/connector/log/messages.rb +54 -0
  68. data/lib/netstack/connector/log/redacting_writer.rb +33 -0
  69. data/lib/netstack/connector/log/transcript.rb +35 -0
  70. data/lib/netstack/connector/log.rb +262 -0
  71. data/lib/netstack/connector/log_error.rb +11 -0
  72. data/lib/netstack/connector/login_timeout.rb +11 -0
  73. data/lib/netstack/connector/output_limit_exceeded.rb +11 -0
  74. data/lib/netstack/connector/parsing_error.rb +11 -0
  75. data/lib/netstack/connector/profile/builder.rb +238 -0
  76. data/lib/netstack/connector/profile.rb +225 -0
  77. data/lib/netstack/connector/prompt_error.rb +11 -0
  78. data/lib/netstack/connector/recovery.rb +59 -0
  79. data/lib/netstack/connector/redactor.rb +100 -0
  80. data/lib/netstack/connector/response.rb +25 -0
  81. data/lib/netstack/connector/response_reader.rb +140 -0
  82. data/lib/netstack/connector/result.rb +50 -0
  83. data/lib/netstack/connector/running_config/rendered.rb +16 -0
  84. data/lib/netstack/connector/running_config/strategy.rb +28 -0
  85. data/lib/netstack/connector/running_config.rb +113 -0
  86. data/lib/netstack/connector/save_config.rb +23 -0
  87. data/lib/netstack/connector/script.rb +59 -0
  88. data/lib/netstack/connector/script_error.rb +11 -0
  89. data/lib/netstack/connector/script_output_limit_exceeded.rb +11 -0
  90. data/lib/netstack/connector/session.rb +414 -0
  91. data/lib/netstack/connector/session_busy.rb +11 -0
  92. data/lib/netstack/connector/storage/backup_lock.rb +117 -0
  93. data/lib/netstack/connector/storage/private_file.rb +111 -0
  94. data/lib/netstack/connector/storage/safe_file.rb +64 -0
  95. data/lib/netstack/connector/storage/saved_config.rb +76 -0
  96. data/lib/netstack/connector/terminal_renderer.rb +186 -0
  97. data/lib/netstack/connector/terminal_text.rb +32 -0
  98. data/lib/netstack/connector/textfsm.rb +71 -0
  99. data/lib/netstack/connector/tftp/file_upload.rb +53 -0
  100. data/lib/netstack/connector/tftp/strategy.rb +67 -0
  101. data/lib/netstack/connector/tftp.rb +116 -0
  102. data/lib/netstack/connector/tftp_completion_error.rb +36 -0
  103. data/lib/netstack/connector/tftp_receipt.rb +54 -0
  104. data/lib/netstack/connector/tftp_target.rb +65 -0
  105. data/lib/netstack/connector/topology/checkpoint_error.rb +16 -0
  106. data/lib/netstack/connector/topology/deployment_adapter.rb +163 -0
  107. data/lib/netstack/connector/topology/execution_result.rb +34 -0
  108. data/lib/netstack/connector/topology/immediate_strategy.rb +39 -0
  109. data/lib/netstack/connector/topology/interface_description.rb +46 -0
  110. data/lib/netstack/connector/topology/interface_name.rb +60 -0
  111. data/lib/netstack/connector/topology/plan.rb +105 -0
  112. data/lib/netstack/connector/topology/strategy.rb +76 -0
  113. data/lib/netstack/connector/topology.rb +255 -0
  114. data/lib/netstack/connector/transport_error.rb +11 -0
  115. data/lib/netstack/connector/transports.rb +201 -0
  116. data/lib/netstack/connector/underlying_error.rb +25 -0
  117. data/lib/netstack/connector/unsupported_operation.rb +11 -0
  118. data/lib/netstack/connector/vendors/cisco_ios/running_config.rb +22 -0
  119. data/lib/netstack/connector/vendors/cisco_ios/tftp_backup.rb +41 -0
  120. data/lib/netstack/connector/vendors/cisco_ios/topology.rb +54 -0
  121. data/lib/netstack/connector/vendors/cisco_ios.rb +44 -0
  122. data/lib/netstack/connector/vendors/cisco_nxos/running_config.rb +25 -0
  123. data/lib/netstack/connector/vendors/cisco_nxos/tftp_backup.rb +43 -0
  124. data/lib/netstack/connector/vendors/cisco_nxos.rb +51 -0
  125. data/lib/netstack/connector/vendors/h3c/tftp_backup.rb +34 -0
  126. data/lib/netstack/connector/vendors/h3c/topology.rb +72 -0
  127. data/lib/netstack/connector/vendors/h3c.rb +75 -0
  128. data/lib/netstack/connector/vendors/h3c_wireless.rb +13 -0
  129. data/lib/netstack/connector/vendors/hillstone/running_config.rb +16 -0
  130. data/lib/netstack/connector/vendors/hillstone/tftp_backup.rb +55 -0
  131. data/lib/netstack/connector/vendors/hillstone/topology.rb +64 -0
  132. data/lib/netstack/connector/vendors/hillstone.rb +39 -0
  133. data/lib/netstack/connector/vendors/huawei/tftp_backup.rb +26 -0
  134. data/lib/netstack/connector/vendors/huawei.rb +62 -0
  135. data/lib/netstack/connector/vendors/palo_alto/running_config.rb +77 -0
  136. data/lib/netstack/connector/vendors/palo_alto/tftp_backup.rb +49 -0
  137. data/lib/netstack/connector/vendors/palo_alto/topology.rb +77 -0
  138. data/lib/netstack/connector/vendors/palo_alto.rb +39 -0
  139. data/lib/netstack/connector/vendors/radware/running_config.rb +21 -0
  140. data/lib/netstack/connector/vendors/radware/tftp_backup.rb +49 -0
  141. data/lib/netstack/connector/vendors/radware/topology.rb +19 -0
  142. data/lib/netstack/connector/vendors/radware.rb +59 -0
  143. data/lib/netstack/connector/write_timeout.rb +11 -0
  144. data/lib/netstack/connector.rb +32 -0
  145. data/lib/netstack/ddi/applier.rb +107 -0
  146. data/lib/netstack/ddi/deployment.rb +137 -0
  147. data/lib/netstack/ddi/inputs.rb +61 -0
  148. data/lib/netstack/ddi/manager.rb +197 -0
  149. data/lib/netstack/ddi/plan.rb +99 -0
  150. data/lib/netstack/ddi/scope.rb +127 -0
  151. data/lib/netstack/ddi/settings.rb +44 -0
  152. data/lib/netstack/ddi/state.rb +166 -0
  153. data/lib/netstack/deployment/checkpoint_error.rb +14 -0
  154. data/lib/netstack/deployment/cli_adapter.rb +64 -0
  155. data/lib/netstack/deployment/executor.rb +131 -0
  156. data/lib/netstack/deployment/plan.rb +82 -0
  157. data/lib/netstack/deployment/receipt.rb +78 -0
  158. data/lib/netstack/deployment/recovery.rb +19 -0
  159. data/lib/netstack/deployment/step.rb +16 -0
  160. data/lib/netstack/dimension.rb +30 -0
  161. data/lib/netstack/error.rb +14 -0
  162. data/lib/netstack/input_error.rb +9 -0
  163. data/lib/netstack/integrations/bluecat/client.rb +84 -0
  164. data/lib/netstack/integrations/http/client.rb +146 -0
  165. data/lib/netstack/integrations/http/error.rb +10 -0
  166. data/lib/netstack/integrations/http/response.rb +28 -0
  167. data/lib/netstack/integrations/http_delivery/adapter.rb +51 -0
  168. data/lib/netstack/integrations/http_delivery/client.rb +127 -0
  169. data/lib/netstack/integrations/http_delivery/result.rb +27 -0
  170. data/lib/netstack/integrations/oxidized/client.rb +89 -0
  171. data/lib/netstack/integrations/zabbix/client.rb +211 -0
  172. data/lib/netstack/integrations/zabbix/error.rb +10 -0
  173. data/lib/netstack/inventory/address.rb +30 -0
  174. data/lib/netstack/inventory/changes.rb +13 -0
  175. data/lib/netstack/inventory/cli_parser.rb +75 -0
  176. data/lib/netstack/inventory/collector.rb +99 -0
  177. data/lib/netstack/inventory/device.rb +14 -0
  178. data/lib/netstack/inventory/interface.rb +25 -0
  179. data/lib/netstack/inventory/neighbor.rb +15 -0
  180. data/lib/netstack/inventory/reconciler.rb +74 -0
  181. data/lib/netstack/inventory/snapshot.rb +69 -0
  182. data/lib/netstack/inventory/transceiver.rb +23 -0
  183. data/lib/netstack/ipv4.rb +204 -0
  184. data/lib/netstack/ipv6.rb +88 -0
  185. data/lib/netstack/monitoring/applier.rb +179 -0
  186. data/lib/netstack/monitoring/device_plan.rb +23 -0
  187. data/lib/netstack/monitoring/host_plan.rb +152 -0
  188. data/lib/netstack/monitoring/inputs.rb +73 -0
  189. data/lib/netstack/monitoring/measurements.rb +89 -0
  190. data/lib/netstack/monitoring/mutation.rb +37 -0
  191. data/lib/netstack/monitoring/plan.rb +69 -0
  192. data/lib/netstack/monitoring/probe_plan.rb +19 -0
  193. data/lib/netstack/monitoring/state_comparison.rb +37 -0
  194. data/lib/netstack/range_set.rb +181 -0
  195. data/lib/netstack/routing/lookup.rb +46 -0
  196. data/lib/netstack/routing/lookup_result.rb +14 -0
  197. data/lib/netstack/routing/route.rb +32 -0
  198. data/lib/netstack/routing/snapshot.rb +23 -0
  199. data/lib/netstack/target.rb +15 -0
  200. data/lib/netstack/unsupported_capability.rb +9 -0
  201. data/lib/netstack/values.rb +70 -0
  202. data/lib/netstack/version.rb +1 -1
  203. data/lib/netstack/vxlan/auditor.rb +187 -0
  204. data/lib/netstack/vxlan/cisco_nxos/config_parser.rb +327 -0
  205. data/lib/netstack/vxlan/cisco_nxos/deployment_adapter.rb +63 -0
  206. data/lib/netstack/vxlan/cisco_nxos/runtime_parser.rb +182 -0
  207. data/lib/netstack/vxlan/cisco_nxos/segment_renderer.rb +42 -0
  208. data/lib/netstack/vxlan/collector.rb +77 -0
  209. data/lib/netstack/vxlan/fabric.rb +29 -0
  210. data/lib/netstack/vxlan/finding.rb +20 -0
  211. data/lib/netstack/vxlan/node.rb +51 -0
  212. data/lib/netstack/vxlan/route_policy.rb +57 -0
  213. data/lib/netstack/vxlan/runtime_result.rb +24 -0
  214. data/lib/netstack/vxlan/segment.rb +35 -0
  215. data/lib/netstack/vxlan/segment_plan.rb +25 -0
  216. data/lib/netstack/vxlan/segment_planner.rb +156 -0
  217. data/lib/netstack/vxlan/snapshot.rb +34 -0
  218. data/lib/netstack/vxlan/validation.rb +58 -0
  219. data/lib/netstack/vxlan/verifier.rb +52 -0
  220. data/lib/netstack/vxlan/vpc_domain.rb +20 -0
  221. data/lib/netstack/wireless/access_point.rb +12 -0
  222. data/lib/netstack/wireless/ap_group.rb +12 -0
  223. data/lib/netstack/wireless/assessment.rb +13 -0
  224. data/lib/netstack/wireless/assessor.rb +83 -0
  225. data/lib/netstack/wireless/authentication_connection.rb +12 -0
  226. data/lib/netstack/wireless/bss.rb +12 -0
  227. data/lib/netstack/wireless/client.rb +12 -0
  228. data/lib/netstack/wireless/collector.rb +191 -0
  229. data/lib/netstack/wireless/configuration.rb +13 -0
  230. data/lib/netstack/wireless/finding.rb +12 -0
  231. data/lib/netstack/wireless/h3c/config_parser.rb +229 -0
  232. data/lib/netstack/wireless/h3c/lldp_parser.rb +69 -0
  233. data/lib/netstack/wireless/h3c/runtime_parser.rb +240 -0
  234. data/lib/netstack/wireless/huawei/runtime_parser.rb +83 -0
  235. data/lib/netstack/wireless/invalid_snapshot.rb +11 -0
  236. data/lib/netstack/wireless/neighbor.rb +12 -0
  237. data/lib/netstack/wireless/normalizer.rb +90 -0
  238. data/lib/netstack/wireless/parse_result.rb +13 -0
  239. data/lib/netstack/wireless/radio.rb +12 -0
  240. data/lib/netstack/wireless/reconciler.rb +115 -0
  241. data/lib/netstack/wireless/reconciliation.rb +11 -0
  242. data/lib/netstack/wireless/security_policy.rb +12 -0
  243. data/lib/netstack/wireless/service_template.rb +12 -0
  244. data/lib/netstack/wireless/service_template_bindings.rb +56 -0
  245. data/lib/netstack/wireless/snapshot.rb +150 -0
  246. data/lib/netstack/wireless/state.rb +23 -0
  247. data/lib/netstack/wireless/view.rb +154 -0
  248. data/lib/netstack.rb +10 -3
  249. data/resources/configuration/cisco-bootstrap.erb +118 -0
  250. data/resources/configuration/cisco-distributed-gateway.erb +40 -0
  251. data/resources/configuration/cisco-n9k-vpc.erb +62 -0
  252. data/resources/configuration/cisco-server-port.erb +43 -0
  253. data/resources/configuration/h3c-bootstrap.erb +108 -0
  254. data/resources/configuration/h3c-irf.erb +57 -0
  255. data/resources/configuration/h3c-server-port.erb +36 -0
  256. data/resources/configuration/hillstone-ipsec-vpn.erb +68 -0
  257. data/resources/configuration/hillstone-policy.erb +31 -0
  258. data/resources/configuration/paloalto-ipsec-vpn.erb +39 -0
  259. data/resources/configuration/radware-load-balancer.erb +26 -0
  260. data/resources/textfsm/cisco_cdp_neighbors_detail.textfsm +7 -0
  261. data/resources/textfsm/cisco_ios_running_config_interfaces.textfsm +12 -0
  262. data/resources/textfsm/cisco_ios_show_ip_interface_brief.textfsm +7 -0
  263. data/resources/textfsm/h3c_interface_descriptions.textfsm +11 -0
  264. data/resources/textfsm/h3c_lldp_local_first.textfsm +8 -0
  265. data/resources/textfsm/h3c_lldp_name_first.textfsm +8 -0
  266. data/resources/textfsm/hillstone_interface_descriptions.textfsm +11 -0
  267. data/resources/textfsm/hillstone_lldp_neighbors.textfsm +7 -0
  268. data/resources/textfsm/index +8 -0
  269. data/resources/textfsm/palo_alto_interface_descriptions.textfsm +6 -0
  270. data/resources/textfsm/palo_alto_lldp_neighbors.textfsm +11 -0
  271. data/resources/textfsm/radware_port_names.textfsm +13 -0
  272. metadata +588 -27
  273. data/.rubocop.yml +0 -13
  274. data/CODE_OF_CONDUCT.md +0 -84
  275. data/Gemfile +0 -10
  276. data/Gemfile.lock +0 -40
  277. data/Rakefile +0 -8
  278. data/bin/console +0 -15
  279. data/bin/setup +0 -8
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require "digest"
5
+
6
+ module Netstack
7
+ module Vxlan
8
+ module CiscoNxos
9
+ # Parses NX-OS configuration contexts, keeping ACL order and routing policy.
10
+ # The caller supplies transport completeness; parsing cannot prove a file is whole.
11
+ class ConfigParser
12
+ def parse(text, target:, complete: false)
13
+ Validation.invalid!("configuration must be a String") unless text.is_a?(String)
14
+ Validation.invalid!("complete must be boolean") unless [true, false].include?(complete)
15
+ issues = []
16
+ sections = tree(text)
17
+ interfaces = parse_interfaces(sections, issues)
18
+ vlan_vnis = parse_vlans(sections, issues)
19
+ nve = parse_nve(sections, issues)
20
+ bgp = parse_bgp(sections)
21
+ role = target.attributes[:role] || target.attributes["role"]
22
+ role = if role
23
+ role.to_sym
24
+ else
25
+ (if nve.empty?
26
+ reflector?(bgp) ? :spine : :unknown
27
+ else
28
+ :leaf
29
+ end)
30
+ end
31
+ hostname = sections.filter_map { |entry| entry[:text][/\Ahostname\s+(\S+)\z/, 1] }.last
32
+ issues << { code: :hostname_missing } unless hostname
33
+ segments = build_segments(interfaces, vlan_vnis, issues)
34
+ management_ip = target.attributes[:management_ip] || target.attributes["management_ip"] || ip_address(target.host)
35
+ Node.new(key: target.key, hostname: hostname, management_ip: management_ip, role: role,
36
+ features: sections.filter_map { |entry| entry[:text][/\Afeature\s+(.+)\z/, 1] },
37
+ interfaces: interfaces, vlan_vnis: vlan_vnis, vrfs: parse_vrfs(sections), nve: nve,
38
+ evpn: parse_evpn(sections), bgp: bgp, vpc_domain: parse_vpc(sections, interfaces),
39
+ acls: parse_acls(sections), prefix_lists: parse_prefix_lists(sections),
40
+ route_maps: parse_route_maps(sections), segments: segments,
41
+ anycast_mac: sections.filter_map { |entry| entry[:text][/\Afabric forwarding anycast-gateway-mac (\S+)\z/, 1] }.last,
42
+ complete: complete && issues.empty?, issues: issues, config_digest: config_digest(text))
43
+ end
44
+
45
+ private
46
+
47
+ def tree(text)
48
+ roots = []
49
+ stack = []
50
+ text.gsub("\r", "").each_line.with_index(1) do |line, number|
51
+ value = line.strip
52
+ next if value.empty? || value.start_with?("!", "#")
53
+
54
+ indentation = line[/\A\s*/].size
55
+ entry = { text: value, line: number, children: [] }
56
+ stack.pop while stack.any? && stack.last[0] >= indentation
57
+ (stack.empty? ? roots : stack.last[1][:children]) << entry
58
+ stack << [indentation, entry]
59
+ end
60
+ roots
61
+ end
62
+
63
+ def direct(entry) = entry[:children].map { |child| child[:text] }
64
+ def value(lines, pattern) = lines.filter_map { |line| line[pattern, 1] }.last
65
+
66
+ def config_digest(text)
67
+ commands = text.gsub("\r", "").lines.reject { |line| line.strip.empty? || line.lstrip.start_with?("!") }
68
+ Digest::SHA256.hexdigest(commands.map(&:rstrip).join("\n"))
69
+ end
70
+
71
+ def parse_interfaces(sections, issues)
72
+ sections.each_with_object({}) do |entry, result|
73
+ name = entry[:text][/\Ainterface (\S+)\z/, 1]
74
+ next unless name
75
+
76
+ name = name.sub(/\Avlan/i, "Vlan")
77
+ lines = direct(entry)
78
+ addresses = lines.filter_map { |line| line[/\Aip address (\S+)(?: secondary)?\z/, 1] }
79
+ issues << { code: :unsupported_interface_address, interface: name, line: entry[:line] } if lines.grep(/\Aip address /).size != addresses.size
80
+ valid_addresses = addresses.filter_map do |address|
81
+ unless %r{\A\d+(?:\.\d+){3}/\d+\z}.match?(address) && ip_prefix?(address)
82
+ issues << { code: :invalid_interface_address, interface: name, line: entry[:line] }
83
+ next
84
+ end
85
+ address
86
+ end
87
+ result[name] = {
88
+ addresses: valid_addresses, vrf: value(lines, /\Avrf member (\S+)\z/),
89
+ anycast: lines.include?("fabric forwarding mode anycast-gateway"),
90
+ shutdown: if lines.include?("shutdown")
91
+ true
92
+ else
93
+ lines.include?("no shutdown") ? false : nil
94
+ end,
95
+ acl_bindings: lines.each_with_object({}) do |line, acls|
96
+ acls[Regexp.last_match(2).to_sym] = Regexp.last_match(1) if /\Aip access-group (\S+) (in|out)\z/.match(line)
97
+ end,
98
+ vpc_id: value(lines, /\Avpc (\d+)\z/)&.to_i,
99
+ peer_link: lines.include?("vpc peer-link"),
100
+ channel_group: value(lines, /\Achannel-group (\d+)(?: mode \S+)?\z/)&.to_i,
101
+ ospf: value(lines, /\Aip router ospf (.+)\z/),
102
+ description: value(lines, /\Adescription (.+)\z/)
103
+ }
104
+ end
105
+ end
106
+
107
+ def parse_vlans(sections, issues)
108
+ sections.each_with_object({}) do |entry, result|
109
+ match = /\Avlan ([\d,-]+)\z/.match(entry[:text])
110
+ next unless match
111
+
112
+ ids = expand_ids(match[1], 1..4094)
113
+ vni = value(direct(entry), /\Avn-segment (\d+)\z/)&.to_i
114
+ issues << { code: :invalid_vlan_mapping, line: entry[:line] } if direct(entry).any? { |line| line.start_with?("vn-segment ") } && vni.nil?
115
+ if !ids || (vni && (ids.size != 1 || !vni.between?(1, 16_777_215)))
116
+ issues << { code: :invalid_vlan_mapping, line: entry[:line] }
117
+ next
118
+ end
119
+ ids.each { |id| result[id] = vni }
120
+ end
121
+ end
122
+
123
+ def expand_ids(text, range)
124
+ entries = text.split(",").flat_map do |part|
125
+ first, last = part.split("-").map(&:to_i)
126
+ return nil unless range.cover?(first) && (!last || (range.cover?(last) && last >= first))
127
+
128
+ last ? (first..last).to_a : [first]
129
+ end
130
+ entries.uniq
131
+ end
132
+
133
+ def parse_nve(sections, issues)
134
+ entries = sections.select { |entry| /\Ainterface nve\d+\z/i.match?(entry[:text]) }
135
+ issues << { code: :multiple_nve_interfaces } if entries.size > 1
136
+ return {} if entries.empty?
137
+
138
+ entry = entries.first
139
+ lines = direct(entry)
140
+ members = {}
141
+ entry[:children].each do |child|
142
+ match = /\Amember vni (\d+)(?: (associate-vrf))?\z/.match(child[:text])
143
+ unless match
144
+ issues << { code: :unsupported_nve_member, line: child[:line] } if child[:text].start_with?("member vni ")
145
+ next
146
+ end
147
+ settings = direct(child)
148
+ members[match[1].to_i] = {
149
+ associate_vrf: !match[2].nil?, replication: settings.include?("ingress-replication protocol bgp") ? :bgp : nil,
150
+ multicast_group: value(settings, /\Amcast-group (\S+)\z/)
151
+ }
152
+ end
153
+ { interface: entry[:text].split.last, source_interface: value(lines, /\Asource-interface (\S+)\z/),
154
+ host_reachability: value(lines, /\Ahost-reachability protocol (\S+)\z/),
155
+ shutdown: if lines.include?("shutdown")
156
+ true
157
+ else
158
+ lines.include?("no shutdown") ? false : nil
159
+ end, members: members }
160
+ end
161
+
162
+ def route_targets(lines)
163
+ lines.filter_map do |line|
164
+ match = /\Aroute-target (import|export|both) (\S+)(?: (evpn))?\z/.match(line)
165
+ { direction: match[1].to_sym, target: match[2], evpn: !match[3].nil? } if match
166
+ end
167
+ end
168
+
169
+ def parse_vrfs(sections)
170
+ sections.each_with_object({}) do |entry, vrfs|
171
+ name = entry[:text][/\Avrf context (\S+)\z/, 1]
172
+ next unless name
173
+
174
+ lines = direct(entry)
175
+ families = entry[:children].select { |child| child[:text].start_with?("address-family ") }
176
+ vrfs[name] = { vni: value(lines, /\Avni (\d+)\z/)&.to_i, rd: value(lines, /\Ard (\S+)\z/),
177
+ route_targets: route_targets(lines + families.flat_map { |family| direct(family) }),
178
+ routes: lines.filter_map do |line|
179
+ match = /\Aip route (\S+) (\S+)(.*)\z/.match(line)
180
+ { prefix: match[1], next_hop: match[2], options: match[3].strip } if match
181
+ end }
182
+ end
183
+ end
184
+
185
+ def parse_evpn(sections)
186
+ sections.select { |entry| entry[:text] == "evpn" }.flat_map { |entry| entry[:children] }.each_with_object({}) do |entry, vnis|
187
+ match = /\Avni (\d+) (L[23])/i.match(entry[:text])
188
+ next unless match
189
+
190
+ lines = direct(entry)
191
+ vnis[match[1].to_i] = { type: match[2].downcase.to_sym, rd: value(lines, /\Ard (\S+)\z/), route_targets: route_targets(lines) }
192
+ end
193
+ end
194
+
195
+ def parse_bgp(sections)
196
+ entry = sections.find { |section| /\Arouter bgp (\d+)\z/.match?(section[:text]) }
197
+ return {} unless entry
198
+
199
+ lines = direct(entry)
200
+ result = { asn: entry[:text].split.last.to_i, router_id: value(lines, /\Arouter-id (\S+)\z/), neighbors: {}, templates: {}, vrfs: {} }
201
+ entry[:children].each do |child|
202
+ match = /\A(neighbor|template peer|vrf) (\S+)\z/.match(child[:text])
203
+ next unless match
204
+
205
+ settings = direct(child)
206
+ families = child[:children].select { |sub| sub[:text].start_with?("address-family ") }.to_h do |family|
207
+ members = direct(family)
208
+ [family[:text].delete_prefix("address-family "), {
209
+ route_reflector_client: if members.include?("route-reflector-client")
210
+ true
211
+ else
212
+ members.include?("no route-reflector-client") ? false : nil
213
+ end,
214
+ extended_community: if members.include?("send-community extended") || members.include?("send-community both")
215
+ true
216
+ else
217
+ members.include?("no send-community extended") ? false : nil
218
+ end,
219
+ advertise_evpn: members.include?("advertise l2vpn evpn"),
220
+ redistribute: members.filter_map do |line|
221
+ m = /\Aredistribute (\S+)(?: route-map (\S+))?\z/.match(line)
222
+ { protocol: m[1], route_map: m[2] } if m
223
+ end
224
+ }]
225
+ end
226
+ kind = { "neighbor" => :neighbors, "template peer" => :templates, "vrf" => :vrfs }.fetch(match[1])
227
+ result[kind][match[2]] = { remote_as: value(settings, /\Aremote-as (\S+)\z/), inherit_peer: value(settings, /\Ainherit peer (\S+)\z/),
228
+ update_source: value(settings, /\Aupdate-source (\S+)\z/), address_families: families }
229
+ end
230
+ result
231
+ end
232
+
233
+ def reflector?(bgp)
234
+ %i[neighbors templates].any? do |kind|
235
+ bgp.fetch(kind, {}).values.any? do |peer|
236
+ peer[:address_families].values.any? do |family|
237
+ family[:route_reflector_client]
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ def parse_vpc(sections, interfaces)
244
+ entry = sections.find { |section| /\Avpc domain \d+\z/.match?(section[:text]) }
245
+ return unless entry
246
+
247
+ lines = direct(entry)
248
+ keepalive = value(lines, /\Apeer-keepalive (.+)\z/)
249
+ keepalive = keepalive ? keepalive.scan(/(destination|source|vrf) (\S+)/).to_h.transform_keys(&:to_sym) : {}
250
+ VpcDomain.new(id: entry[:text].split.last.to_i, peer_keepalive: keepalive,
251
+ peer_links: interfaces.select { |_name, settings| settings[:peer_link] }.keys,
252
+ member_ports: interfaces.filter_map { |name, settings| [settings[:vpc_id], name] if settings[:vpc_id] }.to_h,
253
+ peer_gateway: lines.include?("peer-gateway"))
254
+ end
255
+
256
+ def parse_acls(sections)
257
+ sections.each_with_object({}) do |entry, acls|
258
+ name = entry[:text][/\Aip access-list (\S+)\z/, 1]
259
+ next unless name
260
+
261
+ acls[name] = direct(entry).filter_map do |line|
262
+ next if /\A(?:\d+ )?remark\b/.match?(line) || line == "statistics per-entry"
263
+
264
+ match = /\A(\d+) (permit|deny) (.+)\z/.match(line)
265
+ match ? { sequence: match[1].to_i, action: match[2].to_sym, expression: match[3] } : { unsupported: true }
266
+ end
267
+ end
268
+ end
269
+
270
+ def parse_prefix_lists(sections)
271
+ sections.each_with_object({}) do |entry, lists|
272
+ match = /\Aip prefix-list (\S+) seq (\d+) (permit|deny) (\S+)(.*)\z/.match(entry[:text])
273
+ next unless match
274
+
275
+ (lists[match[1]] ||= []) << { sequence: match[2].to_i, action: match[3].to_sym, prefix: match[4],
276
+ ge: match[5][/\bge (\d+)\b/, 1]&.to_i, le: match[5][/\ble (\d+)\b/, 1]&.to_i }
277
+ end
278
+ end
279
+
280
+ def parse_route_maps(sections)
281
+ sections.each_with_object({}) do |entry, maps|
282
+ match = /\Aroute-map (\S+) (permit|deny) (\d+)\z/.match(entry[:text])
283
+ next unless match
284
+
285
+ lines = direct(entry)
286
+ (maps[match[1]] ||= []) << { action: match[2].to_sym, sequence: match[3].to_i,
287
+ prefix_lists: lines.filter_map { |line| line[/\Amatch ip address prefix-list (.+)\z/, 1] }.flat_map(&:split),
288
+ additional_matches: lines.grep(/\Amatch /).reject { |line| line.start_with?("match ip address prefix-list ") },
289
+ sets: lines.grep(/\Aset /) }
290
+ end
291
+ end
292
+
293
+ def build_segments(interfaces, vlan_vnis, issues)
294
+ interfaces.filter_map do |name, settings|
295
+ match = /\AVlan(\d+)\z/i.match(name)
296
+ next unless match && vlan_vnis[match[1].to_i] && settings[:addresses].any?
297
+
298
+ if settings[:addresses].size > 1 || !settings[:vrf]
299
+ issues << { code: :unsupported_svi, interface: name }
300
+ next
301
+ end
302
+ address = settings[:addresses].first
303
+ network = IPAddr.new(address)
304
+ Segment.new(vlan_id: match[1].to_i, vni: vlan_vnis.fetch(match[1].to_i), vrf: settings[:vrf],
305
+ prefix: "#{network}/#{address.split("/").last}", gateway: address.split("/").first,
306
+ anycast: settings[:anycast], acl_bindings: settings[:acl_bindings])
307
+ rescue InputError
308
+ issues << { code: :invalid_segment, interface: name }
309
+ nil
310
+ end
311
+ end
312
+
313
+ def ip_address(value)
314
+ IPAddr.new(value).to_s
315
+ rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
316
+ nil
317
+ end
318
+
319
+ def ip_prefix?(value)
320
+ IPAddr.new(value).ipv4?
321
+ rescue IPAddr::InvalidAddressError
322
+ false
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ module CiscoNxos
6
+ # No retry of configuration writes. Recovery verifies desired state by readback.
7
+ class DeploymentAdapter
8
+ def initialize(connector: Connector)
9
+ @connector = connector
10
+ end
11
+
12
+ def preflight(plan:, previous:, credentials:) # rubocop:disable Naming/PredicateMethod -- Deployment adapter protocol requires this name.
13
+ validate_target!(plan.target)
14
+ plan.steps.each { |step| validate_step!(step) }
15
+ node = read_node(plan.target, credentials)
16
+ unless node.complete? && node.hostname == plan.preconditions[:hostname]
17
+ raise Error.new("VXLAN target identity or configuration evidence changed", code: :target_changed, stage: :preflight)
18
+ end
19
+ if !previous && node.config_digest != plan.preconditions[:config_digest]
20
+ raise Error.new("VXLAN configuration changed since planning", code: :stale_plan, stage: :preflight)
21
+ end
22
+
23
+ true
24
+ end
25
+
26
+ def apply(step:, target:, credentials:, idempotency_key:)
27
+ validate_target!(target)
28
+ validate_step!(step)
29
+ result = @connector.open(:cisco_nxos, **credentials, host: target.host) { |session| session.execute_script(step.parameters.fetch(:commands)) }
30
+ { status: result.success? ? :applied : :unknown, evidence: { completed_commands: result.steps.size, operation_key: idempotency_key } }
31
+ end
32
+
33
+ def verify(step:, target:, credentials:, receipt:)
34
+ Validation.invalid!("verification receipt must be a Hash") unless receipt.is_a?(Hash)
35
+ validate_target!(target)
36
+ validate_step!(step)
37
+ Verifier.new.verify(node: read_node(target, credentials), expected: step.expected)
38
+ end
39
+
40
+ private
41
+
42
+ def read_node(target, credentials)
43
+ result = @connector.open(:cisco_nxos, **credentials, host: target.host) { |session| session.execute_script(["show running-config"]) }
44
+ raise Error.new("configuration readback failed", code: :readback_failed, stage: :verify) unless result.success? && result.steps.size == 1
45
+
46
+ ConfigParser.new.parse(result.steps.first.output, target: target, complete: true)
47
+ end
48
+
49
+ def validate_target!(target)
50
+ Validation.invalid!("Cisco NX-OS deployment requires cisco_nxos target") unless target.platform.to_s == "cisco_nxos"
51
+ end
52
+
53
+ def validate_step!(step)
54
+ Validation.invalid!("unsupported VXLAN deployment action") unless step.action == "vxlan_segment"
55
+ commands = step.parameters[:commands]
56
+ Validation.invalid!("VXLAN commands must be nonempty single lines") unless commands.is_a?(Array) && !commands.empty? && commands.all? do |command|
57
+ command.is_a?(String) && !command.empty? && !/[\r\n\x00]/.match?(command)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ module CiscoNxos
6
+ class RuntimeParser
7
+ COMMANDS = {
8
+ nve_peers: "show nve peers", nve_vnis: "show nve vni",
9
+ bgp_neighbors: "show bgp l2vpn evpn summary", ospf_neighbors: "show ip ospf neighbors",
10
+ vpc: "show vpc brief", interfaces: "show interface brief",
11
+ port_channels: "show port-channel summary", mac_addresses: "show mac address-table"
12
+ }.freeze
13
+ BGP_FRAMING = [
14
+ /\ABGP summary information for VRF \S+, address family .+\z/i,
15
+ /\ABGP router identifier [\da-f:.]+, local AS number [\d.]+\z/i,
16
+ /\ABGP table version is \d+(?:, L2VPN EVPN config peers \d+, capable peers \d+)?\z/i,
17
+ /\A\d+ network entries and \d+ paths using \d+ bytes of memory\z/i,
18
+ /\ABGP attribute entries \[\d+\/\d+\], BGP AS path entries \[\d+\/\d+\]\z/i,
19
+ /\ABGP community entries \[\d+\/\d+\], BGP clusterlist entries \[\d+\/\d+\]\z/i
20
+ ].freeze
21
+ PORT_CHANNEL_FRAMING = [
22
+ /\AFlags:\s+D - Down\s+P - Up in port-channel \(members\)\z/i,
23
+ /\AI - Individual\s+H - Hot-standby \(LACP only\)\z/i,
24
+ /\As - Suspended\s+r - Module-removed\z/i,
25
+ /\AS - Switched\s+R - Routed\z/i,
26
+ /\AU - Up \(port-channel\)\z/i,
27
+ /\AM - Not in use\. Min-links not met\z/i
28
+ ].freeze
29
+
30
+ def parse(dimension, text, complete: false)
31
+ Validation.invalid!("dimension must be a Symbol or String") unless dimension.is_a?(Symbol) || dimension.is_a?(String)
32
+ dimension = dimension.to_sym
33
+ Validation.invalid!("unknown VXLAN runtime dimension") unless COMMANDS.key?(dimension)
34
+ Validation.invalid!("runtime output must be a String") unless text.is_a?(String)
35
+ Validation.invalid!("complete must be boolean") unless [true, false].include?(complete)
36
+ if /(?:invalid command|invalid input|not supported|feature.*not enabled)/i.match?(text)
37
+ return RuntimeResult.new(dimension: dimension, status: :unsupported, issues: [{ code: :command_unsupported }])
38
+ end
39
+
40
+ rows, recognized, rejected, explicit_empty = send("parse_#{dimension}", text.gsub("\r", ""))
41
+ issues = []
42
+ issues << { code: :output_incomplete } unless complete
43
+ issues << { code: :format_unrecognized } unless recognized
44
+ issues << { code: :unparsed_rows, count: rejected } if rejected.positive?
45
+ issues << { code: :missing_completeness_evidence } if rows.empty? && !explicit_empty
46
+ RuntimeResult.new(dimension: dimension, status: issues.empty? ? :complete : :partial, records: rows, issues: issues)
47
+ end
48
+
49
+ private
50
+
51
+ def table(text, header:, empty:, framing: [])
52
+ rows = []
53
+ rejected = 0
54
+ explicit_empty = false
55
+ recognized_header = false
56
+ text.each_line do |line|
57
+ line = line.strip
58
+ next if line.empty? || /\A[-=]+\z/.match?(line) || framing.any? { |pattern| pattern.match?(line) }
59
+ if header.match?(line)
60
+ recognized_header = true
61
+ next
62
+ end
63
+ if empty.match?(line)
64
+ explicit_empty = true
65
+ next
66
+ end
67
+
68
+ row = yield(line)
69
+ row ? rows << row : rejected += 1
70
+ end
71
+ rejected += 1 if explicit_empty && rows.any?
72
+ [rows, recognized_header || explicit_empty || rows.any?, rejected, explicit_empty]
73
+ end
74
+
75
+ def parse_nve_peers(text)
76
+ table(text, header: /\AInterface\s+Peer-IP\s+State\s+LearnType\s+Uptime\s+Router-Mac\z/i,
77
+ empty: /\A(?:No peers(?: found)?|0 peers)\.?\z/i) do |line|
78
+ match = /\A(nve\d+)\s+(\d+(?:\.\d+){3})\s+(Up|Down)\s+(\S+)\s+(\S+)\s+(\S+)\z/i.match(line)
79
+ { interface: match[1], peer_ip: match[2], state: match[3].downcase.to_sym, learn_type: match[4], uptime: match[5], router_mac: match[6] } if match
80
+ end
81
+ end
82
+
83
+ def parse_nve_vnis(text)
84
+ table(text, header: /\AInterface\s+VNI\s+Multicast-group\s+State\s+Mode\s+Type\s+\[BD\/VRF\]\z/i,
85
+ empty: /\A(?:No VNIs(?: found)?|0 VNIs)\.?\z/i) do |line|
86
+ match = /\A(nve\d+)\s+(\d+)\s+(\S+)\s+(Up|Down)\s+(\S+)\s+(L[23])\s+\[([^\]]+)\](?:\s+.*)?\z/i.match(line)
87
+ if match
88
+ { interface: match[1], vni: match[2].to_i, multicast_group: match[3], state: match[4].downcase.to_sym,
89
+ mode: match[5], type: match[6].downcase.to_sym, association: match[7] }
90
+ end
91
+ end
92
+ end
93
+
94
+ def parse_bgp_neighbors(text)
95
+ table(text, header: /\ANeighbor\s+V\s+AS\s+MsgRcvd\s+MsgSent\s+TblVer\s+InQ\s+OutQ\s+Up\/Down\s+State\/PfxRcd\z/i,
96
+ empty: /\A(?:No (?:BGP )?neighbors(?: found)?|0 neighbors)\.?\z/i, framing: BGP_FRAMING) do |line|
97
+ match = /\A(\d+(?:\.\d+){3})\s+\d+\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+)\z/.match(line)
98
+ next unless match
99
+
100
+ state = match[6].strip
101
+ { peer_ip: match[1], remote_as: match[2].to_i, input_queue: match[3].to_i, output_queue: match[4].to_i,
102
+ uptime: match[5], state: /\A\d+\z/.match?(state) ? :established : state.downcase.tr(" ", "_").to_sym,
103
+ prefixes_received: /\A\d+\z/.match?(state) ? state.to_i : nil }
104
+ end
105
+ end
106
+
107
+ def parse_ospf_neighbors(text)
108
+ table(text, header: /\ANeighbor ID\s+Pri\s+State\s+Up Time\s+Address\s+Interface\z/i,
109
+ empty: /\A(?:No (?:OSPF )?neighbors(?: found)?|0 neighbors)\.?\z/i,
110
+ framing: [/\AOSPF Process ID \S+ VRF \S+\z/i]) do |line|
111
+ match = /\A(\d+(?:\.\d+){3})\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+(?:\.\d+){3})\s+(\S+)\z/.match(line)
112
+ { neighbor_id: match[1], priority: match[2].to_i, state: match[3].downcase, uptime: match[4], address: match[5], interface: match[6] } if match
113
+ end
114
+ end
115
+
116
+ def parse_vpc(text)
117
+ row = {}
118
+ rejected = 0
119
+ fields = { domain_id: /\AvPC domain id\s*:\s*(\d+)\z/i, peer_status: /\APeer status\s*:\s*(.+)\z/i,
120
+ keepalive_status: /\AvPC keep-alive status\s*:\s*(.+)\z/i,
121
+ consistency_status: /\AConfiguration consistency status\s*:\s*(.+)\z/i,
122
+ role: /\AvPC role\s*:\s*(.+)\z/i }
123
+ text.each_line do |line|
124
+ line = line.strip
125
+ next if line.empty? || /\A[-=]+\z/.match?(line)
126
+ matched = fields.find { |_, pattern| pattern.match?(line) }
127
+ unless matched
128
+ rejected += 1
129
+ next
130
+ end
131
+ key, pattern = matched
132
+ value = line[pattern, 1]
133
+ if row.key?(key)
134
+ rejected += 1
135
+ else
136
+ row[key] = key == :domain_id ? value.to_i : value
137
+ end
138
+ end
139
+ return [[], false, rejected] if row.empty?
140
+
141
+ [[row], true, rejected + (%i[domain_id peer_status keepalive_status consistency_status] - row.keys).size]
142
+ end
143
+
144
+ def parse_interfaces(text)
145
+ table(text, header: /\A(?:Port\s+VRF\s+Status\s+IP Address|(?:Port|Interface)\s+Name\s+Status\s+Vlan\s+Duplex\s+Speed\s+Type)\z/i,
146
+ empty: /\A(?:No interfaces(?: found)?|0 interfaces)\.?\z/i) do |line|
147
+ next unless /\A(?:Eth(?:ernet)?\d|Po\d|port-channel\d|Vlan\d|loopback\d|nve\d)/i.match?(line)
148
+ tokens = line.split
149
+ state_index = tokens.index { |token| %w[up down connected notconnect disabled routed].include?(token.downcase) }
150
+ next unless state_index
151
+
152
+ { interface: tokens.first, state: tokens[state_index].downcase.to_sym, detail: tokens[(state_index + 1)..].join(" ") }
153
+ end
154
+ end
155
+
156
+ def parse_port_channels(text)
157
+ table(text, header: /\AGroup\s+Port-Channel\s+Type\s+Protocol\s+Member Ports\z/i,
158
+ empty: /\A(?:No port-channels(?: found)?|0 port-channels)\.?\z/i, framing: PORT_CHANNEL_FRAMING) do |line|
159
+ match = /\A(\d+)\s+(Po\d+)\(([^)]+)\)\s+(\S+)\s+(\S+)\s*(.*)\z/.match(line)
160
+ if match
161
+ members = match[6].split.map { |token| /\A([^\s()]+)\(([^\s()]+)\)\z/.match(token) }
162
+ next if members.any?(&:nil?)
163
+ { group: match[1].to_i, interface: match[2], flags: match[3], type: match[4], protocol: match[5],
164
+ members: members.map { |member| { interface: member[1], flags: member[2] } } }
165
+ end
166
+ end
167
+ end
168
+
169
+ def parse_mac_addresses(text)
170
+ table(text, header: /\AVLAN\s+MAC Address\s+Type\s+age\s+Secure\s+NTFY\s+Ports\z/i,
171
+ empty: /\A(?:No (?:MAC addresses|entries)(?: found)?|0 (?:MAC addresses|entries))\.?\z/i) do |line|
172
+ match = /\A(?:[*+GOCR]\s+)?(\d+)\s+([\da-f]{4}(?:\.[\da-f]{4}){2})\s+(dynamic|static)\s+(.+)\z/i.match(line)
173
+ if match
174
+ { vlan_id: match[1].to_i, mac: match[2].delete(".").scan(/../).join(":"), type: match[3].downcase.to_sym,
175
+ destination: match[4].split.last }
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ module CiscoNxos
6
+ # Renders only known missing objects; it never deletes or replaces existing policy.
7
+ class SegmentRenderer
8
+ def render(segment:, components:, nve_interface:, replication: :bgp, multicast_group: nil,
9
+ prefix_list: nil, prefix_sequence: nil)
10
+ Validation.name(nve_interface, "NVE interface")
11
+ commands = []
12
+ commands.push("vlan #{segment.vlan_id}", "vn-segment #{segment.vni}", "exit") if components.include?(:vlan)
13
+ if components.any? { |part| part.to_s.start_with?("svi") }
14
+ commands << "interface Vlan#{segment.vlan_id}"
15
+ commands << "no shutdown" if components.include?(:svi_enable)
16
+ commands << "vrf member #{segment.vrf}" if components.include?(:svi_vrf)
17
+ commands << "ip address #{segment.interface_address}" if components.include?(:svi_address)
18
+ commands << "fabric forwarding mode anycast-gateway" if components.include?(:svi_anycast)
19
+ segment.acl_bindings.each do |direction, name|
20
+ commands << "ip access-group #{name} #{direction}" if components.include?(:"svi_acl_#{direction}")
21
+ end
22
+ commands << "exit"
23
+ end
24
+ if components.include?(:nve)
25
+ commands.push("interface #{nve_interface}", "member vni #{segment.vni}")
26
+ commands << (replication == :bgp ? "ingress-replication protocol bgp" : "mcast-group #{multicast_group}")
27
+ commands.push("exit", "exit")
28
+ end
29
+ if components.include?(:evpn)
30
+ commands.push("evpn", "vni #{segment.vni} l2", "rd auto", "route-target import auto", "route-target export auto", "exit", "exit")
31
+ end
32
+ if components.include?(:advertisement)
33
+ Validation.name(prefix_list, "prefix-list")
34
+ Validation.integer(prefix_sequence, "prefix-list sequence", 1..4_294_967_294)
35
+ commands << "ip prefix-list #{prefix_list} seq #{prefix_sequence} permit #{segment.prefix}"
36
+ end
37
+ commands.empty? ? [].freeze : ["configure terminal", *commands, "end"].freeze
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end