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