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,35 @@
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
+ # 已渲染、已脱敏的字节按行变成事件,半行在上下文切换前收尾。
10
+ class Transcript
11
+ def initialize(&on_line)
12
+ @on_line = on_line
13
+ @buffer = +"".b
14
+ end
15
+
16
+ def write(bytes)
17
+ @buffer << bytes
18
+ while (ending = @buffer.index("\n"))
19
+ @on_line.call(@buffer.slice!(0, ending + 1).chomp)
20
+ end
21
+ bytes.bytesize
22
+ end
23
+
24
+ def flush; end
25
+
26
+ def finish
27
+ @on_line.call(@buffer) unless @buffer.empty?
28
+ @buffer = +"".b
29
+ end
30
+ end
31
+
32
+ private_constant :RedactingWriter, :Transcript
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
4
+
5
+ require "fileutils"
6
+ require "logger"
7
+ require "securerandom"
8
+
9
+ module Netstack
10
+ module Connector
11
+ # 单个设备会话的日志,按级别控制是否包含完整回显。
12
+ class Log
13
+ LEVELS = { debug: ::Logger::DEBUG, info: ::Logger::INFO,
14
+ warn: ::Logger::WARN, error: ::Logger::ERROR }.freeze
15
+ CONTEXT_FIELDS = %w[host session_id operation command_id text source line].freeze
16
+ private_constant :CONTEXT_FIELDS
17
+
18
+ # 保存日志配置和敏感信息脱敏器。
19
+ def initialize(configuration, redactor:)
20
+ @configuration = configuration
21
+ @redactor = redactor
22
+ @context = {}.freeze
23
+ end
24
+
25
+ # 为会话打开日志文件或应用日志器。
26
+ def open(transport)
27
+ @transport = transport
28
+ @session_id = SecureRandom.hex(8).freeze
29
+ @command_sequence = 0
30
+ @io = open_file(@configuration.log_file) if @configuration.log_file
31
+ if @configuration.log_format == :raw
32
+ @writer = @output = RedactingWriter.new(@io, @redactor) if @io
33
+ elsif @io || @configuration.logger
34
+ @logger = @configuration.logger || ::Logger.new(@io, formatter: Formatter.new)
35
+ open_transcript if @io && @configuration.log_level == :debug
36
+ end
37
+ rescue => error
38
+ close_preserving_error
39
+ raise build_error("unable to open session log", error), cause: nil
40
+ end
41
+
42
+ # 上下文只在持有会话锁期间使用;收尾必须先于恢复父上下文。
43
+ def with_context(**fields)
44
+ previous = @context
45
+ failed = false
46
+ finish_output
47
+ @context = @context.merge(fields).freeze
48
+ yield
49
+ rescue Exception # rubocop:disable Lint/RescueException -- Preserve this body's failure, including interruption, during log cleanup.
50
+ failed = true
51
+ raise
52
+ ensure
53
+ begin
54
+ finish_output
55
+ rescue Error
56
+ raise unless failed
57
+ ensure
58
+ @context = previous
59
+ end
60
+ end
61
+
62
+ def with_operation_context(name, &)
63
+ with_context(operation: name || @context[:operation] || :script, &)
64
+ end
65
+
66
+ def with_command_context(command, &)
67
+ @command_sequence += 1
68
+ with_context(command_id: @command_sequence, phase: :command,
69
+ text: command.sensitive? ? "[REDACTED]" : command.text,
70
+ source: command.source, line: command.line, &)
71
+ end
72
+
73
+ # 将终端回显接入当前会话日志。
74
+ def attach
75
+ return unless @transport && @writer
76
+
77
+ @transport.log_output = self
78
+ @attached = true
79
+ end
80
+
81
+ # 判断当前日志级别是否需要完整设备回显。
82
+ def debug? = @configuration.log_level == :debug && (!!@writer || enabled?(:debug))
83
+
84
+ # 按日志级别写入已脱敏的业务事件。
85
+ def log_event(name, level: :info, **fields)
86
+ return unless @logger || @configuration.on_event
87
+
88
+ finish_output
89
+ write_event(name, level: level, **fields)
90
+ rescue Error
91
+ raise
92
+ rescue => error
93
+ raise build_error("unable to write session event", error), cause: nil
94
+ end
95
+
96
+ # 异常正文由会话先脱敏;类型、错误码和阶段只允许共享词表中的值进入日志。
97
+ def log_failure(name, failure, phase: nil, **fields)
98
+ log_event(name, level: :error, **fields,
99
+ error: ErrorMetadata.type(failure.class.name) || "StandardError",
100
+ code: ErrorMetadata.code(failure.code),
101
+ phase: ErrorMetadata.phase(failure.phase) || ErrorMetadata.phase(phase),
102
+ message: failure.message)
103
+ end
104
+
105
+ # 用户钩子可能把未登记的配置片段放进事件名、字段名或值;敏感范围统一隐藏。
106
+ def log_custom_event(name, level: :info, **fields)
107
+ if @redactor.sensitive?
108
+ log_event("custom", level: level, details: "[REDACTED]")
109
+ else
110
+ log_event(name, level: level, **fields)
111
+ end
112
+ end
113
+
114
+ # 登录完成并收集动态口令后,再记录已脱敏的认证回显。
115
+ def log_authentication_output(bytes)
116
+ return unless debug?
117
+
118
+ with_context(phase: :login) do
119
+ log_event("login_output", level: :debug)
120
+ if @writer
121
+ write(@redactor.call(bytes))
122
+ flush
123
+ else
124
+ log_response_output(bytes)
125
+ end
126
+ end
127
+ end
128
+
129
+ # 注入日志器与文件回显共用事件格式;两次脱敏覆盖终端控制符拼接。
130
+ def log_response_output(bytes)
131
+ return unless debug? && !@writer && !@redactor.output_sensitive?
132
+
133
+ @redactor.call(TerminalRenderer.render(@redactor.call(bytes))).each_line do |line|
134
+ write_event("device_output", level: :debug, output: line.chomp)
135
+ end
136
+ rescue Error
137
+ raise
138
+ rescue => error
139
+ raise build_error("unable to write device output", error), cause: nil
140
+ end
141
+
142
+ # 向终端日志写入已处理的回显字节。
143
+ def write(bytes)
144
+ return unless @writer && !@redactor.output_sensitive?
145
+
146
+ @writer.write(bytes)
147
+ rescue Error
148
+ raise
149
+ rescue => error
150
+ raise build_error("unable to write session log", error), cause: nil
151
+ end
152
+
153
+ # 刷新日志缓冲区。
154
+ def flush
155
+ @writer&.flush
156
+ @io&.flush
157
+ rescue Error
158
+ raise
159
+ rescue => error
160
+ raise build_error("unable to flush session log", error), cause: nil
161
+ end
162
+
163
+ # 执行代码块期间暂停自动记录传输回显。
164
+ def pause
165
+ finish_output
166
+ @transport.log_output = nil if @transport
167
+ yield
168
+ ensure
169
+ @transport.log_output = self if @transport && @writer && @attached
170
+ end
171
+
172
+ # 断开回显记录并关闭日志资源。
173
+ def close
174
+ begin
175
+ @transport.log_output = nil if @transport
176
+ finish_output
177
+ ensure
178
+ begin
179
+ @io&.close
180
+ ensure
181
+ @writer = @output = @transcript = @logger = @io = @transport = @session_id = nil
182
+ @attached = false
183
+ end
184
+ end
185
+ rescue Error
186
+ raise
187
+ rescue => error
188
+ raise build_error("unable to close session log", error), cause: nil
189
+ end
190
+
191
+ private
192
+
193
+ # 每次读取调用方的当前阈值,不缓存级别、不改写共享 Logger。
194
+ def enabled?(level)
195
+ LEVELS.fetch(level) >= LEVELS.fetch(@configuration.log_level) &&
196
+ (@configuration.on_event || (@logger && LEVELS.fetch(level) >= @logger.level))
197
+ end
198
+
199
+ def write_event(name, level:, **fields)
200
+ return unless enabled?(level)
201
+
202
+ values = @context.compact.merge(fields.reject { |key, _| CONTEXT_FIELDS.include?(key.to_s) })
203
+ values = values.merge(host: @configuration.host, session_id: @session_id)
204
+ event = Event.new(name, values, redactor: @redactor)
205
+ @logger.public_send(level, event) if @logger && LEVELS.fetch(level) >= @logger.level
206
+ @configuration.on_event&.call(event)
207
+ end
208
+
209
+ def open_transcript
210
+ @transcript = Transcript.new { |line| write_event("device_output", level: :debug, output: line) }
211
+ @output = RedactingWriter.new(@transcript, @redactor)
212
+ @writer = TerminalRenderer.new(@output, max_line_bytes: @configuration.max_output_bytes)
213
+ end
214
+
215
+ # 先渲染,再结束过滤流,最后发出半行事件;顺序不能反转。
216
+ def finish_output
217
+ @writer.finish if @writer.respond_to?(:finish)
218
+ @output&.finish unless @output.equal?(@writer)
219
+ @transcript&.finish
220
+ rescue Error
221
+ raise
222
+ rescue => error
223
+ raise build_error("unable to finish session output", error), cause: nil
224
+ end
225
+
226
+ # 非阻塞打开后检查同一文件描述符,拒绝 FIFO 等特殊文件,避免连接前无限等待。
227
+ # 仅常规文件可配置私有权限并接收设备日志。
228
+ def open_file(path)
229
+ FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
230
+ file = File.open(path, File::WRONLY | File::CREAT | File::APPEND | File::NOFOLLOW | File::NONBLOCK, 0o600)
231
+ begin
232
+ raise IOError, "session log must be a regular file" unless file.stat.file?
233
+
234
+ file.chmod(0o600)
235
+ file.binmode
236
+ file.write("\n") if file.size.positive? && @configuration.log_format == :text
237
+ file
238
+ rescue Exception # rubocop:disable Lint/RescueException -- Release partially configured files on interrupts too.
239
+ begin
240
+ file.close
241
+ rescue
242
+ # 保留设置失败的原始原因。
243
+ end
244
+ raise
245
+ end
246
+ end
247
+
248
+ # 在日志打开失败后尽力释放资源。
249
+ def close_preserving_error
250
+ close
251
+ rescue
252
+ nil
253
+ end
254
+
255
+ # 将日志异常包装成统一错误并保留脱敏原因。
256
+ def build_error(message, error)
257
+ LogError.new(message, phase: :logging,
258
+ underlying: UnderlyingError.new(error, @redactor, sensitive: @redactor.sensitive?))
259
+ end
260
+ end
261
+ end
262
+ 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 LogError < Error; 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 LoginTimeout < AuthenticationError; 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 OutputLimitExceeded < Error; 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 ParsingError < Error; end
10
+ end
11
+ end
@@ -0,0 +1,238 @@
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 Profile
8
+ # 构造器把声明块中的领域语言转换为设备档案字段。
9
+ class Builder
10
+ # 保存继承值并提供 commands、prompts 等有限的声明入口。
11
+ def initialize(parent)
12
+ @values = parent.to_h
13
+ end
14
+
15
+ # 声明运行配置和保存配置命令。
16
+ def commands(&definition)
17
+ validate_block!(:commands, definition)
18
+ commands = Commands.new(
19
+ @values[:config_commands], @values[:save_commands]
20
+ )
21
+ commands.instance_eval(&definition)
22
+ @values[:config_commands] = commands.config_commands
23
+ @values[:save_commands] = commands.save_commands
24
+ self
25
+ end
26
+
27
+ # 声明登录、命令、凭据和提权提示符。
28
+ def prompts(&definition)
29
+ validate_block!(:prompts, definition)
30
+ Prompts.new(@values).instance_eval(&definition)
31
+ self
32
+ end
33
+
34
+ # 声明分页模式及自动翻页响应。
35
+ def pager(&definition)
36
+ validate_block!(:pager, definition)
37
+ Pager.new(@values).instance_eval(&definition)
38
+ self
39
+ end
40
+
41
+ # 声明认证失败和命令失败的输出模式。
42
+ def errors(&definition)
43
+ validate_block!(:errors, definition)
44
+ Errors.new(@values).instance_eval(&definition)
45
+ self
46
+ end
47
+
48
+ # 声明登录挑战和命令确认交互。
49
+ def interactions(&definition)
50
+ validate_block!(:interactions, definition)
51
+ interactions = Interactions.new(@values)
52
+ interactions.instance_eval(&definition)
53
+ @values[:login_interactions] = interactions.login_interactions
54
+ @values[:confirmation_interactions] = interactions.confirmation_interactions
55
+ self
56
+ end
57
+
58
+ # 声明进入特权模式所需的命令和提示符。
59
+ def privilege(&definition)
60
+ validate_block!(:privilege, definition)
61
+ Privilege.new(@values).instance_eval(&definition)
62
+ self
63
+ end
64
+
65
+ # 声明旧版 SSH 协商参数。
66
+ def ssh(&definition)
67
+ validate_block!(:ssh, definition)
68
+ Ssh.new(@values).instance_eval(&definition)
69
+ self
70
+ end
71
+
72
+ # 设置设备命令的默认超时秒数。
73
+ def command_timeout(seconds)
74
+ @values[:command_timeout] = seconds
75
+ self
76
+ end
77
+
78
+ # 设置避免设备自动换行的终端宽高。
79
+ def terminal_size(width, height = nil)
80
+ @values[:terminal_size] = height.nil? ? width : [width, height]
81
+ self
82
+ end
83
+
84
+ # 绑定已加载的策略类;nil 明确关闭继承的业务能力。
85
+ def tftp_strategy(klass)
86
+ @values[:tftp_strategy] = klass
87
+ self
88
+ end
89
+
90
+ # 指定运行配置采集策略,覆盖继承的厂商规则。
91
+ def running_config_strategy(klass)
92
+ @values[:running_config_strategy] = klass
93
+ self
94
+ end
95
+
96
+ # 指定邻居发现和描述下发策略,nil 表示关闭能力。
97
+ def topology_strategy(klass)
98
+ @values[:topology_strategy] = klass
99
+ self
100
+ end
101
+
102
+ # 将声明字段校验并构造成不可变档案。
103
+ def build = Profile.new(**@values)
104
+
105
+ private
106
+
107
+ # 块式声明入口必须有明确的子 DSL,避免静默忽略配置。
108
+ def validate_block!(name, definition)
109
+ raise ArgumentError, "#{name} requires a block" unless definition
110
+ end
111
+
112
+ # 命令集合的声明对象。
113
+ class Commands
114
+ attr_reader :config_commands, :save_commands
115
+
116
+ # 复制继承的命令,后续修改不会污染父类档案。
117
+ def initialize(config_commands, save_commands)
118
+ @config_commands = config_commands&.dup
119
+ @save_commands = save_commands.dup
120
+ end
121
+
122
+ # 声明读取运行配置的命令序列。
123
+ def running_config(*commands)
124
+ @config_commands = commands.flatten
125
+ end
126
+
127
+ # 声明保存配置的命令序列。
128
+ def save_config(*commands)
129
+ @save_commands = commands.flatten
130
+ end
131
+ end
132
+
133
+ # 提示符的声明对象。
134
+ class Prompts
135
+ # 将登录、命令、用户名、密码和特权提示写入当前档案。
136
+ def initialize(values)
137
+ @values = values
138
+ end
139
+
140
+ # 声明登录结束提示符。
141
+ def login(pattern) = @values[:login_prompt] = pattern
142
+
143
+ # 声明普通命令结束提示符。
144
+ def command(pattern) = @values[:command_prompt] = pattern
145
+
146
+ # 声明用户名输入提示。
147
+ def username(pattern) = @values[:username_prompt] = pattern
148
+
149
+ # 声明密码输入提示。
150
+ def password(pattern) = @values[:password_prompt] = pattern
151
+
152
+ # 声明特权模式提示符。
153
+ def privilege(pattern) = @values[:privilege_prompt] = pattern
154
+ end
155
+
156
+ # 分页规则的声明对象。
157
+ class Pager
158
+ # 将分页模式和响应写入当前档案。
159
+ def initialize(values)
160
+ @values = values
161
+ end
162
+
163
+ # 声明分页提示匹配模式。
164
+ def pattern(value) = @values[:pager_pattern] = value
165
+
166
+ # 声明匹配分页提示后发送的字节。
167
+ def response(value) = @values[:pager_response] = value
168
+ end
169
+
170
+ # 设备错误模式的声明对象。
171
+ class Errors
172
+ # 将错误模式列表写入当前档案。
173
+ def initialize(values)
174
+ @values = values
175
+ end
176
+
177
+ # 替换认证失败模式列表。
178
+ def authentication(*patterns)
179
+ @values[:authentication_error_patterns] = patterns.flatten
180
+ end
181
+
182
+ # 替换命令失败模式列表。
183
+ def command(*patterns)
184
+ @values[:command_error_patterns] = patterns.flatten
185
+ end
186
+ end
187
+
188
+ # 登录和命令交互的声明对象。
189
+ class Interactions
190
+ attr_reader :login_interactions, :confirmation_interactions
191
+
192
+ # 复制继承的交互规则,子类可以在其上追加设备差异。
193
+ def initialize(values)
194
+ @login_interactions = values[:login_interactions].dup
195
+ @confirmation_interactions = values[:confirmation_interactions].dup
196
+ end
197
+
198
+ # 声明登录阶段的一次性应答。
199
+ def login(pattern, response:, sensitive: false, limit: 1, capture: true)
200
+ @login_interactions << Interaction.new(pattern, response, sensitive: sensitive,
201
+ limit: limit, capture: capture)
202
+ end
203
+
204
+ # 声明命令阶段的确认应答。
205
+ def confirm(pattern, response:, sensitive: false, limit: nil, capture: true)
206
+ @confirmation_interactions << Interaction.new(pattern, response, sensitive: sensitive,
207
+ limit: limit, capture: capture)
208
+ end
209
+ end
210
+
211
+ # 特权认证规则的声明对象。
212
+ class Privilege
213
+ # 将特权命令和结束提示写入当前档案。
214
+ def initialize(values)
215
+ @values = values
216
+ end
217
+
218
+ # 声明进入特权模式的设备命令。
219
+ def command(value) = @values[:privilege_command] = value
220
+
221
+ # 声明特权命令完成后的提示符。
222
+ def prompt(value) = @values[:privilege_prompt] = value
223
+ end
224
+
225
+ # SSH 兼容参数的声明对象。
226
+ class Ssh
227
+ # 将旧版 SSH 参数写入当前档案。
228
+ def initialize(values)
229
+ @values = values
230
+ end
231
+
232
+ # 声明连接旧版设备所需的参数序列。
233
+ def legacy_arguments(*arguments) = @values[:legacy_ssh_arguments] = arguments.flatten
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end