net-connector 0.5.0 → 0.7.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 +58 -0
- data/CONTRIBUTING.md +20 -0
- data/README.md +153 -42
- data/docs/VERIFICATION.md +42 -1
- data/docs/architecture.md +20 -1
- data/examples/backup.yml +40 -0
- data/examples/inventory_sql.yml +16 -0
- data/lib/net/connector/device/base.rb +30 -38
- data/lib/net/connector/device/profile/builder.rb +8 -8
- data/lib/net/connector/device/profile.rb +2 -2
- data/lib/net/connector/device/running_config.rb +2 -2
- data/lib/net/connector/device/tftp/receipt.rb +12 -3
- data/lib/net/connector/device/tftp.rb +7 -7
- data/lib/net/connector/device/topology.rb +4 -4
- data/lib/net/connector/engine/authentication.rb +1 -1
- data/lib/net/connector/engine/command.rb +2 -2
- data/lib/net/connector/engine/configuration.rb +5 -2
- data/lib/net/connector/engine/dialogue.rb +4 -4
- data/lib/net/connector/engine/execution.rb +27 -16
- data/lib/net/connector/engine/known_hosts.rb +112 -0
- data/lib/net/connector/engine/log.rb +10 -7
- data/lib/net/connector/engine/session.rb +117 -128
- data/lib/net/connector/engine/transport.rb +32 -7
- data/lib/net/connector/netdisco/backup_run.rb +132 -0
- data/lib/net/connector/netdisco/batch.rb +1 -1
- data/lib/net/connector/netdisco/cli/options.rb +139 -0
- data/lib/net/connector/netdisco/cli.rb +20 -24
- data/lib/net/connector/netdisco/client.rb +8 -7
- data/lib/net/connector/netdisco/config_file.rb +26 -22
- data/lib/net/connector/netdisco/connection.rb +38 -0
- data/lib/net/connector/netdisco/database_client.rb +197 -0
- data/lib/net/connector/netdisco/device.rb +11 -1
- data/lib/net/connector/netdisco/fleet.rb +68 -47
- data/lib/net/connector/netdisco/inventory_budget.rb +4 -4
- data/lib/net/connector/netdisco/plan.rb +14 -9
- data/lib/net/connector/netdisco/planner.rb +18 -14
- data/lib/net/connector/netdisco/progress.rb +253 -0
- data/lib/net/connector/netdisco/report/files.rb +41 -0
- data/lib/net/connector/netdisco/report/text.rb +45 -0
- data/lib/net/connector/netdisco/report.rb +53 -31
- data/lib/net/connector/netdisco/result_store.rb +3 -1
- data/lib/net/connector/netdisco/rules.rb +10 -2
- data/lib/net/connector/netdisco/settings.rb +136 -63
- data/lib/net/connector/netdisco/tftp_archive.rb +175 -0
- data/lib/net/connector/netdisco/tftp_history.rb +16 -0
- data/lib/net/connector/netdisco/tftp_verification.rb +58 -0
- data/lib/net/connector/netdisco/worker.rb +8 -3
- data/lib/net/connector/netdisco.rb +11 -0
- data/lib/net/connector/storage/backup_lock.rb +1 -1
- data/lib/net/connector/storage/batch_directory.rb +29 -0
- data/lib/net/connector/storage.rb +1 -0
- data/lib/net/connector/vendor/h3c.rb +2 -1
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/radware/running_config.rb +19 -0
- data/lib/net/connector/vendor/radware.rb +4 -3
- data/lib/net/connector/version.rb +1 -1
- data/lib/net/connector.rb +2 -2
- metadata +57 -104
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "../storage/backup_lock"
|
|
5
|
+
require_relative "../storage/private_file"
|
|
6
|
+
require_relative "../storage/safe_file"
|
|
7
|
+
require_relative "tftp_verification"
|
|
8
|
+
|
|
9
|
+
module Net
|
|
10
|
+
module Connector
|
|
11
|
+
module Netdisco
|
|
12
|
+
# 设备先上传到服务器根目录,核验后按批次归档;固定文件名不会覆盖历史文件。
|
|
13
|
+
class TftpArchive
|
|
14
|
+
BATCH_DIRECTORY_NAME = /\A\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(?:_\d{2})?\z/
|
|
15
|
+
private_constant :BATCH_DIRECTORY_NAME
|
|
16
|
+
|
|
17
|
+
class Unavailable < Net::Connector::Error
|
|
18
|
+
def initialize(host: nil)
|
|
19
|
+
super("a local TFTP root is required to retain a fixed-name upload", code: :tftp_history_unavailable,
|
|
20
|
+
host: host, phase: :tftp_backup)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ArchiveFailed < Net::Connector::Error
|
|
25
|
+
def initialize(host: nil)
|
|
26
|
+
super("the uploaded file could not be retained in the batch", code: :tftp_archive_failed,
|
|
27
|
+
host: host, phase: :tftp_backup)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(directory:, root: nil)
|
|
32
|
+
@report_directory = File.expand_path(directory)
|
|
33
|
+
@server_root = root && File.realpath(root)
|
|
34
|
+
@server_archive_directory = create_server_archive_directory if @server_root
|
|
35
|
+
@remote_suffix = reserve_remote_suffix unless @server_root
|
|
36
|
+
@verifier = TftpVerification.new(root: @server_root)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def upload_filename(device)
|
|
40
|
+
name = device.tftp_filename
|
|
41
|
+
return name if @server_root || device.vendor == :palo_alto
|
|
42
|
+
|
|
43
|
+
extension = File.extname(name)
|
|
44
|
+
stem = File.basename(name, extension)
|
|
45
|
+
suffix = "-#{@remote_suffix}#{extension}"
|
|
46
|
+
TftpTarget.validate_path!("#{stem.byteslice(0, TftpTarget::MAX_PATH_BYTES - suffix.bytesize)}#{suffix}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def upload_and_archive(device, started_at:)
|
|
50
|
+
raise Unavailable.new(host: device.host) if device.vendor == :palo_alto && !@server_root
|
|
51
|
+
|
|
52
|
+
remote_filename = upload_filename(device)
|
|
53
|
+
return yield remote_filename unless @server_root
|
|
54
|
+
|
|
55
|
+
# The lock spans the upload and copy, including a fixed-name PAN-OS upload.
|
|
56
|
+
Storage::BackupLock.synchronize(File.join(@server_root, remote_filename), host: device.host, timeout: 240) do
|
|
57
|
+
archive_existing_file(device, remote_filename)
|
|
58
|
+
outcome = yield remote_filename
|
|
59
|
+
receipt = outcome.backup
|
|
60
|
+
return outcome unless receipt.is_a?(TftpReceipt) && receipt.path
|
|
61
|
+
|
|
62
|
+
verified = @verifier.verify_outcome(outcome.with(started_at: started_at))
|
|
63
|
+
return outcome.success? ? archive_failure(outcome) : outcome unless verified.backup.verification == :server_verified
|
|
64
|
+
|
|
65
|
+
archive_uploaded_file(verified)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
alias path_for upload_filename
|
|
70
|
+
alias capture upload_and_archive
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# 首次采用固定远端名时,先保留服务器上原有文件,再允许设备覆盖。
|
|
75
|
+
def archive_existing_file(device, remote_filename)
|
|
76
|
+
existing_path = File.join(@server_root, remote_filename)
|
|
77
|
+
bytes = Storage::SafeFile.open(existing_path, missing: true) { |file, _stat| file.read }
|
|
78
|
+
return unless bytes
|
|
79
|
+
|
|
80
|
+
previous_directory = File.join(@server_archive_directory, "previous")
|
|
81
|
+
FileUtils.mkdir_p(previous_directory, mode: 0o700)
|
|
82
|
+
archive_path = File.join(previous_directory, archive_filename(device, File.extname(remote_filename)))
|
|
83
|
+
Storage::PrivateFile.write(archive_path, bytes)
|
|
84
|
+
rescue StandardError
|
|
85
|
+
raise ArchiveFailed.new(host: device.host), cause: nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def create_server_archive_directory
|
|
89
|
+
parent = File.join(@server_root, "archive")
|
|
90
|
+
raise ArgumentError, "TFTP archive directory must not be a symlink" if File.symlink?(parent)
|
|
91
|
+
|
|
92
|
+
FileUtils.mkdir_p(parent, mode: 0o700)
|
|
93
|
+
raise ArgumentError, "TFTP archive directory escaped its root" unless File.realpath(parent) == parent
|
|
94
|
+
|
|
95
|
+
base = batch_name
|
|
96
|
+
number = 0
|
|
97
|
+
loop do
|
|
98
|
+
candidate = number.zero? ? base : "#{base}_#{format("%02d", number)}"
|
|
99
|
+
path = File.join(parent, candidate)
|
|
100
|
+
begin
|
|
101
|
+
Dir.mkdir(path, 0o700)
|
|
102
|
+
return path
|
|
103
|
+
rescue Errno::EEXIST
|
|
104
|
+
number += 1
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 同一目录重跑时用原子预留的序号区分运行,不依赖随机数或进程 ID。
|
|
110
|
+
def reserve_remote_suffix
|
|
111
|
+
base = batch_name
|
|
112
|
+
number = 0
|
|
113
|
+
loop do
|
|
114
|
+
candidate = number.zero? ? base : "#{base}_#{format("%02d", number)}"
|
|
115
|
+
marker = File.join(@report_directory, ".tftp-run-#{candidate}")
|
|
116
|
+
begin
|
|
117
|
+
File.open(marker, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write("\n") }
|
|
118
|
+
return candidate
|
|
119
|
+
rescue Errno::EEXIST
|
|
120
|
+
number += 1
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def batch_name
|
|
126
|
+
name = File.basename(@report_directory)
|
|
127
|
+
name.match?(BATCH_DIRECTORY_NAME) ? name : Time.now.getlocal("+08:00").strftime("%Y-%m-%d_%H-%M-%S")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def archive_uploaded_file(outcome)
|
|
131
|
+
receipt = outcome.backup
|
|
132
|
+
saved_batch_path = nil
|
|
133
|
+
batch_directory = File.join(@report_directory, "tftp")
|
|
134
|
+
FileUtils.mkdir_p(batch_directory, mode: 0o700)
|
|
135
|
+
extension = File.extname(receipt.path)
|
|
136
|
+
filename = archive_filename(outcome.device, extension)
|
|
137
|
+
batch_archive_path = File.join(batch_directory, filename)
|
|
138
|
+
server_archive_path = File.join(@server_archive_directory, filename)
|
|
139
|
+
uploaded_path = receipt.local_path
|
|
140
|
+
bytes = Storage::SafeFile.open(uploaded_path) do |file, stat|
|
|
141
|
+
raise ArchiveFailed.new(host: outcome.device.host) unless stat.size == receipt.server_bytes
|
|
142
|
+
|
|
143
|
+
file.read
|
|
144
|
+
end
|
|
145
|
+
raise ArchiveFailed.new(host: outcome.device.host) unless Digest::SHA256.hexdigest(bytes) == receipt.server_sha256
|
|
146
|
+
|
|
147
|
+
Storage::PrivateFile.write(batch_archive_path, bytes)
|
|
148
|
+
saved_batch_path = batch_archive_path
|
|
149
|
+
Storage::PrivateFile.write(server_archive_path, bytes)
|
|
150
|
+
if outcome.success?
|
|
151
|
+
uploaded_fingerprint = Storage::SafeFile.fingerprint(uploaded_path)
|
|
152
|
+
raise ArchiveFailed.new(host: outcome.device.host) unless uploaded_fingerprint.sha256 == receipt.server_sha256
|
|
153
|
+
raise ArchiveFailed.new(host: outcome.device.host) unless Storage::SafeFile.same_entry?(uploaded_path, uploaded_fingerprint)
|
|
154
|
+
|
|
155
|
+
File.unlink(uploaded_path)
|
|
156
|
+
end
|
|
157
|
+
outcome.with(backup: receipt.with(local_path: batch_archive_path, archive_path: server_archive_path))
|
|
158
|
+
rescue StandardError
|
|
159
|
+
retained = outcome.with(backup: receipt.with(local_path: saved_batch_path))
|
|
160
|
+
outcome.success? ? archive_failure(retained) : retained
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def archive_failure(outcome)
|
|
164
|
+
error = ArchiveFailed.new(host: outcome.device.host)
|
|
165
|
+
outcome.with(status: :reported_with_error, error_code: error.code, error_type: error.class.name,
|
|
166
|
+
diagnostic: Diagnostic.from(error, backup: outcome.backup))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def archive_filename(device, extension)
|
|
170
|
+
"#{device.backup_filename(style: :hostname_ip).delete_suffix(".txt")}#{extension}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../storage/safe_file"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
module Connector
|
|
7
|
+
module Netdisco
|
|
8
|
+
# 只核验本次设备回执指向的本地服务器文件;远端可读不等于本次上传证据。
|
|
9
|
+
class TftpVerification
|
|
10
|
+
def initialize(root: nil)
|
|
11
|
+
@root = root && File.realpath(root)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(report)
|
|
15
|
+
return report unless @root
|
|
16
|
+
|
|
17
|
+
outcomes = report.outcomes.map do |outcome|
|
|
18
|
+
receipt = outcome.backup
|
|
19
|
+
next outcome unless receipt.is_a?(TftpReceipt) && receipt.path && outcome.started_at
|
|
20
|
+
|
|
21
|
+
verify(outcome, receipt)
|
|
22
|
+
end
|
|
23
|
+
report.with(outcomes: outcomes.freeze)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def verify_outcome(outcome)
|
|
27
|
+
return outcome unless @root && outcome.started_at && outcome.backup.is_a?(TftpReceipt) && outcome.backup.path
|
|
28
|
+
|
|
29
|
+
verify(outcome, outcome.backup)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def verify(outcome, receipt)
|
|
35
|
+
path = File.join(@root, receipt.path)
|
|
36
|
+
parent = File.realpath(File.dirname(path))
|
|
37
|
+
return outcome unless parent == @root || parent.start_with?(@root + File::SEPARATOR)
|
|
38
|
+
|
|
39
|
+
size = nil
|
|
40
|
+
fingerprint = Storage::SafeFile.open(path, missing: true) do |file, stat|
|
|
41
|
+
next unless stat.size.positive? && stat.mtime >= outcome.started_at
|
|
42
|
+
|
|
43
|
+
size = stat.size
|
|
44
|
+
value = Storage::SafeFile.fingerprint_io(path, file, stat)
|
|
45
|
+
after = file.stat
|
|
46
|
+
value if [stat.size, stat.mtime, stat.ctime] == [after.size, after.mtime, after.ctime]
|
|
47
|
+
end
|
|
48
|
+
return outcome unless fingerprint && Storage::SafeFile.same_entry?(path, fingerprint)
|
|
49
|
+
|
|
50
|
+
outcome.with(backup: receipt.with(verification: :server_verified, server_sha256: fingerprint.sha256, server_bytes: size, local_path: path))
|
|
51
|
+
rescue IOError, SystemCallError, ArgumentError
|
|
52
|
+
# 核验失败不抹去设备上传回执;要求服务器证据的策略仍判为未完成。
|
|
53
|
+
outcome
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -5,13 +5,18 @@ module Net
|
|
|
5
5
|
module Netdisco
|
|
6
6
|
# 以固定并发上限执行相互隔离的设备任务。
|
|
7
7
|
class Worker
|
|
8
|
-
#
|
|
9
|
-
def
|
|
8
|
+
# 内部预检与实际执行共用规则,不为校验创建工作线程池对象。
|
|
9
|
+
def self.validate_concurrency!(concurrency)
|
|
10
10
|
unless concurrency.is_a?(Integer) && (1..Settings::MAX_CONCURRENCY).cover?(concurrency)
|
|
11
11
|
raise ArgumentError, "concurrency must be an Integer in 1..#{Settings::MAX_CONCURRENCY}"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
concurrency
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 校验并保存最大设备并发数。
|
|
18
|
+
def initialize(concurrency:)
|
|
19
|
+
@concurrency = Worker.validate_concurrency!(concurrency)
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
# 按清单顺序收集结果,并隔离设备及回调异常。
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../connector"
|
|
4
4
|
require_relative "netdisco/client"
|
|
5
|
+
require_relative "netdisco/database_client"
|
|
5
6
|
require_relative "netdisco/rules"
|
|
6
7
|
require_relative "netdisco/device"
|
|
7
8
|
require_relative "netdisco/config_file"
|
|
@@ -11,4 +12,14 @@ require_relative "netdisco/result_store"
|
|
|
11
12
|
require_relative "netdisco/batch"
|
|
12
13
|
require_relative "netdisco/planner"
|
|
13
14
|
require_relative "netdisco/fleet"
|
|
15
|
+
require_relative "netdisco/progress"
|
|
14
16
|
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
|
+
require_relative "netdisco/backup_run"
|
|
@@ -25,7 +25,7 @@ module Net
|
|
|
25
25
|
File.join(directory, ".net-connector-#{Digest::SHA256.hexdigest(filename)}.lock")
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def self.synchronize(path, **options, &
|
|
28
|
+
def self.synchronize(path, **options, &) = new(path, **options).synchronize(&)
|
|
29
29
|
|
|
30
30
|
def initialize(path, timeout: 0, host: nil)
|
|
31
31
|
raise ArgumentError, "path must be a nonempty String" unless path.is_a?(String) && !path.empty?
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
module Connector
|
|
7
|
+
module Storage
|
|
8
|
+
# 使用本地业务时区命名,原子创建互不覆盖的批次目录。
|
|
9
|
+
class BatchDirectory
|
|
10
|
+
# 固定 UTC+8,不依赖运行主机时区;mkdir 原子防止并发任务覆盖。
|
|
11
|
+
def self.create(root, time: Time.now)
|
|
12
|
+
FileUtils.mkdir_p(root, mode: 0o700)
|
|
13
|
+
stamp = time.getlocal("+08:00").strftime("%Y-%m-%d_%H-%M-%S")
|
|
14
|
+
suffix = 0
|
|
15
|
+
loop do
|
|
16
|
+
name = suffix.zero? ? stamp : "#{stamp}_#{format("%02d", suffix)}"
|
|
17
|
+
path = File.join(root, name)
|
|
18
|
+
begin
|
|
19
|
+
Dir.mkdir(path, 0o700)
|
|
20
|
+
return path
|
|
21
|
+
rescue Errno::EEXIST
|
|
22
|
+
suffix += 1
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -4,6 +4,7 @@ module Net
|
|
|
4
4
|
module Connector
|
|
5
5
|
# 文件存储不依赖设备连接;备份、离线导出和批量报告共享这些原语。
|
|
6
6
|
module Storage
|
|
7
|
+
autoload :BatchDirectory, File.expand_path("storage/batch_directory", __dir__)
|
|
7
8
|
autoload :BackupLock, File.expand_path("storage/backup_lock", __dir__)
|
|
8
9
|
autoload :PrivateFile, File.expand_path("storage/private_file", __dir__)
|
|
9
10
|
autoload :SafeFile, File.expand_path("storage/safe_file", __dir__)
|
|
@@ -12,9 +12,9 @@ module Net
|
|
|
12
12
|
# 在连接前拒绝源文件及远端目录;未指定文件名时仍由设备生成。
|
|
13
13
|
def validate_options!(target, source_file:, **)
|
|
14
14
|
raise ArgumentError, "Hillstone TFTP export does not use source_file" unless source_file.nil?
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
return unless target.explicit_path? && target.path.include?("/")
|
|
16
|
+
|
|
17
|
+
raise ArgumentError, "Hillstone TFTP filename cannot contain a directory"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def receipt_metadata(target, **)
|
|
@@ -15,9 +15,9 @@ module Net
|
|
|
15
15
|
def validate_options!(target, source_file:, vrf: nil)
|
|
16
16
|
raise ArgumentError, "PAN-OS does not use source_file" unless source_file.nil?
|
|
17
17
|
raise ArgumentError, "PAN-OS TFTP backup does not use vrf" unless vrf.nil?
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
return unless target.explicit_path? && target.path != FILENAME
|
|
19
|
+
|
|
20
|
+
raise ArgumentError, "PAN-OS TFTP export uses the fixed filename #{FILENAME}"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def receipt_metadata(_target, **)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../device/running_config/strategy"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
module Connector
|
|
7
|
+
module Radware
|
|
8
|
+
class RunningConfig < Net::Connector::RunningConfig::Rendered
|
|
9
|
+
# /cfg/dump 进入 Configuration 菜单;保留设备前缀,只替换菜单名称。
|
|
10
|
+
def prompt_text(command)
|
|
11
|
+
prompt = super
|
|
12
|
+
return prompt unless command.text == "/cfg/dump"
|
|
13
|
+
|
|
14
|
+
prompt.sub(/\A(>>[ \t]+(?:.* - )?)[^\r\n#]+#\z/, '\1Configuration#')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../device/base"
|
|
4
|
+
require_relative "radware/running_config"
|
|
4
5
|
require_relative "radware/tftp_backup"
|
|
5
6
|
require_relative "radware/topology"
|
|
6
7
|
|
|
@@ -12,7 +13,7 @@ module Net
|
|
|
12
13
|
vendor :radware
|
|
13
14
|
# 声明菜单式 CLI 的命令、提示、超时、诊断和确认规则。
|
|
14
15
|
profile do
|
|
15
|
-
running_config_strategy Net::Connector::RunningConfig
|
|
16
|
+
running_config_strategy Net::Connector::Radware::RunningConfig
|
|
16
17
|
tftp_strategy Net::Connector::Radware::TftpBackup
|
|
17
18
|
topology_strategy Net::Connector::Radware::Topology
|
|
18
19
|
commands do
|
|
@@ -21,8 +22,8 @@ module Net
|
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
prompts do
|
|
24
|
-
login(/(?:\A|(?<=[\r\n]))(?:>>[^\r\n]
|
|
25
|
-
command(/(?:\A|(?<=[\r\n]))
|
|
25
|
+
login(/(?:\A|(?<=[\r\n]))(?:>>[^\r\n]*#|(?!>>)[^\r\n]+[>#])\s*\z/)
|
|
26
|
+
command(/(?:\A|(?<=[\r\n]))[^\r\n]+#\s*\z/)
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
errors do
|
data/lib/net/connector.rb
CHANGED
|
@@ -34,10 +34,10 @@ module Net
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# 在代码块内连接设备并确保会话关闭。
|
|
37
|
-
def self.open(vendor, **settings)
|
|
37
|
+
def self.open(vendor, **settings, &)
|
|
38
38
|
raise ArgumentError, "open requires a block" unless block_given?
|
|
39
39
|
|
|
40
|
-
vendor_class(vendor).open(**settings)
|
|
40
|
+
vendor_class(vendor).open(**settings, &)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|