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,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from Motor Netconf scenario definitions and safe template dialect.
4
+ require_relative "support"
5
+
6
+ require "ipaddr"
7
+ require "socket"
8
+ require_relative "bootstrap/payload"
9
+
10
+ module Netstack
11
+ module Configuration
12
+ class Payload
13
+ def self.call(scenario:, input:, system_config: {}, now: Time.now.utc)
14
+ data = Support.object(input).deep_dup
15
+ data["system"] = system_config.deep_stringify_keys if scenario.to_s == "bootstrap"
16
+ case scenario.to_s
17
+ when "bootstrap" then bootstrap(data)
18
+ when "h3c-irf" then irf(data)
19
+ when "distributed-gateway" then distributed_gateway(data)
20
+ when "server-port" then server_port(data)
21
+ when "hillstone-policy" then hillstone_policy(data, now: now)
22
+ when "load-balancer" then load_balancer(data)
23
+ when "ipsec-vpn" then ipsec(data)
24
+ end
25
+ data
26
+ end
27
+
28
+ class << self
29
+ include Bootstrap::Payload
30
+
31
+ private
32
+
33
+ def irf(data)
34
+ range = IPAddr.new(data.fetch("mad_subnet")).to_range
35
+ data.merge!("mad_mask" => mask(data.fetch("mad_subnet")), "mad_member1_ip" => ipv4(range.first.to_i + 1), "mad_member2_ip" => ipv4(range.first.to_i + 2))
36
+ end
37
+
38
+ def server_port(data)
39
+ data.fetch("allocations").each { |row| decorate_switchport(row) }
40
+ data["aggregates"] = data.fetch("allocations").select { |row| row["port_channel"] }.group_by { |row| row["aggregate_id"] }.map { |id, rows| rows.first.merge("aggregate_id" => id) }
41
+ data["aggregates"].each { |row| row["vpc"] = data.fetch("stack_mode") == "vpc" }
42
+ end
43
+
44
+ def distributed_gateway(data)
45
+ data.fetch("gateways").each { |row| row["gateway_cidr"] = "#{row.fetch("gateway")}/#{row.fetch("subnet").split("/").last}" }
46
+ end
47
+
48
+ def hillstone_policy(data, now:)
49
+ policies = data.fetch("policies")
50
+ data["address_objects"] = []
51
+ service_index = {}
52
+ generated_date = now.strftime("%Y%m%d")
53
+
54
+ policies.each_with_index do |policy, index|
55
+ sequence = index + 1
56
+ policy["object_key"] = format("POLICY_%s_%03d", generated_date, sequence)
57
+ policy["description"] = policy["description"].presence || format("%s-%03d", generated_date, sequence)
58
+ policy["has_description"] = true
59
+ decorate_policy_addresses(data, policy, "src")
60
+ decorate_policy_addresses(data, policy, "dst")
61
+
62
+ policy["service_any"] = policy.fetch("services") == [ "any" ]
63
+ policy["service_names"] = []
64
+ policy["service_group"] = nil
65
+ next if policy["service_any"]
66
+
67
+ policy["service_names"] = policy.fetch("services").map do |ports|
68
+ name = hillstone_object_name(policy.fetch("protocol"), ports)
69
+ service_index[name] ||= hillstone_service(name, policy.fetch("protocol"), ports)
70
+ name
71
+ end
72
+ if policy["service_names"].size > 3
73
+ policy["service_group"] = hillstone_object_name(policy.fetch("object_key"), "SVC")
74
+ end
75
+ end
76
+
77
+ data["service_objects"] = service_index.values
78
+ data["service_groups"] = policies.filter_map do |policy|
79
+ next unless policy["service_group"]
80
+ { "name" => policy.fetch("service_group"), "services" => policy.fetch("service_names") }
81
+ end
82
+ end
83
+
84
+ def decorate_policy_addresses(data, policy, side)
85
+ addresses = policy.fetch("#{side}_addresses")
86
+ policy["#{side}_any"] = addresses == [ "any" ]
87
+ policy["#{side}_object"] = nil
88
+ policy["#{side}_entries"] = []
89
+ return if policy["#{side}_any"]
90
+
91
+ entries = addresses.map { |address| hillstone_address_entry(address) }
92
+ if entries.size > 3
93
+ object_name = hillstone_object_name(policy.fetch("object_key"), side.upcase)
94
+ policy["#{side}_object"] = object_name
95
+ data.fetch("address_objects") << { "name" => object_name, "entries" => entries }
96
+ else
97
+ policy["#{side}_entries"] = entries
98
+ end
99
+ end
100
+
101
+ def hillstone_address_entry(address)
102
+ return { "range" => false, "value" => address } unless address.include?("-")
103
+ first, last = address.split("-", 2)
104
+ { "range" => true, "first" => first, "last" => last }
105
+ end
106
+
107
+ def hillstone_service(name, protocol, ports)
108
+ bounds = ports.split("-", 2)
109
+ { "name" => name, "protocol" => protocol, "first_port" => bounds.first,
110
+ "last_port" => bounds.last || bounds.first, "port_range" => bounds.size == 2 }
111
+ end
112
+
113
+ def hillstone_object_name(*parts)
114
+ parts.join("_").upcase
115
+ end
116
+
117
+ def load_balancer(data)
118
+ services = data.fetch("services")
119
+ data["real_servers"] = services.flat_map { |row| row.fetch("real_ips") }.uniq.map { |address| { "address" => address } }
120
+ data["groups"] = services.group_by { |row| row.fetch("group_name").downcase }.values.map do |rows|
121
+ rows.first.slice("group_name", "real_ips", "health_check")
122
+ end
123
+ services.each { |row| row["client_ip_persistence"] = row.fetch("client_ip_persistence", false) }
124
+ data["virtuals"] = services.group_by { |row| [ row.fetch("flag"), row.fetch("virtual_ip") ] }.map do |(flag, virtual_ip), rows|
125
+ { "virtual_ip" => virtual_ip, "virtual_name" => "VIP_#{flag}_#{virtual_ip}", "services" => rows }
126
+ end
127
+ end
128
+
129
+ def ipsec(data)
130
+ data["ikev1"] = data.fetch("ike_version") == "ikev1"
131
+ data["ikev2"] = data.fetch("ike_version") == "ikev2"
132
+ data["local_ip"] = data["local_address"]&.split("/")&.first
133
+ encryption = { "aes-256-cbc" => "aes-256", "3des" => "3des" }
134
+ authentication = { "sha256" => "sha256", "sha1" => "sha", "md5" => "md5" }
135
+ data["hillstone_ike_encryption"] = encryption[data.fetch("ike_encryption")]
136
+ data["hillstone_ipsec_encryption"] = encryption[data.fetch("ipsec_encryption")]
137
+ data["hillstone_ikev2_encryption"] = data["hillstone_ike_encryption"]&.delete("-")
138
+ data["hillstone_ipsec_v2_encryption"] = data["hillstone_ipsec_encryption"]&.delete("-")
139
+ data["hillstone_ike_hash"] = authentication.fetch(data.fetch("ike_authentication"))
140
+ data["hillstone_ipsec_hash"] = authentication.fetch(data.fetch("ipsec_authentication"))
141
+ data["hillstone_ike_group"] = data.fetch("ike_dh_group").delete_prefix("group")
142
+ data["proxy_ids"] = data.fetch("local_subnets").product(data.fetch("remote_subnets")).map.with_index { |(local, remote), index| { "name" => "proxy-#{index + 1}", "local" => local, "remote" => remote } }
143
+ data["routes"] = data.fetch("remote_subnets").map.with_index { |subnet, index| { "name" => "#{data.fetch("site_name")}-#{index + 1}", "subnet" => subnet } }
144
+ end
145
+
146
+ def mask(cidr) = IPAddr.new("255.255.255.255").mask(cidr.split("/").last.to_i).to_s
147
+ def ipv4(number) = IPAddr.new(number, Socket::AF_INET).to_s
148
+
149
+ def entries(addresses)
150
+ addresses.map.with_index { |address, index| { "address" => address, "role" => index.zero? ? "primary" : "secondary", "name" => "SERVER#{index + 1}" } }
151
+ end
152
+
153
+ def wildcard(cidr)
154
+ network = IPAddr.new(cidr)
155
+ mask_value = IPAddr.new("255.255.255.255").mask(cidr.split("/").last.to_i)
156
+ "#{network.to_range.first} #{ipv4(mask_value.to_i ^ 0xffffffff)}"
157
+ end
158
+
159
+ def decorate_switchport(row)
160
+ row["trunk_all"] = row["vlan"] == "trunk"
161
+ row["trunk_list"] = !row["trunk_all"] && row["vlan"].match?(/[,-]/)
162
+ row["access"] = !row["trunk_all"] && !row["trunk_list"]
163
+ row["vlan_space"] = row["vlan"].tr(",", " ").gsub("-", " to ")
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "active_support/core_ext/object/blank"
5
+
6
+ module Netstack
7
+ module Configuration
8
+ # Source bytes and line endings are immutable; consumption belongs to this reader only.
9
+ class Reader
10
+ attr_reader :content, :lines, :vendor, :device_id, :device_name, :created_at, :checksum, :position
11
+
12
+ def initialize(content, vendor: nil, device_id: nil, device_name: nil, created_at: Time.now.utc)
13
+ raise ArgumentError, "Configuration content must be a string" unless content.is_a?(String)
14
+
15
+ @content = content.dup
16
+ unless [Encoding::UTF_8, Encoding::BINARY, Encoding::US_ASCII].include?(@content.encoding)
17
+ raise EncodingError, "Configuration must contain UTF-8 bytes"
18
+ end
19
+ @content.force_encoding(Encoding::UTF_8)
20
+ raise EncodingError, "Configuration is not valid UTF-8" unless @content.valid_encoding?
21
+ raise ArgumentError, "Configuration contains a NUL byte" if @content.include?("\0")
22
+
23
+ @content.freeze
24
+ @vendor = vendor.nil? ? nil : identifier(vendor, "Vendor").downcase.tr(" -", "__").freeze
25
+ @device_id = device_id.nil? ? nil : identifier(device_id, "Device ID")
26
+ @device_name = device_name.nil? ? nil : identifier(device_name, "Device name")
27
+ raise ArgumentError, "Creation time must be a Time" unless created_at.is_a?(Time)
28
+
29
+ @created_at = created_at.dup.freeze
30
+ @lines = @content.scan(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+\z/).map(&:freeze).freeze
31
+ @checksum = Digest::SHA256.hexdigest(@content).freeze
32
+ reset!
33
+ end
34
+
35
+ def size = lines.size
36
+ def inspect = "#<#{self.class.name} bytes=#{content.bytesize} lines=#{size} checksum=#{checksum}>"
37
+ def parsed_flags = @parsed.dup.freeze
38
+ def parsed?(index) = valid_index?(index) ? @parsed[index] : nil
39
+
40
+ def mark(index, parsed: true)
41
+ raise IndexError, "Line index is outside the document" unless valid_index?(index)
42
+ raise ArgumentError, "Parsed flag must be true or false" unless parsed == true || parsed == false
43
+
44
+ @parsed[index] = parsed
45
+ self
46
+ end
47
+
48
+ def rewind
49
+ @position = 0
50
+ self
51
+ end
52
+
53
+ def reset!
54
+ @parsed = Array.new(size, false)
55
+ rewind
56
+ end
57
+
58
+ def read
59
+ return nil if position >= size
60
+
61
+ line = lines[position]
62
+ @position += 1
63
+ line
64
+ end
65
+
66
+ def read_pending
67
+ while position < size
68
+ index = position
69
+ line = read
70
+ next if @parsed[index]
71
+
72
+ @parsed[index] = true
73
+ line = line.chomp
74
+ next if line.blank?
75
+
76
+ return line.freeze
77
+ end
78
+ nil
79
+ end
80
+
81
+ # Cursor commands return whether they changed state, rather than answering a predicate.
82
+ # rubocop:disable Naming/PredicateMethod
83
+ def step_back
84
+ return false if position.zero?
85
+
86
+ @position -= 1
87
+ true
88
+ end
89
+
90
+ def unread
91
+ return false unless step_back
92
+
93
+ @parsed[position] = false
94
+ true
95
+ end
96
+
97
+ def defer
98
+ return false if position.zero?
99
+
100
+ @parsed[position - 1] = false
101
+ true
102
+ end
103
+ # rubocop:enable Naming/PredicateMethod
104
+
105
+ def unparsed_lines = lines.each_index.filter_map { |index| lines[index] unless @parsed[index] }.freeze
106
+ def unparsed_content = unparsed_lines.join.freeze
107
+
108
+ private
109
+
110
+ def valid_index?(index) = index.is_a?(Integer) && index >= 0 && index < size
111
+
112
+ def identifier(value, label)
113
+ text = case value
114
+ when String then value
115
+ when Symbol then value.to_s
116
+ when Integer then value.to_s if value >= 0
117
+ end
118
+ raise ArgumentError, "#{label} must be nonempty text or a nonnegative integer" unless text && !text.strip.empty?
119
+
120
+ text.dup.freeze
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/blank"
4
+ require "active_support/core_ext/object/deep_dup"
5
+ require "active_support/core_ext/object/inclusion"
6
+ require "active_support/core_ext/hash/keys"
7
+ require "active_support/core_ext/enumerable"
8
+ require "active_support/core_ext/string/access"
9
+
10
+ module Netstack
11
+ module Configuration
12
+ # Shared framework extensions, without loading Rails or Active Record.
13
+ module Support
14
+ def self.object(value, name: "input")
15
+ raise InputError, "#{name} must be an object" unless value.is_a?(Hash)
16
+ duplicate = value.keys.map(&:to_s).tally.values.any? { |count| count > 1 }
17
+ raise InputError, "#{name} contains duplicate field names" if duplicate
18
+
19
+ value.to_h do |key, item|
20
+ normalized = case item
21
+ when Hash then object(item, name: name)
22
+ when Array then item.map { |element| element.is_a?(Hash) ? object(element, name: name) : Values.copy(element) }
23
+ else Values.copy(item)
24
+ end
25
+ [key.to_s, normalized]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support"
4
+
5
+ module Netstack
6
+ module Configuration
7
+ class SystemConfiguration
8
+ REQUIRED_FIELDS = %w[domain_name timezone management_vlan ntp_servers dns_servers syslog_servers
9
+ snmp_community tacacs_servers tacacs_secret ssh_acl_cidrs local_users base_security_enabled
10
+ disable_telnet disable_http enable_bpdu_protection password_min_length vty_timeout_minutes].freeze
11
+ OPTIONAL_FIELDS = %w[radius_servers radius_secret].freeze
12
+ FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze
13
+ SECRET_FIELDS = %w[snmp_community tacacs_secret radius_secret].freeze
14
+
15
+ def self.resolve(overrides, defaults: {})
16
+ current = Support.object(defaults, name: "system defaults")
17
+ incoming = Support.object(overrides, name: "system")
18
+ unexpected = (current.keys | incoming.keys) - FIELDS
19
+ raise InputError, "unexpected system fields: #{unexpected.join(", ")}" if unexpected.any?
20
+
21
+ SECRET_FIELDS.each do |field|
22
+ [current, incoming].each do |values|
23
+ value = values[field]
24
+ raise InputError, "system #{field} must be a string" unless value.nil? || value.is_a?(String)
25
+ end
26
+ incoming.delete(field) if omitted_secret?(incoming[field])
27
+ end
28
+ if incoming.key?("local_users")
29
+ raise InputError, "system local users must be an array" unless incoming["local_users"].is_a?(Array)
30
+
31
+ saved = Array(current["local_users"]).to_h { |user| [user.fetch("username"), user] }
32
+ incoming["local_users"] = incoming["local_users"].map do |user|
33
+ normalized = Support.object(user, name: "system local user")
34
+ normalized["password"] = saved.dig(normalized["username"], "password") if omitted_secret?(normalized["password"])
35
+ normalized
36
+ end
37
+ end
38
+ result = current.merge(incoming)
39
+ missing = REQUIRED_FIELDS.select { |field| result[field].nil? || (result[field].respond_to?(:empty?) && result[field].empty?) }
40
+ raise InputError, "system configuration is incomplete: #{missing.join(", ")}" if missing.any?
41
+
42
+ result
43
+ end
44
+
45
+ def self.frontend_defaults(config)
46
+ result = Support.object(config, name: "system")
47
+ SECRET_FIELDS.each { |field| result[field] = "" }
48
+ result["local_users"] = Array(result["local_users"]).map { |user| user.merge("password" => "") }
49
+ result
50
+ end
51
+
52
+ def self.omitted_secret?(value) = value.nil? || (value.is_a?(String) && value.blank?)
53
+ private_class_method :omitted_secret?
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from Motor Netconf scenario definitions and safe template dialect.
4
+ require_relative "support"
5
+
6
+ module Netstack
7
+ module Configuration
8
+ # A deliberately small, safe ERB dialect: interpolation, collection loops and
9
+ # boolean conditionals only. It never evaluates Ruby code.
10
+ class TemplateManager
11
+ Error = Class.new(Netstack::InputError)
12
+ TOKEN = /(<%=?[\s\S]*?%>)/
13
+ PATH = /\A[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*\z/i
14
+ EACH = /\A([a-z_][a-z0-9_.]*)\.each\s+do\s+\|([a-z_][a-z0-9_]*)\|\z/i
15
+ IF = /\Aif\s+([a-z_][a-z0-9_.]*)\z/i
16
+ MAX_SOURCE_BYTES = 1_048_576
17
+ MAX_OUTPUT_BYTES = 16_777_216
18
+ MAX_DEPTH = 32
19
+ MAX_RENDER_STEPS = 1_000_000
20
+
21
+ attr_reader :template_name, :data
22
+
23
+ def initialize(template_name, data, source: nil, store: TemplateStore.new)
24
+ name = template_name.to_s
25
+ @template_name = name.end_with?(".erb") ? name : "#{name}.erb"
26
+ @data = Values.copy(Support.object(data, name: "template data"))
27
+ @source = source
28
+ @store = store
29
+ end
30
+
31
+ def generate
32
+ nodes = self.class.parse(@source || @store.fetch(@template_name))
33
+ output = +""
34
+ render_nodes(nodes, @data, output, [MAX_RENDER_STEPS])
35
+ output
36
+ end
37
+
38
+ def inspect = "#<#{self.class.name} template=#{template_name.inspect}>"
39
+
40
+ def self.validate!(source, variables: nil)
41
+ nodes = parse(source)
42
+ validate_variables!(nodes, variables.map(&:to_s), []) if variables
43
+ nodes
44
+ end
45
+
46
+ def self.validate_variables!(nodes, variables, locals)
47
+ nodes.each do |type, *parts|
48
+ case type
49
+ when :value
50
+ validate_root!(parts[0], variables, locals)
51
+ when :each
52
+ collection, variable, children = parts
53
+ validate_root!(collection, variables, locals)
54
+ validate_variables!(children, variables, locals + [ variable ])
55
+ when :if
56
+ expression, truthy, falsy = parts
57
+ validate_root!(expression, variables, locals)
58
+ validate_variables!(truthy, variables, locals)
59
+ validate_variables!(falsy, variables, locals)
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.validate_root!(path, variables, locals)
65
+ root = path.split(".").first
66
+ raise Error, "unknown template variable: #{root}" unless variables.include?(root) || locals.include?(root)
67
+ end
68
+ private_class_method :validate_variables!, :validate_root!
69
+
70
+ def self.parse(source)
71
+ raise Error, "template must be UTF-8 text" unless source.is_a?(String) && source.valid_encoding?
72
+ raise Error, "template exceeds the byte limit" if source.bytesize > MAX_SOURCE_BYTES
73
+
74
+ root = []
75
+ frames = [ { type: :root, nodes: root } ]
76
+ source.split(TOKEN).reject(&:empty?).each do |token|
77
+ raise Error, "unclosed template delimiter" if token.start_with?("<%") && !token.end_with?("%>")
78
+
79
+ if token.start_with?("<%=")
80
+ expression = token.delete_prefix("<%=").delete_suffix("%>").strip
81
+ validate_path!(expression)
82
+ frames.last.fetch(:nodes) << [ :value, expression ]
83
+ elsif token.start_with?("<%")
84
+ parse_directive!(token.delete_prefix("<%").delete_suffix("%>").strip, frames)
85
+ else
86
+ raise Error, "invalid template delimiter" if token.include?("<%") || token.include?("%>")
87
+
88
+ frames.last.fetch(:nodes) << [ :text, token ]
89
+ end
90
+ raise Error, "template nesting exceeds the limit" if frames.size > MAX_DEPTH
91
+ end
92
+ raise Error, "template block is not closed" unless frames.one?
93
+ root
94
+ end
95
+
96
+ def self.parse_directive!(directive, frames)
97
+ if (match = EACH.match(directive))
98
+ node = [ :each, match[1], match[2], [] ]
99
+ frames.last.fetch(:nodes) << node
100
+ frames << { type: :each, nodes: node[3] }
101
+ elsif (match = IF.match(directive))
102
+ node = [ :if, match[1], [], [] ]
103
+ frames.last.fetch(:nodes) << node
104
+ frames << { type: :if, nodes: node[2], node: node }
105
+ elsif directive == "else"
106
+ frame = frames.last
107
+ raise Error, "else must belong to an if block" unless frame[:type] == :if && !frame[:else]
108
+ frame[:else] = true
109
+ frame[:nodes] = frame.fetch(:node)[3]
110
+ elsif directive == "end"
111
+ raise Error, "unexpected end" if frames.one?
112
+ frames.pop
113
+ else
114
+ raise Error, "unsupported template expression"
115
+ end
116
+ end
117
+
118
+ def self.validate_path!(path)
119
+ raise Error, "invalid template variable" unless PATH.match?(path)
120
+ end
121
+ private_class_method :parse_directive!, :validate_path!
122
+
123
+ private
124
+
125
+ def render_nodes(nodes, context, output, budget)
126
+ nodes.each do |type, *parts|
127
+ consume_work!(budget)
128
+ case type
129
+ when :text then append!(output, parts[0])
130
+ when :value
131
+ value = resolve(parts[0], context)
132
+ unless value.nil? || value.is_a?(String) || value.is_a?(Numeric) || [true, false].include?(value)
133
+ raise Error, "interpolation must resolve to a scalar"
134
+ end
135
+ append!(output, value.to_s)
136
+ when :each
137
+ collection, variable, children = parts
138
+ items = resolve(collection, context)
139
+ raise Error, "template collection must be an array" unless items.is_a?(Array)
140
+
141
+ items.each do |item|
142
+ consume_work!(budget)
143
+ render_nodes(children, context.merge(variable => item), output, budget)
144
+ end
145
+ when :if
146
+ expression, truthy, falsy = parts
147
+ render_nodes(resolve(expression, context) ? truthy : falsy, context, output, budget)
148
+ end
149
+ end
150
+ end
151
+
152
+ def consume_work!(budget)
153
+ budget[0] -= 1
154
+ raise Error, "template rendering exceeds the work limit" if budget[0].negative?
155
+ end
156
+
157
+ def append!(output, value)
158
+ raise Error, "rendered output exceeds the byte limit" if output.bytesize + value.bytesize > MAX_OUTPUT_BYTES
159
+
160
+ output << value
161
+ end
162
+
163
+ def resolve(path, context)
164
+ segments = path.split(".")
165
+ segments.drop(1).reduce(context.fetch(segments.first)) do |value, segment|
166
+ raise Error, "#{path} does not resolve to template data" unless value.is_a?(Hash)
167
+ value.fetch(segment)
168
+ end
169
+ rescue KeyError
170
+ raise Error, "missing template data: #{path}"
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require_relative "support"
5
+
6
+ module Netstack
7
+ module Configuration
8
+ # 不可变模板快照;基于数据库的模板版本管理由宿主负责。
9
+ class TemplateStore
10
+ Error = Class.new(Netstack::InputError)
11
+ ROOT = File.expand_path("../../../resources/configuration", __dir__).freeze
12
+
13
+ # 构造时显式读取随包文件;预加载该类不会读取模板。
14
+ # 覆盖模板经过复制,并按相同目录契约和安全语法校验。
15
+ def initialize(overrides: {})
16
+ supplied = Support.object(overrides, name: "template overrides")
17
+ raise Error, "unknown template override" unless (supplied.keys - Catalog.template_keys).empty?
18
+
19
+ @sources = Catalog.template_keys.to_h do |key|
20
+ source = supplied.fetch(key) { File.read(File.join(ROOT, key), encoding: Encoding::UTF_8) }
21
+ TemplateManager.validate!(source, variables: Catalog.variables_for_template(key))
22
+ [key, source.dup.freeze]
23
+ end.freeze
24
+ freeze
25
+ end
26
+
27
+ def fetch(key)
28
+ @sources.fetch(key.to_s) { raise Error, "unknown configuration template" }
29
+ end
30
+
31
+ def metadata(key)
32
+ source = fetch(key)
33
+ Values.copy({ key: key.to_s, sha256: Digest::SHA256.hexdigest(source), bytes: source.bytesize,
34
+ variables: Catalog.variables_for_template(key) })
35
+ end
36
+
37
+ def inspect = "#<#{self.class.name} count=#{@sources.size}>"
38
+ end
39
+ end
40
+ end