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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -3
- data/{LICENSE.txt → LICENSE} +7 -6
- data/NOTICE +25 -0
- data/README.md +58 -20
- data/docs/acceptance.md +92 -0
- data/docs/backups.md +100 -0
- data/docs/configuration.md +198 -0
- data/docs/connector.md +187 -0
- data/docs/ddi.md +142 -0
- data/docs/deployment.md +47 -0
- data/docs/foundations.md +45 -0
- data/docs/implementation.md +68 -0
- data/docs/inventory.md +42 -0
- data/docs/monitoring.md +227 -0
- data/docs/motor_integration.md +96 -0
- data/docs/performance.md +39 -0
- data/docs/release.md +20 -0
- data/docs/vxlan.md +91 -0
- data/docs/wireless.md +81 -0
- data/lib/netstack/backups/artifact.rb +23 -0
- data/lib/netstack/backups/diff.rb +15 -0
- data/lib/netstack/backups/repository.rb +77 -0
- data/lib/netstack/backups/request.rb +78 -0
- data/lib/netstack/circuits/business_calendar.rb +51 -0
- data/lib/netstack/circuits/capacity_review.rb +191 -0
- data/lib/netstack/circuits/endpoint.rb +25 -0
- data/lib/netstack/circuits/monitoring_plan.rb +212 -0
- data/lib/netstack/configuration/artifact.rb +85 -0
- data/lib/netstack/configuration/artifact_resolver.rb +30 -0
- data/lib/netstack/configuration/bootstrap/payload.rb +67 -0
- data/lib/netstack/configuration/bootstrap/validator.rb +224 -0
- data/lib/netstack/configuration/catalog.rb +60 -0
- data/lib/netstack/configuration/generator.rb +87 -0
- data/lib/netstack/configuration/input_contract.rb +82 -0
- data/lib/netstack/configuration/payload.rb +168 -0
- data/lib/netstack/configuration/reader.rb +124 -0
- data/lib/netstack/configuration/support.rb +30 -0
- data/lib/netstack/configuration/system_configuration.rb +56 -0
- data/lib/netstack/configuration/template_manager.rb +174 -0
- data/lib/netstack/configuration/template_store.rb +40 -0
- data/lib/netstack/configuration/validator.rb +566 -0
- data/lib/netstack/connector/authentication.rb +77 -0
- data/lib/netstack/connector/authentication_error.rb +11 -0
- data/lib/netstack/connector/backup.rb +19 -0
- data/lib/netstack/connector/backup_busy.rb +11 -0
- data/lib/netstack/connector/backup_persistence_error.rb +32 -0
- data/lib/netstack/connector/command.rb +63 -0
- data/lib/netstack/connector/command_result.rb +24 -0
- data/lib/netstack/connector/command_timeout.rb +11 -0
- data/lib/netstack/connector/configuration.rb +138 -0
- data/lib/netstack/connector/connection_closed.rb +11 -0
- data/lib/netstack/connector/connection_error.rb +11 -0
- data/lib/netstack/connector/device.rb +284 -0
- data/lib/netstack/connector/device_error.rb +11 -0
- data/lib/netstack/connector/dialogue.rb +55 -0
- data/lib/netstack/connector/error.rb +44 -0
- data/lib/netstack/connector/error_metadata.rb +58 -0
- data/lib/netstack/connector/event.rb +24 -0
- data/lib/netstack/connector/execution.rb +110 -0
- data/lib/netstack/connector/interaction.rb +55 -0
- data/lib/netstack/connector/internal_error.rb +11 -0
- data/lib/netstack/connector/known_hosts.rb +128 -0
- data/lib/netstack/connector/local_backup.rb +63 -0
- data/lib/netstack/connector/log/event.rb +65 -0
- data/lib/netstack/connector/log/formatter.rb +18 -0
- data/lib/netstack/connector/log/messages.rb +54 -0
- data/lib/netstack/connector/log/redacting_writer.rb +33 -0
- data/lib/netstack/connector/log/transcript.rb +35 -0
- data/lib/netstack/connector/log.rb +262 -0
- data/lib/netstack/connector/log_error.rb +11 -0
- data/lib/netstack/connector/login_timeout.rb +11 -0
- data/lib/netstack/connector/output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/parsing_error.rb +11 -0
- data/lib/netstack/connector/profile/builder.rb +238 -0
- data/lib/netstack/connector/profile.rb +225 -0
- data/lib/netstack/connector/prompt_error.rb +11 -0
- data/lib/netstack/connector/recovery.rb +59 -0
- data/lib/netstack/connector/redactor.rb +100 -0
- data/lib/netstack/connector/response.rb +25 -0
- data/lib/netstack/connector/response_reader.rb +140 -0
- data/lib/netstack/connector/result.rb +50 -0
- data/lib/netstack/connector/running_config/rendered.rb +16 -0
- data/lib/netstack/connector/running_config/strategy.rb +28 -0
- data/lib/netstack/connector/running_config.rb +113 -0
- data/lib/netstack/connector/save_config.rb +23 -0
- data/lib/netstack/connector/script.rb +59 -0
- data/lib/netstack/connector/script_error.rb +11 -0
- data/lib/netstack/connector/script_output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/session.rb +414 -0
- data/lib/netstack/connector/session_busy.rb +11 -0
- data/lib/netstack/connector/storage/backup_lock.rb +117 -0
- data/lib/netstack/connector/storage/private_file.rb +111 -0
- data/lib/netstack/connector/storage/safe_file.rb +64 -0
- data/lib/netstack/connector/storage/saved_config.rb +76 -0
- data/lib/netstack/connector/terminal_renderer.rb +186 -0
- data/lib/netstack/connector/terminal_text.rb +32 -0
- data/lib/netstack/connector/textfsm.rb +71 -0
- data/lib/netstack/connector/tftp/file_upload.rb +53 -0
- data/lib/netstack/connector/tftp/strategy.rb +67 -0
- data/lib/netstack/connector/tftp.rb +116 -0
- data/lib/netstack/connector/tftp_completion_error.rb +36 -0
- data/lib/netstack/connector/tftp_receipt.rb +54 -0
- data/lib/netstack/connector/tftp_target.rb +65 -0
- data/lib/netstack/connector/topology/checkpoint_error.rb +16 -0
- data/lib/netstack/connector/topology/deployment_adapter.rb +163 -0
- data/lib/netstack/connector/topology/execution_result.rb +34 -0
- data/lib/netstack/connector/topology/immediate_strategy.rb +39 -0
- data/lib/netstack/connector/topology/interface_description.rb +46 -0
- data/lib/netstack/connector/topology/interface_name.rb +60 -0
- data/lib/netstack/connector/topology/plan.rb +105 -0
- data/lib/netstack/connector/topology/strategy.rb +76 -0
- data/lib/netstack/connector/topology.rb +255 -0
- data/lib/netstack/connector/transport_error.rb +11 -0
- data/lib/netstack/connector/transports.rb +201 -0
- data/lib/netstack/connector/underlying_error.rb +25 -0
- data/lib/netstack/connector/unsupported_operation.rb +11 -0
- data/lib/netstack/connector/vendors/cisco_ios/running_config.rb +22 -0
- data/lib/netstack/connector/vendors/cisco_ios/tftp_backup.rb +41 -0
- data/lib/netstack/connector/vendors/cisco_ios/topology.rb +54 -0
- data/lib/netstack/connector/vendors/cisco_ios.rb +44 -0
- data/lib/netstack/connector/vendors/cisco_nxos/running_config.rb +25 -0
- data/lib/netstack/connector/vendors/cisco_nxos/tftp_backup.rb +43 -0
- data/lib/netstack/connector/vendors/cisco_nxos.rb +51 -0
- data/lib/netstack/connector/vendors/h3c/tftp_backup.rb +34 -0
- data/lib/netstack/connector/vendors/h3c/topology.rb +72 -0
- data/lib/netstack/connector/vendors/h3c.rb +75 -0
- data/lib/netstack/connector/vendors/h3c_wireless.rb +13 -0
- data/lib/netstack/connector/vendors/hillstone/running_config.rb +16 -0
- data/lib/netstack/connector/vendors/hillstone/tftp_backup.rb +55 -0
- data/lib/netstack/connector/vendors/hillstone/topology.rb +64 -0
- data/lib/netstack/connector/vendors/hillstone.rb +39 -0
- data/lib/netstack/connector/vendors/huawei/tftp_backup.rb +26 -0
- data/lib/netstack/connector/vendors/huawei.rb +62 -0
- data/lib/netstack/connector/vendors/palo_alto/running_config.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/palo_alto/topology.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto.rb +39 -0
- data/lib/netstack/connector/vendors/radware/running_config.rb +21 -0
- data/lib/netstack/connector/vendors/radware/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/radware/topology.rb +19 -0
- data/lib/netstack/connector/vendors/radware.rb +59 -0
- data/lib/netstack/connector/write_timeout.rb +11 -0
- data/lib/netstack/connector.rb +32 -0
- data/lib/netstack/ddi/applier.rb +107 -0
- data/lib/netstack/ddi/deployment.rb +137 -0
- data/lib/netstack/ddi/inputs.rb +61 -0
- data/lib/netstack/ddi/manager.rb +197 -0
- data/lib/netstack/ddi/plan.rb +99 -0
- data/lib/netstack/ddi/scope.rb +127 -0
- data/lib/netstack/ddi/settings.rb +44 -0
- data/lib/netstack/ddi/state.rb +166 -0
- data/lib/netstack/deployment/checkpoint_error.rb +14 -0
- data/lib/netstack/deployment/cli_adapter.rb +64 -0
- data/lib/netstack/deployment/executor.rb +131 -0
- data/lib/netstack/deployment/plan.rb +82 -0
- data/lib/netstack/deployment/receipt.rb +78 -0
- data/lib/netstack/deployment/recovery.rb +19 -0
- data/lib/netstack/deployment/step.rb +16 -0
- data/lib/netstack/dimension.rb +30 -0
- data/lib/netstack/error.rb +14 -0
- data/lib/netstack/input_error.rb +9 -0
- data/lib/netstack/integrations/bluecat/client.rb +84 -0
- data/lib/netstack/integrations/http/client.rb +146 -0
- data/lib/netstack/integrations/http/error.rb +10 -0
- data/lib/netstack/integrations/http/response.rb +28 -0
- data/lib/netstack/integrations/http_delivery/adapter.rb +51 -0
- data/lib/netstack/integrations/http_delivery/client.rb +127 -0
- data/lib/netstack/integrations/http_delivery/result.rb +27 -0
- data/lib/netstack/integrations/oxidized/client.rb +89 -0
- data/lib/netstack/integrations/zabbix/client.rb +211 -0
- data/lib/netstack/integrations/zabbix/error.rb +10 -0
- data/lib/netstack/inventory/address.rb +30 -0
- data/lib/netstack/inventory/changes.rb +13 -0
- data/lib/netstack/inventory/cli_parser.rb +75 -0
- data/lib/netstack/inventory/collector.rb +99 -0
- data/lib/netstack/inventory/device.rb +14 -0
- data/lib/netstack/inventory/interface.rb +25 -0
- data/lib/netstack/inventory/neighbor.rb +15 -0
- data/lib/netstack/inventory/reconciler.rb +74 -0
- data/lib/netstack/inventory/snapshot.rb +69 -0
- data/lib/netstack/inventory/transceiver.rb +23 -0
- data/lib/netstack/ipv4.rb +204 -0
- data/lib/netstack/ipv6.rb +88 -0
- data/lib/netstack/monitoring/applier.rb +179 -0
- data/lib/netstack/monitoring/device_plan.rb +23 -0
- data/lib/netstack/monitoring/host_plan.rb +152 -0
- data/lib/netstack/monitoring/inputs.rb +73 -0
- data/lib/netstack/monitoring/measurements.rb +89 -0
- data/lib/netstack/monitoring/mutation.rb +37 -0
- data/lib/netstack/monitoring/plan.rb +69 -0
- data/lib/netstack/monitoring/probe_plan.rb +19 -0
- data/lib/netstack/monitoring/state_comparison.rb +37 -0
- data/lib/netstack/range_set.rb +181 -0
- data/lib/netstack/routing/lookup.rb +46 -0
- data/lib/netstack/routing/lookup_result.rb +14 -0
- data/lib/netstack/routing/route.rb +32 -0
- data/lib/netstack/routing/snapshot.rb +23 -0
- data/lib/netstack/target.rb +15 -0
- data/lib/netstack/unsupported_capability.rb +9 -0
- data/lib/netstack/values.rb +70 -0
- data/lib/netstack/version.rb +1 -1
- data/lib/netstack/vxlan/auditor.rb +187 -0
- data/lib/netstack/vxlan/cisco_nxos/config_parser.rb +327 -0
- data/lib/netstack/vxlan/cisco_nxos/deployment_adapter.rb +63 -0
- data/lib/netstack/vxlan/cisco_nxos/runtime_parser.rb +182 -0
- data/lib/netstack/vxlan/cisco_nxos/segment_renderer.rb +42 -0
- data/lib/netstack/vxlan/collector.rb +77 -0
- data/lib/netstack/vxlan/fabric.rb +29 -0
- data/lib/netstack/vxlan/finding.rb +20 -0
- data/lib/netstack/vxlan/node.rb +51 -0
- data/lib/netstack/vxlan/route_policy.rb +57 -0
- data/lib/netstack/vxlan/runtime_result.rb +24 -0
- data/lib/netstack/vxlan/segment.rb +35 -0
- data/lib/netstack/vxlan/segment_plan.rb +25 -0
- data/lib/netstack/vxlan/segment_planner.rb +156 -0
- data/lib/netstack/vxlan/snapshot.rb +34 -0
- data/lib/netstack/vxlan/validation.rb +58 -0
- data/lib/netstack/vxlan/verifier.rb +52 -0
- data/lib/netstack/vxlan/vpc_domain.rb +20 -0
- data/lib/netstack/wireless/access_point.rb +12 -0
- data/lib/netstack/wireless/ap_group.rb +12 -0
- data/lib/netstack/wireless/assessment.rb +13 -0
- data/lib/netstack/wireless/assessor.rb +83 -0
- data/lib/netstack/wireless/authentication_connection.rb +12 -0
- data/lib/netstack/wireless/bss.rb +12 -0
- data/lib/netstack/wireless/client.rb +12 -0
- data/lib/netstack/wireless/collector.rb +191 -0
- data/lib/netstack/wireless/configuration.rb +13 -0
- data/lib/netstack/wireless/finding.rb +12 -0
- data/lib/netstack/wireless/h3c/config_parser.rb +229 -0
- data/lib/netstack/wireless/h3c/lldp_parser.rb +69 -0
- data/lib/netstack/wireless/h3c/runtime_parser.rb +240 -0
- data/lib/netstack/wireless/huawei/runtime_parser.rb +83 -0
- data/lib/netstack/wireless/invalid_snapshot.rb +11 -0
- data/lib/netstack/wireless/neighbor.rb +12 -0
- data/lib/netstack/wireless/normalizer.rb +90 -0
- data/lib/netstack/wireless/parse_result.rb +13 -0
- data/lib/netstack/wireless/radio.rb +12 -0
- data/lib/netstack/wireless/reconciler.rb +115 -0
- data/lib/netstack/wireless/reconciliation.rb +11 -0
- data/lib/netstack/wireless/security_policy.rb +12 -0
- data/lib/netstack/wireless/service_template.rb +12 -0
- data/lib/netstack/wireless/service_template_bindings.rb +56 -0
- data/lib/netstack/wireless/snapshot.rb +150 -0
- data/lib/netstack/wireless/state.rb +23 -0
- data/lib/netstack/wireless/view.rb +154 -0
- data/lib/netstack.rb +10 -3
- data/resources/configuration/cisco-bootstrap.erb +118 -0
- data/resources/configuration/cisco-distributed-gateway.erb +40 -0
- data/resources/configuration/cisco-n9k-vpc.erb +62 -0
- data/resources/configuration/cisco-server-port.erb +43 -0
- data/resources/configuration/h3c-bootstrap.erb +108 -0
- data/resources/configuration/h3c-irf.erb +57 -0
- data/resources/configuration/h3c-server-port.erb +36 -0
- data/resources/configuration/hillstone-ipsec-vpn.erb +68 -0
- data/resources/configuration/hillstone-policy.erb +31 -0
- data/resources/configuration/paloalto-ipsec-vpn.erb +39 -0
- data/resources/configuration/radware-load-balancer.erb +26 -0
- data/resources/textfsm/cisco_cdp_neighbors_detail.textfsm +7 -0
- data/resources/textfsm/cisco_ios_running_config_interfaces.textfsm +12 -0
- data/resources/textfsm/cisco_ios_show_ip_interface_brief.textfsm +7 -0
- data/resources/textfsm/h3c_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/h3c_lldp_local_first.textfsm +8 -0
- data/resources/textfsm/h3c_lldp_name_first.textfsm +8 -0
- data/resources/textfsm/hillstone_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/hillstone_lldp_neighbors.textfsm +7 -0
- data/resources/textfsm/index +8 -0
- data/resources/textfsm/palo_alto_interface_descriptions.textfsm +6 -0
- data/resources/textfsm/palo_alto_lldp_neighbors.textfsm +11 -0
- data/resources/textfsm/radware_port_names.textfsm +13 -0
- metadata +588 -27
- data/.rubocop.yml +0 -13
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -40
- data/Rakefile +0 -8
- data/bin/console +0 -15
- data/bin/setup +0 -8
|
@@ -0,0 +1,113 @@
|
|
|
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 RunningConfig
|
|
8
|
+
# 设备只引入此能力;采集入口和策略作用域与采集流程一起维护。
|
|
9
|
+
module Capability
|
|
10
|
+
# 每次采集使用独立执行对象,设备只提供这一采集入口。
|
|
11
|
+
def running_config = RunningConfig.new(self).call
|
|
12
|
+
|
|
13
|
+
# 返回读取运行配置所需的设备命令;厂商必须声明。
|
|
14
|
+
def config_commands
|
|
15
|
+
commands = profile.config_commands
|
|
16
|
+
return commands if commands
|
|
17
|
+
|
|
18
|
+
raise NotImplementedError, "#{self.class} must define running configuration commands"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 子类可覆盖并调用 super,复用本次响应检查积累的策略状态。
|
|
22
|
+
def clean_config(text) = config_strategy.clean(text)
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
# 默认取最后一个完成步骤;厂商策略可选择配置所在的业务步骤。
|
|
27
|
+
def config_result_step(result) = config_strategy.result_step(result)
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# 仅在持有会话锁的结果处理阶段绑定策略,退出时恢复原有作用域。
|
|
32
|
+
# 同一设备上其他 Fiber 的离线清理不能借用此次采集状态。
|
|
33
|
+
def with_config_strategy(strategy)
|
|
34
|
+
previous = @config_strategy_scope
|
|
35
|
+
@config_strategy_scope = [Fiber.current, strategy]
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
@config_strategy_scope = previous
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def config_strategy
|
|
42
|
+
scope = @config_strategy_scope
|
|
43
|
+
scope && scope.first.equal?(Fiber.current) ? scope.last : RunningConfig.strategy(self)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 保存需要读取运行配置的设备对象。
|
|
48
|
+
def initialize(device)
|
|
49
|
+
@device = device
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 从设备档案选定本次采集策略;无厂商规则时使用公共策略。
|
|
53
|
+
def self.strategy(device)
|
|
54
|
+
(device.profile.running_config_strategy || Strategy).new(device)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 每次采集创建独立策略;响应校验、选择与清理共享状态,均在同一脚本锁内完成。
|
|
58
|
+
def call
|
|
59
|
+
script = Script.new(@device.config_commands)
|
|
60
|
+
return Result.new(error: build_incomplete_error("configuration collection has no commands")) if script.empty?
|
|
61
|
+
|
|
62
|
+
# 所有厂商的采集步骤都收紧输出边界,包括候选差异、模式切换及扩展查询。
|
|
63
|
+
script = Script.new(script.map(&:with_output_sensitive))
|
|
64
|
+
strategy = self.class.strategy(@device)
|
|
65
|
+
prompt = ->(command) { prompt_for(command, strategy) }
|
|
66
|
+
@device.execute_operation(script, name: :running_config, prompt: prompt,
|
|
67
|
+
after_command: strategy.method(:validate_response!)) do |result|
|
|
68
|
+
@device.send(:with_config_strategy, strategy) do
|
|
69
|
+
finish(result)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# 只从完成的配置步骤提取非空内容,并保留此前所有步骤。
|
|
77
|
+
def finish(result)
|
|
78
|
+
step = @device.send(:config_result_step, result)
|
|
79
|
+
raise build_incomplete_error("configuration collection has no completed configuration step") unless step
|
|
80
|
+
|
|
81
|
+
content = @device.clean_config(step.output)
|
|
82
|
+
unless content.is_a?(String) && !content.strip.empty? && config_body?(step)
|
|
83
|
+
raise build_incomplete_error("configuration collection returned empty content")
|
|
84
|
+
end
|
|
85
|
+
Result.new(steps: result.steps, config: content)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 用当前完整提示符构建结束匹配,避免配置正文中的单个 # 提前结束采集。
|
|
89
|
+
def prompt_for(command, strategy)
|
|
90
|
+
return command.prompt if command.prompt
|
|
91
|
+
|
|
92
|
+
prompt = strategy.prompt_text(command)
|
|
93
|
+
if prompt.empty?
|
|
94
|
+
raise PromptError.new("configuration collection has no known device prompt", host: @device.host, phase: :collect)
|
|
95
|
+
end
|
|
96
|
+
/(?:\A|(?<=[\r\n]))[ \t\x00]*#{Regexp.escape(prompt)}[ \t\x00]*[\r\n]*\z/
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 排除命令回显和提示符,确认响应确实包含配置正文。
|
|
100
|
+
def config_body?(step)
|
|
101
|
+
body = TerminalRenderer.render(step.output.delete_suffix(step.prompt.to_s)).strip
|
|
102
|
+
lines = body.lines
|
|
103
|
+
lines.shift if lines.first&.strip == step.command.text
|
|
104
|
+
!lines.join.strip.empty?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 将缺少配置的情况统一映射为可识别的设备错误码。
|
|
108
|
+
def build_incomplete_error(message)
|
|
109
|
+
DeviceError.new(message, code: :incomplete_configuration, host: @device.host, phase: :collect)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
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 SaveConfig
|
|
9
|
+
# 执行厂商保存配置命令;不支持时返回显式失败结果。
|
|
10
|
+
def save_config
|
|
11
|
+
if save_commands.empty?
|
|
12
|
+
return Result.new(error: @session.build_error(UnsupportedOperation, "saving configuration is not supported",
|
|
13
|
+
phase: :save))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
execute_script(save_commands)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 返回保存配置命令;空数组表示设备不支持保存。
|
|
20
|
+
def save_commands = profile.save_commands
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
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 Script
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
attr_reader :name
|
|
14
|
+
|
|
15
|
+
# 按行过滤空行和整行注释,再构造带来源信息的命令。
|
|
16
|
+
def self.parse(text, name: nil)
|
|
17
|
+
raise ArgumentError, "script must be a String" unless text.is_a?(String)
|
|
18
|
+
|
|
19
|
+
commands = text.each_line.with_index(1).filter_map do |line, line_number|
|
|
20
|
+
line = line.chomp
|
|
21
|
+
next if line.strip.empty? || line.lstrip.start_with?("#")
|
|
22
|
+
|
|
23
|
+
Command.new(line, source: name, line: line_number)
|
|
24
|
+
end
|
|
25
|
+
new(commands, name: name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 读取脚本文件;文件错误转换为带来源的脚本错误。
|
|
29
|
+
def self.load(path)
|
|
30
|
+
parse(File.binread(path), name: File.expand_path(path))
|
|
31
|
+
rescue IOError, SystemCallError
|
|
32
|
+
raise ScriptError.new("unable to read script", phase: :parse, source: path.to_s), cause: nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 校验并冻结命令数组,给非 Command 输入补充来源和行号。
|
|
36
|
+
def initialize(commands, name: nil)
|
|
37
|
+
raise ArgumentError, "commands must be an Array" unless commands.is_a?(Array)
|
|
38
|
+
|
|
39
|
+
@name = name&.dup&.freeze
|
|
40
|
+
@commands = commands.each_with_index.map do |command, index|
|
|
41
|
+
command.is_a?(Command) ? command : Command.new(command, source: name, line: index + 1)
|
|
42
|
+
end.freeze
|
|
43
|
+
freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 遍历脚本命令。
|
|
47
|
+
def each(&) = @commands.each(&)
|
|
48
|
+
|
|
49
|
+
# 判断脚本是否没有可执行命令。
|
|
50
|
+
def empty? = @commands.empty?
|
|
51
|
+
|
|
52
|
+
# 返回脚本命令数量。
|
|
53
|
+
def size = @commands.size
|
|
54
|
+
|
|
55
|
+
# 返回脚本名称和命令数量摘要。
|
|
56
|
+
def inspect = "#<#{self.class} name=#{name.inspect} commands=#{size}>"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
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 ScriptOutputLimitExceeded < OutputLimitExceeded; end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,414 @@
|
|
|
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 Session
|
|
9
|
+
MAX_ERROR_OUTPUT_BYTES = 4096
|
|
10
|
+
|
|
11
|
+
attr_reader :configuration, :transport, :reader, :redactor, :state, :prompt
|
|
12
|
+
|
|
13
|
+
# 创建认证、响应读取、脱敏和日志对象,并把会话置于关闭状态。
|
|
14
|
+
def initialize(configuration:, dialogue:, transport:, recovery:, after_login:)
|
|
15
|
+
@configuration = configuration
|
|
16
|
+
@dialogue = dialogue
|
|
17
|
+
@transport = transport
|
|
18
|
+
@recovery = recovery
|
|
19
|
+
@after_login = after_login
|
|
20
|
+
@redactor = Redactor.new(configuration.password, configuration.enable_password)
|
|
21
|
+
@reader = ResponseReader.new(self)
|
|
22
|
+
@authentication = Authentication.new(self, @dialogue)
|
|
23
|
+
@log = Log.new(configuration, redactor: redactor)
|
|
24
|
+
@state = :closed
|
|
25
|
+
@privileged = false
|
|
26
|
+
@lock = Mutex.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def connected? = state != :closed && !transport.closed?
|
|
30
|
+
|
|
31
|
+
def privileged? = @privileged
|
|
32
|
+
|
|
33
|
+
def mark_privileged! = @privileged = true
|
|
34
|
+
|
|
35
|
+
# 记录脚本之外的业务结果,例如设备报告的 TFTP 上传状态。
|
|
36
|
+
def log_event(name, **fields) = @log.log_custom_event(name, **fields)
|
|
37
|
+
|
|
38
|
+
# 业务名称贯穿脚本及其钩子,底层命令另外分配独立序号。
|
|
39
|
+
def log_script(operation:, steps:)
|
|
40
|
+
@log.with_operation_context(operation) do
|
|
41
|
+
started = Expect.monotonic
|
|
42
|
+
@log.log_event("operation_start", level: :debug, phase: :script)
|
|
43
|
+
result = yield
|
|
44
|
+
log_operation_completion(started, result.is_a?(Result) ? result.error : nil, steps: steps.size)
|
|
45
|
+
result
|
|
46
|
+
rescue => error
|
|
47
|
+
failure = normalize_error(error, phase: :script)
|
|
48
|
+
log_operation_completion(started, failure, steps: steps.size)
|
|
49
|
+
raise failure, cause: nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 在会话锁内建立连接。
|
|
54
|
+
def connect
|
|
55
|
+
perform(:connect) { self }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 锁覆盖完整操作而不是单次写入,保证批处理命令不会交错。
|
|
59
|
+
def perform(phase, &)
|
|
60
|
+
if @operation_owner == [Thread.current, Fiber.current] && !@performing
|
|
61
|
+
return perform_locked(phase, &)
|
|
62
|
+
end
|
|
63
|
+
unless @lock.try_lock
|
|
64
|
+
raise build_error(SessionBusy, "session already belongs to another operation", phase: phase), cause: nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
perform_locked(phase, &)
|
|
69
|
+
ensure
|
|
70
|
+
@lock.unlock
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 业务操作在多次脚本之间持有租约;只有同一线程和 Fiber 可顺序使用。
|
|
75
|
+
def with_operation(phase)
|
|
76
|
+
unless @lock.try_lock
|
|
77
|
+
raise build_error(SessionBusy, "session already belongs to another operation", phase: phase), cause: nil
|
|
78
|
+
end
|
|
79
|
+
@operation_owner = [Thread.current, Fiber.current]
|
|
80
|
+
result = nil
|
|
81
|
+
begin
|
|
82
|
+
@log.with_operation_context(phase) { result = yield }
|
|
83
|
+
rescue Error => error
|
|
84
|
+
raise unless result.is_a?(Result)
|
|
85
|
+
|
|
86
|
+
# 日志收尾失败不能抹去已完成的设备步骤,也不能覆盖原业务错误。
|
|
87
|
+
Result.new(steps: result.steps, config: result.config,
|
|
88
|
+
error: result.error || normalize_error(error, phase: phase))
|
|
89
|
+
ensure
|
|
90
|
+
@operation_owner = nil
|
|
91
|
+
@lock.unlock
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 文件业务必须先取得路径锁,再调用会话;不能持有会话租约去等待另一个备份者。
|
|
96
|
+
def assert_path_lock_order!(phase)
|
|
97
|
+
return unless @lock.locked?
|
|
98
|
+
|
|
99
|
+
raise build_error(SessionBusy, "backup path ownership must be acquired before a session operation", phase: phase), cause: nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# 独占关闭会话;已有操作占用时返回会话繁忙错误。
|
|
103
|
+
def close
|
|
104
|
+
unless @lock.try_lock
|
|
105
|
+
raise build_error(SessionBusy, "cannot close a session owned by another operation", phase: :close), cause: nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
close_resources
|
|
110
|
+
ensure
|
|
111
|
+
@lock.unlock
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 执行特权认证,并更新提示符及特权状态。
|
|
116
|
+
def enable(command, prompt)
|
|
117
|
+
unless command
|
|
118
|
+
raise build_error(UnsupportedOperation, "privilege authentication is not supported", phase: :enable), cause: nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@privileged = false
|
|
122
|
+
response = @log.pause { @authentication.enable(command, prompt) }
|
|
123
|
+
@prompt = response.prompt
|
|
124
|
+
@privileged = true
|
|
125
|
+
response
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# 命令准备、交换、后处理及回调共用一份临时词表,退出时一并清除。
|
|
129
|
+
def with_command_redaction(command)
|
|
130
|
+
redactor.with_scope do
|
|
131
|
+
protect_command(command)
|
|
132
|
+
yield
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# 批次准备和最终清理也可能读取配置;在作用域退出前归一化其异常,
|
|
137
|
+
# 防止清理钩子的消息或回溯将已完成步骤中的正文带回诊断通道。
|
|
138
|
+
def with_sensitive_output(enabled)
|
|
139
|
+
return yield unless enabled
|
|
140
|
+
|
|
141
|
+
redactor.with_scope do
|
|
142
|
+
redactor.output_sensitive!
|
|
143
|
+
yield
|
|
144
|
+
rescue => error
|
|
145
|
+
raise normalize_error(error, phase: :script), cause: nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# 复用完整命令的脱敏范围;厂商后续查询产生的秘密保留到外层回调结束。
|
|
150
|
+
def execute_command(command, timeout:, prompt: nil)
|
|
151
|
+
redactor.with_scope(reuse: true) do
|
|
152
|
+
completed_response = nil
|
|
153
|
+
protect_command(command)
|
|
154
|
+
@log.with_command_context(command) do
|
|
155
|
+
execute_command_with_logging(command, timeout: timeout, prompt: prompt) do |response|
|
|
156
|
+
completed_response = response
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
ensure
|
|
160
|
+
# 日志或上下文收尾失败不能抹去完整响应;失败探测也要在词表恢复前通知执行器。
|
|
161
|
+
yield completed_response if block_given?
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# 按截止时间限制传输写入,并统一包装底层错误。
|
|
166
|
+
def write(bytes, deadline:, phase:, command: nil)
|
|
167
|
+
remaining = [deadline - Expect.monotonic, 0].max
|
|
168
|
+
transport.write(bytes, timeout: [remaining, configuration.write_timeout].min)
|
|
169
|
+
rescue => error
|
|
170
|
+
raise normalize_error(error, phase: phase, command: command), cause: nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# 人工交互会结束当前连接,未完成的输入行不能在之后重放。
|
|
174
|
+
def interact(**options)
|
|
175
|
+
perform(:interact) do
|
|
176
|
+
@state = :interacting
|
|
177
|
+
failed = false
|
|
178
|
+
begin
|
|
179
|
+
transport.interact(**options)
|
|
180
|
+
rescue Exception # rubocop:disable Lint/RescueException -- Preserve this interaction's failure, not a caller's rescued exception.
|
|
181
|
+
failed = true
|
|
182
|
+
raise
|
|
183
|
+
ensure
|
|
184
|
+
failed ? close_preserving_failure : close_resources
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# 创建带脱敏上下文、阶段、命令和输出尾部的领域错误。
|
|
190
|
+
def build_error(klass, message, phase:, command: nil, output: "".b, underlying: nil, **context)
|
|
191
|
+
sensitive = redactor.sensitive? || command&.sensitive? || command&.output_sensitive?
|
|
192
|
+
message = safe_error_text(message, sensitive: sensitive)
|
|
193
|
+
output = safe_error_text(output, sensitive: sensitive)
|
|
194
|
+
klass.new(message, host: configuration.host, phase: phase,
|
|
195
|
+
command: command && (command.sensitive? ? "[REDACTED]" : redactor.call(command.text)),
|
|
196
|
+
source: command&.source, line: command&.line,
|
|
197
|
+
output: truncate_output(output),
|
|
198
|
+
underlying: underlying && UnderlyingError.new(underlying, redactor, sensitive: sensitive), **context)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# 将底层异常映射为连接、传输、超时或内部错误,并保留安全上下文。
|
|
202
|
+
def normalize_error(exception, phase:, command: nil)
|
|
203
|
+
if exception.is_a?(Error)
|
|
204
|
+
sensitive = redactor.sensitive? || command&.sensitive? || command&.output_sensitive?
|
|
205
|
+
failed_command = exception.command || command&.text
|
|
206
|
+
if command&.sensitive? || (sensitive && exception.command && exception.command != command&.text)
|
|
207
|
+
failed_command = "[REDACTED]"
|
|
208
|
+
end
|
|
209
|
+
message = safe_error_text(exception.message, sensitive: sensitive)
|
|
210
|
+
output = safe_error_text(exception.output, sensitive: sensitive)
|
|
211
|
+
return exception.with_diagnostics(
|
|
212
|
+
message: message,
|
|
213
|
+
code: ErrorMetadata.code(exception.code) || ErrorMetadata.default_code(exception.class.name),
|
|
214
|
+
host: configuration.host, phase: ErrorMetadata.phase(exception.phase) || ErrorMetadata.phase(phase),
|
|
215
|
+
command: failed_command && redactor.call(failed_command),
|
|
216
|
+
source: command ? command.source && redactor.call(command.source) : exception.source && safe_error_text(exception.source, sensitive: sensitive),
|
|
217
|
+
line: command ? command.line : diagnostic_line(exception.line),
|
|
218
|
+
output: truncate_output(output),
|
|
219
|
+
underlying: exception.underlying && UnderlyingError.new(exception.underlying, redactor, sensitive: sensitive)
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
klass = case exception
|
|
224
|
+
when Expect::WriteTimeout then WriteTimeout
|
|
225
|
+
when IOError, SystemCallError, Expect::SpawnError then TransportError
|
|
226
|
+
else InternalError
|
|
227
|
+
end
|
|
228
|
+
build_error(klass, "#{phase} failed: #{exception.class}", phase: phase, command: command, underlying: exception)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def inspect = "#<#{self.class} state=#{state} host=#{configuration.host.inspect}>"
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def diagnostic_line(value)
|
|
236
|
+
value if value.is_a?(Integer) && value.positive?
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# 一次操作从连接到结果处理始终持锁;异常或 throw 中断都关闭未完成会话。
|
|
240
|
+
def perform_locked(phase)
|
|
241
|
+
@performing = true
|
|
242
|
+
completed = false
|
|
243
|
+
begin
|
|
244
|
+
unless connected?
|
|
245
|
+
close_resources unless state == :closed
|
|
246
|
+
connect_session
|
|
247
|
+
end
|
|
248
|
+
@state = (phase == :connect) ? :ready : :executing
|
|
249
|
+
result = yield
|
|
250
|
+
completed = true
|
|
251
|
+
result
|
|
252
|
+
rescue => error
|
|
253
|
+
failure = normalize_error(error, phase: phase)
|
|
254
|
+
close_preserving_failure
|
|
255
|
+
raise failure, cause: nil
|
|
256
|
+
rescue Exception # rubocop:disable Lint/RescueException -- Interrupts release the transport, then propagate.
|
|
257
|
+
close_preserving_failure
|
|
258
|
+
raise
|
|
259
|
+
ensure
|
|
260
|
+
# throw/catch 也会离开此块;对话未完成时必须关闭连接,不能把
|
|
261
|
+
# 半途的设备提示符误当成下一条命令的响应。
|
|
262
|
+
close_preserving_failure unless completed || !connected?
|
|
263
|
+
@state = :ready if connected?
|
|
264
|
+
@performing = false
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def execute_command_with_logging(command, timeout:, prompt:)
|
|
269
|
+
started = Expect.monotonic
|
|
270
|
+
@log.log_event("command_start")
|
|
271
|
+
sensitive_dialogue = [*command.interactions, *@dialogue.command_interactions].any?(&:sensitive?)
|
|
272
|
+
private_output = command.sensitive? || redactor.output_sensitive? || sensitive_dialogue
|
|
273
|
+
@log.log_event("device_output", level: :debug) if @log.debug? && !private_output
|
|
274
|
+
response = exchange_command(command, timeout: timeout, prompt: prompt)
|
|
275
|
+
yield response
|
|
276
|
+
@log.log_response_output(response.raw) unless private_output
|
|
277
|
+
details = { duration_ms: elapsed_ms(started), response_bytes: response.raw.bytesize }
|
|
278
|
+
@log.log_event("command_complete", status: "response_received", **details)
|
|
279
|
+
response
|
|
280
|
+
rescue => error
|
|
281
|
+
failure = normalize_error(error, phase: :command, command: command)
|
|
282
|
+
log_failure_preserving_error("command_complete", failure, status: "failed", duration_ms: elapsed_ms(started), phase: :command)
|
|
283
|
+
raise failure, cause: nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def elapsed_ms(started) = ((Expect.monotonic - started) * 1000).round
|
|
287
|
+
|
|
288
|
+
def log_operation_completion(started, failure, steps: nil)
|
|
289
|
+
fields = { status: failure ? "failed" : "completed", duration_ms: elapsed_ms(started), steps: steps,
|
|
290
|
+
phase: :script }
|
|
291
|
+
if failure
|
|
292
|
+
log_failure_preserving_error("operation_complete", failure, **fields, failed_command: failure.command)
|
|
293
|
+
else
|
|
294
|
+
@log.log_event("operation_complete", **fields)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# 记录原失败时日志也可能损坏;次生故障不能改变错误码、恢复判定或设备结果。
|
|
299
|
+
def log_failure_preserving_error(name, failure, **fields)
|
|
300
|
+
@log.log_failure(name, failure, **fields)
|
|
301
|
+
rescue Error
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# 在用户钩子运行前标记敏感上下文,动态交互尚未返回时也能保护其异常。
|
|
306
|
+
def protect_command(command)
|
|
307
|
+
redactor.output_sensitive! if command.output_sensitive?
|
|
308
|
+
if command.sensitive? || [*command.interactions, *@dialogue.command_interactions].any?(&:sensitive?)
|
|
309
|
+
redactor.sensitive!
|
|
310
|
+
end
|
|
311
|
+
redactor.remember(command.text) if command.sensitive?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# 写入命令、读取对话、识别设备诊断,并返回响应对象。
|
|
315
|
+
def exchange_command(command, timeout:, prompt: nil)
|
|
316
|
+
deadline = Expect.monotonic + (command.timeout || timeout)
|
|
317
|
+
interactions = [*command.interactions, *@dialogue.command_interactions]
|
|
318
|
+
operation = lambda do
|
|
319
|
+
write("#{command.text}\n", deadline: deadline, phase: :command, command: command)
|
|
320
|
+
response = reader.read(prompt: command.prompt || prompt || @dialogue.command_prompt,
|
|
321
|
+
interactions: interactions,
|
|
322
|
+
deadline: deadline, phase: :command, command: command)
|
|
323
|
+
@prompt = response.prompt
|
|
324
|
+
if (diagnostic = @dialogue.diagnostic(response.raw))
|
|
325
|
+
raise build_error(DeviceError, diagnostic, phase: :command, command: command, output: response.raw), cause: nil
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
response
|
|
329
|
+
end
|
|
330
|
+
if command.sensitive? || redactor.output_sensitive? || interactions.any?(&:sensitive?)
|
|
331
|
+
@log.pause(&operation)
|
|
332
|
+
else
|
|
333
|
+
operation.call
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# 敏感上下文的任意异常和设备输出可能只包含局部秘密,保留类型和错误码诊断。
|
|
338
|
+
def safe_error_text(text, sensitive:)
|
|
339
|
+
sensitive && !text.to_s.empty? ? "[REDACTED]".b : redactor.call(text)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# 必须先脱敏再截取尾部,避免截断凭据后逃过完整词匹配。
|
|
343
|
+
def truncate_output(text)
|
|
344
|
+
text.byteslice(-MAX_ERROR_OUTPUT_BYTES, MAX_ERROR_OUTPUT_BYTES) || text
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# 建立传输并登录;失败时最多执行一次连接级恢复,然后重试登录。
|
|
348
|
+
def connect_session
|
|
349
|
+
attempted_recovery = false
|
|
350
|
+
begin
|
|
351
|
+
@state = :connecting
|
|
352
|
+
started = Expect.monotonic
|
|
353
|
+
login_once(started)
|
|
354
|
+
rescue => error
|
|
355
|
+
phase = (@state == :connecting) ? :connect : :login
|
|
356
|
+
failure = normalize_error(error, phase: phase)
|
|
357
|
+
log_failure_preserving_error(phase == :connect ? "connect_failed" : "login_complete", failure,
|
|
358
|
+
status: "failed", phase: phase, duration_ms: elapsed_ms(started))
|
|
359
|
+
close_preserving_failure
|
|
360
|
+
replacement = attempted_recovery ? nil : @recovery.recover(failure, transport)
|
|
361
|
+
if replacement
|
|
362
|
+
@transport = replacement
|
|
363
|
+
attempted_recovery = true
|
|
364
|
+
retry
|
|
365
|
+
end
|
|
366
|
+
raise failure, cause: nil
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def login_once(started)
|
|
371
|
+
redactor.reset
|
|
372
|
+
@log.open(transport)
|
|
373
|
+
@log.log_event("connect", host: configuration.host, username: configuration.username,
|
|
374
|
+
phase: :connect, protocol: transport.respond_to?(:protocol) ? transport.protocol : configuration.protocol)
|
|
375
|
+
transport.open
|
|
376
|
+
@state = :authenticating
|
|
377
|
+
@log.log_event("login_start", level: :debug, phase: :login)
|
|
378
|
+
response = @authentication.login
|
|
379
|
+
transport.authenticated if transport.respond_to?(:authenticated)
|
|
380
|
+
safe_response = Response.new(raw: redactor.call(response.raw), output: redactor.call(response.output),
|
|
381
|
+
prompt: response.prompt)
|
|
382
|
+
@prompt = response.prompt
|
|
383
|
+
@after_login.call(self, safe_response)
|
|
384
|
+
@log.log_authentication_output(response.raw)
|
|
385
|
+
@log.log_event("login_complete", status: "ok", prompt: response.prompt, phase: :login, duration_ms: elapsed_ms(started))
|
|
386
|
+
@log.attach
|
|
387
|
+
@state = :ready
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# 清除会话状态并关闭传输和日志资源。
|
|
391
|
+
def close_resources
|
|
392
|
+
@state = :closed
|
|
393
|
+
@privileged = false
|
|
394
|
+
@prompt = nil
|
|
395
|
+
begin
|
|
396
|
+
transport.close
|
|
397
|
+
ensure
|
|
398
|
+
@log.close
|
|
399
|
+
end
|
|
400
|
+
nil
|
|
401
|
+
rescue => error
|
|
402
|
+
raise normalize_error(error, phase: :close), cause: nil
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# 尝试清理资源但不覆盖原始操作异常。
|
|
406
|
+
def close_preserving_failure
|
|
407
|
+
close_resources
|
|
408
|
+
rescue
|
|
409
|
+
# 所有清理路径完成后,原始操作异常仍然是权威结果。
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|