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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "digest"
6
+
7
+ module Netstack
8
+ module Connector
9
+ Backup = Data.define(:path, :bytes, :sha256, :collected_at, :change, :previous_sha256) do
10
+ # 保存配置备份的路径、摘要和变更状态。
11
+ def initialize(path:, bytes:, sha256:, collected_at:, change: nil, previous_sha256: nil)
12
+ super
13
+ end
14
+
15
+ # 判断本次采集是否产生新的配置内容。
16
+ def changed? = [:created, :changed].include?(change)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "digest"
6
+
7
+ module Netstack
8
+ module Connector
9
+ class BackupBusy < Error; end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "digest"
6
+
7
+ module Netstack
8
+ module Connector
9
+ # 只携带库自身的提交回执和配置元数据,不携带正文或原始文件系统异常。
10
+ class BackupPersistenceError < Error
11
+ attr_reader :backup, :receipt, :underlying_type
12
+
13
+ def initialize(backup:, write_error:, host: nil)
14
+ unless backup.instance_of?(Backup) && Storage::PrivateFile.receipt_error?(write_error) &&
15
+ write_error.receipt.committed? && write_error.receipt.path == backup.path
16
+ raise ArgumentError, "backup persistence error requires a matching committed backup receipt"
17
+ end
18
+
19
+ @backup, @receipt = backup, write_error.receipt
20
+ @underlying_type = write_error.underlying_type
21
+ code = if write_error.instance_of?(Storage::PrivateFile::DirectorySyncUnsupported)
22
+ :backup_durability_unsupported
23
+ elsif receipt.durable?
24
+ :backup_finalize_failed
25
+ else
26
+ :backup_persistence_unconfirmed
27
+ end
28
+ super(write_error.message, code: code, host: host, phase: :backup)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "expect/pty"
6
+
7
+ module Netstack
8
+ module Connector
9
+ # 一条设备命令行,可附带独立截止时间和交互规则。
10
+ class Command
11
+ attr_reader :text, :timeout, :interactions, :prompt, :source, :line
12
+
13
+ # 在任何输入输出前校验命令、提示、交互和敏感标记,并冻结命令。
14
+ def initialize(text, timeout: nil, interactions: [], prompt: nil, sensitive: false, output_sensitive: false,
15
+ source: nil, line: nil)
16
+ unless text.is_a?(String) && !text.strip.empty? && !text.match?(/[\r\n\x00]/)
17
+ raise ScriptError.new("a command must contain exactly one nonempty CLI line", phase: :parse,
18
+ source: source, line: line)
19
+ end
20
+ unless interactions.is_a?(Array) && interactions.all?(Interaction)
21
+ raise ArgumentError, "interactions must be an Array of Interaction objects"
22
+ end
23
+ valid_prompt = prompt.nil? || (prompt.is_a?(Regexp) && !prompt.match?(""))
24
+ unless valid_prompt
25
+ raise ArgumentError, "prompt must be a nonempty Regexp or nil"
26
+ end
27
+ raise ArgumentError, "sensitive must be true or false" unless [true, false].include?(sensitive)
28
+ raise ArgumentError, "output_sensitive must be true or false" unless [true, false].include?(output_sensitive)
29
+
30
+ @text = text.dup.freeze
31
+ @timeout = Expect.duration(timeout)
32
+ @interactions = interactions.dup.freeze
33
+ @prompt = prompt
34
+ @sensitive = sensitive
35
+ @output_sensitive = output_sensitive
36
+ @source = source&.dup&.freeze
37
+ @line = line
38
+ freeze
39
+ end
40
+
41
+ # 判断命令文本是否需要脱敏。
42
+ def sensitive? = @sensitive
43
+
44
+ # 响应正文可能包含尚未登记的秘密,与命令文本是否敏感无关。
45
+ def output_sensitive? = @output_sensitive
46
+
47
+ # 复制命令元数据,只替换命令文本。
48
+ def with_text(text)
49
+ self.class.new(text, timeout: timeout, interactions: interactions, prompt: prompt,
50
+ sensitive: sensitive?, output_sensitive: output_sensitive?, source: source, line: line)
51
+ end
52
+
53
+ # 配置采集保留调用方的全部命令元数据,只收紧输出的诊断边界。
54
+ def with_output_sensitive
55
+ self.class.new(text, timeout: timeout, interactions: interactions, prompt: prompt,
56
+ sensitive: sensitive?, output_sensitive: true, source: source, line: line)
57
+ end
58
+
59
+ # 返回命令来源摘要,不展开命令内容。
60
+ def inspect = "#<#{self.class} source=#{source.inspect} line=#{line.inspect}>"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ module Netstack
6
+ module Connector
7
+ # 一条已完成命令的结果;后续命令失败时仍保留已完成步骤。
8
+ class CommandResult
9
+ attr_reader :command, :output, :prompt, :duration
10
+
11
+ # 保存命令输出、提示符和耗时,并冻结结果。
12
+ def initialize(command:, output:, prompt:, duration:)
13
+ @command = command
14
+ @output = output.frozen? ? output : output.dup.freeze
15
+ @prompt = prompt&.dup&.freeze
16
+ @duration = duration
17
+ freeze
18
+ end
19
+
20
+ # 返回输出字节数和耗时摘要。
21
+ def inspect = "#<#{self.class} bytes=#{output.bytesize} duration=#{duration.round(6)}>"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "expect/redactor"
6
+
7
+ module Netstack
8
+ module Connector
9
+ class CommandTimeout < Error; end
10
+ end
11
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "expect/pty"
6
+
7
+ module Netstack
8
+ module Connector
9
+ # 显式且不可变的连接设置;环境变量读取由调用库的可执行程序负责。
10
+ class Configuration
11
+ attr_reader :host, :username, :password, :enable_password, :protocol, :port, :login_timeout,
12
+ :command_timeout, :write_timeout, :max_output_bytes, :max_script_output_bytes, :log_file, :log_format, :log_level,
13
+ :known_hosts, :host_key_policy, :challenges, :logger, :on_event
14
+
15
+ # 校验端点、凭据、超时、日志、主机密钥和挑战配置,然后冻结设置。
16
+ def initialize(host: nil, username: nil, password: nil, enable_password: nil, protocol: :ssh, port: nil,
17
+ login_timeout: 10, command_timeout: nil, write_timeout: 10,
18
+ max_output_bytes: 32 * 1024 * 1024, max_script_output_bytes: nil, log_file: nil, logger: nil,
19
+ log_format: :text, log_level: :info, on_event: nil,
20
+ known_hosts: nil, host_key_policy: :strict, telnet_fallback: false, legacy_ssh: false,
21
+ challenges: [])
22
+ @host = frozen_string(host)
23
+ @username = frozen_string(username)
24
+ @password = single_line_credential(password)
25
+ @enable_password = single_line_credential(enable_password)
26
+ @protocol = enum_value(protocol, %i[ssh telnet], :protocol)
27
+ @port = port
28
+ valid_port = port.nil? || (port.is_a?(Integer) && (1..65_535).cover?(port))
29
+ unless valid_port
30
+ raise ArgumentError, "port must be in 1..65535"
31
+ end
32
+
33
+ @login_timeout = timeout_seconds(login_timeout, :login_timeout)
34
+ @command_timeout = command_timeout.nil? ? nil : timeout_seconds(command_timeout, :command_timeout)
35
+ @write_timeout = timeout_seconds(write_timeout, :write_timeout)
36
+ @max_output_bytes = positive_integer(max_output_bytes, :max_output_bytes)
37
+ @max_script_output_bytes = positive_integer(max_script_output_bytes, :max_script_output_bytes) unless max_script_output_bytes.nil?
38
+ @log_file = absolute_path(log_file)
39
+ if logger && (!logger.respond_to?(:debug) || !logger.respond_to?(:info) ||
40
+ !logger.respond_to?(:warn) || !logger.respond_to?(:error) ||
41
+ !logger.respond_to?(:level))
42
+ raise ArgumentError, "logger must be Logger-compatible"
43
+ end
44
+ raise ArgumentError, "logger and log_file cannot be combined" if logger && @log_file
45
+ @log_format = enum_value(log_format, %i[raw text], :log_format)
46
+ raise ArgumentError, "logger cannot be combined with raw logging" if logger && @log_format == :raw
47
+
48
+ @logger = logger
49
+ raise ArgumentError, "on_event must respond to call" if on_event && !on_event.respond_to?(:call)
50
+
51
+ @on_event = on_event
52
+ @log_level = enum_value(log_level, %i[debug info warn error], :log_level)
53
+ @known_hosts = absolute_path(known_hosts)
54
+ @host_key_policy = enum_value(host_key_policy, %i[strict accept_new replace], :host_key_policy)
55
+ if @host_key_policy == :replace && !@known_hosts
56
+ raise ArgumentError, "host_key_policy :replace requires an explicit known_hosts file"
57
+ end
58
+
59
+ @telnet_fallback = boolean_value(telnet_fallback)
60
+ @legacy_ssh = boolean_value(legacy_ssh)
61
+ unless challenges.is_a?(Array) && challenges.all?(Interaction)
62
+ raise ArgumentError, "challenges must be an Array of Interaction objects"
63
+ end
64
+
65
+ @challenges = challenges.dup.freeze
66
+ freeze
67
+ end
68
+
69
+ # 判断是否允许 SSH 失败后回退到 Telnet。
70
+ def telnet_fallback? = @telnet_fallback
71
+
72
+ # 判断是否允许使用旧版 SSH 协商参数恢复。
73
+ def legacy_ssh? = @legacy_ssh
74
+
75
+ # 校验最终连接主机和用户名能安全进入外部命令参数。
76
+ def validate_endpoint!
77
+ unless host.is_a?(String) && /\A[A-Za-z0-9:][A-Za-z0-9.:%_-]*\z/.match?(host)
78
+ raise ArgumentError, "host must be a device address or hostname"
79
+ end
80
+ return if username.is_a?(String) && /\A[A-Za-z0-9_][A-Za-z0-9_.@$\\-]*\z/.match?(username)
81
+
82
+ raise ArgumentError, "username is empty or contains unsupported characters"
83
+ end
84
+
85
+ # 返回不包含凭据的连接配置摘要。
86
+ def inspect = "#<#{self.class} host=#{host.inspect} protocol=#{protocol.inspect}>"
87
+
88
+ private
89
+
90
+ def positive_integer(value, name)
91
+ raise ArgumentError, "#{name} must be a positive Integer" unless value.is_a?(Integer) && value.positive?
92
+
93
+ value
94
+ end
95
+
96
+ # 将可选值校验为字符串副本。
97
+ def frozen_string(value)
98
+ return nil if value.nil?
99
+ raise ArgumentError, "expected a String or nil" unless value.is_a?(String)
100
+
101
+ value.dup.freeze
102
+ end
103
+
104
+ # 校验凭据是单行文本。
105
+ def single_line_credential(value)
106
+ value = frozen_string(value)
107
+ raise ArgumentError, "credentials must be a single line" if value&.match?(/[\r\n\x00]/)
108
+
109
+ value
110
+ end
111
+
112
+ # 将路径转换为绝对冻结文本。
113
+ def absolute_path(value) = value.nil? ? nil : File.expand_path(frozen_string(value)).freeze
114
+
115
+ # 将超时转换为有限秒数。
116
+ def timeout_seconds(value, name)
117
+ result = Expect.duration(value)
118
+ raise ArgumentError, "#{name} must be finite" unless result
119
+
120
+ result
121
+ end
122
+
123
+ # 校验布尔设置只能是 true 或 false。
124
+ def boolean_value(value)
125
+ raise ArgumentError, "expected true or false" unless [true, false].include?(value)
126
+
127
+ value
128
+ end
129
+
130
+ # 将枚举输入转换为允许的符号。
131
+ def enum_value(value, choices, name)
132
+ return value.to_sym if value.respond_to?(:to_sym) && choices.include?(value.to_sym)
133
+
134
+ raise ArgumentError, "#{name} must be one of #{choices.join(", ")}"
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "expect/redactor"
6
+
7
+ module Netstack
8
+ module Connector
9
+ class ConnectionClosed < ConnectionError; end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "expect/redactor"
6
+
7
+ module Netstack
8
+ module Connector
9
+ class ConnectionError < Error; end
10
+ end
11
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "forwardable"
6
+
7
+ module Netstack
8
+ module Connector
9
+ # 设备连接门面;厂商子类提供语法和钩子,组合对象负责实际执行。
10
+ class Device
11
+ extend Forwardable
12
+ include RunningConfig::Capability
13
+ include SaveConfig
14
+ include LocalBackup::Capability
15
+ include Tftp::Capability
16
+ include Topology::Capability
17
+ include TextFSM::Capability
18
+
19
+ attr_reader :configuration, :command_timeout
20
+
21
+ def_delegators :@session, :connected?, :privileged?, :state
22
+ def_delegators :@configuration, :host, :username, :log_file
23
+
24
+ class << self
25
+ def vendor(key = nil)
26
+ return @vendor = key if key
27
+
28
+ @vendor || (superclass.vendor if superclass.respond_to?(:vendor))
29
+ end
30
+
31
+ # 在子类中声明设备规则;没有块时返回当前类继承到的档案。
32
+ def profile(&definition)
33
+ return inherited_profile unless definition
34
+
35
+ @profile = Profile.define(parent: inherited_profile, &definition)
36
+ end
37
+
38
+ # 让子类共享不可变父档案,子类声明时再生成自己的副本。
39
+ def inherited(subclass)
40
+ super
41
+ subclass.instance_variable_set(:@profile, profile)
42
+ end
43
+
44
+ private
45
+
46
+ # 查找当前类或最近父类的设备档案。
47
+ def inherited_profile
48
+ return @profile if instance_variable_defined?(:@profile)
49
+
50
+ parent = superclass
51
+ parent.respond_to?(:profile) ? parent.profile : nil
52
+ end
53
+ end
54
+
55
+ # 连接设备并执行代码块;无论正常返回、break、异常还是中断,都会释放会话资源。
56
+ def self.open(**)
57
+ raise ArgumentError, "open requires a block" unless block_given?
58
+
59
+ device = new(**)
60
+ failed = false
61
+ begin
62
+ device.connect
63
+ yield device
64
+ rescue Exception # rubocop:disable Lint/RescueException -- Preserve failures from this block, including interrupts, through cleanup.
65
+ failed = true
66
+ raise
67
+ ensure
68
+ begin
69
+ device.close
70
+ rescue StandardError
71
+ raise unless failed
72
+ end
73
+ end
74
+ end
75
+
76
+ # 校验连接配置,创建对话语法、传输层、恢复器和会话对象。
77
+ def initialize(configuration: nil, transport: nil, **settings)
78
+ if configuration
79
+ raise ArgumentError, "configuration cannot be combined with settings" unless settings.empty?
80
+ raise ArgumentError, "configuration must be a Configuration" unless configuration.is_a?(Configuration)
81
+ end
82
+ @configuration = configuration || Configuration.new(**settings)
83
+ @command_timeout = @configuration.command_timeout || default_command_timeout
84
+ @dialogue = build_dialogue
85
+ transport ||= Transports.build(@configuration, terminal_size: terminal_size)
86
+ @session = Session.new(configuration: @configuration, dialogue: @dialogue, transport: transport,
87
+ recovery: Recovery.new(@configuration, legacy_arguments: legacy_ssh_arguments),
88
+ after_login: method(:after_login))
89
+ end
90
+
91
+ # 返回当前连接器使用的不可变设备档案,便于检查厂商声明。
92
+ def profile = self.class.profile || Profile.default
93
+
94
+ def vendor = self.class.vendor
95
+
96
+ # 只查询实现能力,不连接设备;不代表现场权限或固件验证成功。
97
+ def supports?(capability)
98
+ capability = capability.to_sym if capability.is_a?(String)
99
+ case capability
100
+ when :running_config, :backup
101
+ !config_commands.empty?
102
+ when :save_config
103
+ !save_commands.empty?
104
+ when :tftp_backup
105
+ !profile.tftp_strategy.nil?
106
+ when :neighbors, :interface_descriptions, :interface_description_changes
107
+ !!profile.topology_strategy&.supports?(capability)
108
+ else
109
+ false
110
+ end
111
+ rescue NotImplementedError
112
+ false
113
+ end
114
+
115
+ # 建立会话并返回当前设备对象。
116
+ def connect
117
+ @session.connect
118
+ self
119
+ end
120
+
121
+ # 关闭会话及其传输资源。
122
+ def close = @session.close
123
+
124
+ # 将一条文本命令包装成脚本并执行。
125
+ def execute_command(text, **, &)
126
+ execute_script(Script.new([Command.new(text, **)]), &)
127
+ end
128
+
129
+ # 接收脚本或命令数组,统一交给单次会话执行。
130
+ def execute_script(script, &)
131
+ script = Script.new(script) unless script.is_a?(Script)
132
+ perform_script(script, &)
133
+ end
134
+
135
+ # 多步骤业务操作独占当前会话,内部脚本仍禁止回调重入。
136
+ def with_operation(name, &) = @session.with_operation(name, &)
137
+
138
+ # 业务层可扩展脚本准备、响应校验和最终结果,所有钩子均在会话锁内执行。
139
+ def execute_operation(script, name:, prompt: nil, after_command: nil, privilege: true, &finalize)
140
+ perform_script(script, operation: name, prompt: prompt, after_command: after_command,
141
+ privilege: privilege, finalize: finalize)
142
+ end
143
+
144
+ # 返回已确认的提示,供业务层为只读采集绑定准确的结束条件。
145
+ def current_prompt = @session.prompt
146
+
147
+ # 将业务事件写入当前设备会话日志。
148
+ def log_event(name, **details)
149
+ @session.log_event(name, **details)
150
+ end
151
+
152
+ # 进入特权模式,并把当前提示符记录到会话。
153
+ def enable
154
+ @session.perform(:enable) { @session.enable(enable_command, enable_prompt) }
155
+ self
156
+ end
157
+
158
+ # 将会话交给人工交互;交互结束后会话按终端语义关闭。
159
+ def interact(input: $stdin, output: $stdout, escape: "\x1d".b, timeout: nil)
160
+ @session.interact(input: input, output: output, escape: escape, timeout: timeout)
161
+ end
162
+
163
+ def inspect = "#<#{self.class} host=#{host.inspect} state=#{state}>"
164
+
165
+ protected
166
+
167
+ def pager_pattern = profile.pager_pattern
168
+
169
+ def pager_response = profile.pager_response
170
+
171
+ def password_prompt = profile.password_prompt
172
+
173
+ def username_prompt = profile.username_prompt
174
+
175
+ def authentication_error_patterns = profile.authentication_error_patterns
176
+
177
+ def command_error_patterns = profile.command_error_patterns
178
+
179
+ # 返回命令期间需要自动应答的确认对话。
180
+ def confirmation_interactions = profile.confirmation_interactions
181
+
182
+ # 返回登录期间需要自动应答的附加对话。
183
+ def login_interactions = profile.login_interactions
184
+
185
+ def legacy_ssh_arguments = profile.legacy_ssh_arguments
186
+
187
+ def enable_command = profile.privilege_command
188
+
189
+ def enable_prompt = profile.privilege_prompt
190
+
191
+ def default_command_timeout = profile.command_timeout
192
+
193
+ def terminal_size = profile.terminal_size
194
+
195
+ # 返回登录完成提示;厂商必须实现。
196
+ def login_prompt
197
+ prompt = profile.login_prompt
198
+ return prompt if prompt
199
+
200
+ raise NotImplementedError, "#{self.class} must define login_prompt"
201
+ end
202
+
203
+ # 返回普通命令提示;厂商必须实现。
204
+ def command_prompt
205
+ prompt = profile.command_prompt
206
+ return prompt if prompt
207
+
208
+ raise NotImplementedError, "#{self.class} must define command_prompt"
209
+ end
210
+
211
+ # 登录成功后执行厂商初始化钩子。
212
+ def after_login(_session, _response) end
213
+
214
+ # 脚本开始前执行厂商批处理准备钩子。
215
+ def before_batch(execution)
216
+ return unless enable_command && execution.context.fetch(:privilege, true)
217
+
218
+ execution.enable(enable_command, enable_prompt)
219
+ end
220
+
221
+ # 调整单条命令或返回 nil 跳过该命令。
222
+ def prepare_command(command, _execution) = command
223
+
224
+ # 单条命令完成后执行厂商后处理钩子。
225
+ def after_command(_command, _response, _execution) end
226
+
227
+ private
228
+
229
+ # 将厂商提示、失败模式和对话钩子组装成不可变对话语法。
230
+ def build_dialogue
231
+ Dialogue.new(
232
+ login_prompt: login_prompt, command_prompt: command_prompt,
233
+ password_prompt: password_prompt, username_prompt: username_prompt, enable_prompt: enable_prompt,
234
+ authentication_errors: authentication_error_patterns, command_errors: command_error_patterns,
235
+ login_interactions: login_interactions,
236
+ command_interactions: [Interaction.new(pager_pattern, pager_response, capture: false),
237
+ *confirmation_interactions]
238
+ )
239
+ end
240
+
241
+ # 在会话锁内运行脚本及业务回调,保留已完成步骤和统一错误边界。
242
+ def perform_script(script, operation: nil, prompt: nil, after_command: nil, privilege: true,
243
+ finalize: nil, &on_step)
244
+ return Result.new if script.empty?
245
+
246
+ execution = build_execution(operation: operation, prompt: prompt, after_command: after_command, privilege: privilege)
247
+ output_sensitive = script.any?(&:output_sensitive?)
248
+ @session.perform(:script) do
249
+ @session.log_script(operation: operation, steps: execution.steps) do
250
+ @session.with_sensitive_output(output_sensitive) { before_batch(execution) }
251
+ result = execution.execute_script(script, &on_step)
252
+ sensitive_result = output_sensitive || script.any?(&:sensitive?) || execution.sensitive?
253
+ @session.with_sensitive_output(sensitive_result) do
254
+ finalize_script_result(result, finalize)
255
+ end
256
+ end
257
+ end
258
+ rescue Error => error
259
+ Result.new(steps: execution ? execution.steps : [], error: error)
260
+ end
261
+
262
+ def build_execution(operation:, prompt:, after_command:, privilege:)
263
+ finish_step = lambda do |command, response, context|
264
+ self.after_command(command, response, context)
265
+ after_command&.call(command, response, context)
266
+ end
267
+ execution = Execution.new(session: @session, timeout: command_timeout,
268
+ prepare: method(:prepare_command), after_command: finish_step, prompt: prompt)
269
+ execution.context[:operation] = operation if operation
270
+ execution.context[:privilege] = privilege
271
+ execution
272
+ end
273
+
274
+ # 回调也可直接返回失败;与抛错共用脱敏边界,保留已完成步骤及业务配置。
275
+ def finalize_script_result(result, finalize)
276
+ result = finalize ? finalize.call(result) : result
277
+ return result unless result.is_a?(Result) && result.failure?
278
+
279
+ Result.new(steps: result.steps, config: result.config,
280
+ error: @session.normalize_error(result.error, phase: :script))
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "expect/redactor"
6
+
7
+ module Netstack
8
+ module Connector
9
+ class DeviceError < Error; end
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "expect/pty"
6
+
7
+ module Netstack
8
+ module Connector
9
+ # 每台设备一份不可变对话语法,由厂商方法组装而不是合并默认值。
10
+ class Dialogue
11
+ attr_reader :login_prompt, :command_prompt, :password_prompt, :username_prompt, :enable_prompt,
12
+ :authentication_errors, :command_errors, :login_interactions, :command_interactions
13
+
14
+ # 校验提示和交互列表,并冻结整套设备对话语法。
15
+ def initialize(login_prompt:, command_prompt:, password_prompt:, username_prompt:, enable_prompt:,
16
+ authentication_errors:, command_errors:, login_interactions:, command_interactions:)
17
+ @login_prompt = login_prompt
18
+ @command_prompt = command_prompt
19
+ @password_prompt = password_prompt
20
+ @username_prompt = username_prompt
21
+ @enable_prompt = enable_prompt
22
+ [login_prompt, command_prompt, password_prompt, username_prompt, enable_prompt].each do |pattern|
23
+ raise ArgumentError, "prompt must be a nonempty Regexp" unless pattern.is_a?(Regexp) && !pattern.match?("")
24
+ end
25
+ @authentication_errors = authentication_errors.freeze
26
+ @command_errors = command_errors.freeze
27
+ @login_interactions = login_interactions.freeze
28
+ @command_interactions = command_interactions.freeze
29
+ freeze
30
+ end
31
+
32
+ # 从命令输出中返回包含错误模式的诊断行,而不是光标或此前全部输出。
33
+ def diagnostic(output)
34
+ return if command_errors.empty?
35
+
36
+ # 先保留被回车覆写的失败,再识别颜色或退格处理后的可见诊断。
37
+ raw = output.gsub(/\r(?!\n)/, "\n")
38
+ diagnostic_line(raw) || (diagnostic_line(TerminalRenderer.render(output)) if output.match?(/[\e\b]/))
39
+ end
40
+
41
+ private
42
+
43
+ # 只截取命中错误模式的当前行,避免把整段设备配置写进诊断信息。
44
+ def diagnostic_line(output)
45
+ match = command_errors.lazy.filter_map { |pattern| pattern.match(output) }.first
46
+ return unless match
47
+
48
+ start = match.begin(0).positive? ? output.rindex(/[\r\n]/, match.begin(0) - 1) : nil
49
+ start = start ? start + 1 : 0
50
+ finish = output.index(/[\r\n]/, match.end(0)) || output.bytesize
51
+ output.byteslice(start, finish - start).strip
52
+ end
53
+ end
54
+ end
55
+ end