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,44 @@
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 Error < StandardError
11
+ attr_reader :code, :host, :phase, :command, :output, :source, :line, :underlying
12
+
13
+ # 保存错误阶段、命令和输出尾部,并为缺省错误类生成稳定错误码。
14
+ def initialize(message, code: nil, host: nil, phase: nil, command: nil, output: "".b,
15
+ source: nil, line: nil, underlying: nil)
16
+ super(message)
17
+ @code = code || self.class.name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
18
+ @host = host
19
+ @phase = phase
20
+ @command = command
21
+ @output = output.freeze
22
+ @source = source
23
+ @line = line
24
+ @underlying = underlying
25
+ end
26
+
27
+ # 内置错误保留业务回执,但不复制可能含秘密的原生 cause 和调用栈。
28
+ # 自定义子类重新构造,避免其 message 等方法继续读取未脱敏的私有字段。
29
+ def with_diagnostics(message:, **context)
30
+ context = { code: code, host: host, phase: phase, command: command, output: output,
31
+ source: source, line: line, underlying: underlying }.merge(context)
32
+ return self.class.new(message, **context) unless ErrorMetadata.known_type?(self.class.name)
33
+
34
+ copy = self.class.allocate
35
+ instance_variables.each { |name| copy.instance_variable_set(name, instance_variable_get(name)) }
36
+ Error.instance_method(:initialize).bind_call(copy, message, **context)
37
+ copy
38
+ end
39
+
40
+ # 返回不展开命令和输出的错误摘要。
41
+ def inspect = "#<#{self.class} code=#{ErrorMetadata.code(code).inspect} phase=#{ErrorMetadata.phase(phase).inspect}>"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
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
+ module ErrorMetadata
9
+ CONNECTOR_TYPES = %w[Error ConnectionError AuthenticationError LoginTimeout CommandTimeout WriteTimeout
10
+ PromptError ConnectionClosed TransportError DeviceError ScriptError InternalError
11
+ OutputLimitExceeded ScriptOutputLimitExceeded SessionBusy UnsupportedOperation LogError ParsingError BackupBusy
12
+ BackupPersistenceError TftpCompletionError].freeze
13
+ TYPES = (CONNECTOR_TYPES.map { |name| "Netstack::Connector::#{name}" } +
14
+ %w[StandardError RuntimeError ArgumentError TypeError IOError EOFError SystemCallError ThreadError
15
+ RangeError KeyError IndexError NoMethodError NameError FrozenError NotImplementedError
16
+ Timeout::Error IO::TimeoutError Errno::EIO Errno::EACCES Errno::EPERM Errno::ENOSPC Errno::EROFS
17
+ Errno::ENOENT Errno::ENOTDIR Errno::EISDIR Errno::ELOOP Errno::EINVAL Errno::ENOSYS
18
+ Errno::ENOTSUP Errno::EOPNOTSUPP Errno::ETIMEDOUT Errno::ECONNREFUSED Errno::ECONNRESET
19
+ Netstack::Connector::Storage::PrivateFile::WriteError
20
+ Netstack::Connector::Storage::PrivateFile::PersistenceError
21
+ Netstack::Connector::Storage::PrivateFile::DirectorySyncUnsupported]).freeze
22
+ CODES = (CONNECTOR_TYPES.map { |name| name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym } +
23
+ %i[authentication_failed connection_failed connection_refused no_route connection_reset
24
+ connection_timeout host_key_changed host_key_untrusted rsa_too_small unsupported_cipher
25
+ ambiguous_description ambiguous_neighbor confirmation_required description_stages_unsupported
26
+ description_unconfirmed description_unsupported incomplete_configuration interface_missing
27
+ neighbor_discovery_unsupported parse_failed invalid_output_encoding persistence_unconfirmed stale_plan startup_config_missing
28
+ template_invalid template_missing template_unreadable transfer_failed transfer_finalize_failed
29
+ transfer_path_mismatch transfer_path_unconfirmed transfer_unconfirmed uncommitted_configuration
30
+ unrecognized_output unsupported_configuration_format verification_plan_changed candidate_isolation_unavailable
31
+ backup_durability_unsupported backup_finalize_failed backup_persistence_unconfirmed
32
+ file_finalize_failed file_persistence_unconfirmed file_write_failed directory_sync_unsupported]).freeze
33
+ PHASES = %i[connect login enable command script read write close logging backup collect discover parse
34
+ plan apply verify persist save interact tftp_backup report callback].freeze
35
+ private_constant :CONNECTOR_TYPES, :TYPES, :CODES, :PHASES
36
+
37
+ module_function
38
+
39
+ def code(value) = CODES.include?(value) ? value : nil
40
+
41
+ def default_code(name)
42
+ return :error unless known_type?(name)
43
+
44
+ code(name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym) || :error
45
+ end
46
+
47
+ def phase(value) = PHASES.include?(value) ? value : nil
48
+
49
+ def known_type?(name) = name.instance_of?(String) && TYPES.include?(name)
50
+
51
+ def type(name)
52
+ return if name.nil?
53
+
54
+ known_type?(name) ? name.dup.freeze : "StandardError"
55
+ end
56
+ end
57
+ end
58
+ 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
+ require "open3"
6
+ require "expect/pty"
7
+
8
+ module Netstack
9
+ module Connector
10
+ # 传输事件使用从零开始的模式索引;最后一个额外索引表示流式数据。
11
+ Event = Struct.new(:index, :before, :match, :error) do
12
+ # 规范化事件的默认字节字段,便于读取器统一处理。
13
+ def initialize(index: nil, before: "".b, match: "".b, error: nil)
14
+ super
15
+ end
16
+
17
+ # 判断读取是否命中了模式而不是传输错误。
18
+ def matched? = error.nil?
19
+
20
+ # 返回事件索引和错误类型摘要。
21
+ def inspect = "#<#{self.class} index=#{index.inspect} error=#{error&.class}>"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,110 @@
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 Execution
9
+ attr_reader :context, :steps
10
+
11
+ # 保存命令准备和后处理钩子,并初始化批次上下文及已完成步骤。
12
+ def initialize(session:, timeout:, prepare:, after_command:, prompt: nil)
13
+ @session = session
14
+ @command_timeout = timeout
15
+ @prepare_command = prepare
16
+ @after_command = after_command
17
+ @prompt_resolver = prompt
18
+ @context = {}
19
+ @steps = []
20
+ @output_bytes = 0
21
+ @output_limit = session.configuration.max_script_output_bytes
22
+ @sensitive = false
23
+ end
24
+
25
+ # 汇总实际执行的命令及交互,包括批次准备和厂商追加查询。
26
+ def sensitive? = @sensitive
27
+
28
+ # 依次准备、执行和记录脚本命令;失败时保留已完成步骤并统一抛错。
29
+ def execute_script(script)
30
+ script.each do |original_command|
31
+ @session.with_command_redaction(original_command) do
32
+ @current_command = original_command
33
+ command = @prepare_command.call(original_command, self)
34
+ next unless command
35
+
36
+ @current_command = command
37
+ started = Expect.monotonic
38
+ step = nil
39
+ response = execute_command(command) do |received|
40
+ step = CommandResult.new(command: command, output: received.output, prompt: received.prompt,
41
+ duration: Expect.monotonic - started)
42
+ steps << step
43
+ end
44
+ # 设备已经完成命令;后处理失败也不能从部分结果中抹去其副作用。
45
+ @after_command.call(command, response, self)
46
+ yield step if block_given?
47
+ validate_response_budget!(@last_executed_command)
48
+ rescue => error
49
+ raise @session.normalize_error(error, phase: :script, command: @current_command), cause: nil
50
+ end
51
+ end
52
+ validate_response_budget!(@last_executed_command)
53
+ Result.new(steps: steps)
54
+ end
55
+
56
+ # 厂商后续查询复用同一信道和错误处理,不开启新的批处理。
57
+ def execute_command(command)
58
+ command = Command.new(command) unless command.is_a?(Command)
59
+ validate_send_budget!(command)
60
+ prompt = @prompt_resolver&.call(command)
61
+ # 提示符回调也可能追加查询,实际发送前重新检查它消耗的预算。
62
+ validate_send_budget!(command) if @prompt_resolver
63
+ response = @session.execute_command(command, timeout: @command_timeout, prompt: prompt) do |received|
64
+ # 必须在 Session 恢复命令词表前读取;最终处理只能继承敏感性,不长期保留秘密。
65
+ @sensitive ||= @session.redactor.sensitive?
66
+ if received
67
+ @last_executed_command = command
68
+ @output_bytes += received.raw.bytesize
69
+ # 日志收尾失败也保留完整响应和预算;追加查询不改变原 steps 结构。
70
+ yield received if block_given?
71
+ end
72
+ end
73
+ validate_response_budget!(command)
74
+ response
75
+ end
76
+
77
+ # 只有当前会话未特权时才执行特权认证。
78
+ def enable(command, prompt)
79
+ @session.enable(command, prompt) unless @session.privileged?
80
+ end
81
+
82
+ # 根据当前命令建立脚本阶段错误。
83
+ def build_error(message)
84
+ @session.build_error(ScriptError, message, phase: :script, command: @current_command)
85
+ end
86
+
87
+ private
88
+
89
+ # 单响应上限仍约束正在读取的命令;累计预算阻止继续发送,不承诺设备尚未执行。
90
+ def validate_send_budget!(command)
91
+ return unless @output_limit && @output_bytes >= @output_limit
92
+
93
+ raise_output_limit!(command)
94
+ end
95
+
96
+ # 恰好达到预算的已完成响应仍然有效;下一次发送由发送前检查阻止。
97
+ def validate_response_budget!(command)
98
+ return unless @output_limit && @output_bytes > @output_limit
99
+
100
+ raise_output_limit!(command)
101
+ end
102
+
103
+ def raise_output_limit!(command)
104
+ raise @session.build_error(ScriptOutputLimitExceeded,
105
+ "script output reached max_script_output_bytes; commands already sent may have executed",
106
+ phase: :script, command: command), cause: nil
107
+ end
108
+ end
109
+ end
110
+ 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 Interaction
11
+ attr_reader :pattern, :limit
12
+
13
+ # 校验提示模式、响应类型和次数限制,然后冻结规则定义。
14
+ def initialize(pattern, response, sensitive: false, limit: nil, capture: true)
15
+ unless pattern.is_a?(Regexp) && !pattern.match?("")
16
+ raise ArgumentError, "interaction pattern must be a nonempty Regexp"
17
+ end
18
+ unless response.is_a?(String) || response.respond_to?(:call)
19
+ raise ArgumentError, "response must be a String or callable"
20
+ end
21
+ valid_limit = limit.nil? || (limit.is_a?(Integer) && limit.positive?)
22
+ unless valid_limit
23
+ raise ArgumentError, "limit must be positive or nil"
24
+ end
25
+ unless [true, false].include?(sensitive) && [true, false].include?(capture)
26
+ raise ArgumentError, "sensitive and capture must be true or false"
27
+ end
28
+
29
+ @pattern = pattern
30
+ @response = response.is_a?(String) ? response.dup.freeze : response
31
+ @sensitive = sensitive
32
+ @limit = limit
33
+ @capture = capture
34
+ freeze
35
+ end
36
+
37
+ # 根据当前提示计算响应文本或调用动态响应函数。
38
+ def response(prompt) = @response.respond_to?(:call) ? @response.call(prompt) : @response
39
+
40
+ # 判断响应是否需要从日志和错误上下文中脱敏。
41
+ def sensitive? = @sensitive
42
+
43
+ # 判断提示文本是否应保留在响应输出中。
44
+ def capture? = @capture
45
+
46
+ # 返回不包含响应内容的规则摘要。
47
+ def inspect = "#<#{self.class} sensitive=#{sensitive?}>"
48
+
49
+ # 将普通交互转换为一次性的敏感挑战。
50
+ def challenge
51
+ self.class.new(pattern, @response, sensitive: true, limit: limit || 1, capture: false)
52
+ end
53
+ end
54
+ end
55
+ 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 InternalError < Error; end
10
+ end
11
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "fileutils"
6
+ require "tempfile"
7
+ require "open3"
8
+
9
+ module Netstack
10
+ module Connector
11
+ # SSH 只写会话副本;认证后在跨进程锁内合并,网络等待不占用共享文件锁。
12
+ class KnownHosts
13
+ attr_reader :path
14
+
15
+ def initialize(configuration, replace: false)
16
+ destination = configuration.known_hosts || File.join(Dir.home, ".ssh", "known_hosts")
17
+ FileUtils.mkdir_p(File.dirname(destination), mode: 0o700)
18
+ @destination = File.join(File.realpath(File.dirname(destination)), File.basename(destination))
19
+ @host = configuration.host
20
+ @host = "[#{@host}]:#{configuration.port}" if configuration.port && configuration.port != 22
21
+ @replace = replace
22
+ @lock_timeout = configuration.login_timeout
23
+ @original = read_shared
24
+ # OpenSSH 以追加模式登记新密钥;私有副本必须在完整行后开始,且差量基线保持一致。
25
+ @original << "\n" unless @original.empty? || @original.end_with?("\n")
26
+ @file = Tempfile.new([".nc-known-hosts-", ""], File.dirname(@destination))
27
+ @path = @file.path
28
+ @file.write(@original)
29
+ @file.flush
30
+ remove_host(@path) if @replace
31
+ rescue Exception # rubocop:disable Lint/RescueException -- Remove the private snapshot on interruption.
32
+ close
33
+ raise
34
+ end
35
+
36
+ def commit
37
+ additions = File.binread(path).lines - @original.lines
38
+ return if additions.empty?
39
+
40
+ with_lock do
41
+ current = read_shared
42
+ Tempfile.create([".nc-known-hosts-merge-", ""], File.dirname(@destination)) do |file|
43
+ file.write(current)
44
+ file.flush
45
+ reject_changed_key!(file.path) unless @replace
46
+ remove_host(file.path) if @replace
47
+ contents = File.binread(file.path)
48
+ entries = contents.lines
49
+ additions.each { |line| entries << line unless entries.include?(line) }
50
+ File.open(file.path, "wb", 0o600) do |replacement|
51
+ replacement.write(entries.map { |line| line.end_with?("\n") ? line : line + "\n" }.join)
52
+ replacement.flush
53
+ replacement.fsync
54
+ end
55
+ File.rename(file.path, @destination)
56
+ ensure
57
+ File.unlink(file.path + ".old") if file && File.exist?(file.path + ".old")
58
+ end
59
+ end
60
+ end
61
+
62
+ def close
63
+ File.unlink(@path + ".old") if @path && File.exist?(@path + ".old")
64
+ @file&.close!
65
+ @file = nil
66
+ end
67
+
68
+ private
69
+
70
+ def read_shared
71
+ File.open(@destination, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
72
+ raise IOError, "known_hosts must be a regular file" unless file.stat.file?
73
+
74
+ file.read
75
+ end
76
+ rescue Errno::ENOENT
77
+ ""
78
+ end
79
+
80
+ def reject_changed_key!(current_path)
81
+ current = host_keys(current_path)
82
+ staged = host_keys(path)
83
+ return if current.empty? || (staged - current).empty?
84
+
85
+ raise ConnectionError.new("host key changed during concurrent registration", code: :host_key_changed)
86
+ end
87
+
88
+ def host_keys(path)
89
+ output, status = Open3.capture2e("ssh-keygen", "-f", path, "-F", @host)
90
+ raise IOError, "reading device host key failed" unless [0, 1].include?(status.exitstatus)
91
+
92
+ output.lines.reject { |line| line.start_with?("#") }.map { |line| line.split[1, 2] }.uniq
93
+ end
94
+
95
+ def remove_host(path)
96
+ _output, status = Open3.capture2e("ssh-keygen", "-f", path, "-R", @host)
97
+ raise IOError, "removing device host key failed" unless status.success?
98
+ end
99
+
100
+ def with_lock
101
+ lock_path = @destination + ".nc-lock"
102
+ File.open(lock_path, File::RDWR | File::CREAT | File::NOFOLLOW | File::NONBLOCK, 0o600) do |file|
103
+ stat = file.stat
104
+ unless stat.file? && stat.uid == Process.euid && stat.nlink == 1 && (stat.mode & 0o7777) == 0o600
105
+ raise IOError, "known_hosts lock must be an owned private regular file"
106
+ end
107
+ acquire_lock(file)
108
+ current = File.lstat(lock_path)
109
+ raise IOError, "known_hosts lock changed" unless current.dev == stat.dev && current.ino == stat.ino
110
+
111
+ yield
112
+ end
113
+ end
114
+
115
+ def acquire_lock(file)
116
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @lock_timeout
117
+ until file.flock(File::LOCK_EX | File::LOCK_NB)
118
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
119
+ if remaining <= 0
120
+ raise LoginTimeout.new("known_hosts lock acquisition timed out", phase: :login), cause: nil
121
+ end
122
+
123
+ sleep([remaining, 0.05].min)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ 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 "digest"
6
+
7
+ module Netstack
8
+ module Connector
9
+ class LocalBackup
10
+ # 设备入口与功能实现共置,Device 只组合能力,不重复业务流程。
11
+ module Capability
12
+ # 采集配置并以原子方式保存为私有文件。
13
+ # 采集失败时保留已有备份文件。
14
+ def backup(path:, lock_timeout: 0)
15
+ @session.assert_path_lock_order!(:backup)
16
+ LocalBackup.new(self).call(path: path, lock_timeout: lock_timeout)
17
+ end
18
+ end
19
+
20
+ # 保存执行配置采集的设备对象。
21
+ def initialize(device) = @device = device
22
+
23
+ # 采集配置、识别变更并原子写入私有文件。
24
+ def call(path:, lock_timeout: 0)
25
+ host = @device.host if @device.respond_to?(:host)
26
+ Storage::BackupLock.synchronize(path, timeout: lock_timeout, host: host) { |lock| collect_and_write(lock.path) }
27
+ end
28
+
29
+ private
30
+
31
+ def collect_and_write(destination)
32
+ contents = @device.running_config.value!
33
+ digest = Digest::SHA256.hexdigest(contents)
34
+ previous = Storage::SafeFile.fingerprint(destination, missing: true, replace_symlink: true)
35
+ previous_digest = previous&.sha256
36
+ change = if previous_digest.nil?
37
+ :created
38
+ elsif previous_digest == digest
39
+ :unchanged
40
+ else
41
+ :changed
42
+ end
43
+
44
+ backup = Backup.new(path: destination, bytes: contents.bytesize,
45
+ sha256: digest, collected_at: Time.now.utc, change: change,
46
+ previous_sha256: previous_digest)
47
+ if change != :unchanged || previous.mode != 0o600 || !Storage::SafeFile.same_entry?(destination, previous)
48
+ write_backup(backup, contents)
49
+ end
50
+ backup
51
+ end
52
+
53
+ def write_backup(backup, contents)
54
+ Storage::PrivateFile.write(backup.path, contents)
55
+ rescue Storage::PrivateFile::WriteError => error
56
+ raise unless Storage::PrivateFile.receipt_error?(error) && error.receipt.committed?
57
+
58
+ host = @device.host if @device.respond_to?(:host)
59
+ raise BackupPersistenceError.new(backup: backup, write_error: error, host: host), cause: nil
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,65 @@
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
+ class Log
8
+ # 在敏感作用域结束前冻结安全副本;应用 formatter 可直接读取字段。
9
+ class Event
10
+ attr_reader :name, :fields
11
+
12
+ def initialize(name, fields, redactor:)
13
+ @name = safe_text(name.is_a?(String) || name.is_a?(Symbol) ? name.to_s : "custom", redactor).freeze
14
+ @fields = fields.each_with_object({}) do |(key, value), result|
15
+ next unless (key.is_a?(Symbol) || key.is_a?(String)) && key.to_s.match?(/\A[a-z][a-z0-9_]{0,63}\z/)
16
+ next unless redactor.call(key.to_s) == key.to_s
17
+ next if key.to_s == "event"
18
+
19
+ result[key.to_sym] = safe_value(value, redactor)
20
+ end.freeze
21
+ freeze
22
+ end
23
+
24
+ # 与文本呈现共用同一安全记录,调用方不能借 formatter 取得原始秘密。
25
+ def to_h = { event: name, **fields }.freeze
26
+
27
+ def to_s
28
+ details = to_h.reject { |key, _| key == :host }.map { |key, value| "#{key}=#{token(value)}" }.join(" ")
29
+ "[host=#{token(fields[:host])}] #{Messages.format(name, fields)} | #{details}"
30
+ end
31
+
32
+ # Ruby Logger 的默认 formatter 对非 String 消息调用 inspect。
33
+ alias inspect to_s
34
+
35
+ private
36
+
37
+ def safe_value(value, redactor)
38
+ case value
39
+ when String, Symbol
40
+ safe_text(value.to_s, redactor).freeze
41
+ when Float
42
+ value.finite? && redactor.call(value.to_s) == value.to_s ? value : "[REDACTED]"
43
+ when Integer, TrueClass, FalseClass, NilClass
44
+ redactor.call(value.to_s) == value.to_s ? value : "[REDACTED]"
45
+ else
46
+ # 不调用任意对象的 inspect/to_s,避免展开异常、配置对象或容器中的秘密。
47
+ "[REDACTED]"
48
+ end
49
+ end
50
+
51
+ def safe_text(text, redactor)
52
+ rendered = TerminalRenderer.render(redactor.call(text))
53
+ redactor.call(rendered).force_encoding(Encoding::UTF_8).scrub.gsub(/[[:cntrl:]]+/, " ").strip
54
+ end
55
+
56
+ def token(value)
57
+ return "nil" if value.nil?
58
+
59
+ text = value.to_s
60
+ text.match?(/\A[A-Za-z0-9_.:\/-]+\z/) ? text : text.inspect
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "logger"
6
+
7
+ module Netstack
8
+ module Connector
9
+ class Log
10
+ # 只安装在连接器自有的 Logger 上;注入的日志器保留应用自己的格式。
11
+ class Formatter < ::Logger::Formatter
12
+ def call(severity, time, _program, message)
13
+ "[#{time.getlocal.strftime("%Y-%m-%d %H:%M:%S.%L %:z")}] #{severity} #{message}\n"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ 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
+ class Log
8
+ # 独立于日志目标的人类可读事件文案。
9
+ module Messages
10
+ module_function
11
+
12
+ # 将结构化会话事件转换为便于人工阅读的中文日志。
13
+ def format(name, fields)
14
+ case name
15
+ when "connect"
16
+ "开始连接 #{fields[:host]}(#{fields[:protocol].to_s.upcase},账号 #{fields[:username]})"
17
+ when "connect_failed"
18
+ "连接失败:#{fields[:message]}(#{fields[:error]})"
19
+ when "login_start"
20
+ "等待设备登录提示"
21
+ when "login_output"
22
+ "登录过程回显(已脱敏):"
23
+ when "login_complete"
24
+ fields[:status] == "ok" ? "登录成功,设备提示符:#{fields[:prompt]}" :
25
+ "登录失败:#{fields[:message]}(#{fields[:error]})"
26
+ when "command_start"
27
+ "下发命令:#{fields[:text]}"
28
+ when "device_output"
29
+ "设备回显:"
30
+ when "command_complete"
31
+ fields[:status] == "response_received" ? "命令回显结束,已收到设备提示符" :
32
+ "命令执行失败:#{fields[:message]}(#{fields[:error]})"
33
+ when "operation_start"
34
+ "开始处理设备脚本"
35
+ when "operation_complete"
36
+ fields[:status] == "completed" ? "设备脚本处理完成" :
37
+ "设备脚本处理失败:#{fields[:message]}(#{fields[:error]})"
38
+ when "tftp_backup"
39
+ case fields[:status]
40
+ when "reported_uploaded"
41
+ "TFTP 备份:设备报告上传成功,目标 #{fields[:server]}/#{fields[:path]};服务器文件尚未核验"
42
+ when "transfer_failed"
43
+ "TFTP 备份失败:设备报告传输失败,服务器 #{fields[:server]}"
44
+ else
45
+ "TFTP 备份未确认:设备未报告上传成功,服务器 #{fields[:server]}"
46
+ end
47
+ else
48
+ name.to_s
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,33 @@
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
+ class Log
8
+ # 每个目标独占 expect-pty 的过滤流;此层只适配 IO,不实现匹配算法。
9
+ class RedactingWriter
10
+ def initialize(target, redactor)
11
+ @target = target
12
+ @redactor = redactor
13
+ @filter = redactor.stream
14
+ end
15
+
16
+ def write(bytes)
17
+ @filter.patterns = @redactor.patterns
18
+ @target.write(@filter.append(bytes.b))
19
+ bytes.bytesize
20
+ end
21
+
22
+ def flush = @target.flush
23
+
24
+ def finish
25
+ @filter.patterns = @redactor.patterns
26
+ # 配置正文由 Session 的敏感范围隔离;保留完整词匹配的收尾契约。
27
+ @target.write(@filter.finish(partial: false))
28
+ flush
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end