net-connector 0.7.2 → 0.8.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 +18 -1
- data/CONTRIBUTING.md +1 -1
- data/README.md +16 -11
- data/docs/RELEASING.md +2 -2
- data/docs/architecture.md +22 -1
- data/lib/net/connector/device/base.rb +7 -1
- data/lib/net/connector/device/tftp.rb +14 -8
- data/lib/net/connector/engine/authentication.rb +1 -0
- data/lib/net/connector/engine/dialogue.rb +0 -132
- data/lib/net/connector/engine/error_metadata.rb +5 -3
- data/lib/net/connector/engine/errors.rb +0 -92
- data/lib/net/connector/engine/known_hosts.rb +14 -1
- data/lib/net/connector/engine/log.rb +17 -1
- data/lib/net/connector/engine/redactor.rb +98 -0
- data/lib/net/connector/engine/response_reader.rb +139 -0
- data/lib/net/connector/engine/session.rb +6 -2
- data/lib/net/connector/netdisco/backup_run.rb +22 -3
- data/lib/net/connector/netdisco/cli/options.rb +3 -0
- data/lib/net/connector/netdisco/cli.rb +11 -8
- data/lib/net/connector/netdisco/connection.rb +0 -1
- data/lib/net/connector/netdisco/database_client.rb +4 -2
- data/lib/net/connector/netdisco/device.rb +2 -0
- data/lib/net/connector/netdisco/diagnostic.rb +4 -0
- data/lib/net/connector/netdisco/fleet.rb +10 -1
- data/lib/net/connector/netdisco/plan.rb +13 -0
- data/lib/net/connector/netdisco/planner.rb +2 -1
- data/lib/net/connector/netdisco/progress.rb +25 -8
- data/lib/net/connector/netdisco/report/files.rb +11 -6
- data/lib/net/connector/netdisco/report/text.rb +1 -1
- data/lib/net/connector/netdisco/report.rb +19 -20
- data/lib/net/connector/netdisco/result_store.rb +5 -7
- data/lib/net/connector/netdisco/rules.rb +1 -0
- data/lib/net/connector/netdisco/settings.rb +6 -0
- data/lib/net/connector/netdisco/tftp_archive.rb +28 -13
- data/lib/net/connector/netdisco/tftp_verification.rb +1 -0
- data/lib/net/connector/netdisco/worker.rb +2 -0
- data/lib/net/connector/netdisco.rb +1 -21
- data/lib/net/connector/vendor/palo_alto/running_config.rb +26 -6
- data/lib/net/connector/version.rb +1 -1
- metadata +7 -6
- data/lib/net/connector/netdisco/tftp_history.rb +0 -16
- /data/{exe/net-connector-backup → bin/net-backup} +0 -0
|
@@ -61,15 +61,14 @@ module Net
|
|
|
61
61
|
policy: policy, duration_ms: duration_ms, report_diagnostic: diagnostic, log_directory: log_directory)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
#
|
|
65
|
-
def
|
|
64
|
+
# 统计不创建逐设备明细;文本与进度输出复用此入口,不缓存调用方持有的批次。
|
|
65
|
+
def statistics
|
|
66
66
|
batch_data = batch_summary
|
|
67
67
|
batch_data.merge(
|
|
68
68
|
schema_version: 2, policy: policy, policy_success: policy_success?, duration_ms: duration_ms,
|
|
69
69
|
coverage: { complete: !outcomes.empty? && outcomes.all? { |outcome| ATTEMPTED_STATUSES.include?(outcome.status) },
|
|
70
70
|
attempted: outcomes.count { |outcome| ATTEMPTED_STATUSES.include?(outcome.status) },
|
|
71
71
|
skipped: outcomes.count { |outcome| !ATTEMPTED_STATUSES.include?(outcome.status) } },
|
|
72
|
-
devices: device_summaries(batch_data.fetch(:devices)),
|
|
73
72
|
callback_errors: callback_errors.map { |entry| { host: entry[:host], error_type: ErrorMetadata.type(entry[:error_type]) } },
|
|
74
73
|
report_location: report_location, report_error: ErrorMetadata.type(report_error),
|
|
75
74
|
verification: mode == :tftp ? { verified: outcomes.count { |outcome| server_verified?(outcome) },
|
|
@@ -78,6 +77,9 @@ module Net
|
|
|
78
77
|
)
|
|
79
78
|
end
|
|
80
79
|
|
|
80
|
+
# 手工构造的批次同样经过诊断白名单,不序列化任意异常内容。
|
|
81
|
+
def summary = statistics.merge(devices: device_summaries)
|
|
82
|
+
|
|
81
83
|
def inspect = "#<#{self.class} policy=#{policy} status=#{status} policy_success=#{policy_success?}>"
|
|
82
84
|
|
|
83
85
|
private
|
|
@@ -92,19 +94,7 @@ module Net
|
|
|
92
94
|
failed: outcomes.count { |outcome| outcome.status == :failed },
|
|
93
95
|
skipped: outcomes.count(&:skipped?),
|
|
94
96
|
counts: counts,
|
|
95
|
-
callback_errors: callback_errors
|
|
96
|
-
devices: outcomes.map do |outcome|
|
|
97
|
-
{ host: outcome.device.host || outcome.device.source_ip, name: outcome.device.name,
|
|
98
|
-
vendor: outcome.device.vendor,
|
|
99
|
-
status: outcome.status, path: outcome.backup&.path,
|
|
100
|
-
bytes: outcome.backup.is_a?(Backup) ? outcome.backup.bytes : nil,
|
|
101
|
-
sha256: outcome.backup.is_a?(Backup) ? outcome.backup.sha256 : nil,
|
|
102
|
-
change: outcome.backup.is_a?(Backup) ? outcome.backup.change : nil,
|
|
103
|
-
previous_sha256: outcome.backup.is_a?(Backup) ? outcome.backup.previous_sha256 : nil,
|
|
104
|
-
started_at: outcome.started_at&.iso8601, finished_at: outcome.finished_at&.iso8601,
|
|
105
|
-
duration_ms: outcome.duration_ms,
|
|
106
|
-
error_code: outcome.error_code, error_type: outcome.error_type }
|
|
107
|
-
end
|
|
97
|
+
callback_errors: callback_errors
|
|
108
98
|
}
|
|
109
99
|
end
|
|
110
100
|
|
|
@@ -122,13 +112,22 @@ module Net
|
|
|
122
112
|
bytes: receipt&.server_bytes, sha256: receipt&.server_sha256 }
|
|
123
113
|
end
|
|
124
114
|
|
|
125
|
-
def device_summaries
|
|
126
|
-
|
|
115
|
+
def device_summaries
|
|
116
|
+
outcomes.map do |outcome|
|
|
127
117
|
diagnostic = outcome.diagnostic || Diagnostic.new(error_code: outcome.error_code, error_type: outcome.error_type)
|
|
118
|
+
entry = { host: outcome.device.host || outcome.device.source_ip, name: outcome.device.name,
|
|
119
|
+
vendor: outcome.device.vendor, status: outcome.status, path: outcome.backup&.path,
|
|
120
|
+
bytes: outcome.backup.is_a?(Backup) ? outcome.backup.bytes : nil,
|
|
121
|
+
sha256: outcome.backup.is_a?(Backup) ? outcome.backup.sha256 : nil,
|
|
122
|
+
change: outcome.backup.is_a?(Backup) ? outcome.backup.change : nil,
|
|
123
|
+
previous_sha256: outcome.backup.is_a?(Backup) ? outcome.backup.previous_sha256 : nil,
|
|
124
|
+
started_at: outcome.started_at&.iso8601, finished_at: outcome.finished_at&.iso8601,
|
|
125
|
+
duration_ms: outcome.duration_ms,
|
|
126
|
+
error_code: diagnostic.error_code, error_type: diagnostic.error_type,
|
|
127
|
+
diagnostic: diagnostic.to_h.except(:error_code, :error_type) }
|
|
128
128
|
entry = entry.merge(tftp_summary(outcome)) if mode == :tftp
|
|
129
129
|
entry[:session_log] = File.join(log_directory, "#{outcome.device.host.tr(":", "_")}.log") if log_directory && outcome.device.host && outcome.started_at
|
|
130
|
-
entry
|
|
131
|
-
diagnostic: diagnostic.to_h.except(:error_code, :error_type))
|
|
130
|
+
entry
|
|
132
131
|
end
|
|
133
132
|
end
|
|
134
133
|
end
|
|
@@ -12,17 +12,15 @@ module Net
|
|
|
12
12
|
# 每批保存一份私有 JSON 报告,通过重命名防止半写文件。
|
|
13
13
|
class Json
|
|
14
14
|
# 以临时文件和重命名原子写入私有 JSON 报告。
|
|
15
|
-
def write(
|
|
15
|
+
def write(report, directory:)
|
|
16
16
|
destination = File.expand_path(directory)
|
|
17
17
|
FileUtils.mkdir_p(destination, mode: 0o700)
|
|
18
|
-
filename = "netdisco-#{
|
|
18
|
+
filename = "netdisco-#{report.mode}-#{report.started_at.strftime("%Y%m%dT%H%M%SZ")}-#{SecureRandom.hex(4)}.json"
|
|
19
19
|
path = File.join(destination, filename)
|
|
20
|
-
Storage::PrivateFile.write(path, JSON.pretty_generate(
|
|
20
|
+
Storage::PrivateFile.write(path, JSON.pretty_generate(report.summary)).path
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
Text = Json # Compatibility with the former name of the JSON writer.
|
|
25
|
-
|
|
26
24
|
# 数据库仓储由调用方提供,可使用 Active Record 或其他对象。
|
|
27
25
|
# 仓储须实现 create!,本项目不负责数据库表结构。
|
|
28
26
|
class Database
|
|
@@ -34,8 +32,8 @@ module Net
|
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
# 把批量摘要写入调用方仓储并返回记录位置。
|
|
37
|
-
def write(
|
|
38
|
-
record = @repository.create!(
|
|
35
|
+
def write(report, **_options)
|
|
36
|
+
record = @repository.create!(report.summary)
|
|
39
37
|
record.respond_to?(:id) ? "database:#{record.id}" : "database"
|
|
40
38
|
end
|
|
41
39
|
end
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "../../connector"
|
|
5
|
+
require_relative "client"
|
|
6
|
+
require_relative "database_client"
|
|
7
|
+
require_relative "config_file"
|
|
8
|
+
require_relative "rules"
|
|
9
|
+
require_relative "planner"
|
|
4
10
|
|
|
5
11
|
module Net
|
|
6
12
|
module Connector
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "tempfile"
|
|
5
|
+
require "digest"
|
|
5
6
|
require_relative "../storage/backup_lock"
|
|
6
7
|
require_relative "../storage/private_file"
|
|
7
8
|
require_relative "../storage/safe_file"
|
|
8
9
|
require_relative "tftp_verification"
|
|
10
|
+
require_relative "diagnostic"
|
|
9
11
|
|
|
10
12
|
module Net
|
|
11
13
|
module Connector
|
|
@@ -32,6 +34,7 @@ module Net
|
|
|
32
34
|
def initialize(directory:, root: nil)
|
|
33
35
|
@report_directory = File.expand_path(directory)
|
|
34
36
|
@server_root = root && File.realpath(root)
|
|
37
|
+
FileUtils.mkdir_p(@report_directory, mode: 0o700)
|
|
35
38
|
@server_archive_directory = create_server_archive_directory if @server_root
|
|
36
39
|
@remote_suffix = reserve_remote_suffix unless @server_root
|
|
37
40
|
@verifier = TftpVerification.new(root: @server_root)
|
|
@@ -68,9 +71,6 @@ module Net
|
|
|
68
71
|
end
|
|
69
72
|
end
|
|
70
73
|
|
|
71
|
-
alias path_for upload_filename
|
|
72
|
-
alias capture upload_and_archive
|
|
73
|
-
|
|
74
74
|
private
|
|
75
75
|
|
|
76
76
|
# 文件系统的 mtime 可能落后于 Time.now;用同一服务器目录的时间建立上传边界。
|
|
@@ -137,7 +137,7 @@ module Net
|
|
|
137
137
|
|
|
138
138
|
def archive_uploaded_file(outcome)
|
|
139
139
|
receipt = outcome.backup
|
|
140
|
-
|
|
140
|
+
saved_paths = { local_path: nil, archive_path: nil }
|
|
141
141
|
batch_directory = File.join(@report_directory, "tftp")
|
|
142
142
|
FileUtils.mkdir_p(batch_directory, mode: 0o700)
|
|
143
143
|
extension = File.extname(receipt.path)
|
|
@@ -152,9 +152,8 @@ module Net
|
|
|
152
152
|
end
|
|
153
153
|
raise ArchiveFailed.new(host: outcome.device.host) unless Digest::SHA256.hexdigest(bytes) == receipt.server_sha256
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Storage::PrivateFile.write(server_archive_path, bytes)
|
|
155
|
+
persist_archive(saved_paths, :local_path, batch_archive_path, bytes)
|
|
156
|
+
persist_archive(saved_paths, :archive_path, server_archive_path, bytes)
|
|
158
157
|
if outcome.success?
|
|
159
158
|
uploaded_fingerprint = Storage::SafeFile.fingerprint(uploaded_path)
|
|
160
159
|
raise ArchiveFailed.new(host: outcome.device.host) unless uploaded_fingerprint.sha256 == receipt.server_sha256
|
|
@@ -162,16 +161,32 @@ module Net
|
|
|
162
161
|
|
|
163
162
|
File.unlink(uploaded_path)
|
|
164
163
|
end
|
|
165
|
-
outcome.with(backup: receipt.with(
|
|
166
|
-
rescue StandardError
|
|
167
|
-
retained = outcome.with(backup: receipt.with(
|
|
168
|
-
outcome.success? ? archive_failure(retained) : retained
|
|
164
|
+
outcome.with(backup: receipt.with(**saved_paths))
|
|
165
|
+
rescue StandardError => error
|
|
166
|
+
retained = outcome.with(backup: receipt.with(**saved_paths))
|
|
167
|
+
outcome.success? ? archive_failure(retained, persistence_error: error) : retained
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# 路径仅由本次写入器的可信回执确认;同步或清理失败不能抹掉已替换的文件。
|
|
171
|
+
def persist_archive(saved_paths, field, path, bytes)
|
|
172
|
+
Storage::PrivateFile.write(path, bytes)
|
|
173
|
+
saved_paths[field] = path
|
|
174
|
+
rescue StandardError => error
|
|
175
|
+
if Storage::PrivateFile.receipt_error?(error) && error.receipt.committed? && error.receipt.path == path
|
|
176
|
+
saved_paths[field] = path
|
|
177
|
+
end
|
|
178
|
+
raise
|
|
169
179
|
end
|
|
170
180
|
|
|
171
|
-
def archive_failure(outcome)
|
|
181
|
+
def archive_failure(outcome, persistence_error: nil)
|
|
172
182
|
error = ArchiveFailed.new(host: outcome.device.host)
|
|
183
|
+
diagnostic = Diagnostic.from(error, backup: outcome.backup)
|
|
184
|
+
if Storage::PrivateFile.receipt_error?(persistence_error)
|
|
185
|
+
diagnostic = diagnostic.with(artifact_state: persistence_error.receipt.state, artifact_phase: persistence_error.receipt.phase,
|
|
186
|
+
underlying_type: persistence_error.underlying_type)
|
|
187
|
+
end
|
|
173
188
|
outcome.with(status: :reported_with_error, error_code: error.code, error_type: error.class.name,
|
|
174
|
-
diagnostic:
|
|
189
|
+
diagnostic: diagnostic)
|
|
175
190
|
end
|
|
176
191
|
|
|
177
192
|
def archive_filename(device, extension)
|
|
@@ -1,25 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "netdisco/client"
|
|
5
|
-
require_relative "netdisco/database_client"
|
|
6
|
-
require_relative "netdisco/rules"
|
|
7
|
-
require_relative "netdisco/device"
|
|
8
|
-
require_relative "netdisco/config_file"
|
|
9
|
-
require_relative "netdisco/settings"
|
|
10
|
-
require_relative "netdisco/worker"
|
|
11
|
-
require_relative "netdisco/result_store"
|
|
12
|
-
require_relative "netdisco/batch"
|
|
13
|
-
require_relative "netdisco/planner"
|
|
14
|
-
require_relative "netdisco/fleet"
|
|
15
|
-
require_relative "netdisco/progress"
|
|
3
|
+
# 公共入口只组合应用流程;每个模块自行声明依赖,不要求调用方维护加载顺序。
|
|
16
4
|
require_relative "netdisco/cli"
|
|
17
|
-
require_relative "storage/batch_directory"
|
|
18
|
-
require_relative "netdisco/report/text"
|
|
19
|
-
require_relative "netdisco/connection"
|
|
20
|
-
require_relative "netdisco/cli/options"
|
|
21
|
-
require_relative "netdisco/report/files"
|
|
22
|
-
require_relative "netdisco/tftp_verification"
|
|
23
|
-
require_relative "netdisco/tftp_archive"
|
|
24
|
-
require_relative "netdisco/tftp_history"
|
|
25
5
|
require_relative "netdisco/backup_run"
|
|
@@ -48,19 +48,39 @@ module Net
|
|
|
48
48
|
return false if text.empty?
|
|
49
49
|
|
|
50
50
|
pending = +""
|
|
51
|
+
quote = nil
|
|
51
52
|
text.each_line do |line|
|
|
52
53
|
next if pending.empty? && line.strip.empty?
|
|
53
54
|
return false if pending.empty? && !line.start_with?("set ")
|
|
54
55
|
|
|
55
56
|
pending << line
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
end
|
|
57
|
+
quote = quote_after(line, quote)
|
|
58
|
+
next if quote
|
|
59
|
+
|
|
60
|
+
Shellwords.split(pending)
|
|
61
|
+
pending.clear
|
|
62
62
|
end
|
|
63
63
|
pending.empty?
|
|
64
|
+
rescue ArgumentError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 每行只扫描新增字节,整条逻辑命令闭合后才交给 Shellwords 校验。
|
|
69
|
+
# 单引号中的反斜杠是普通字符;其他位置的转义只影响紧随的一个字节。
|
|
70
|
+
def quote_after(line, quote)
|
|
71
|
+
escaped = false
|
|
72
|
+
line.each_byte do |byte|
|
|
73
|
+
if escaped
|
|
74
|
+
escaped = false
|
|
75
|
+
elsif byte == 92 && quote != 39
|
|
76
|
+
escaped = true
|
|
77
|
+
elsif byte == quote
|
|
78
|
+
quote = nil
|
|
79
|
+
elsif quote.nil? && (byte == 34 || byte == 39)
|
|
80
|
+
quote = byte
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
quote
|
|
64
84
|
end
|
|
65
85
|
|
|
66
86
|
# 去掉命令回显和提示符,保留真正需要校验的设备响应。
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-connector
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- net-connector contributors
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: expect-pty
|
|
@@ -333,7 +333,7 @@ dependencies:
|
|
|
333
333
|
version: '0.2'
|
|
334
334
|
description: 通过 SSH 或 Telnet 登录网络设备,采集配置、执行命令脚本、处理交互提示,并提供脱敏日志与基于 Netdisco 清单的批量备份。
|
|
335
335
|
executables:
|
|
336
|
-
- net-
|
|
336
|
+
- net-backup
|
|
337
337
|
extensions: []
|
|
338
338
|
extra_rdoc_files: []
|
|
339
339
|
files:
|
|
@@ -342,12 +342,12 @@ files:
|
|
|
342
342
|
- LICENSE
|
|
343
343
|
- README.md
|
|
344
344
|
- SECURITY.md
|
|
345
|
+
- bin/net-backup
|
|
345
346
|
- docs/RELEASING.md
|
|
346
347
|
- docs/VERIFICATION.md
|
|
347
348
|
- docs/architecture.md
|
|
348
349
|
- examples/backup.yml
|
|
349
350
|
- examples/inventory_sql.yml
|
|
350
|
-
- exe/net-connector-backup
|
|
351
351
|
- lib/net-connector.rb
|
|
352
352
|
- lib/net/connector.rb
|
|
353
353
|
- lib/net/connector/device/base.rb
|
|
@@ -382,6 +382,8 @@ files:
|
|
|
382
382
|
- lib/net/connector/engine/log/messages.rb
|
|
383
383
|
- lib/net/connector/engine/log/stream.rb
|
|
384
384
|
- lib/net/connector/engine/recovery.rb
|
|
385
|
+
- lib/net/connector/engine/redactor.rb
|
|
386
|
+
- lib/net/connector/engine/response_reader.rb
|
|
385
387
|
- lib/net/connector/engine/result.rb
|
|
386
388
|
- lib/net/connector/engine/session.rb
|
|
387
389
|
- lib/net/connector/engine/terminal_renderer.rb
|
|
@@ -410,7 +412,6 @@ files:
|
|
|
410
412
|
- lib/net/connector/netdisco/rules.rb
|
|
411
413
|
- lib/net/connector/netdisco/settings.rb
|
|
412
414
|
- lib/net/connector/netdisco/tftp_archive.rb
|
|
413
|
-
- lib/net/connector/netdisco/tftp_history.rb
|
|
414
415
|
- lib/net/connector/netdisco/tftp_verification.rb
|
|
415
416
|
- lib/net/connector/netdisco/worker.rb
|
|
416
417
|
- lib/net/connector/storage.rb
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "tftp_archive"
|
|
4
|
-
|
|
5
|
-
module Net
|
|
6
|
-
module Connector
|
|
7
|
-
module Netdisco
|
|
8
|
-
# Compatibility with the first batch archive entry point.
|
|
9
|
-
class TftpHistory < TftpArchive
|
|
10
|
-
def initialize(server:, **options) # rubocop:disable Lint/UnusedMethodArgument -- Accept the former constructor keyword.
|
|
11
|
-
super(**options)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
File without changes
|