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,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "open3"
6
+ require "expect/pty"
7
+
8
+ module Netstack
9
+ module Connector
10
+ # 可替换的字节传输层;厂商语法属于设备边界,不属于此层。
11
+ module Transports
12
+ # Expect 适配器拥有伪终端和进程,在限制未匹配尾部时不丢弃数据。
13
+ class Pty
14
+ # 即使读到完整行也保留 32 KiB,因为提示符可能跨越多次读取和多行。
15
+ STREAM = /\A[\s\S]{32768}(?=[\s\S]{32768})/n
16
+ attr_reader :configuration
17
+
18
+ # 保存配置和可替换的信道工厂,便于测试和设备差异注入。
19
+ def initialize(configuration, channel_factory: nil, terminal_size: nil)
20
+ @configuration = configuration
21
+ @channel_factory = channel_factory
22
+ @terminal_size = terminal_size
23
+ end
24
+
25
+ # 打开伪终端、设置终端大小并启动设备进程。
26
+ def open
27
+ configuration.validate_endpoint!
28
+ @channel = if @channel_factory
29
+ @channel_factory.call
30
+ else
31
+ Expect.new(raw_pty: true, reset_timeout_on_read: false, buffer_limit: nil,
32
+ preserve_buffer: false, log_stdout: false, log_listeners: false,
33
+ debug_level: 0, write_timeout: configuration.write_timeout)
34
+ end
35
+ # Profile 使用宽、高;PTY 使用行数、列数。
36
+ @channel.slave.winsize = @terminal_size.reverse if @terminal_size
37
+ @channel.log_output = method(:write_log_output)
38
+ @channel.spawn(*argv)
39
+ self
40
+ rescue Exception # rubocop:disable Lint/RescueException -- A half-open PTY must be released on interrupts.
41
+ begin
42
+ close
43
+ rescue
44
+ # 尝试释放半打开信道后,仍然抛出原始失败。
45
+ end
46
+ raise
47
+ end
48
+
49
+ # 从信道读取下一事件,并把字符串字段转换为字节字符串。
50
+ def read(patterns, timeout:)
51
+ # 0.5.3 起 expect 直接返回 Result;旧版 expect 只返回模式序号。
52
+ result = if @channel.respond_to?(:expect_result)
53
+ @channel.expect_result(*patterns, STREAM, timeout: timeout)
54
+ else
55
+ @channel.expect(*patterns, STREAM, timeout: timeout)
56
+ end
57
+ Event.new(index: result.number && (result.number - 1), before: result.before.to_s.b,
58
+ match: result.match.to_s.b, error: result.error)
59
+ end
60
+
61
+ # 临时设置写入超时,完成写入后恢复原设置。
62
+ def write(bytes, timeout: configuration.write_timeout)
63
+ previous = @channel.write_timeout
64
+ @channel.write_timeout = timeout
65
+ @channel.write(bytes)
66
+ ensure
67
+ @channel.write_timeout = previous if @channel
68
+ end
69
+
70
+ # 判断信道尚未建立或已经关闭。
71
+ def closed? = !@channel || @channel.closed?
72
+
73
+ # 设置日志输出目标;nil 表示暂时停止记录。
74
+ def log_output=(target)
75
+ @log_target = target
76
+ end
77
+
78
+ # 清除信道和日志目标,并强制关闭底层进程。
79
+ def close
80
+ channel = @channel
81
+ @channel = nil
82
+ @log_target = nil
83
+ channel&.hard_close
84
+ end
85
+
86
+ # 把人工交互结果转换为设备 EOF、超时或输入状态。
87
+ def interact(**)
88
+ stopped = @channel.interact(**)
89
+ return :device_eof if stopped.equal?(@channel)
90
+
91
+ stopped.nil? ? :timeout : :input
92
+ end
93
+
94
+ # 返回传输主机和关闭状态摘要。
95
+ def inspect = "#<#{self.class} host=#{configuration.host.inspect} closed=#{closed?}>"
96
+
97
+ private
98
+
99
+ # 检查缓冲上限并把设备输出写入当前日志目标。
100
+ def write_log_output(bytes)
101
+ if @channel.buffer.bytesize > configuration.max_output_bytes
102
+ raise OutputLimitExceeded.new("transport buffer exceeded max_output_bytes",
103
+ phase: :read, output: @channel.buffer.dup), cause: nil
104
+ end
105
+
106
+ @log_target&.write(bytes)
107
+ @log_target.flush if @log_target.respond_to?(:flush)
108
+ end
109
+ end
110
+
111
+ # 生成 OpenSSH 参数,并处理显式请求的旧版协商和主机密钥操作。
112
+ class Ssh < Pty
113
+ attr_reader :legacy_arguments
114
+
115
+ # 保存旧版 SSH 参数并复用伪终端初始化。
116
+ def initialize(configuration, legacy_arguments: [], **)
117
+ super(configuration, **)
118
+ @legacy_arguments = legacy_arguments.dup.freeze
119
+ end
120
+
121
+ def open
122
+ configuration.validate_endpoint!
123
+ unless configuration.host_key_policy == :strict
124
+ @known_hosts = KnownHosts.new(configuration, replace: @replace_host_key)
125
+ end
126
+ super
127
+ rescue Exception # rubocop:disable Lint/RescueException -- Release a staged trust file on failed startup.
128
+ close
129
+ raise
130
+ end
131
+
132
+ def authenticated
133
+ @known_hosts&.commit
134
+ @replace_host_key = false
135
+ end
136
+
137
+ def close
138
+ super
139
+ ensure
140
+ @known_hosts&.close
141
+ @known_hosts = nil
142
+ end
143
+
144
+ # 返回 SSH 协议标识。
145
+ def protocol = :ssh
146
+
147
+ # 根据配置生成经过端点校验的 SSH 参数数组。
148
+ def argv
149
+ configuration.validate_endpoint!
150
+ checking = (configuration.host_key_policy == :strict) ? "yes" : "accept-new"
151
+ arguments = ["ssh", *legacy_arguments, "-tt", "-o", "StrictHostKeyChecking=#{checking}",
152
+ "-o", "NumberOfPasswordPrompts=1", "-o", "UpdateHostKeys=no",
153
+ "-o", "ConnectTimeout=#{[configuration.login_timeout.ceil, 1].max}"]
154
+ known_hosts = @known_hosts&.path || configuration.known_hosts
155
+ arguments += ["-o", "UserKnownHostsFile=#{known_hosts}"] if known_hosts
156
+ arguments += ["-p", configuration.port.to_s] if configuration.port
157
+ arguments + ["-l", configuration.username, configuration.host]
158
+ end
159
+
160
+ # 创建带旧版协商参数的同类传输对象。
161
+ def with_legacy(arguments)
162
+ self.class.new(configuration, legacy_arguments: arguments, channel_factory: @channel_factory,
163
+ terminal_size: @terminal_size)
164
+ end
165
+
166
+ # 创建不继承 SSH 专用端口的 Telnet 传输对象。
167
+ def as_telnet
168
+ Telnet.new(configuration, channel_factory: @channel_factory, terminal_size: @terminal_size)
169
+ end
170
+
171
+ # 恢复只标记下一次会话副本;共享信任文件在认证成功后才更新。
172
+ def replace_host_key
173
+ raise IOError, "host key replacement is not enabled" unless configuration.host_key_policy == :replace
174
+
175
+ @replace_host_key = true
176
+ end
177
+
178
+ end
179
+
180
+ # Telnet 只能显式启用;SSH 专用端口不能带入回退连接。
181
+ class Telnet < Pty
182
+ # 返回 Telnet 协议标识。
183
+ def protocol = :telnet
184
+
185
+ # 根据配置生成经过端点校验的 Telnet 参数数组。
186
+ def argv
187
+ configuration.validate_endpoint!
188
+ port = (configuration.protocol == :telnet) ? configuration.port : nil
189
+ ["telnet", "-l", configuration.username, configuration.host, *(port ? [port.to_s] : [])]
190
+ end
191
+ end
192
+
193
+ TYPES = { ssh: Ssh, telnet: Telnet }.freeze
194
+
195
+ # 按协议选择具体传输实现。
196
+ def self.build(configuration, **)
197
+ TYPES.fetch(configuration.protocol).new(configuration, **)
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,25 @@
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
+ # 安全保存底层异常,不保留包含凭据的原始消息。
10
+ class UnderlyingError
11
+ attr_reader :type, :message, :backtrace
12
+
13
+ # 保存脱敏后的异常类型、消息和调用栈。
14
+ def initialize(error, redactor, sensitive: false)
15
+ @type = error.is_a?(UnderlyingError) ? error.type : error.class.name
16
+ @message = (sensitive ? "[REDACTED]" : redactor.call(error.message)).freeze
17
+ @backtrace = (sensitive ? [] : Array(error.backtrace).map { |line| redactor.call(line).freeze }).freeze
18
+ freeze
19
+ end
20
+
21
+ # 返回底层异常类型摘要。
22
+ def inspect = "#<#{self.class} type=#{type}>"
23
+ end
24
+ end
25
+ 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 UnsupportedOperation < Error; end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
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
+ module Vendors
8
+ class CiscoIos
9
+ class RunningConfig < Netstack::Connector::RunningConfig::Rendered
10
+ # 去掉终端长度设置、保存命令回显和易变化的配置时间注释。
11
+ def clean(text)
12
+ config = super
13
+ config.gsub!(/^[^\n]*#\s*terminal\s+length\s+0\s*\n/i, "")
14
+ config.sub!(/^[^\n]*#\s*copy\s+run(?:ning-config)?\s+(?:start|startup-config)\s*\n.*\z/im, "")
15
+ config.gsub!(/^!.*(?:Last\s+configuration\s+change|NVRAM\s+config).*\n/i, "")
16
+ config
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
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
+ module Vendors
8
+ class CiscoIos
9
+ class TftpBackup < Tftp::Strategy
10
+ # 在连接前拒绝当前厂商不支持的参数组合。
11
+ def validate_options!(_target, source_file:, vrf: nil)
12
+ raise ArgumentError, "Cisco IOS does not use source_file" unless source_file.nil?
13
+ raise ArgumentError, "Cisco IOS TFTP backup does not use vrf" unless vrf.nil?
14
+ end
15
+
16
+ def receipt_metadata(_target, **) = { configuration_kind: :running, format: :cfg }
17
+
18
+ # 构造当前厂商的 TFTP 导出交互脚本。
19
+ def script(target, source_file:, vrf: nil)
20
+ validate_options!(target, source_file: source_file, vrf: vrf)
21
+
22
+ Script.new([Command.new("copy running-config tftp:", timeout: 180,
23
+ interactions: [
24
+ interaction(/Address\s+or\s+name\s+of\s+remote\s+host\s*\[[^\]]*\]\?\s*\z/i,
25
+ "#{target.host}\n"),
26
+ interaction(/Destination\s+filename\s*\[[^\]]*\]\?\s*\z/i,
27
+ "#{target.path}\n")
28
+ ])])
29
+ end
30
+
31
+ # 检查设备回显是否确认 TFTP 传输完成。
32
+ def device_reported_complete?(result)
33
+ completion_lines(result).any? do |line|
34
+ copied_bytes?(line) || line.match?(/\A\[OK(?:\s*-\s*[1-9]\d*\s+bytes)?\]\z/i)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
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
+ module Vendors
8
+ class CiscoIos
9
+ class Topology < Netstack::Connector::Topology::ImmediateStrategy
10
+ # IOS 提供 CDP 邻居、配置描述读取和描述变更。
11
+ def self.supports?(capability)
12
+ %i[neighbors interface_descriptions interface_description_changes].include?(capability)
13
+ end
14
+
15
+ # 使用详细 CDP 输出获取对端设备与端口。
16
+ def neighbor_command = "show cdp neighbors detail"
17
+
18
+ # 从运行配置解析接口描述。
19
+ def description_template = "cisco_ios_running_config_interfaces.textfsm"
20
+
21
+ # 标记发现协议,供邻居记录使用。
22
+ def protocol = :cdp
23
+
24
+ # 进入全局配置视图。
25
+ def enter_configuration_command = "configure terminal"
26
+
27
+ # 先回到执行视图读回,随后才执行档案中的保存命令。
28
+ def leave_configuration_commands = ["end"]
29
+
30
+ def persistence_pattern = /\A(?:\[OK\]|Copy complete\.)\z/i
31
+
32
+ # 每个 Device ID 块都须解析;设备提供总数时还须与块数相符。
33
+ def expected_neighbor_count(output, _template)
34
+ count = output.scan(/^\s*Device ID:/i).size
35
+ totals = output.scan(/^\s*Total cdp entries displayed\s*:\s*(\d+)\b/i).flatten
36
+ return unless totals.all? { |total| total.to_i == count }
37
+
38
+ count
39
+ end
40
+
41
+ # 只有设备明确报告零 CDP 邻居时才认可空表。
42
+ def empty_neighbor_output?(output)
43
+ output.match?(/Total cdp entries displayed\s*:\s*0\b|no cdp neighbors/i)
44
+ end
45
+
46
+ # 复用公共接口描述命令,保留本机接口原名。
47
+ def change_commands(change)
48
+ Netstack::Connector::Topology::InterfaceDescription.commands(interface: change.interface, description: change.new_description)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
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
+ module Vendors
8
+ class CiscoIos < Netstack::Connector::Device
9
+ vendor :cisco_ios
10
+ # 声明 IOS 的命令、提示、分页、诊断和保存确认规则。
11
+ profile do
12
+ running_config_strategy Netstack::Connector::Vendors::CiscoIos::RunningConfig
13
+ tftp_strategy Netstack::Connector::Vendors::CiscoIos::TftpBackup
14
+ topology_strategy Netstack::Connector::Vendors::CiscoIos::Topology
15
+ commands do
16
+ running_config "terminal length 0", "show running-config"
17
+ save_config "copy running-config startup-config"
18
+ end
19
+
20
+ prompts do
21
+ login(/(?:\A|(?<=[\r\n]))\S+[>#]\s*\z/)
22
+ command(/(?:\A|(?<=[\r\n]))\S+#\s*\z/)
23
+ end
24
+
25
+ pager do
26
+ pattern(/(?:\A|(?<=[\r\n]))[^\S\r\n]*--More--[^\r\n]*(?=\r|\n|\z)/i)
27
+ end
28
+
29
+ errors do
30
+ command(/^[ \t]*%[ \t]+(?:Invalid|Incomplete|Ambiguous)\s+(?:input|command)\b/i, /^[ \t]*\^/)
31
+ end
32
+
33
+ command_timeout 60
34
+
35
+ interactions do
36
+ confirm(/Destination\s+filename\s+\[[^\]]+\]\?\s*\z/i, response: "\n")
37
+ confirm(%r{Do\s+you\s+want\s+to\s+overwrite\?\s*\[yes/no\]\s*\z}i, response: "yes\n")
38
+ confirm(/Proceed\s+with\s+copy\?\s*\[confirm\]\s*\z/i, response: "\n")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,25 @@
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
+ module Vendors
8
+ class CiscoNxos
9
+ class RunningConfig < CiscoIos::RunningConfig
10
+ # 先清除 NX-OS 分页与进度回显,再复用 IOS 的配置清理规则。
11
+ def clean(text)
12
+ without_pager = text.to_s.gsub(
13
+ /[ \t]*(?:\x1b\[[0-9;]*[A-Za-z])*--More--
14
+ (?:\x1b\[[0-9;]*[A-Za-z])*(?:[\r\x00\x08 ]|\x1b\[[0-9;]*[A-Za-z])*/ix, ""
15
+ )
16
+ config = super(without_pager)
17
+ config.sub!(/\A.*?^(?=[^\n]*#\s*show\s+running-config\s*$)/im, "")
18
+ config.gsub(/^[ \t]*(?:\[[# ]*\]|\])[ \t]*\d+%[ \t]*\n?/i, "")
19
+ .gsub(/^[ \t]*Copy\s+complete\b[^\n]*\n?/i, "")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
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
+ module Vendors
8
+ class CiscoNxos
9
+ class TftpBackup < Tftp::Strategy
10
+ # 在连接前拒绝当前厂商不支持的参数组合。
11
+ def validate_options!(_target, source_file:, **)
12
+ raise ArgumentError, "Cisco NX-OS does not use source_file" unless source_file.nil?
13
+ end
14
+
15
+ def receipt_metadata(_target, **) = { configuration_kind: :running, format: :cfg }
16
+
17
+ # 构造当前厂商的 TFTP 导出交互脚本。
18
+ def script(target, source_file:, vrf: nil)
19
+ validate_options!(target, source_file: source_file, vrf: vrf)
20
+
21
+ vrf ||= "management"
22
+ command = "copy running-config tftp://#{target.host}/#{target.path} vrf #{vrf}"
23
+ Script.new([Command.new(command, timeout: 180,
24
+ interactions: [
25
+ interaction(/Enter\s+vrf\s*\([^\r\n]*\):\s*\z/i,
26
+ "#{vrf}\n")
27
+ ])])
28
+ end
29
+
30
+ # 检查设备回显是否确认 TFTP 传输完成。
31
+ def device_reported_complete?(result)
32
+ completion_lines(result).any? do |line|
33
+ line.match?(/\A
34
+ (?:transfer|copy)(?:\s+operation)?\s+
35
+ (?:success(?:ful(?:ly)?)?|succeeded|complete(?:d)?(?:\s+successfully)?)[.!]?
36
+ \z/ix) || copied_bytes?(line)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,51 @@
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
+ module Vendors
8
+ class CiscoNxos < Netstack::Connector::Device
9
+ vendor :cisco_nxos
10
+ # 声明 NX-OS 的命令、提示、分页、诊断和保存确认规则。
11
+ profile do
12
+ running_config_strategy Netstack::Connector::Vendors::CiscoNxos::RunningConfig
13
+ tftp_strategy Netstack::Connector::Vendors::CiscoNxos::TftpBackup
14
+ topology_strategy Netstack::Connector::Vendors::CiscoIos::Topology
15
+ commands do
16
+ running_config "terminal length 0", "show running-config"
17
+ save_config "copy run start"
18
+ end
19
+
20
+ prompts do
21
+ login(/\S+[>#]\s*\z/)
22
+ command(/\S+#\s*\z/)
23
+ end
24
+
25
+ pager do
26
+ pattern(/(?:\A|(?<=[\r\n]))[ \t]*(?:\x1b\[[0-9;]*[A-Za-z])*--More--
27
+ (?:[ \t\x00\x08]|\x1b\[[0-9;]*[A-Za-z])*(?=\r|\n|\z)/ix)
28
+ end
29
+
30
+ errors do
31
+ command(
32
+ /^[ \t]*%?[ \t]*(?:(?:Invalid|Ambiguous|Incomplete)\s+command(?:\s+\([^)]+\))?|
33
+ Invalid\s+parameter\s+detected)\s+at\s+'\^'\s+marker\./ix,
34
+ /^[ \t]*\^/,
35
+ /^[ \t]*%[ \t]+(?:Invalid|Ambiguous|Incomplete)\s+command\b/i,
36
+ /^[ \t]*%?[ \t]*Invalid\s+parameter\s+detected\b/i,
37
+ /^[ \t]*syntax\s+error\b/i
38
+ )
39
+ end
40
+
41
+ command_timeout 60
42
+
43
+ interactions do
44
+ confirm(/Destination\s+filename\s+\[[^\]]+\]\?\s*\z/i, response: "\n")
45
+ confirm(%r{Do\s+you\s+want\s+to\s+overwrite\?\s*\[yes/no\]\s*\z}i, response: "yes\n")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,34 @@
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
+ module Vendors
8
+ class H3c
9
+ class TftpBackup < Tftp::FileUpload
10
+ # 自动探测选择的是启动配置;显式文件仍不能推断为运行或启动配置。
11
+ def receipt_metadata(target, source_file:, explicit_source:)
12
+ super.merge(configuration_kind: explicit_source ? :saved_file : :startup)
13
+ end
14
+
15
+ # 选取并校验 H3C 启动配置文件路径。
16
+ def resolve_source_file(value)
17
+ return TftpTarget.validate_source_file!(value) if value
18
+
19
+ output = @device.execute_command("display startup").value!
20
+ rendered = TerminalRenderer.render(output)
21
+ path = rendered[/^\s*Next main startup saved-configuration file:\s*(\S+)/i, 1]
22
+ path ||= rendered[/^\s*Current startup saved-configuration file:\s*(\S+)/i, 1]
23
+ if path.nil? || path.casecmp?("NULL")
24
+ raise DeviceError.new("device has no saved startup configuration",
25
+ code: :startup_config_missing, host: @device.host, phase: :tftp_backup)
26
+ end
27
+
28
+ TftpTarget.validate_source_file!(path.sub(/\(\*\)\z/, ""))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,72 @@
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
+ module Vendors
8
+ class H3c
9
+ class Topology < Netstack::Connector::Topology::ImmediateStrategy
10
+ TABLE_HEADER = /^\s*(?:System Name\s+Local Interface\b|(?:Local Interface|LocalIf)\s+)/i
11
+ # H3C 提供 LLDP 邻居、配置描述读取和描述变更。
12
+ def self.supports?(capability)
13
+ %i[neighbors interface_descriptions interface_description_changes].include?(capability)
14
+ end
15
+
16
+ # 读取设备的 LLDP 邻居列表。
17
+ def neighbor_command = "display lldp neighbor-information list"
18
+
19
+ # 从运行配置解析当前接口描述。
20
+ def description_template = "h3c_interface_descriptions.textfsm"
21
+
22
+ # 进入系统视图以修改接口描述。
23
+ def enter_configuration_command = "system-view"
24
+
25
+ # 返回用户视图读回;保存交给后续独立阶段。
26
+ def leave_configuration_commands = ["return"]
27
+
28
+ def persistence_pattern = /\ASaved the current configuration to mainboard device successfully\.\z/i
29
+
30
+ # H3C 不同版本的列顺序不同,按表头选对应模板。
31
+ def neighbor_template(output)
32
+ return "h3c_lldp_name_first.textfsm" if output.match?(/^\s*System Name\s+Local Interface\b/im)
33
+ return "h3c_lldp_local_first.textfsm" if output.match?(/^\s*(?:Local Interface|LocalIf)\s+/im)
34
+
35
+ raise ParsingError.new("H3C LLDP table header was not recognized", code: :unrecognized_output,
36
+ host: @device.host, phase: :discover)
37
+ end
38
+
39
+ # 按表头后的有效数据行计数,未知行会让完整性校验失败。
40
+ def expected_neighbor_count(output, template)
41
+ local_column = template == "h3c_lldp_name_first.textfsm" ? 1 : 0
42
+ lines = neighbor_lines(output)
43
+ return unless lines.all? do |line|
44
+ interface = line.split.fetch(local_column, nil)
45
+ interface&.match?(/\A[A-Za-z][A-Za-z-]*\d+(?:\/\d+)+\z/)
46
+ end
47
+
48
+ lines.size
49
+ end
50
+
51
+ # 表头之后的未知行不能作为零邻居证据。
52
+ def empty_neighbor_output?(output) = neighbor_lines(output).empty?
53
+
54
+ # 在接口视图设置描述后用 quit 返回系统视图。
55
+ def change_commands(change)
56
+ Netstack::Connector::Topology::InterfaceDescription.commands(interface: change.interface, description: change.new_description, leave: "quit")
57
+ end
58
+
59
+ private
60
+
61
+ # 过滤表头、分隔线和设备提示符,保留待解析的邻居行。
62
+ def neighbor_lines(output)
63
+ output.lines.drop_while { |line| !line.match?(TABLE_HEADER) }.map(&:strip).reject do |line|
64
+ line.empty? || line.match?(TABLE_HEADER) || line.match?(/\A[-=+ ]+\z/) ||
65
+ line.match?(/\A(?:<[^>]+>|\[[^\]]+\])\z/)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end