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,225 @@
|
|
|
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 Profile
|
|
11
|
+
DEFAULT_PAGER_PATTERN = /(?:\A|(?<=[\r\n]))[ \t]*(?:--More--|---- More ----)[ \t\x00\x08]*(?=\r|\n|\z)/i.freeze
|
|
12
|
+
DEFAULT_AUTHENTICATION_ERROR_PATTERNS = [
|
|
13
|
+
/(?:authentication failed|permission denied|access denied|login incorrect)/i
|
|
14
|
+
].freeze
|
|
15
|
+
DEFAULT_PASSWORD_PROMPT = /password:\s*\z/i.freeze
|
|
16
|
+
DEFAULT_USERNAME_PROMPT = /(?:login|username|name):\s*\z/i.freeze
|
|
17
|
+
DEFAULT_PRIVILEGE_PROMPT = /#\s*\z/.freeze
|
|
18
|
+
DEFAULT_LEGACY_SSH_ARGUMENTS = ["-1"].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :config_commands, :save_commands,
|
|
21
|
+
:login_prompt, :command_prompt, :username_prompt, :password_prompt,
|
|
22
|
+
:privilege_prompt, :pager_pattern, :pager_response,
|
|
23
|
+
:authentication_error_patterns, :command_error_patterns,
|
|
24
|
+
:login_interactions, :confirmation_interactions, :legacy_ssh_arguments,
|
|
25
|
+
:privilege_command, :command_timeout, :terminal_size, :tftp_strategy, :topology_strategy, :running_config_strategy
|
|
26
|
+
|
|
27
|
+
# 使用默认的公共规则创建空设备档案;登录和命令提示符仍由厂商声明。
|
|
28
|
+
def self.default
|
|
29
|
+
@default ||= new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 在继承档案的基础上执行一次声明块,生成新的不可变设备档案。
|
|
33
|
+
def self.define(parent: nil, &definition)
|
|
34
|
+
raise ArgumentError, "profile requires a block" unless definition
|
|
35
|
+
|
|
36
|
+
builder = Builder.new(parent || default)
|
|
37
|
+
builder.instance_eval(&definition)
|
|
38
|
+
builder.build
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 校验并冻结设备命令、提示符、交互和连接参数。
|
|
42
|
+
def initialize(config_commands: nil, save_commands: [],
|
|
43
|
+
login_prompt: nil, command_prompt: nil,
|
|
44
|
+
username_prompt: DEFAULT_USERNAME_PROMPT, password_prompt: DEFAULT_PASSWORD_PROMPT,
|
|
45
|
+
privilege_prompt: DEFAULT_PRIVILEGE_PROMPT,
|
|
46
|
+
pager_pattern: DEFAULT_PAGER_PATTERN, pager_response: " ",
|
|
47
|
+
authentication_error_patterns: DEFAULT_AUTHENTICATION_ERROR_PATTERNS,
|
|
48
|
+
command_error_patterns: [], login_interactions: [], confirmation_interactions: [],
|
|
49
|
+
legacy_ssh_arguments: DEFAULT_LEGACY_SSH_ARGUMENTS, privilege_command: nil,
|
|
50
|
+
command_timeout: 25, terminal_size: nil, tftp_strategy: nil, topology_strategy: nil, running_config_strategy: nil)
|
|
51
|
+
@config_commands = command_list(config_commands, :config_commands, allow_nil: true)
|
|
52
|
+
@save_commands = command_list(save_commands, :save_commands)
|
|
53
|
+
@login_prompt = pattern(login_prompt, :login_prompt, allow_nil: true)
|
|
54
|
+
@command_prompt = pattern(command_prompt, :command_prompt, allow_nil: true)
|
|
55
|
+
@username_prompt = pattern(username_prompt, :username_prompt)
|
|
56
|
+
@password_prompt = pattern(password_prompt, :password_prompt)
|
|
57
|
+
@privilege_prompt = pattern(privilege_prompt, :privilege_prompt)
|
|
58
|
+
@pager_pattern = pattern(pager_pattern, :pager_pattern)
|
|
59
|
+
@pager_response = response_text(pager_response, :pager_response)
|
|
60
|
+
@authentication_error_patterns = patterns(authentication_error_patterns, :authentication_error_patterns)
|
|
61
|
+
@command_error_patterns = patterns(command_error_patterns, :command_error_patterns)
|
|
62
|
+
@login_interactions = interactions(login_interactions, :login_interactions)
|
|
63
|
+
@confirmation_interactions = interactions(confirmation_interactions, :confirmation_interactions)
|
|
64
|
+
@legacy_ssh_arguments = strings(legacy_ssh_arguments, :legacy_ssh_arguments)
|
|
65
|
+
@privilege_command = optional_text(privilege_command, :privilege_command)
|
|
66
|
+
@command_timeout = duration(command_timeout, :command_timeout)
|
|
67
|
+
@terminal_size = validate_terminal_size!(terminal_size)
|
|
68
|
+
@running_config_strategy = strategy_class(running_config_strategy, :running_config_strategy,
|
|
69
|
+
%i[clean result_step prompt_text validate_response!])
|
|
70
|
+
@tftp_strategy = strategy_class(tftp_strategy, :tftp_strategy,
|
|
71
|
+
%i[resolve_source_file default_path remote_path script device_reported_complete? validate_options! receipt_metadata])
|
|
72
|
+
if @tftp_strategy && !@tftp_strategy.respond_to?(:filename)
|
|
73
|
+
raise ArgumentError, "tftp_strategy must expose filename"
|
|
74
|
+
end
|
|
75
|
+
@topology_strategy = strategy_class(topology_strategy, :topology_strategy,
|
|
76
|
+
%i[neighbor_command neighbor_template expected_neighbor_count empty_neighbor_output?
|
|
77
|
+
protocol description_template interface_key configuration_interface
|
|
78
|
+
decode_description validate_descriptions! change_error_code supports?
|
|
79
|
+
enter_configuration_command change_commands script_command
|
|
80
|
+
leave_configuration_commands verification_commands persistence_commands persistence_confirmed?])
|
|
81
|
+
if @topology_strategy && !@topology_strategy.respond_to?(:supports?)
|
|
82
|
+
raise ArgumentError, "topology_strategy must expose supports?"
|
|
83
|
+
end
|
|
84
|
+
freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 返回一份供子类声明块继续编辑的关键字参数。
|
|
88
|
+
def to_h
|
|
89
|
+
{
|
|
90
|
+
config_commands: config_commands,
|
|
91
|
+
save_commands: save_commands,
|
|
92
|
+
login_prompt: login_prompt,
|
|
93
|
+
command_prompt: command_prompt,
|
|
94
|
+
username_prompt: username_prompt,
|
|
95
|
+
password_prompt: password_prompt,
|
|
96
|
+
privilege_prompt: privilege_prompt,
|
|
97
|
+
pager_pattern: pager_pattern,
|
|
98
|
+
pager_response: pager_response,
|
|
99
|
+
authentication_error_patterns: authentication_error_patterns,
|
|
100
|
+
command_error_patterns: command_error_patterns,
|
|
101
|
+
login_interactions: login_interactions,
|
|
102
|
+
confirmation_interactions: confirmation_interactions,
|
|
103
|
+
legacy_ssh_arguments: legacy_ssh_arguments,
|
|
104
|
+
privilege_command: privilege_command,
|
|
105
|
+
command_timeout: command_timeout,
|
|
106
|
+
terminal_size: terminal_size,
|
|
107
|
+
tftp_strategy: tftp_strategy,
|
|
108
|
+
topology_strategy: topology_strategy,
|
|
109
|
+
running_config_strategy: running_config_strategy
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# 返回档案中最有用的设备规则摘要。
|
|
114
|
+
def inspect
|
|
115
|
+
"#<#{self.class} commands=#{config_commands&.size || 0} " \
|
|
116
|
+
"save_commands=#{save_commands.size}>"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# 校验静态接口,不构造策略实例或触发设备 I/O。
|
|
122
|
+
def strategy_class(value, name, required_methods)
|
|
123
|
+
return nil if value.nil?
|
|
124
|
+
unless value.is_a?(Class) && (required_methods - value.public_instance_methods).empty?
|
|
125
|
+
raise ArgumentError, "#{name} must be a strategy class implementing #{required_methods.join(", ")}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
value
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# 校验单条命令数组并复制为深度不可变值。
|
|
132
|
+
def command_list(value, name, allow_nil: false)
|
|
133
|
+
return nil if allow_nil && value.nil?
|
|
134
|
+
unless value.is_a?(Array)
|
|
135
|
+
raise ArgumentError, "#{name} must be an Array of command strings"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
value.map do |command|
|
|
139
|
+
unless command.is_a?(String) && !command.strip.empty? && !command.match?(/[\r\n\x00]/)
|
|
140
|
+
raise ArgumentError, "#{name} must contain nonempty single-line commands"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
command.dup.freeze
|
|
144
|
+
end.freeze
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# 校验提示模式并复制为不可变正则表达式。
|
|
148
|
+
def pattern(value, name, allow_nil: false)
|
|
149
|
+
return nil if allow_nil && value.nil?
|
|
150
|
+
unless value.is_a?(Regexp) && !value.match?("")
|
|
151
|
+
raise ArgumentError, "#{name} must be a nonempty Regexp"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
value.dup.freeze
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# 校验正则表达式列表并冻结每一项。
|
|
158
|
+
def patterns(values, name)
|
|
159
|
+
unless values.is_a?(Array)
|
|
160
|
+
raise ArgumentError, "#{name} must be an Array of Regexp objects"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
values.map { |value| pattern(value, name) }.freeze
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 校验交互对象列表并冻结列表。
|
|
167
|
+
def interactions(values, name)
|
|
168
|
+
unless values.is_a?(Array) && values.all?(Interaction)
|
|
169
|
+
raise ArgumentError, "#{name} must be an Array of Interaction objects"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
values.dup.freeze
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# 校验并复制字符串数组。
|
|
176
|
+
def strings(values, name)
|
|
177
|
+
unless values.is_a?(Array) && values.all?(String)
|
|
178
|
+
raise ArgumentError, "#{name} must be an Array of strings"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
values.map { |value| value.dup.freeze }.freeze
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# 校验可选单行文本。
|
|
185
|
+
def optional_text(value, name)
|
|
186
|
+
return if value.nil?
|
|
187
|
+
text(value, name)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# 校验并复制单行文本。
|
|
191
|
+
def text(value, name)
|
|
192
|
+
unless value.is_a?(String) && !value.match?(/[\r\n\x00]/)
|
|
193
|
+
raise ArgumentError, "#{name} must be a single-line String"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
value.dup.freeze
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# 校验并复制分页或交互响应;响应可以包含换行。
|
|
200
|
+
def response_text(value, name)
|
|
201
|
+
raise ArgumentError, "#{name} must be a String" unless value.is_a?(String)
|
|
202
|
+
|
|
203
|
+
value.dup.freeze
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# 校验默认命令超时。
|
|
207
|
+
def duration(value, name)
|
|
208
|
+
result = Expect.duration(value)
|
|
209
|
+
raise ArgumentError, "#{name} must be finite" unless result
|
|
210
|
+
|
|
211
|
+
result
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# 校验终端宽高并冻结数组。
|
|
215
|
+
def validate_terminal_size!(value)
|
|
216
|
+
return if value.nil?
|
|
217
|
+
unless value.is_a?(Array) && value.size == 2 && value.all? { |dimension| dimension.is_a?(Integer) && dimension.positive? }
|
|
218
|
+
raise ArgumentError, "terminal_size must contain two positive Integers"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
value.dup.freeze
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
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
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
# 只选择一次连接级恢复;任何设备命令都不会重放。
|
|
8
|
+
class Recovery
|
|
9
|
+
# 只有明确允许的 SSH 拒绝才会建立一次 Telnet 连接。
|
|
10
|
+
class TelnetFallback
|
|
11
|
+
# 将连接被拒绝转换为 Telnet 传输,否则不提供恢复。
|
|
12
|
+
def recover(error, transport)
|
|
13
|
+
transport.as_telnet if error.code == :connection_refused && transport.respond_to?(:as_telnet)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 主机密钥替换仅限调用方指定的 known_hosts 文件。
|
|
18
|
+
class HostKeyReplacement
|
|
19
|
+
# 删除旧密钥并返回原传输对象,其他错误不触发替换。
|
|
20
|
+
def recover(error, transport)
|
|
21
|
+
return unless error.code == :host_key_changed && transport.respond_to?(:replace_host_key)
|
|
22
|
+
|
|
23
|
+
transport.replace_host_key
|
|
24
|
+
transport
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 旧版协商参数来自厂商配置,不能由设备输出决定。
|
|
29
|
+
class LegacySsh
|
|
30
|
+
# 保存厂商提供的旧版参数。
|
|
31
|
+
def initialize(arguments)
|
|
32
|
+
@arguments = arguments
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 根据明确错误码创建一次旧版 SSH 传输。
|
|
36
|
+
def recover(error, transport)
|
|
37
|
+
return unless transport.respond_to?(:with_legacy)
|
|
38
|
+
|
|
39
|
+
arguments = { rsa_too_small: @arguments, unsupported_cipher: ["-c", "des"] }[error.code]
|
|
40
|
+
transport.with_legacy(arguments) if arguments
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 按配置顺序建立可用的连接级恢复规则。
|
|
45
|
+
def initialize(configuration, legacy_arguments:)
|
|
46
|
+
@rules = []
|
|
47
|
+
@rules << TelnetFallback.new if configuration.telnet_fallback?
|
|
48
|
+
@rules << HostKeyReplacement.new if configuration.host_key_policy == :replace
|
|
49
|
+
@rules << LegacySsh.new(legacy_arguments) if configuration.legacy_ssh?
|
|
50
|
+
@rules.freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 依次尝试恢复规则,并只返回第一个新传输对象。
|
|
54
|
+
def recover(error, transport)
|
|
55
|
+
@rules.lazy.filter_map { |rule| rule.recover(error, transport) }.first
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
# 管理连接器的秘密作用域和输出敏感性;字节过滤统一委托给 expect-pty。
|
|
10
|
+
class Redactor
|
|
11
|
+
MASK = "[REDACTED]".b.freeze
|
|
12
|
+
|
|
13
|
+
# 保存初始凭据,并建立第一版脱敏词表。
|
|
14
|
+
def initialize(*secrets)
|
|
15
|
+
unless Expect.constants(false).include?(:Redactor) && Expect::Redactor.respond_to?(:redact)
|
|
16
|
+
raise LoadError, "netstack requires expect-pty with the public Expect::Redactor API"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@configured = secrets.compact.map(&:b).map(&:freeze).freeze
|
|
20
|
+
reset
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 重置动态词表,同时保留配置中的固定凭据。
|
|
24
|
+
def reset
|
|
25
|
+
@secrets = []
|
|
26
|
+
@patterns = nil
|
|
27
|
+
@scoped = false
|
|
28
|
+
@sensitive = false
|
|
29
|
+
@output_sensitive = false
|
|
30
|
+
@configured.each { |secret| remember(secret) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 临时命令响应在错误归一化期间保留,离开作用域后不进入长期会话。
|
|
34
|
+
def with_scope(reuse: false)
|
|
35
|
+
return yield if reuse && @scoped
|
|
36
|
+
|
|
37
|
+
previous = @secrets
|
|
38
|
+
previous_patterns = @patterns
|
|
39
|
+
previous_scoped = @scoped
|
|
40
|
+
previous_sensitive = @sensitive
|
|
41
|
+
previous_output_sensitive = @output_sensitive
|
|
42
|
+
@secrets = previous.dup
|
|
43
|
+
@scoped = true
|
|
44
|
+
begin
|
|
45
|
+
yield
|
|
46
|
+
ensure
|
|
47
|
+
@secrets = previous
|
|
48
|
+
@patterns = previous_patterns
|
|
49
|
+
@scoped = previous_scoped
|
|
50
|
+
@sensitive = previous_sensitive
|
|
51
|
+
@output_sensitive = previous_output_sensitive
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 敏感命令和交互的异常可能只引用秘密的一部分,不能只依赖完整词匹配。
|
|
56
|
+
def sensitive! = @sensitive = true
|
|
57
|
+
|
|
58
|
+
# 判断当前脱敏范围是否包含敏感交互。
|
|
59
|
+
def sensitive? = @sensitive
|
|
60
|
+
|
|
61
|
+
# 输出敏感时不把正文加入词表;只在当前范围内屏蔽日志和任意错误正文。
|
|
62
|
+
def output_sensitive!
|
|
63
|
+
@output_sensitive = true
|
|
64
|
+
sensitive!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 后续查询继承输出边界,即使查询命令本身没有敏感标记。
|
|
68
|
+
def output_sensitive? = @output_sensitive
|
|
69
|
+
|
|
70
|
+
# 只维护当前作用域的注册词;匹配次序和重叠区间交给共享过滤器。
|
|
71
|
+
def remember(secret)
|
|
72
|
+
return if secret.nil? || secret.empty?
|
|
73
|
+
|
|
74
|
+
bytes = secret.b
|
|
75
|
+
return if @secrets.include?(bytes)
|
|
76
|
+
|
|
77
|
+
@secrets << bytes.freeze
|
|
78
|
+
@patterns = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 一次性诊断不共享流状态;原标记作为不透明区间,避免再次展开它。
|
|
82
|
+
def call(text)
|
|
83
|
+
Expect::Redactor.redact(text.to_s.b, patterns, replacement: MASK)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# 每个日志目标独占过滤流,跨分片尾部和重叠掩码由依赖库维护。
|
|
87
|
+
def stream
|
|
88
|
+
Expect::Redactor.new(patterns, replacement: MASK)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 临时范围恢复时,日志流可以同步恢复注册词表,不缓存配置正文。
|
|
92
|
+
def patterns
|
|
93
|
+
@patterns ||= (@secrets.empty? ? [] : (@secrets + [MASK]).uniq).freeze
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 返回脱敏器类型摘要。
|
|
97
|
+
def inspect = "#<#{self.class}>"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
require "expect/pty"
|
|
6
|
+
|
|
7
|
+
module Netstack
|
|
8
|
+
module Connector
|
|
9
|
+
# 在厂商清理前保留原始字节,供诊断和日志使用。
|
|
10
|
+
class Response
|
|
11
|
+
attr_reader :raw, :output, :prompt
|
|
12
|
+
|
|
13
|
+
# 保存原始响应、清理后输出和命令结束提示,并冻结三者。
|
|
14
|
+
def initialize(raw:, output:, prompt:)
|
|
15
|
+
@raw = raw.freeze
|
|
16
|
+
@output = output.freeze
|
|
17
|
+
@prompt = prompt.freeze
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 返回响应大小摘要,不展开原始内容。
|
|
22
|
+
def inspect = "#<#{self.class} bytes=#{raw.bytesize}>"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
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 ResponseReader
|
|
11
|
+
# 保存会话引用,以便读取器使用传输、配置和脱敏上下文。
|
|
12
|
+
def initialize(session)
|
|
13
|
+
@session = session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 读取到结束提示、失败模式或超时,并处理交互响应和输出上限。
|
|
17
|
+
def read(prompt:, interactions:, phase:, timeout: nil, deadline: nil, command: nil, failures: {})
|
|
18
|
+
deadline ||= Expect.monotonic + timeout
|
|
19
|
+
patterns = [*failures.keys, *interactions.map(&:pattern), prompt]
|
|
20
|
+
raw = []
|
|
21
|
+
output = []
|
|
22
|
+
counts = Hash.new(0)
|
|
23
|
+
bytes = 0
|
|
24
|
+
loop do
|
|
25
|
+
event = read_event(patterns, deadline)
|
|
26
|
+
raw << event.before unless event.before.empty?
|
|
27
|
+
output << event.before unless event.before.empty?
|
|
28
|
+
bytes += event.before.bytesize + event.match.bytesize
|
|
29
|
+
validate_event!(event, bytes, raw, phase, command)
|
|
30
|
+
|
|
31
|
+
if event.index == patterns.size
|
|
32
|
+
raw << event.match
|
|
33
|
+
output << event.match
|
|
34
|
+
elsif event.index == patterns.size - 1
|
|
35
|
+
return finish_response(event.match, raw, output, phase, command)
|
|
36
|
+
elsif event.index < failures.size
|
|
37
|
+
raise build_connection_error(failures.values.fetch(event.index), phase, command), cause: nil
|
|
38
|
+
else
|
|
39
|
+
reply = interactions.fetch(event.index - failures.size)
|
|
40
|
+
raw << event.match
|
|
41
|
+
output << event.match if reply.capture?
|
|
42
|
+
respond(reply, event.match, counts, deadline, phase, command)
|
|
43
|
+
end
|
|
44
|
+
next if Expect.monotonic < deadline
|
|
45
|
+
|
|
46
|
+
raise build_read_error(nil, phase, command, raw.join), cause: nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# 分页和流式输出只消耗剩余时间,不为下一次读取重新计时。
|
|
53
|
+
def read_event(patterns, deadline)
|
|
54
|
+
@session.transport.read(patterns, timeout: [deadline - Expect.monotonic, 0].max)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 连接失败模式携带稳定错误码,供连接级恢复规则选择处理方式。
|
|
58
|
+
def build_connection_error(failure, phase, command)
|
|
59
|
+
klass, code = failure
|
|
60
|
+
@session.build_error(klass, "device connection failed (#{code})", phase: phase, code: code, command: command)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 先限制原始字节数,再处理传输失败,避免错误输出绕过总量限制。
|
|
64
|
+
def validate_event!(event, bytes, raw, phase, command)
|
|
65
|
+
if bytes > @session.configuration.max_output_bytes
|
|
66
|
+
raise @session.build_error(OutputLimitExceeded, "device output exceeded max_output_bytes",
|
|
67
|
+
phase: phase, command: command, output: (raw + [event.match]).join), cause: nil
|
|
68
|
+
end
|
|
69
|
+
raise build_read_error(event, phase, command, raw.join), cause: nil unless event.matched?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 完整提示符必须消费字节;交互标记可从业务输出排除,但原始响应始终保留。
|
|
73
|
+
def finish_response(prompt, raw, output, phase, command)
|
|
74
|
+
if prompt.empty?
|
|
75
|
+
raise @session.build_error(PromptError, "prompt pattern did not consume any output",
|
|
76
|
+
phase: phase, command: command, output: raw.join), cause: nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raw << prompt
|
|
80
|
+
output << prompt
|
|
81
|
+
raw_text = raw.join
|
|
82
|
+
Response.new(raw: raw_text, output: (output == raw) ? raw_text : output.join, prompt: prompt)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 重复提示和缺失应答必须在写入前失败;所有应答共用读取操作的截止时间。
|
|
86
|
+
def respond(reply, prompt, counts, deadline, phase, command)
|
|
87
|
+
counts[reply] += 1
|
|
88
|
+
if prompt.empty? || (reply.limit && counts[reply] > reply.limit)
|
|
89
|
+
raise build_interaction_error("response rejected or repeated", phase, command), cause: nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
value = interaction_response(reply, prompt, phase, command)
|
|
93
|
+
raise build_interaction_error("response unavailable", phase, command), cause: nil unless value.is_a?(String)
|
|
94
|
+
|
|
95
|
+
@session.redactor.remember(value.chomp) if reply.sensitive?
|
|
96
|
+
@session.write(value, deadline: deadline, phase: phase, command: command)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 动态口令尚未返回时无法加入词表,须在敏感范围内先归一化回调异常。
|
|
100
|
+
# 成功后由调用方登记响应,临时敏感标记不会影响后续普通命令的诊断。
|
|
101
|
+
def interaction_response(reply, prompt, phase, command)
|
|
102
|
+
return reply.response(prompt) unless reply.sensitive?
|
|
103
|
+
|
|
104
|
+
@session.redactor.with_scope(reuse: true) do
|
|
105
|
+
@session.redactor.sensitive!
|
|
106
|
+
reply.response(prompt)
|
|
107
|
+
rescue => error
|
|
108
|
+
raise @session.normalize_error(error, phase: phase, command: command), cause: nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 将交互拒绝或响应缺失转换为认证或脚本错误。
|
|
113
|
+
def build_interaction_error(message, phase, command)
|
|
114
|
+
authenticating = %i[login enable].include?(phase)
|
|
115
|
+
klass = authenticating ? AuthenticationError : ScriptError
|
|
116
|
+
label = authenticating ? "authentication" : "command interaction"
|
|
117
|
+
@session.build_error(klass, "#{label} #{message}", phase: phase, command: command)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 根据 EOF、提示符、阶段和底层异常选择具体读取错误类型。
|
|
121
|
+
def build_read_error(event, phase, command, output)
|
|
122
|
+
if event&.error.is_a?(Exception)
|
|
123
|
+
return @session.build_error(TransportError, "transport read failed", phase: phase, command: command,
|
|
124
|
+
underlying: event.error, output: output)
|
|
125
|
+
end
|
|
126
|
+
klass = if event&.error == :eof
|
|
127
|
+
ConnectionClosed
|
|
128
|
+
elsif command&.prompt
|
|
129
|
+
PromptError
|
|
130
|
+
elsif %i[login enable].include?(phase)
|
|
131
|
+
LoginTimeout
|
|
132
|
+
else
|
|
133
|
+
CommandTimeout
|
|
134
|
+
end
|
|
135
|
+
@session.build_error(klass, "device response #{event&.error || :timeout}", phase: phase,
|
|
136
|
+
command: command, output: output)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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 Result
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
attr_reader :steps, :error, :config
|
|
12
|
+
|
|
13
|
+
# 校验步骤、错误和配置文本,并冻结结果容器。
|
|
14
|
+
def initialize(steps: [], error: nil, config: nil)
|
|
15
|
+
raise ArgumentError, "error must be a Connector::Error or nil" unless error.nil? || error.is_a?(Error)
|
|
16
|
+
unless steps.is_a?(Array) && steps.all?(CommandResult)
|
|
17
|
+
raise ArgumentError, "steps must be an Array of CommandResult objects"
|
|
18
|
+
end
|
|
19
|
+
raise ArgumentError, "config must be a String or nil" unless config.nil? || config.is_a?(String)
|
|
20
|
+
|
|
21
|
+
@steps = steps.dup.freeze
|
|
22
|
+
@error = error
|
|
23
|
+
@config = config&.dup&.freeze
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 判断执行是否成功。
|
|
28
|
+
def success? = error.nil?
|
|
29
|
+
|
|
30
|
+
# 判断执行是否失败。
|
|
31
|
+
def failure? = !success?
|
|
32
|
+
|
|
33
|
+
# 遍历已完成命令步骤。
|
|
34
|
+
def each(&) = steps.each(&)
|
|
35
|
+
|
|
36
|
+
# 拼接所有已完成步骤的输出字节。
|
|
37
|
+
def output = steps.map(&:output).join.b
|
|
38
|
+
|
|
39
|
+
# 成功时返回配置或输出,失败时重新抛出领域错误。
|
|
40
|
+
def value!
|
|
41
|
+
raise error, cause: nil if failure?
|
|
42
|
+
|
|
43
|
+
config || output
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 返回结果成功状态、步骤数量和错误码摘要。
|
|
47
|
+
def inspect = "#<#{self.class} success=#{success?} steps=#{steps.size} code=#{ErrorMetadata.code(error&.code).inspect}>"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
# 使用终端行编辑规则渲染 H3C、Huawei 和 Radware 配置。
|
|
10
|
+
class Rendered < Strategy
|
|
11
|
+
# 应用终端行编辑规则,还原设备最终显示的配置文本。
|
|
12
|
+
def clean(text) = TerminalRenderer.render(text)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
class Strategy
|
|
9
|
+
# 保存策略所属设备,供提示符和厂商校验使用。
|
|
10
|
+
def initialize(device) = @device = device
|
|
11
|
+
|
|
12
|
+
# 默认保留设备响应原文。
|
|
13
|
+
def clean(text) = text
|
|
14
|
+
|
|
15
|
+
# 默认取脚本最后一个完成步骤作为运行配置。
|
|
16
|
+
def result_step(result) = result.steps.last
|
|
17
|
+
|
|
18
|
+
# 普通采集命令保持视图不变;需要切换视图的采集策略显式调整。
|
|
19
|
+
def prompt_text(_command)
|
|
20
|
+
TerminalRenderer.render(@device.current_prompt.to_s).lines.last.to_s.strip
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 公共策略不增加逐命令响应检查。
|
|
24
|
+
def validate_response!(_command, _response, _execution) end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|