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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ # Fabric 证据、审计与网段意图;NX-OS 语法保留在 CiscoNxos 中。
5
+ module Vxlan
6
+ # 顺序读取明确的目标,保留失败情况与预期覆盖范围。
7
+ # 配置完整性与运行状态维度分别作为证据记录。
8
+ class Collector
9
+ DEFAULT_DIMENSIONS = %i[configuration nve_peers nve_vnis bgp_neighbors].freeze
10
+
11
+ def initialize(connector: Connector, clock: -> { Time.now.utc })
12
+ @connector = connector
13
+ @clock = clock
14
+ end
15
+
16
+ def collect(targets:, fabric_key:, connection_options: {}, dimensions: DEFAULT_DIMENSIONS)
17
+ unless targets.is_a?(Array) && !targets.empty? && targets.all?(Target) && targets.map(&:key).uniq.size == targets.size
18
+ Validation.invalid!("collection needs a nonempty Array of unique Targets")
19
+ end
20
+ supported = [:configuration, *CiscoNxos::RuntimeParser::COMMANDS.keys]
21
+ unless dimensions.is_a?(Array) && !dimensions.empty? && dimensions.uniq == dimensions && (dimensions - supported).empty?
22
+ Validation.invalid!("unknown or duplicate VXLAN dimensions")
23
+ end
24
+ Validation.invalid!("connection_options must be a Hash") unless connection_options.is_a?(Hash)
25
+ nodes = []
26
+ runtime = {}
27
+ collection = {}
28
+ targets.each do |target|
29
+ runtime[target.key] = {}
30
+ if target.platform != "cisco_nxos"
31
+ collection[target.key] = :unsupported
32
+ dimensions.reject { |dimension| dimension == :configuration }.each do |dimension|
33
+ runtime[target.key][dimension] = RuntimeResult.new(dimension: dimension, status: :unsupported)
34
+ end
35
+ next
36
+ end
37
+ collection[target.key] = :complete
38
+ begin
39
+ @connector.open(:cisco_nxos, **connection_options, host: target.host) do |session|
40
+ dimensions.each do |dimension|
41
+ command = dimension == :configuration ? "show running-config" : CiscoNxos::RuntimeParser::COMMANDS.fetch(dimension)
42
+ result = session.execute_script([command])
43
+ successful = result.success? && result.steps.size == 1
44
+ unless successful
45
+ collection[target.key] = :partial
46
+ runtime[target.key][dimension] =
47
+ RuntimeResult.new(dimension: dimension, status: :failed, issues: [{ code: result.error&.code || :collection_failed }])
48
+ next
49
+ end
50
+ output = result.steps.first.output
51
+ if dimension == :configuration
52
+ node = CiscoNxos::ConfigParser.new.parse(output, target: target, complete: true)
53
+ nodes << node
54
+ collection[target.key] = :partial unless node.complete?
55
+ else
56
+ parsed = CiscoNxos::RuntimeParser.new.parse(dimension, output, complete: true)
57
+ runtime[target.key][dimension] = parsed
58
+ collection[target.key] = :partial unless parsed.complete?
59
+ end
60
+ end
61
+ end
62
+ rescue StandardError => e
63
+ collection[target.key] = nodes.any? { |node| node.key == target.key } || runtime[target.key].any? ? :partial : :failed
64
+ dimensions.each do |dimension|
65
+ next if runtime[target.key].key?(dimension) || (dimension == :configuration && nodes.any? { |node| node.key == target.key })
66
+
67
+ runtime[target.key][dimension] = RuntimeResult.new(dimension: dimension, status: :failed,
68
+ issues: [{ code: e.respond_to?(:code) ? e.code : :collection_failed }])
69
+ end
70
+ end
71
+ end
72
+ Snapshot.new(fabric: Fabric.new(key: fabric_key, nodes: nodes, expected_node_keys: targets.map(&:key)),
73
+ runtime: runtime, collection: collection, observed_at: @clock.call)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ class Fabric
6
+ attr_reader :key, :nodes, :expected_node_keys
7
+
8
+ def initialize(key:, nodes:, expected_node_keys: nil)
9
+ @key = Validation.name(key, "fabric key")
10
+ Validation.invalid!("nodes must be an Array of Node") unless nodes.is_a?(Array) && nodes.all?(Node)
11
+ Validation.invalid!("fabric contains duplicate node keys") unless nodes.map(&:key).uniq.size == nodes.size
12
+ expected_node_keys = nodes.map(&:key) if expected_node_keys.nil?
13
+ Validation.invalid!("expected_node_keys must be an Array") unless expected_node_keys.is_a?(Array)
14
+ @expected_node_keys = expected_node_keys.map { |name| Validation.name(name, "expected node key") }.freeze
15
+ Validation.invalid!("fabric contains duplicate expected node keys") unless @expected_node_keys.uniq.size == @expected_node_keys.size
16
+ Validation.invalid!("fabric contains an unexpected node") unless (nodes.map(&:key) - @expected_node_keys).empty?
17
+ @nodes = nodes.dup.freeze
18
+ freeze
19
+ end
20
+
21
+ def vteps = nodes.select(&:vtep?).freeze
22
+ def spines = nodes.select { |node| node.role == :spine }.freeze
23
+ def node(key) = nodes.find { |entry| entry.key == key }
24
+ def complete? = !nodes.empty? && nodes.size == expected_node_keys.size && nodes.all?(&:complete?)
25
+ def digest = Validation.fingerprint(expected_node_keys: expected_node_keys.sort, nodes: nodes.sort_by(&:key).map { |node| [node.key, node.config_digest] })
26
+ def to_h = { key: key, nodes: nodes.map(&:to_h), expected_node_keys: expected_node_keys, digest: digest }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ class Finding
6
+ attr_reader :code, :severity, :node_keys, :message, :details
7
+
8
+ def initialize(code:, severity:, message:, node_keys: [], details: {})
9
+ @code = code.to_sym
10
+ @severity = severity.to_sym
11
+ @message = message.dup.freeze
12
+ @node_keys = Values.copy(node_keys)
13
+ @details = Values.copy(details)
14
+ freeze
15
+ end
16
+
17
+ def to_h = { code: code, severity: severity, node_keys: node_keys, message: message, details: details }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ # Parsed configuration facts. Missing facts stay absent, with completeness explicit.
6
+ class Node
7
+ attr_reader :key, :hostname, :management_ip, :role, :features, :interfaces, :vlan_vnis,
8
+ :vrfs, :nve, :evpn, :bgp, :vpc_domain, :acls, :prefix_lists, :route_maps,
9
+ :segments, :anycast_mac, :issues, :complete, :config_digest
10
+
11
+ def initialize(key:, hostname: nil, management_ip: nil, role: :unknown, features: [], interfaces: {},
12
+ vlan_vnis: {}, vrfs: {}, nve: {}, evpn: {}, bgp: {}, vpc_domain: nil,
13
+ acls: {}, prefix_lists: {}, route_maps: {}, segments: [], anycast_mac: nil,
14
+ complete: false, issues: [], config_digest: nil)
15
+ @key = Validation.name(key, "node key")
16
+ @hostname = hostname && Values.text(hostname, name: "hostname").dup.freeze
17
+ @management_ip = management_ip && Validation.address(management_ip, "management_ip")
18
+ Validation.invalid!("unknown VXLAN node role") unless %i[leaf border spine unknown].include?(role)
19
+ Validation.invalid!("complete must be true or false") unless [true, false].include?(complete)
20
+ @role = role
21
+ @complete = complete
22
+ Validation.invalid!("vpc_domain must be a VpcDomain or nil") unless vpc_domain.nil? || vpc_domain.is_a?(VpcDomain)
23
+ @vpc_domain = vpc_domain
24
+ @anycast_mac = anycast_mac&.dup&.freeze
25
+ { features: features, interfaces: interfaces, vlan_vnis: vlan_vnis, vrfs: vrfs, nve: nve,
26
+ evpn: evpn, bgp: bgp, acls: acls, prefix_lists: prefix_lists, route_maps: route_maps,
27
+ issues: issues }.each do |name, value|
28
+ expected = %i[features issues].include?(name) ? Array : Hash
29
+ Validation.invalid!("#{name} must be a #{expected}") unless value.is_a?(expected)
30
+ instance_variable_set("@#{name}", Values.copy(value))
31
+ end
32
+ Validation.invalid!("segments must be an Array of Segment") unless segments.is_a?(Array) && segments.all?(Segment)
33
+ @segments = segments.dup.freeze
34
+ @config_digest = (config_digest || Validation.fingerprint(configuration)).freeze
35
+ freeze
36
+ end
37
+
38
+ def vtep? = %i[leaf border].include?(role) || !nve.empty?
39
+ def complete? = complete
40
+ def segment(vlan_id) = segments.find { |entry| entry.vlan_id == vlan_id }
41
+
42
+ def configuration
43
+ { hostname: hostname, role: role, features: features, interfaces: interfaces, vlan_vnis: vlan_vnis,
44
+ vrfs: vrfs, nve: nve, evpn: evpn, bgp: bgp, vpc_domain: vpc_domain&.to_h, acls: acls,
45
+ prefix_lists: prefix_lists, route_maps: route_maps, segments: segments.map(&:to_h), anycast_mac: anycast_mac }
46
+ end
47
+
48
+ def to_h = { key: key, management_ip: management_ip, complete: complete, issues: issues, config_digest: config_digest, **configuration }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ # Evaluates supported prefix-list/route-map predicates in device order.
6
+ class RoutePolicy
7
+ def advertisement(node, segment)
8
+ family = node.bgp.fetch(:vrfs, {}).dig(segment.vrf, :address_families, "ipv4 unicast")
9
+ return :missing unless family && family[:advertise_evpn]
10
+
11
+ rules = family.fetch(:redistribute, []).select { |rule| %w[direct connected].include?(rule[:protocol]) }
12
+ return :missing if rules.empty?
13
+
14
+ decisions = rules.map { |rule| rule[:route_map] ? route_map(node, rule[:route_map], segment.prefix) : :permitted }
15
+ return :permitted if decisions.include?(:permitted)
16
+
17
+ decisions.include?(:unknown) ? :unknown : :denied
18
+ end
19
+
20
+ def route_map(node, name, prefix)
21
+ entries = node.route_maps[name]
22
+ return :unknown unless entries
23
+
24
+ entries.sort_by { |entry| entry[:sequence] }.each do |entry|
25
+ return :unknown if entry[:additional_matches].any?
26
+
27
+ decisions = entry[:prefix_lists].map { |list| prefix_list(node, list, prefix) }
28
+ return :unknown if decisions.include?(:unknown)
29
+ next unless decisions.empty? || decisions.include?(:permitted)
30
+
31
+ return entry[:action] == :permit ? :permitted : :denied
32
+ end
33
+ :denied
34
+ end
35
+
36
+ def prefix_list(node, name, prefix)
37
+ entries = node.prefix_lists[name]
38
+ return :unknown unless entries
39
+
40
+ network = IPAddr.new(prefix)
41
+ length = Integer(prefix.split("/").last, 10)
42
+ entries.sort_by { |entry| entry[:sequence] }.each do |entry|
43
+ parent = IPAddr.new(entry[:prefix])
44
+ bits = Integer(entry[:prefix].split("/").last, 10)
45
+ minimum = entry[:ge] || bits
46
+ maximum = entry[:le] || (entry[:ge] ? 32 : bits)
47
+ next unless parent.include?(network) && length.between?(minimum, maximum)
48
+
49
+ return entry[:action] == :permit ? :permitted : :denied
50
+ end
51
+ :denied
52
+ rescue ArgumentError
53
+ :unknown
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ class RuntimeResult
6
+ STATUSES = %i[complete partial unsupported failed not_requested].freeze
7
+ attr_reader :dimension, :status, :records, :issues
8
+
9
+ def initialize(dimension:, status:, records: [], issues: [])
10
+ Validation.invalid!("invalid runtime result status") unless STATUSES.include?(status)
11
+ Validation.invalid!("dimension must be a Symbol or String") unless dimension.is_a?(Symbol) || dimension.is_a?(String)
12
+ Validation.invalid!("runtime records and issues must be arrays") unless records.is_a?(Array) && issues.is_a?(Array)
13
+ @dimension = dimension.to_sym
14
+ @status = status
15
+ @records = Values.copy(records)
16
+ @issues = Values.copy(issues)
17
+ freeze
18
+ end
19
+
20
+ def complete? = status == :complete
21
+ def to_h = { dimension: dimension, status: status, records: records, issues: issues }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ # A routed L2 segment; IDs and gateway are explicit, never guessed from a name.
6
+ class Segment
7
+ attr_reader :vlan_id, :vni, :vrf, :prefix, :gateway, :anycast, :acl_bindings
8
+
9
+ def initialize(vlan_id:, vni:, vrf:, prefix:, gateway:, anycast: true, acl_bindings: {})
10
+ @vlan_id = Validation.integer(vlan_id, "vlan_id", 1..4094)
11
+ @vni = Validation.integer(vni, "vni", 1..16_777_215)
12
+ @vrf = Validation.name(vrf, "vrf")
13
+ @prefix = Validation.prefix(prefix)
14
+ @gateway = Validation.address(gateway, "gateway")
15
+ network = IPAddr.new(@prefix)
16
+ ip = IPAddr.new(@gateway)
17
+ Validation.invalid!("gateway must be inside prefix") unless network.ipv4? && ip.ipv4? && network.include?(ip)
18
+ Validation.invalid!("gateway cannot be the network or broadcast address") if prefix_length < 31 && [network.to_range.first,
19
+ network.to_range.last].include?(ip)
20
+ Validation.invalid!("anycast must be true or false") unless [true, false].include?(anycast)
21
+ Validation.invalid!("acl_bindings must map in/out to ACL names") unless acl_bindings.is_a?(Hash) && (acl_bindings.keys - %i[in out]).empty?
22
+ acl_bindings.each_value { |value| Validation.name(value, "ACL name") }
23
+ @anycast = anycast
24
+ @acl_bindings = Values.copy(acl_bindings)
25
+ freeze
26
+ end
27
+
28
+ def prefix_length = Integer(prefix.split("/").last, 10)
29
+ def interface_address = "#{gateway}/#{prefix_length}"
30
+ def identity = [vrf, prefix].freeze
31
+ def to_h = { vlan_id: vlan_id, vni: vni, vrf: vrf, prefix: prefix, gateway: gateway, anycast: anycast, acl_bindings: acl_bindings }
32
+ def ==(other) = other.is_a?(Segment) && to_h == other.to_h
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ class SegmentPlan
6
+ attr_reader :segment, :plans, :status, :fabric_key, :baseline_digest, :target_count, :complete_count
7
+
8
+ def initialize(segment:, plans:, status:, fabric_key:, baseline_digest:, target_count:, complete_count:)
9
+ @segment = segment
10
+ @plans = plans.dup.freeze
11
+ @status = status
12
+ @fabric_key = fabric_key.dup.freeze
13
+ @baseline_digest = baseline_digest.dup.freeze
14
+ @target_count = target_count
15
+ @complete_count = complete_count
16
+ freeze
17
+ end
18
+
19
+ def to_h
20
+ { segment: segment.to_h, plans: plans.map(&:payload), status: status, fabric_key: fabric_key,
21
+ baseline_digest: baseline_digest, target_count: target_count, complete_count: complete_count }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ # 基于传入的 Fabric 进行纯增量规划;冲突或不完整的基线
6
+ # 会阻止命令生成;已经存在的组件不重复改写。
7
+ class SegmentPlanner
8
+ def plan(fabric:, segment:, target_keys: nil, replication: :bgp, multicast_group: nil, prefix_list: nil)
9
+ Validation.invalid!("fabric and segment must be VXLAN domain objects") unless fabric.is_a?(Fabric) && segment.is_a?(Segment)
10
+ Validation.invalid!("replication must be bgp or multicast") unless %i[bgp multicast].include?(replication)
11
+ if replication == :multicast
12
+ multicast_group = Validation.address(multicast_group, "multicast_group")
13
+ Validation.invalid!("multicast group must be in 224.0.0.0/4") unless IPAddr.new("224.0.0.0/4").include?(IPAddr.new(multicast_group))
14
+ end
15
+ Validation.name(prefix_list, "prefix-list") if prefix_list
16
+ nodes = selected_nodes(fabric, target_keys)
17
+ validate_conflicts!(fabric, segment)
18
+ plans = []
19
+ complete_count = 0
20
+ observed_count = 0
21
+ nodes.each do |node|
22
+ validate_node!(node, segment)
23
+ components = missing_components(node, segment, replication, multicast_group, prefix_list)
24
+ observed_count += 1 if node.vlan_vnis.key?(segment.vlan_id) || node.nve.fetch(:members, {}).key?(segment.vni)
25
+ if components.empty?
26
+ complete_count += 1
27
+ next
28
+ end
29
+ sequence = prefix_list && next_sequence(node, prefix_list)
30
+ commands = CiscoNxos::SegmentRenderer.new.render(segment: segment, components: components, nve_interface: node.nve.fetch(:interface),
31
+ replication: replication, multicast_group: multicast_group,
32
+ prefix_list: prefix_list, prefix_sequence: sequence)
33
+ target = Target.new(key: node.key, host: node.management_ip, platform: :cisco_nxos, attributes: { role: node.role, fabric_key: fabric.key })
34
+ expected = { segment: segment.to_h, replication: replication, multicast_group: multicast_group, prefix_list: prefix_list }
35
+ step = Deployment::Step.new(id: "segment-#{segment.vlan_id}-#{segment.vni}", action: "vxlan_segment",
36
+ parameters: { commands: commands, components: components }, expected: expected)
37
+ plans << Deployment::Plan.new(target: target, steps: [step], preconditions: { config_digest: node.config_digest, hostname: node.hostname },
38
+ metadata: { domain: "vxlan", fabric_key: fabric.key, baseline_digest: fabric.digest,
39
+ prefix: segment.prefix, vrf: segment.vrf })
40
+ end
41
+ SegmentPlan.new(segment: segment, plans: plans, status: if plans.empty?
42
+ :existing
43
+ else
44
+ observed_count.positive? ? :partial : :new
45
+ end,
46
+ fabric_key: fabric.key, baseline_digest: fabric.digest, target_count: nodes.size, complete_count: complete_count)
47
+ end
48
+
49
+ private
50
+
51
+ def selected_nodes(fabric, keys)
52
+ Validation.invalid!("fabric evidence must be complete before planning", code: :incomplete_snapshot) unless fabric.complete?
53
+ keys ||= fabric.vteps.map(&:key)
54
+ Validation.invalid!("target_keys must be unique nonempty node keys") unless keys.is_a?(Array) && !keys.empty? && keys.uniq.size == keys.size
55
+ nodes = keys.map { |key| fabric.node(key) }
56
+ Validation.invalid!("all selected targets must be VTEPs") unless nodes.all? { |node| node&.vtep? }
57
+ nodes
58
+ end
59
+
60
+ def validate_conflicts!(fabric, segment)
61
+ network = IPAddr.new(segment.prefix)
62
+ fabric.nodes.each do |node|
63
+ existing_vni = node.vlan_vnis[segment.vlan_id]
64
+ conflict!(node, :vlan_conflict) if existing_vni && existing_vni != segment.vni
65
+ conflict!(node, :vni_conflict) if node.vlan_vnis.any? { |vlan, vni| vni == segment.vni && vlan != segment.vlan_id }
66
+ conflict!(node, :vni_conflict) if node.vrfs.values.any? { |vrf| vrf[:vni] == segment.vni }
67
+ node.segments.each do |existing|
68
+ same_address = existing.vrf == segment.vrf && existing.prefix == segment.prefix && existing.gateway == segment.gateway
69
+ conflict!(node, :segment_conflict) if existing.vlan_id == segment.vlan_id && !same_address
70
+ next unless existing.vrf == segment.vrf
71
+
72
+ other = IPAddr.new(existing.prefix)
73
+ conflict!(node, :prefix_conflict) if (network.include?(other) || other.include?(network)) && existing.vlan_id != segment.vlan_id
74
+ end
75
+ end
76
+ end
77
+
78
+ def validate_node!(node, segment)
79
+ Validation.invalid!("management IP is required", code: :management_ip_missing, details: { node_key: node.key }) unless node.management_ip
80
+ conflict!(node, :nve_missing) unless node.nve[:interface] && node.nve[:source_interface] && node.nve[:host_reachability] == "bgp"
81
+ vrf = node.vrfs[segment.vrf]
82
+ conflict!(node, :vrf_prerequisite_missing) unless vrf && vrf[:vni] && node.nve.fetch(:members, {}).dig(vrf[:vni], :associate_vrf)
83
+ conflict!(node, :anycast_mac_missing) if segment.anycast && !node.anycast_mac
84
+ segment.acl_bindings.each_value { |name| conflict!(node, :acl_missing) unless node.acls.key?(name) }
85
+ existing = node.interfaces["Vlan#{segment.vlan_id}"] || node.interfaces["vlan#{segment.vlan_id}"]
86
+ return unless existing
87
+
88
+ conflict!(node, :svi_vrf_conflict) if existing[:vrf] && existing[:vrf] != segment.vrf
89
+ conflict!(node, :svi_address_conflict) if existing[:addresses].any? && existing[:addresses] != [segment.interface_address]
90
+ existing[:acl_bindings].each do |direction, name|
91
+ conflict!(node, :acl_conflict) if segment.acl_bindings[direction] && segment.acl_bindings[direction] != name
92
+ end
93
+ conflict!(node, :anycast_conflict) if existing[:anycast] && !segment.anycast
94
+ end
95
+
96
+ def missing_components(node, segment, replication, multicast_group, prefix_list)
97
+ missing = []
98
+ missing << :vlan unless node.vlan_vnis[segment.vlan_id] == segment.vni
99
+ interface = node.interfaces["Vlan#{segment.vlan_id}"] || {}
100
+ missing << :svi_vrf unless interface[:vrf] == segment.vrf
101
+ missing << :svi_address unless interface[:addresses] == [segment.interface_address]
102
+ missing << :svi_enable if interface.empty? || interface[:shutdown] == true
103
+ missing << :svi_anycast if segment.anycast && !interface[:anycast]
104
+ segment.acl_bindings.each { |direction, name| missing << :"svi_acl_#{direction}" unless interface.fetch(:acl_bindings, {})[direction] == name }
105
+ member = node.nve.fetch(:members, {})[segment.vni]
106
+ if member
107
+ incompatible = member[:associate_vrf] || (member[:replication] && member[:replication] != replication)
108
+ incompatible ||= member[:multicast_group] && member[:multicast_group] != multicast_group
109
+ conflict!(node, :nve_member_conflict) if incompatible
110
+ end
111
+ missing << :nve unless member && (replication == :bgp ? member[:replication] == :bgp : member[:multicast_group] == multicast_group)
112
+ evpn = node.evpn[segment.vni]
113
+ if evpn
114
+ conflict!(node, :evpn_type_conflict) unless evpn[:type] == :l2
115
+ targets = evpn[:route_targets]
116
+ directions = targets.flat_map { |target| target[:direction] == :both ? %i[import export] : [target[:direction]] }
117
+ unless evpn[:rd] && (%i[import export] - directions).empty?
118
+ conflict!(node, :evpn_policy_conflict) if targets.any? { |target| target[:target] != "auto" } || (evpn[:rd] && evpn[:rd] != "auto")
119
+ missing << :evpn
120
+ end
121
+ else
122
+ missing << :evpn
123
+ end
124
+ if prefix_list && RoutePolicy.new.advertisement(node, segment) != :permitted
125
+ validate_advertisement!(node, segment, prefix_list)
126
+ missing << :advertisement
127
+ end
128
+ missing
129
+ end
130
+
131
+ def validate_advertisement!(node, segment, prefix_list)
132
+ family = node.bgp.fetch(:vrfs, {}).dig(segment.vrf, :address_families, "ipv4 unicast")
133
+ conflict!(node, :advertisement_prerequisite_missing) unless family && family[:advertise_evpn]
134
+ routes = family[:redistribute].select { |rule| %w[direct connected].include?(rule[:protocol]) && rule[:route_map] }
135
+ matching = routes.any? do |rule|
136
+ entries = node.route_maps[rule[:route_map]]
137
+ entries && entries.size == 1 && entries.first[:action] == :permit &&
138
+ entries.first[:additional_matches].empty? && entries.first[:prefix_lists] == [prefix_list]
139
+ end
140
+ conflict!(node, :advertisement_policy_unsupported) unless matching && node.prefix_lists.key?(prefix_list)
141
+ # An earlier deny would shadow an appended permit; do not claim remediation.
142
+ trial = node.prefix_lists[prefix_list]
143
+ conflict!(node, :advertisement_policy_conflict) if trial.any? { |entry| entry[:action] == :deny }
144
+ end
145
+
146
+ def next_sequence(node, prefix_list)
147
+ value = node.prefix_lists.fetch(prefix_list, []).map { |entry| entry[:sequence] }.max.to_i + 5
148
+ Validation.integer(value, "prefix-list sequence", 1..4_294_967_294)
149
+ end
150
+
151
+ def conflict!(node, code)
152
+ Validation.invalid!("VXLAN planning prerequisite or conflict: #{code}", code: code, details: { node_key: node.key })
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Netstack
6
+ module Vxlan
7
+ class Snapshot
8
+ attr_reader :fabric, :runtime, :collection, :observed_at
9
+
10
+ def initialize(fabric:, runtime: {}, collection: {}, observed_at: Time.now.utc)
11
+ Validation.invalid!("fabric must be a Fabric") unless fabric.is_a?(Fabric)
12
+ Validation.invalid!("runtime and collection must be hashes") unless runtime.is_a?(Hash) && collection.is_a?(Hash)
13
+ @fabric = fabric
14
+ @collection = Values.copy(collection)
15
+ @runtime = runtime.to_h do |key, dimensions|
16
+ Validation.invalid!("runtime dimensions must contain RuntimeResult values") unless dimensions.is_a?(Hash) && dimensions.values.all?(RuntimeResult)
17
+ [key.dup.freeze, dimensions.dup.freeze]
18
+ end.freeze
19
+ @observed_at = observed_at.is_a?(Time) ? observed_at.getutc.iso8601(6).freeze : Time.iso8601(observed_at).getutc.iso8601(6).freeze
20
+ freeze
21
+ end
22
+
23
+ def complete?
24
+ fabric.complete? && collection.values.all? { |value| value == :complete } && runtime.values.all? { |dimensions| dimensions.values.all?(&:complete?) }
25
+ end
26
+
27
+ def to_h
28
+ { fabric: fabric.to_h, runtime: runtime.transform_values do |dimensions|
29
+ dimensions.transform_values(&:to_h)
30
+ end, collection: collection, observed_at: observed_at }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require "digest"
5
+ require "json"
6
+
7
+ module Netstack
8
+ module Vxlan
9
+ # Validation and canonical fingerprints shared by the VXLAN value objects.
10
+ module Validation
11
+ module_function
12
+
13
+ def integer(value, name, range)
14
+ invalid!("#{name} must be an Integer in #{range}") unless value.is_a?(Integer) && range.cover?(value)
15
+ value
16
+ end
17
+
18
+ def name(value, field)
19
+ invalid!("#{field} must be a nonempty CLI identifier") unless value.is_a?(String) && /\A[a-zA-Z0-9_.:-]+\z/.match?(value)
20
+ value.dup.freeze
21
+ end
22
+
23
+ def address(value, field = "address")
24
+ invalid!("#{field} must be an IP address") unless value.is_a?(String) && !value.include?("/")
25
+ IPAddr.new(value).to_s.freeze
26
+ rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
27
+ invalid!("#{field} must be an IP address")
28
+ end
29
+
30
+ def prefix(value)
31
+ invalid!("prefix must be an IPv4 network in CIDR notation") unless value.is_a?(String) && %r{\A\d+(?:\.\d+){3}/\d+\z}.match?(value)
32
+ network = IPAddr.new(value)
33
+ bits = Integer(value.split("/").last, 10)
34
+ invalid!("prefix must specify a network address") unless network.ipv4? && bits.between?(0, 32) && network.to_s == value.split("/").first
35
+ "#{network}/#{bits}".freeze
36
+ rescue IPAddr::InvalidAddressError
37
+ invalid!("prefix must be an IPv4 network in CIDR notation")
38
+ end
39
+
40
+ def fingerprint(value)
41
+ Digest::SHA256.hexdigest(JSON.generate(canonical(value)))
42
+ end
43
+
44
+ def canonical(value)
45
+ case value
46
+ when Hash then value.keys.sort_by(&:to_s).to_h { |key| [key.to_s, canonical(value.fetch(key))] }
47
+ when Array then value.map { |item| canonical(item) }
48
+ when Symbol then value.to_s
49
+ else value.respond_to?(:to_h) ? canonical(value.to_h) : value
50
+ end
51
+ end
52
+
53
+ def invalid!(message, code: :invalid_vxlan_input, details: {})
54
+ raise InputError.new(message, code: code, stage: :validation, details: details)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ # 根据解析后的回读确认配置意图;不证明实际流量转发成功。
6
+ # 证据不完整或不匹配时保留未知状态,不能据此授权重放。
7
+ class Verifier
8
+ def verify(node:, expected:)
9
+ return { status: :unknown, evidence: { reason: :configuration_incomplete } } unless node.complete?
10
+
11
+ expected = symbolize(expected)
12
+ segment = Segment.new(**expected.fetch(:segment))
13
+ actual = node.segment(segment.vlan_id)
14
+ missing = []
15
+ missing << :vlan unless node.vlan_vnis[segment.vlan_id] == segment.vni
16
+ unless actual && actual.vrf == segment.vrf && actual.prefix == segment.prefix && actual.gateway == segment.gateway && actual.anycast == segment.anycast
17
+ missing << :svi
18
+ end
19
+ # SVI 标志和引用不能证明依赖仍存在;回读须保持规划时要求的前提。
20
+ missing << :anycast_mac if segment.anycast && !node.anycast_mac
21
+ missing << :acl unless actual && segment.acl_bindings.all? { |direction, name| actual.acl_bindings[direction] == name && node.acls.key?(name) }
22
+ missing << :svi_shutdown if node.interfaces.dig("Vlan#{segment.vlan_id}", :shutdown) == true
23
+ member = node.nve.fetch(:members, {})[segment.vni]
24
+ replication_ok = if expected[:replication].to_sym == :bgp
25
+ member && member[:replication] == :bgp
26
+ else
27
+ member && member[:multicast_group] == expected[:multicast_group]
28
+ end
29
+ missing << :nve unless member && !member[:associate_vrf] && replication_ok
30
+ evpn = node.evpn[segment.vni]
31
+ directions = if evpn
32
+ evpn[:route_targets].flat_map { |target| target[:direction] == :both ? %i[import export] : [target[:direction]] }
33
+ else
34
+ []
35
+ end
36
+ missing << :evpn unless evpn && evpn[:type] == :l2 && evpn[:rd] && (%i[import export] - directions).empty?
37
+ missing << :advertisement if expected[:prefix_list] && RoutePolicy.new.advertisement(node, segment) != :permitted
38
+ { status: missing.empty? ? :verified : :unknown, evidence: { config_digest: node.config_digest, missing: missing } }
39
+ end
40
+
41
+ private
42
+
43
+ def symbolize(value)
44
+ case value
45
+ when Hash then value.to_h { |key, entry| [key.to_sym, symbolize(entry)] }
46
+ when Array then value.map { |entry| symbolize(entry) }
47
+ else value
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ class VpcDomain
6
+ attr_reader :id, :peer_keepalive, :peer_links, :member_ports, :peer_gateway
7
+
8
+ def initialize(id:, peer_keepalive: {}, peer_links: [], member_ports: {}, peer_gateway: false)
9
+ @id = Validation.integer(id, "vPC domain", 1..1000)
10
+ @peer_keepalive = Values.copy(peer_keepalive)
11
+ @peer_links = Values.copy(peer_links)
12
+ @member_ports = Values.copy(member_ports)
13
+ @peer_gateway = peer_gateway
14
+ freeze
15
+ end
16
+
17
+ def to_h = { id: id, peer_keepalive: peer_keepalive, peer_links: peer_links, member_ports: member_ports, peer_gateway: peer_gateway }
18
+ end
19
+ end
20
+ end