net-connector 0.4.0 → 0.4.1
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 +73 -65
- data/README.md +88 -95
- data/docs/VERIFICATION.md +2 -1
- data/docs/architecture.md +144 -248
- data/lib/net/connector/device/base.rb +1 -0
- data/lib/net/connector/device/interface_description.rb +3 -0
- data/lib/net/connector/device/profile/builder.rb +236 -0
- data/lib/net/connector/device/profile.rb +2 -227
- data/lib/net/connector/device/running_config/strategy.rb +5 -0
- data/lib/net/connector/device/running_config.rb +5 -0
- data/lib/net/connector/engine/dialogue.rb +1 -0
- data/lib/net/connector/engine/errors.rb +2 -0
- data/lib/net/connector/engine/log.rb +13 -9
- data/lib/net/connector/engine/session.rb +1 -0
- data/lib/net/connector/netdisco/cli.rb +20 -20
- data/lib/net/connector/netdisco/device.rb +4 -8
- data/lib/net/connector/netdisco/fleet.rb +2 -48
- data/lib/net/connector/netdisco/plan.rb +77 -0
- data/lib/net/connector/netdisco/planner.rb +12 -23
- data/lib/net/connector/netdisco/worker.rb +31 -24
- data/lib/net/connector/operations/tftp/file_upload.rb +42 -0
- data/lib/net/connector/operations/tftp/strategy.rb +9 -11
- data/lib/net/connector/operations/topology/strategy.rb +14 -0
- data/lib/net/connector/operations/topology.rb +1 -0
- data/lib/net/connector/vendor/cisco_ios/running_config.rb +1 -0
- data/lib/net/connector/vendor/cisco_ios/topology.rb +9 -0
- data/lib/net/connector/vendor/cisco_nxos/running_config.rb +1 -0
- data/lib/net/connector/vendor/h3c/tftp_backup.rb +2 -18
- data/lib/net/connector/vendor/h3c/topology.rb +9 -0
- data/lib/net/connector/vendor/hillstone/running_config.rb +1 -0
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +4 -0
- data/lib/net/connector/vendor/hillstone/topology.rb +9 -0
- data/lib/net/connector/vendor/huawei/tftp_backup.rb +2 -18
- data/lib/net/connector/vendor/palo_alto/running_config.rb +4 -0
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +7 -5
- data/lib/net/connector/vendor/palo_alto/topology.rb +10 -0
- data/lib/net/connector/vendor/radware/tftp_backup.rb +2 -2
- data/lib/net/connector/vendor/radware/topology.rb +2 -0
- data/lib/net/connector/version.rb +1 -1
- metadata +26 -24
|
@@ -50,7 +50,7 @@ module Net
|
|
|
50
50
|
end
|
|
51
51
|
if options[:host]
|
|
52
52
|
host = IPAddr.new(options[:host]).to_s
|
|
53
|
-
raise ArgumentError, "
|
|
53
|
+
raise ArgumentError, "清单中未找到设备 #{options[:host]}" unless plan.inventory.any? { |device| device.host == host }
|
|
54
54
|
end
|
|
55
55
|
if options[:plan]
|
|
56
56
|
@output.puts JSON.pretty_generate(plan_summary(plan))
|
|
@@ -74,7 +74,7 @@ module Net
|
|
|
74
74
|
@error.puts "net-connector-backup: #{exception.message}"
|
|
75
75
|
2
|
|
76
76
|
rescue StandardError => exception
|
|
77
|
-
@error.puts "net-connector-backup
|
|
77
|
+
@error.puts "net-connector-backup 失败(#{exception.class})"
|
|
78
78
|
2
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -84,25 +84,25 @@ module Net
|
|
|
84
84
|
def parse_options
|
|
85
85
|
options = { config: @env["NET_CONNECTOR_CONFIG"] }
|
|
86
86
|
parser = OptionParser.new do |args|
|
|
87
|
-
args.banner = "
|
|
88
|
-
args.on("--config FILE", "
|
|
89
|
-
args.on("--show-config", "
|
|
90
|
-
args.on("--plan", "
|
|
91
|
-
args.on("--tftp", "
|
|
92
|
-
args.on("--export IP", "
|
|
93
|
-
args.on("--output FILE", "
|
|
94
|
-
args.on("--directory PATH", "
|
|
95
|
-
args.on("--host IP", "
|
|
96
|
-
args.on("--concurrency N", Integer, "
|
|
97
|
-
args.on("--limit-per-vendor N", Integer, "
|
|
87
|
+
args.banner = "用法:net-connector-backup [--config FILE] [--plan | --show-config | --export IP] [--tftp]"
|
|
88
|
+
args.on("--config FILE", "读取不含凭据的 YAML 设置") { |value| options[:config] = value }
|
|
89
|
+
args.on("--show-config", "显示生效的非敏感设置") { options[:show_config] = true }
|
|
90
|
+
args.on("--plan", "预览设备清单,不连接设备") { options[:plan] = true }
|
|
91
|
+
args.on("--tftp", "由设备发起 TFTP 备份") { options[:tftp] = true }
|
|
92
|
+
args.on("--export IP", "导出已保存的本地配置") { |value| options[:export] = value }
|
|
93
|
+
args.on("--output FILE", "将导出配置写入私有文件") { |value| options[:output] = value }
|
|
94
|
+
args.on("--directory PATH", "备份和报告目录") { |value| options[:directory] = value }
|
|
95
|
+
args.on("--host IP", "按管理地址选择一台设备") { |value| options[:host] = value }
|
|
96
|
+
args.on("--concurrency N", Integer, "最大并发设备数") { |value| options[:concurrency] = value }
|
|
97
|
+
args.on("--limit-per-vendor N", Integer, "每厂商选择 N 台设备(1 至 5)") do |value|
|
|
98
98
|
options[:limit] = value
|
|
99
99
|
end
|
|
100
|
-
args.on("--all", "
|
|
101
|
-
args.on("-v", "--version", "
|
|
100
|
+
args.on("--all", "选择全部就绪设备") { options[:all] = true }
|
|
101
|
+
args.on("-v", "--version", "显示版本") do
|
|
102
102
|
@output.puts Net::Connector::VERSION
|
|
103
103
|
options[:done] = true
|
|
104
104
|
end
|
|
105
|
-
args.on("-h", "--help", "
|
|
105
|
+
args.on("-h", "--help", "显示帮助") do
|
|
106
106
|
@output.puts args
|
|
107
107
|
options[:done] = true
|
|
108
108
|
end
|
|
@@ -116,11 +116,11 @@ module Net
|
|
|
116
116
|
# 拒绝互斥或缺少依赖的命令行选项。
|
|
117
117
|
def validate_options!(options)
|
|
118
118
|
modes = [:show_config, :plan, :export].count { |key| options[key] }
|
|
119
|
-
raise ArgumentError, "
|
|
120
|
-
raise ArgumentError, "--output
|
|
121
|
-
raise ArgumentError, "--all
|
|
119
|
+
raise ArgumentError, "--show-config、--plan 和 --export 只能选择一项" if modes > 1
|
|
120
|
+
raise ArgumentError, "--output 需要同时指定 --export" if options[:output] && !options[:export]
|
|
121
|
+
raise ArgumentError, "--all 不能与 --limit-per-vendor 同时使用" if options[:all] && options[:limit]
|
|
122
122
|
if options[:export] && (options[:tftp] || options[:all] || options[:limit] || options[:concurrency] || options[:host])
|
|
123
|
-
raise ArgumentError, "--export
|
|
123
|
+
raise ArgumentError, "--export 不能与备份选项同时使用"
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
|
|
@@ -48,18 +48,14 @@ module Net
|
|
|
48
48
|
|
|
49
49
|
# 生成设备 CLI 可用的 ASCII 远端文件名。
|
|
50
50
|
def tftp_filename
|
|
51
|
-
return "running-config.xml" if vendor == :palo_alto
|
|
52
|
-
|
|
53
51
|
label = name.to_s.encode(Encoding::US_ASCII, invalid: :replace, undef: :replace, replace: "-")
|
|
54
52
|
.gsub(/[^A-Za-z0-9._-]+/, "-").byteslice(0, 180)
|
|
55
53
|
.gsub(/\A[._-]+|[._-]+\z/, "")
|
|
56
54
|
label = vendor&.to_s || "device" if label.empty?
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
end
|
|
62
|
-
TftpTarget.filename(host, extension: extension, label: label)
|
|
55
|
+
strategy = Net::Connector.vendor_class(vendor).profile.tftp_strategy if vendor
|
|
56
|
+
# 旧版自定义策略只需实现实例接口,缺少命名接口时沿用通用 cfg 名称。
|
|
57
|
+
strategy = Operations::Tftp::Strategy unless strategy.respond_to?(:filename)
|
|
58
|
+
TftpTarget.validate_path!(strategy.filename(host, label: label))
|
|
63
59
|
end
|
|
64
60
|
|
|
65
61
|
# 按厂商及设备地址实例化连接器。
|
|
@@ -63,7 +63,7 @@ module Net
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
#
|
|
66
|
+
# 让设备主动导出配置,在执行前检查同批目标文件名冲突。
|
|
67
67
|
def tftp_backup_all(server:, source_files: {}, concurrency: @settings.concurrency,
|
|
68
68
|
limit_per_vendor: 5, vrfs: {}, on_start: nil, on_result: nil, plan: nil,
|
|
69
69
|
report_directory: @settings.backup_directory)
|
|
@@ -131,53 +131,7 @@ module Net
|
|
|
131
131
|
def validate_plan!(plan, mode)
|
|
132
132
|
raise ArgumentError, "plan must be a #{mode} Netdisco plan" unless plan.is_a?(Plan) && plan.mode == mode
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
ready = plan.ready
|
|
136
|
-
outcomes = plan.outcomes
|
|
137
|
-
unless inventory.is_a?(Array) && inventory.all?(Device) && ready.is_a?(Array) &&
|
|
138
|
-
outcomes.is_a?(Array) && outcomes.size == inventory.size
|
|
139
|
-
raise ArgumentError, "plan inventory and outcomes must have the same device slots"
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
selected = {}
|
|
143
|
-
previous_index = -1
|
|
144
|
-
ready.each do |task|
|
|
145
|
-
unless task.is_a?(Array) && task.size == 2 && task.first.is_a?(Integer)
|
|
146
|
-
raise ArgumentError, "plan contains an invalid device task"
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
index, device = task
|
|
150
|
-
unless index > previous_index && index < inventory.size && inventory[index].equal?(device) && device.ready?
|
|
151
|
-
raise ArgumentError, "plan tasks must reference ready inventory devices in order"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
selected[index] = true
|
|
155
|
-
previous_index = index
|
|
156
|
-
end
|
|
157
|
-
if mode == :tftp && ready.count { |_index, device| device.vendor == :palo_alto } > 1
|
|
158
|
-
raise ArgumentError, "plan cannot upload multiple Palo Alto devices to one TFTP filename"
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
inventory.each_with_index do |device, index|
|
|
162
|
-
result = outcomes[index]
|
|
163
|
-
next if selected[index] && result.nil?
|
|
164
|
-
|
|
165
|
-
unless !selected[index] && valid_skipped_outcome?(result, device, mode)
|
|
166
|
-
raise ArgumentError, "plan outcome does not match its inventory slot"
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# 未执行设备必须保留与清单一致的跳过原因,且不能带有执行产物。
|
|
172
|
-
def valid_skipped_outcome?(result, device, mode)
|
|
173
|
-
return false unless result.is_a?(Outcome) && result.device.equal?(device) && result.backup.nil? &&
|
|
174
|
-
result.error_code.nil? && result.error_type.nil? &&
|
|
175
|
-
result.started_at.nil? && result.finished_at.nil?
|
|
176
|
-
|
|
177
|
-
return result.status == device.issue unless device.ready?
|
|
178
|
-
|
|
179
|
-
result.status == :sample_limit ||
|
|
180
|
-
(mode == :tftp && device.vendor == :palo_alto && result.status == :remote_filename_collision)
|
|
134
|
+
plan.validate!
|
|
181
135
|
end
|
|
182
136
|
|
|
183
137
|
# 构造包含错误类型的单台设备结果。
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
module Netdisco
|
|
6
|
+
# 固定一份清单上的选择结果与跳过原因,供预览和执行复用。
|
|
7
|
+
Plan = Data.define(:mode, :inventory, :ready, :outcomes) do
|
|
8
|
+
# Data 只冻结对象自身;这里复制并冻结可能由调用方修改的计划容器。
|
|
9
|
+
def initialize(mode:, inventory:, ready:, outcomes:)
|
|
10
|
+
unless inventory.is_a?(Array) && ready.is_a?(Array) && outcomes.is_a?(Array)
|
|
11
|
+
raise ArgumentError, "plan inventory, ready tasks, and outcomes must be Arrays"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
tasks = ready.map { |task| task.is_a?(Array) ? task.dup.freeze : task }.freeze
|
|
15
|
+
super(mode: mode, inventory: inventory.dup.freeze, ready: tasks, outcomes: outcomes.dup.freeze)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 按清单顺序返回选中的设备。
|
|
19
|
+
def selected = ready.map(&:last)
|
|
20
|
+
|
|
21
|
+
# 在任何设备 I/O 前校验完整快照,外部构造或修改的计划也必须遵守相同约束。
|
|
22
|
+
def validate!
|
|
23
|
+
raise ArgumentError, "plan mode must be backup or tftp" unless %i[backup tftp].include?(mode)
|
|
24
|
+
unless inventory.all?(Device) && outcomes.size == inventory.size
|
|
25
|
+
raise ArgumentError, "plan inventory and outcomes must have the same device slots"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
selected_indices = {}
|
|
29
|
+
filenames = {}
|
|
30
|
+
previous_index = -1
|
|
31
|
+
ready.each do |task|
|
|
32
|
+
unless task.is_a?(Array) && task.size == 2 && task.first.is_a?(Integer)
|
|
33
|
+
raise ArgumentError, "plan contains an invalid device task"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
index, device = task
|
|
37
|
+
unless index > previous_index && index < inventory.size && inventory[index].equal?(device) && device.ready?
|
|
38
|
+
raise ArgumentError, "plan tasks must reference ready inventory devices in order"
|
|
39
|
+
end
|
|
40
|
+
if mode == :tftp
|
|
41
|
+
filename = device.tftp_filename
|
|
42
|
+
raise ArgumentError, "plan cannot upload multiple devices to one TFTP filename" if filenames.key?(filename)
|
|
43
|
+
|
|
44
|
+
filenames[filename] = true
|
|
45
|
+
end
|
|
46
|
+
selected_indices[index] = true
|
|
47
|
+
previous_index = index
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
inventory.each_with_index do |device, index|
|
|
51
|
+
result = outcomes[index]
|
|
52
|
+
next if selected_indices[index] && result.nil?
|
|
53
|
+
|
|
54
|
+
unless !selected_indices[index] && valid_skipped_outcome?(result, device, filenames)
|
|
55
|
+
raise ArgumentError, "plan outcome does not match its inventory slot"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# 跳过结果不携带执行产物;文件冲突必须有本批次实际执行的目标作为依据。
|
|
64
|
+
def valid_skipped_outcome?(result, device, filenames)
|
|
65
|
+
return false unless result.is_a?(Outcome) && result.device.equal?(device) && result.backup.nil? &&
|
|
66
|
+
result.error_code.nil? && result.error_type.nil? &&
|
|
67
|
+
result.started_at.nil? && result.finished_at.nil?
|
|
68
|
+
|
|
69
|
+
return result.status == device.issue unless device.ready?
|
|
70
|
+
|
|
71
|
+
result.status == :sample_limit ||
|
|
72
|
+
(mode == :tftp && result.status == :remote_filename_collision && filenames.key?(device.tftp_filename))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "plan"
|
|
4
|
+
|
|
3
5
|
module Net
|
|
4
6
|
module Connector
|
|
5
7
|
module Netdisco
|
|
6
|
-
# 固定一份清单上的选择结果与跳过原因,供预览和执行复用。
|
|
7
|
-
Plan = Data.define(:mode, :inventory, :ready, :outcomes) do
|
|
8
|
-
# Data 只冻结对象自身;这里复制并冻结可能由调用方修改的计划容器。
|
|
9
|
-
def initialize(mode:, inventory:, ready:, outcomes:)
|
|
10
|
-
unless inventory.is_a?(Array) && ready.is_a?(Array) && outcomes.is_a?(Array)
|
|
11
|
-
raise ArgumentError, "plan inventory, ready tasks, and outcomes must be Arrays"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
tasks = ready.map { |task| task.is_a?(Array) ? task.dup.freeze : task }.freeze
|
|
15
|
-
super(mode: mode, inventory: inventory.dup.freeze, ready: tasks, outcomes: outcomes.dup.freeze)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# 按清单顺序返回选中的设备。
|
|
19
|
-
def selected = ready.map(&:last)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
8
|
# 根据设备就绪状态、厂商采样及远端文件冲突建立备份计划。
|
|
23
9
|
class Planner
|
|
24
10
|
# 保存本次规划使用的清单快照。
|
|
@@ -37,22 +23,25 @@ module Net
|
|
|
37
23
|
else
|
|
38
24
|
candidates
|
|
39
25
|
end
|
|
40
|
-
# PAN-OS 导出使用固定远端文件名,同批只允许一台设备写入。
|
|
41
|
-
palo_alto_index = selected.find { |device, _index| device.vendor == :palo_alto }&.last if mode == :tftp
|
|
42
26
|
selected_indices = selected.to_h { |_device, index| [index, true] }
|
|
27
|
+
filenames = {}
|
|
28
|
+
if mode == :tftp
|
|
29
|
+
# 先按采样顺序保留每个目标的首台设备,再按清单顺序输出结果。
|
|
30
|
+
selected.each { |device, index| filenames[device.tftp_filename] ||= index }
|
|
31
|
+
end
|
|
43
32
|
outcomes = Array.new(@inventory.size)
|
|
44
33
|
ready = @inventory.each_with_index.filter_map do |device, index|
|
|
45
34
|
if !device.ready?
|
|
46
35
|
outcomes[index] = skipped(device, device.issue)
|
|
47
36
|
nil
|
|
48
|
-
elsif
|
|
37
|
+
elsif !selected_indices.key?(index)
|
|
38
|
+
outcomes[index] = skipped(device, :sample_limit)
|
|
39
|
+
nil
|
|
40
|
+
elsif mode == :tftp && filenames.fetch(device.tftp_filename) != index
|
|
49
41
|
outcomes[index] = skipped(device, :remote_filename_collision)
|
|
50
42
|
nil
|
|
51
|
-
elsif selected_indices.key?(index)
|
|
52
|
-
[index, device].freeze
|
|
53
43
|
else
|
|
54
|
-
|
|
55
|
-
nil
|
|
44
|
+
[index, device].freeze
|
|
56
45
|
end
|
|
57
46
|
end
|
|
58
47
|
Plan.new(mode: mode, inventory: @inventory, ready: ready.freeze, outcomes: outcomes.freeze)
|
|
@@ -21,37 +21,44 @@ module Net
|
|
|
21
21
|
worker_count = [tasks.size, @concurrency].min
|
|
22
22
|
worker_count.times { queue << nil }
|
|
23
23
|
callback_errors = Queue.new
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
result = begin
|
|
36
|
-
yield device
|
|
37
|
-
rescue StandardError => error
|
|
38
|
-
on_error.call(device, error)
|
|
39
|
-
end
|
|
40
|
-
result = result.with(started_at: started_at, finished_at: Time.now.utc)
|
|
41
|
-
outcomes[index] = result
|
|
42
|
-
Array(on_result).each do |callback|
|
|
24
|
+
finished = Queue.new
|
|
25
|
+
threads = []
|
|
26
|
+
completed = false
|
|
27
|
+
begin
|
|
28
|
+
# 创建线程也属于批次生命周期;后续创建失败时要关闭已有任务。
|
|
29
|
+
worker_count.times do
|
|
30
|
+
threads << Thread.new do
|
|
31
|
+
Thread.current.report_on_exception = false
|
|
32
|
+
while (task = queue.pop)
|
|
33
|
+
index, device = task
|
|
34
|
+
started_at = Time.now.utc
|
|
43
35
|
begin
|
|
44
|
-
|
|
36
|
+
on_start&.call(device)
|
|
45
37
|
rescue StandardError => error
|
|
46
38
|
callback_errors << { host: device.host, error_type: error.class.name }.freeze
|
|
47
39
|
end
|
|
40
|
+
result = begin
|
|
41
|
+
yield device
|
|
42
|
+
rescue StandardError => error
|
|
43
|
+
on_error.call(device, error)
|
|
44
|
+
end
|
|
45
|
+
result = result.with(started_at: started_at, finished_at: Time.now.utc)
|
|
46
|
+
outcomes[index] = result
|
|
47
|
+
Array(on_result).each do |callback|
|
|
48
|
+
begin
|
|
49
|
+
callback.call(result)
|
|
50
|
+
rescue StandardError => error
|
|
51
|
+
callback_errors << { host: device.host, error_type: error.class.name }.freeze
|
|
52
|
+
end
|
|
53
|
+
end
|
|
48
54
|
end
|
|
55
|
+
ensure
|
|
56
|
+
finished << Thread.current
|
|
49
57
|
end
|
|
50
58
|
end
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
threads.each(&:value)
|
|
59
|
+
# 按完成顺序观察失败,避免后启动线程的中断被前面的慢设备阻塞。
|
|
60
|
+
# 结果仍写回清单槽位,完成通知不会改变公开结果的顺序。
|
|
61
|
+
worker_count.times { finished.pop.value }
|
|
55
62
|
completed = true
|
|
56
63
|
ensure
|
|
57
64
|
# 中断或某个 worker 抛出非 StandardError 时,不让其他设备任务
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "strategy"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
module Connector
|
|
7
|
+
module Operations
|
|
8
|
+
module Tftp
|
|
9
|
+
# H3C 和华为共用已保存文件上传流程,源文件发现仍由厂商策略负责。
|
|
10
|
+
class FileUpload < Strategy
|
|
11
|
+
# 未指定目标名时,设备使用配置源文件的 basename。
|
|
12
|
+
def default_path(source_file) = File.basename(source_file)
|
|
13
|
+
|
|
14
|
+
# 仅在调用方指定目标名时追加参数,保留设备原生命名行为。
|
|
15
|
+
def script(target, source_file:, vrf: nil)
|
|
16
|
+
raise ArgumentError, "TFTP file upload does not use vrf" if vrf
|
|
17
|
+
|
|
18
|
+
destination = target.explicit_path? ? " #{target.path}" : ""
|
|
19
|
+
Script.new([Command.new("tftp #{target.host} put #{source_file}#{destination}", timeout: 180)])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 只接受明确的成功消息、非零传输字节数或完整进度行。
|
|
23
|
+
def complete?(result)
|
|
24
|
+
completion_lines(result).any? { |line| completed_upload?(line) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# 两个厂商的上传完成消息使用相同的文本及进度格式。
|
|
30
|
+
def completed_upload?(line)
|
|
31
|
+
line.match?(/\A
|
|
32
|
+
(?:File\s+)?(?:transfer|upload)(?:\s+(?:is|was))?\s+
|
|
33
|
+
(?:success(?:ful(?:ly)?)?|succeeded|complete(?:d)?(?:\s+successfully)?)[.!]?
|
|
34
|
+
\z/ix) ||
|
|
35
|
+
line.match?(/\A[1-9]\d*\s+bytes?\s+sent(?:\s+in\s+[\d.]+\s+(?:secs?|seconds?))?[.!]?\z/i) ||
|
|
36
|
+
completed_progress?(line)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -13,6 +13,14 @@ module Net
|
|
|
13
13
|
autoload :PaloAlto, File.expand_path("palo_alto", __dir__)
|
|
14
14
|
|
|
15
15
|
class Strategy
|
|
16
|
+
# 厂商只声明格式差异,清单规划与设备直连共用同一命名规则。
|
|
17
|
+
def self.file_extension = "cfg"
|
|
18
|
+
|
|
19
|
+
# 不创建会话即可生成目标名,供批量计划检查文件覆盖风险。
|
|
20
|
+
def self.filename(host, label: nil)
|
|
21
|
+
TftpTarget.filename(host, extension: file_extension, label: label)
|
|
22
|
+
end
|
|
23
|
+
|
|
16
24
|
# 保存 TFTP 厂商策略使用的设备对象。
|
|
17
25
|
def initialize(device)
|
|
18
26
|
@device = device
|
|
@@ -23,7 +31,7 @@ module Net
|
|
|
23
31
|
def source_file(value) = value
|
|
24
32
|
|
|
25
33
|
# 根据设备地址生成默认远端文件名。
|
|
26
|
-
def default_path(_source_file) =
|
|
34
|
+
def default_path(_source_file) = self.class.filename(@device.host)
|
|
27
35
|
|
|
28
36
|
# 返回目标中指定的远端文件名。
|
|
29
37
|
def remote_path(target, _result) = target.path
|
|
@@ -53,16 +61,6 @@ module Net
|
|
|
53
61
|
line.match?(/\A100\s+([1-9]\d*(?:\.\d+)?[kMGT]?)\s+0\s+0\s+100\s+\1(?:\s+[\d.:kMGT-]+)*\z/i)
|
|
54
62
|
end
|
|
55
63
|
|
|
56
|
-
# H3C 和华为的上传完成消息使用相同的文本及进度格式。
|
|
57
|
-
def completed_upload?(line)
|
|
58
|
-
line.match?(/\A
|
|
59
|
-
(?:File\s+)?(?:transfer|upload)(?:\s+(?:is|was))?\s+
|
|
60
|
-
(?:success(?:ful(?:ly)?)?|succeeded|complete(?:d)?(?:\s+successfully)?)[.!]?
|
|
61
|
-
\z/ix) ||
|
|
62
|
-
line.match?(/\A[1-9]\d*\s+bytes?\s+sent(?:\s+in\s+[\d.]+\s+(?:secs?|seconds?))?[.!]?\z/i) ||
|
|
63
|
-
completed_progress?(line)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
64
|
# Cisco 明确报告复制完成的字节数,可附带耗时和速率。
|
|
67
65
|
def copied_bytes?(line)
|
|
68
66
|
line.match?(/\A
|
|
@@ -15,24 +15,38 @@ module Net
|
|
|
15
15
|
|
|
16
16
|
# 厂商拓扑规则;默认不提供任何设备能力。
|
|
17
17
|
class Strategy
|
|
18
|
+
# 保存设备供厂商规则查询档案和会话状态。
|
|
18
19
|
def initialize(device)
|
|
19
20
|
@device = device
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# 声明策略类支持的拓扑能力,默认全部关闭。
|
|
22
24
|
def self.supports?(_capability) = false
|
|
25
|
+
# 将实例级能力查询交给策略类声明。
|
|
23
26
|
def supports?(capability) = self.class.supports?(capability)
|
|
24
27
|
|
|
28
|
+
# 返回读取邻居的命令,未定义时不支持发现。
|
|
25
29
|
def neighbor_command = nil
|
|
30
|
+
# 根据原始输出选择邻居解析模板。
|
|
26
31
|
def neighbor_template(_output) = nil
|
|
32
|
+
# 返回当前接口描述所用的解析模板。
|
|
27
33
|
def description_template = nil
|
|
34
|
+
# 返回输出中应解析出的邻居数量,供完整性校验使用。
|
|
28
35
|
def expected_neighbor_count(_output, _template) = 0
|
|
36
|
+
# 只有明确的空表证据才允许返回空邻居列表。
|
|
29
37
|
def empty_neighbor_output?(_output) = false
|
|
38
|
+
# 返回进入设备配置视图的命令。
|
|
30
39
|
def enter_configuration = nil
|
|
40
|
+
# 返回离开配置视图和保存配置所需的命令。
|
|
31
41
|
def finish_commands = []
|
|
42
|
+
# 为一条接口描述变更生成厂商命令。
|
|
32
43
|
def change_commands(_change) = []
|
|
44
|
+
# 标明邻居发现使用的协议。
|
|
33
45
|
def protocol = :lldp
|
|
46
|
+
# 在下发前允许厂商调整命令对象及超时。
|
|
34
47
|
def script_command(command) = command
|
|
35
48
|
|
|
49
|
+
# 将发现接口名归一为查找当前配置所用的键。
|
|
36
50
|
def interface_key(name) = InterfaceName.key(name)
|
|
37
51
|
|
|
38
52
|
# 将发现协议常用的接口简称展开为配置视图可识别的名称。
|
|
@@ -6,24 +6,33 @@ module Net
|
|
|
6
6
|
module Connector
|
|
7
7
|
module CiscoIos
|
|
8
8
|
class Topology < Operations::Topology::Strategy
|
|
9
|
+
# IOS 提供 CDP 邻居、配置描述读取和描述变更。
|
|
9
10
|
def self.supports?(capability)
|
|
10
11
|
%i[neighbors interface_descriptions interface_description_changes].include?(capability)
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
# 使用详细 CDP 输出获取对端设备与端口。
|
|
13
15
|
def neighbor_command = "show cdp neighbors detail"
|
|
16
|
+
# 从运行配置解析接口描述。
|
|
14
17
|
def description_template = "cisco_ios_running_config_interfaces.textfsm"
|
|
18
|
+
# 标记发现协议,供邻居记录使用。
|
|
15
19
|
def protocol = :cdp
|
|
20
|
+
# 进入全局配置视图。
|
|
16
21
|
def enter_configuration = "configure terminal"
|
|
22
|
+
# 退出配置视图并执行档案定义的保存命令。
|
|
17
23
|
def finish_commands = ["end"] + @device.profile.save_commands
|
|
18
24
|
|
|
25
|
+
# 每个 Device ID 表示一条应由模板解析的邻居记录。
|
|
19
26
|
def expected_neighbor_count(output, _template)
|
|
20
27
|
output.scan(/^\s*Device ID:\s*\S+/i).size
|
|
21
28
|
end
|
|
22
29
|
|
|
30
|
+
# 只有设备明确报告零 CDP 邻居时才认可空表。
|
|
23
31
|
def empty_neighbor_output?(output)
|
|
24
32
|
output.match?(/Total cdp entries displayed\s*:\s*0|no cdp neighbors/i)
|
|
25
33
|
end
|
|
26
34
|
|
|
35
|
+
# 复用公共接口描述命令,保留本机接口原名。
|
|
27
36
|
def change_commands(change)
|
|
28
37
|
InterfaceDescription.commands(interface: change.interface, description: change.new_description)
|
|
29
38
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../operations/tftp/
|
|
3
|
+
require_relative "../../operations/tftp/file_upload"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module H3c
|
|
8
|
-
class TftpBackup < Operations::Tftp::
|
|
8
|
+
class TftpBackup < Operations::Tftp::FileUpload
|
|
9
9
|
# 选取并校验 H3C 启动配置文件路径。
|
|
10
10
|
def source_file(value)
|
|
11
11
|
return TftpTarget.validate_source_file!(value) if value
|
|
@@ -21,22 +21,6 @@ module Net
|
|
|
21
21
|
|
|
22
22
|
TftpTarget.validate_source_file!(path.sub(/\(\*\)\z/, ""))
|
|
23
23
|
end
|
|
24
|
-
|
|
25
|
-
# 根据 H3C 源文件确定默认导出名称。
|
|
26
|
-
def default_path(source_file) = File.basename(source_file)
|
|
27
|
-
|
|
28
|
-
# 构造当前厂商的 TFTP 导出交互脚本。
|
|
29
|
-
def script(target, source_file:, vrf: nil)
|
|
30
|
-
raise ArgumentError, "H3C TFTP backup does not use vrf" if vrf
|
|
31
|
-
|
|
32
|
-
destination = target.explicit_path? ? " #{target.path}" : ""
|
|
33
|
-
Script.new([Command.new("tftp #{target.host} put #{source_file}#{destination}", timeout: 180)])
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# 检查设备回显是否确认 TFTP 传输完成。
|
|
37
|
-
def complete?(result)
|
|
38
|
-
completion_lines(result).any? { |line| completed_upload?(line) }
|
|
39
|
-
end
|
|
40
24
|
end
|
|
41
25
|
end
|
|
42
26
|
end
|
|
@@ -7,15 +7,21 @@ module Net
|
|
|
7
7
|
module H3c
|
|
8
8
|
class Topology < Operations::Topology::Strategy
|
|
9
9
|
TABLE_HEADER = /^\s*(?:System Name\s+Local Interface\b|(?:Local Interface|LocalIf)\s+)/i
|
|
10
|
+
# H3C 提供 LLDP 邻居、配置描述读取和描述变更。
|
|
10
11
|
def self.supports?(capability)
|
|
11
12
|
%i[neighbors interface_descriptions interface_description_changes].include?(capability)
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
# 读取设备的 LLDP 邻居列表。
|
|
14
16
|
def neighbor_command = "display lldp neighbor-information list"
|
|
17
|
+
# 从运行配置解析当前接口描述。
|
|
15
18
|
def description_template = "h3c_interface_descriptions.textfsm"
|
|
19
|
+
# 进入系统视图以修改接口描述。
|
|
16
20
|
def enter_configuration = "system-view"
|
|
21
|
+
# 返回用户视图并执行保存命令。
|
|
17
22
|
def finish_commands = ["return"] + @device.profile.save_commands
|
|
18
23
|
|
|
24
|
+
# H3C 不同版本的列顺序不同,按表头选对应模板。
|
|
19
25
|
def neighbor_template(output)
|
|
20
26
|
return "h3c_lldp_name_first.textfsm" if output.match?(/^\s*System Name\s+Local Interface\b/im)
|
|
21
27
|
return "h3c_lldp_local_first.textfsm" if output.match?(/^\s*(?:Local Interface|LocalIf)\s+/im)
|
|
@@ -24,6 +30,7 @@ module Net
|
|
|
24
30
|
host: @device.host, phase: :discover)
|
|
25
31
|
end
|
|
26
32
|
|
|
33
|
+
# 按表头后的有效数据行计数,未知行会让完整性校验失败。
|
|
27
34
|
def expected_neighbor_count(output, template)
|
|
28
35
|
local_column = template == "h3c_lldp_name_first.textfsm" ? 1 : 0
|
|
29
36
|
lines = neighbor_lines(output)
|
|
@@ -38,12 +45,14 @@ module Net
|
|
|
38
45
|
# 表头之后的未知行不能作为零邻居证据。
|
|
39
46
|
def empty_neighbor_output?(output) = neighbor_lines(output).empty?
|
|
40
47
|
|
|
48
|
+
# 在接口视图设置描述后用 quit 返回系统视图。
|
|
41
49
|
def change_commands(change)
|
|
42
50
|
InterfaceDescription.commands(interface: change.interface, description: change.new_description, leave: "quit")
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
private
|
|
46
54
|
|
|
55
|
+
# 过滤表头、分隔线和设备提示符,保留待解析的邻居行。
|
|
47
56
|
def neighbor_lines(output)
|
|
48
57
|
output.lines.drop_while { |line| !line.match?(TABLE_HEADER) }.map(&:strip).reject do |line|
|
|
49
58
|
line.empty? || line.match?(TABLE_HEADER) || line.match?(/\A[-=+ ]+\z/) ||
|