net-connector 0.4.0 → 0.4.2

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +110 -68
  3. data/CONTRIBUTING.md +40 -0
  4. data/README.md +140 -95
  5. data/SECURITY.md +11 -0
  6. data/docs/VERIFICATION.md +129 -5
  7. data/docs/architecture.md +321 -248
  8. data/lib/net/connector/device/base.rb +12 -4
  9. data/lib/net/connector/device/interface_description.rb +3 -0
  10. data/lib/net/connector/device/profile/builder.rb +236 -0
  11. data/lib/net/connector/device/profile.rb +2 -227
  12. data/lib/net/connector/device/running_config/strategy.rb +5 -0
  13. data/lib/net/connector/device/running_config.rb +7 -0
  14. data/lib/net/connector/engine/command.rb +14 -2
  15. data/lib/net/connector/engine/configuration.rb +10 -7
  16. data/lib/net/connector/engine/dialogue.rb +53 -28
  17. data/lib/net/connector/engine/errors.rb +41 -54
  18. data/lib/net/connector/engine/execution.rb +32 -5
  19. data/lib/net/connector/engine/log.rb +23 -17
  20. data/lib/net/connector/engine/session.rb +39 -7
  21. data/lib/net/connector/engine/terminal_renderer.rb +7 -4
  22. data/lib/net/connector/netdisco/batch.rb +31 -3
  23. data/lib/net/connector/netdisco/cli.rb +92 -50
  24. data/lib/net/connector/netdisco/client.rb +206 -73
  25. data/lib/net/connector/netdisco/config_file.rb +26 -4
  26. data/lib/net/connector/netdisco/device.rb +4 -8
  27. data/lib/net/connector/netdisco/diagnostic.rb +91 -0
  28. data/lib/net/connector/netdisco/fleet.rb +105 -108
  29. data/lib/net/connector/netdisco/inventory_budget.rb +49 -0
  30. data/lib/net/connector/netdisco/plan.rb +77 -0
  31. data/lib/net/connector/netdisco/planner.rb +12 -23
  32. data/lib/net/connector/netdisco/report.rb +94 -0
  33. data/lib/net/connector/netdisco/rules.rb +28 -5
  34. data/lib/net/connector/netdisco/settings.rb +194 -85
  35. data/lib/net/connector/netdisco/worker.rb +43 -27
  36. data/lib/net/connector/operations/backup_lock.rb +116 -0
  37. data/lib/net/connector/operations/local_backup.rb +49 -9
  38. data/lib/net/connector/operations/parse_output.rb +20 -3
  39. data/lib/net/connector/operations/private_file.rb +94 -5
  40. data/lib/net/connector/operations/safe_file.rb +62 -0
  41. data/lib/net/connector/operations/saved_config/legacy_index.rb +109 -0
  42. data/lib/net/connector/operations/saved_config.rb +40 -10
  43. data/lib/net/connector/operations/tftp/file_upload.rb +55 -0
  44. data/lib/net/connector/operations/tftp/strategy.rb +9 -11
  45. data/lib/net/connector/operations/tftp_backup.rb +78 -15
  46. data/lib/net/connector/operations/tftp_receipt.rb +73 -0
  47. data/lib/net/connector/operations/topology/immediate_strategy.rb +45 -0
  48. data/lib/net/connector/operations/topology/strategy.rb +34 -0
  49. data/lib/net/connector/operations/topology.rb +98 -29
  50. data/lib/net/connector/operations.rb +6 -0
  51. data/lib/net/connector/vendor/cisco_ios/running_config.rb +1 -0
  52. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +9 -2
  53. data/lib/net/connector/vendor/cisco_ios/topology.rb +18 -3
  54. data/lib/net/connector/vendor/cisco_nxos/running_config.rb +1 -0
  55. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +8 -1
  56. data/lib/net/connector/vendor/h3c/tftp_backup.rb +7 -18
  57. data/lib/net/connector/vendor/h3c/topology.rb +17 -3
  58. data/lib/net/connector/vendor/hillstone/running_config.rb +1 -0
  59. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +16 -3
  60. data/lib/net/connector/vendor/hillstone/topology.rb +17 -3
  61. data/lib/net/connector/vendor/huawei/tftp_backup.rb +8 -18
  62. data/lib/net/connector/vendor/palo_alto/running_config.rb +4 -0
  63. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +19 -8
  64. data/lib/net/connector/vendor/palo_alto/topology.rb +16 -1
  65. data/lib/net/connector/vendor/radware/tftp_backup.rb +11 -4
  66. data/lib/net/connector/vendor/radware/topology.rb +3 -0
  67. data/lib/net/connector/version.rb +1 -1
  68. metadata +60 -28
@@ -172,8 +172,9 @@ module Net
172
172
 
173
173
  # 采集配置并以原子方式保存为私有文件。
174
174
  # 采集失败时保留已有备份文件。
175
- def backup(path:)
176
- Operations::LocalBackup.new(self).call(path: path)
175
+ def backup(path:, lock_timeout: 0)
176
+ @session.assert_path_lock_order!(:backup)
177
+ Operations::LocalBackup.new(self).call(path: path, lock_timeout: lock_timeout)
177
178
  end
178
179
 
179
180
  # 要求设备直接向 TFTP 服务器导出原生配置。
@@ -182,6 +183,11 @@ module Net
182
183
  Operations::TftpBackup.new(self).call(host: host, path: path, source_file: source_file, vrf: vrf)
183
184
  end
184
185
 
186
+ # 明确请求来源、格式及设备报告等级;旧入口继续返回原有三字段对象。
187
+ def tftp_backup_receipt(host:, path: nil, source_file: nil, vrf: nil)
188
+ Operations::TftpBackup.new(self).call_receipt(host: host, path: path, source_file: source_file, vrf: vrf)
189
+ end
190
+
185
191
  # 配置采集是设备的基础能力,两个公共入口共享同一流程。
186
192
  def collect_config = RunningConfig.new(self).call
187
193
 
@@ -325,6 +331,7 @@ module Net
325
331
  @config_strategy_scope = previous
326
332
  end
327
333
 
334
+ # 只允许当前 Fiber 复用采集中的策略;离线调用创建独立策略。
328
335
  def config_strategy
329
336
  scope = @config_strategy_scope
330
337
  scope && scope.first.equal?(Fiber.current) ? scope.last : RunningConfig.strategy(self)
@@ -355,10 +362,11 @@ module Net
355
362
  prepare: method(:prepare_command), after_command: finish_step, prompt: prompt)
356
363
  execution.context[:operation] = operation if operation
357
364
  execution.context[:privilege] = privilege
365
+ output_sensitive = script.any?(&:output_sensitive?)
358
366
  @session.perform(:script) do
359
- before_batch(execution)
367
+ @session.output_scope(output_sensitive) { before_batch(execution) }
360
368
  result = execution.execute(script, &on_step)
361
- finalize ? finalize.call(result) : result
369
+ @session.output_scope(output_sensitive) { finalize ? finalize.call(result) : result }
362
370
  end
363
371
  rescue Error => error
364
372
  Result.new(steps: execution ? execution.steps : [], error: error)
@@ -6,6 +6,7 @@ module Net
6
6
  module Connector
7
7
  # 描述文本与常见接口视图命令的纯构造方法,不连接或修改设备。
8
8
  module InterfaceDescription
9
+ # 将邻居名称和接口拼成默认描述;只缩写邻居接口,保留原始大小写。
9
10
  def self.format(neighbor, abbreviate: true, lowercase: false)
10
11
  port = if abbreviate
11
12
  InterfaceName.short(neighbor.neighbor_interface, lowercase: lowercase)
@@ -15,6 +16,7 @@ module Net
15
16
  validate!("To #{neighbor.neighbor_name} #{port}")
16
17
  end
17
18
 
19
+ # 生成进入接口、设置描述和退出视图的命令,不执行设备操作。
18
20
  def self.commands(interface:, description:, leave: "exit")
19
21
  validate_interface!(interface)
20
22
  validate!(description)
@@ -31,6 +33,7 @@ module Net
31
33
  raise ArgumentError, "description must be 1-80 bytes of plain interface text"
32
34
  end
33
35
 
36
+ # 拒绝可能把描述命令拆成多条命令的不安全接口名。
34
37
  def self.validate_interface!(value)
35
38
  return value if value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9.\/-]*\z/)
36
39
 
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Connector
5
+ class Profile
6
+ # 构造器把声明块中的领域语言转换为设备档案字段。
7
+ class Builder
8
+ # 保存继承值并提供 commands、prompts 等有限的声明入口。
9
+ def initialize(parent)
10
+ @values = parent.to_h
11
+ end
12
+
13
+ # 声明运行配置和保存配置命令。
14
+ def commands(&definition)
15
+ check_block!(:commands, definition)
16
+ commands = Commands.new(
17
+ @values[:config_commands], @values[:save_commands]
18
+ )
19
+ commands.instance_eval(&definition)
20
+ @values[:config_commands] = commands.config_commands
21
+ @values[:save_commands] = commands.save_commands
22
+ self
23
+ end
24
+
25
+ # 声明登录、命令、凭据和提权提示符。
26
+ def prompts(&definition)
27
+ check_block!(:prompts, definition)
28
+ Prompts.new(@values).instance_eval(&definition)
29
+ self
30
+ end
31
+
32
+ # 声明分页模式及自动翻页响应。
33
+ def pager(&definition)
34
+ check_block!(:pager, definition)
35
+ Pager.new(@values).instance_eval(&definition)
36
+ self
37
+ end
38
+
39
+ # 声明认证失败和命令失败的输出模式。
40
+ def errors(&definition)
41
+ check_block!(:errors, definition)
42
+ Errors.new(@values).instance_eval(&definition)
43
+ self
44
+ end
45
+
46
+ # 声明登录挑战和命令确认交互。
47
+ def interactions(&definition)
48
+ check_block!(:interactions, definition)
49
+ interactions = Interactions.new(@values)
50
+ interactions.instance_eval(&definition)
51
+ @values[:login_interactions] = interactions.login_interactions
52
+ @values[:confirmation_interactions] = interactions.confirmation_interactions
53
+ self
54
+ end
55
+
56
+ # 声明进入特权模式所需的命令和提示符。
57
+ def privilege(&definition)
58
+ check_block!(:privilege, definition)
59
+ Privilege.new(@values).instance_eval(&definition)
60
+ self
61
+ end
62
+
63
+ # 声明旧版 SSH 协商参数。
64
+ def ssh(&definition)
65
+ check_block!(:ssh, definition)
66
+ Ssh.new(@values).instance_eval(&definition)
67
+ self
68
+ end
69
+
70
+ # 设置设备命令的默认超时秒数。
71
+ def command_timeout(seconds)
72
+ @values[:command_timeout] = seconds
73
+ self
74
+ end
75
+
76
+ # 设置避免设备自动换行的终端宽高。
77
+ def terminal_size(width, height = nil)
78
+ @values[:terminal_size] = height.nil? ? width : [width, height]
79
+ self
80
+ end
81
+
82
+ # 绑定已加载的策略类;nil 明确关闭继承的业务能力。
83
+ def tftp_strategy(klass)
84
+ @values[:tftp_strategy] = klass
85
+ self
86
+ end
87
+
88
+ # 指定运行配置采集策略,覆盖继承的厂商规则。
89
+ def running_config_strategy(klass)
90
+ @values[:running_config_strategy] = klass
91
+ self
92
+ end
93
+
94
+ # 指定邻居发现和描述下发策略,nil 表示关闭能力。
95
+ def topology_strategy(klass)
96
+ @values[:topology_strategy] = klass
97
+ self
98
+ end
99
+
100
+ # 将声明字段校验并构造成不可变档案。
101
+ def build = Profile.new(**@values)
102
+
103
+ private
104
+
105
+ # 块式声明入口必须有明确的子 DSL,避免静默忽略配置。
106
+ def check_block!(name, definition)
107
+ raise ArgumentError, "#{name} requires a block" unless definition
108
+ end
109
+
110
+ # 命令集合的声明对象。
111
+ class Commands
112
+ attr_reader :config_commands, :save_commands
113
+
114
+ # 复制继承的命令,后续修改不会污染父类档案。
115
+ def initialize(config_commands, save_commands)
116
+ @config_commands = config_commands&.dup
117
+ @save_commands = save_commands.dup
118
+ end
119
+
120
+ # 声明读取运行配置的命令序列。
121
+ def running_config(*commands)
122
+ @config_commands = commands.flatten
123
+ end
124
+
125
+ # 声明保存配置的命令序列。
126
+ def save_config(*commands)
127
+ @save_commands = commands.flatten
128
+ end
129
+ end
130
+
131
+ # 提示符的声明对象。
132
+ class Prompts
133
+ # 将登录、命令、用户名、密码和特权提示写入当前档案。
134
+ def initialize(values)
135
+ @values = values
136
+ end
137
+
138
+ # 声明登录结束提示符。
139
+ def login(pattern) = @values[:login_prompt] = pattern
140
+
141
+ # 声明普通命令结束提示符。
142
+ def command(pattern) = @values[:command_prompt] = pattern
143
+
144
+ # 声明用户名输入提示。
145
+ def username(pattern) = @values[:username_prompt] = pattern
146
+
147
+ # 声明密码输入提示。
148
+ def password(pattern) = @values[:password_prompt] = pattern
149
+
150
+ # 声明特权模式提示符。
151
+ def privilege(pattern) = @values[:privilege_prompt] = pattern
152
+ end
153
+
154
+ # 分页规则的声明对象。
155
+ class Pager
156
+ # 将分页模式和响应写入当前档案。
157
+ def initialize(values)
158
+ @values = values
159
+ end
160
+
161
+ # 声明分页提示匹配模式。
162
+ def pattern(value) = @values[:pager_pattern] = value
163
+
164
+ # 声明匹配分页提示后发送的字节。
165
+ def response(value) = @values[:pager_response] = value
166
+ end
167
+
168
+ # 设备错误模式的声明对象。
169
+ class Errors
170
+ # 将错误模式列表写入当前档案。
171
+ def initialize(values)
172
+ @values = values
173
+ end
174
+
175
+ # 替换认证失败模式列表。
176
+ def authentication(*patterns)
177
+ @values[:authentication_error_patterns] = patterns.flatten
178
+ end
179
+
180
+ # 替换命令失败模式列表。
181
+ def command(*patterns)
182
+ @values[:command_error_patterns] = patterns.flatten
183
+ end
184
+ end
185
+
186
+ # 登录和命令交互的声明对象。
187
+ class Interactions
188
+ attr_reader :login_interactions, :confirmation_interactions
189
+
190
+ # 复制继承的交互规则,子类可以在其上追加设备差异。
191
+ def initialize(values)
192
+ @login_interactions = values[:login_interactions].dup
193
+ @confirmation_interactions = values[:confirmation_interactions].dup
194
+ end
195
+
196
+ # 声明登录阶段的一次性应答。
197
+ def login(pattern, response:, sensitive: false, limit: 1, capture: true)
198
+ @login_interactions << Interaction.new(pattern, response, sensitive: sensitive,
199
+ limit: limit, capture: capture)
200
+ end
201
+
202
+ # 声明命令阶段的确认应答。
203
+ def confirm(pattern, response:, sensitive: false, limit: nil, capture: true)
204
+ @confirmation_interactions << Interaction.new(pattern, response, sensitive: sensitive,
205
+ limit: limit, capture: capture)
206
+ end
207
+ end
208
+
209
+ # 特权认证规则的声明对象。
210
+ class Privilege
211
+ # 将特权命令和结束提示写入当前档案。
212
+ def initialize(values)
213
+ @values = values
214
+ end
215
+
216
+ # 声明进入特权模式的设备命令。
217
+ def command(value) = @values[:privilege_command] = value
218
+
219
+ # 声明特权命令完成后的提示符。
220
+ def prompt(value) = @values[:privilege_prompt] = value
221
+ end
222
+
223
+ # SSH 兼容参数的声明对象。
224
+ class Ssh
225
+ # 将旧版 SSH 参数写入当前档案。
226
+ def initialize(values)
227
+ @values = values
228
+ end
229
+
230
+ # 声明连接旧版设备所需的参数序列。
231
+ def legacy_arguments(*arguments) = @values[:legacy_ssh_arguments] = arguments.flatten
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -110,233 +110,6 @@ module Net
110
110
  "save_commands=#{save_commands.size}>"
111
111
  end
112
112
 
113
- # 构造器把声明块中的领域语言转换为设备档案字段。
114
- class Builder
115
- # 保存继承值并提供 commands、prompts 等有限的声明入口。
116
- def initialize(parent)
117
- @values = parent.to_h
118
- end
119
-
120
- # 声明运行配置和保存配置命令。
121
- def commands(&definition)
122
- check_block!(:commands, definition)
123
- commands = Commands.new(
124
- @values[:config_commands], @values[:save_commands]
125
- )
126
- commands.instance_eval(&definition)
127
- @values[:config_commands] = commands.config_commands
128
- @values[:save_commands] = commands.save_commands
129
- self
130
- end
131
-
132
- # 声明登录、命令、凭据和提权提示符。
133
- def prompts(&definition)
134
- check_block!(:prompts, definition)
135
- Prompts.new(@values).instance_eval(&definition)
136
- self
137
- end
138
-
139
- # 声明分页模式及自动翻页响应。
140
- def pager(&definition)
141
- check_block!(:pager, definition)
142
- Pager.new(@values).instance_eval(&definition)
143
- self
144
- end
145
-
146
- # 声明认证失败和命令失败的输出模式。
147
- def errors(&definition)
148
- check_block!(:errors, definition)
149
- Errors.new(@values).instance_eval(&definition)
150
- self
151
- end
152
-
153
- # 声明登录挑战和命令确认交互。
154
- def interactions(&definition)
155
- check_block!(:interactions, definition)
156
- interactions = Interactions.new(@values)
157
- interactions.instance_eval(&definition)
158
- @values[:login_interactions] = interactions.login_interactions
159
- @values[:confirmation_interactions] = interactions.confirmation_interactions
160
- self
161
- end
162
-
163
- # 声明进入特权模式所需的命令和提示符。
164
- def privilege(&definition)
165
- check_block!(:privilege, definition)
166
- Privilege.new(@values).instance_eval(&definition)
167
- self
168
- end
169
-
170
- # 声明旧版 SSH 协商参数。
171
- def ssh(&definition)
172
- check_block!(:ssh, definition)
173
- Ssh.new(@values).instance_eval(&definition)
174
- self
175
- end
176
-
177
- # 设置设备命令的默认超时秒数。
178
- def command_timeout(seconds)
179
- @values[:command_timeout] = seconds
180
- self
181
- end
182
-
183
- # 设置避免设备自动换行的终端宽高。
184
- def terminal_size(width, height = nil)
185
- @values[:terminal_size] = height.nil? ? width : [width, height]
186
- self
187
- end
188
-
189
- # 绑定已加载的策略类;nil 明确关闭继承的业务能力。
190
- def tftp_strategy(klass)
191
- @values[:tftp_strategy] = klass
192
- self
193
- end
194
-
195
- def running_config_strategy(klass)
196
- @values[:running_config_strategy] = klass
197
- self
198
- end
199
-
200
- def topology_strategy(klass)
201
- @values[:topology_strategy] = klass
202
- self
203
- end
204
-
205
- # 将声明字段校验并构造成不可变档案。
206
- def build = Profile.new(**@values)
207
-
208
- private
209
-
210
- # 块式声明入口必须有明确的子 DSL,避免静默忽略配置。
211
- def check_block!(name, definition)
212
- raise ArgumentError, "#{name} requires a block" unless definition
213
- end
214
-
215
- # 命令集合的声明对象。
216
- class Commands
217
- attr_reader :config_commands, :save_commands
218
-
219
- # 复制继承的命令,后续修改不会污染父类档案。
220
- def initialize(config_commands, save_commands)
221
- @config_commands = config_commands&.dup
222
- @save_commands = save_commands.dup
223
- end
224
-
225
- # 声明读取运行配置的命令序列。
226
- def running_config(*commands)
227
- @config_commands = commands.flatten
228
- end
229
-
230
- # 声明保存配置的命令序列。
231
- def save_config(*commands)
232
- @save_commands = commands.flatten
233
- end
234
- end
235
-
236
- # 提示符的声明对象。
237
- class Prompts
238
- # 将登录、命令、用户名、密码和特权提示写入当前档案。
239
- def initialize(values)
240
- @values = values
241
- end
242
-
243
- # 声明登录结束提示符。
244
- def login(pattern) = @values[:login_prompt] = pattern
245
-
246
- # 声明普通命令结束提示符。
247
- def command(pattern) = @values[:command_prompt] = pattern
248
-
249
- # 声明用户名输入提示。
250
- def username(pattern) = @values[:username_prompt] = pattern
251
-
252
- # 声明密码输入提示。
253
- def password(pattern) = @values[:password_prompt] = pattern
254
-
255
- # 声明特权模式提示符。
256
- def privilege(pattern) = @values[:privilege_prompt] = pattern
257
- end
258
-
259
- # 分页规则的声明对象。
260
- class Pager
261
- # 将分页模式和响应写入当前档案。
262
- def initialize(values)
263
- @values = values
264
- end
265
-
266
- # 声明分页提示匹配模式。
267
- def pattern(value) = @values[:pager_pattern] = value
268
-
269
- # 声明匹配分页提示后发送的字节。
270
- def response(value) = @values[:pager_response] = value
271
- end
272
-
273
- # 设备错误模式的声明对象。
274
- class Errors
275
- # 将错误模式列表写入当前档案。
276
- def initialize(values)
277
- @values = values
278
- end
279
-
280
- # 追加认证失败模式。
281
- def authentication(*patterns)
282
- @values[:authentication_error_patterns] = patterns.flatten
283
- end
284
-
285
- # 追加命令失败模式。
286
- def command(*patterns)
287
- @values[:command_error_patterns] = patterns.flatten
288
- end
289
- end
290
-
291
- # 登录和命令交互的声明对象。
292
- class Interactions
293
- attr_reader :login_interactions, :confirmation_interactions
294
-
295
- # 复制继承的交互规则,子类可以在其上追加设备差异。
296
- def initialize(values)
297
- @login_interactions = values[:login_interactions].dup
298
- @confirmation_interactions = values[:confirmation_interactions].dup
299
- end
300
-
301
- # 声明登录阶段的一次性应答。
302
- def login(pattern, response:, sensitive: false, limit: 1, capture: true)
303
- @login_interactions << Interaction.new(pattern, response, sensitive: sensitive,
304
- limit: limit, capture: capture)
305
- end
306
-
307
- # 声明命令阶段的确认应答。
308
- def confirm(pattern, response:, sensitive: false, limit: nil, capture: true)
309
- @confirmation_interactions << Interaction.new(pattern, response, sensitive: sensitive,
310
- limit: limit, capture: capture)
311
- end
312
- end
313
-
314
- # 特权认证规则的声明对象。
315
- class Privilege
316
- # 将特权命令和结束提示写入当前档案。
317
- def initialize(values)
318
- @values = values
319
- end
320
-
321
- # 声明进入特权模式的设备命令。
322
- def command(value) = @values[:privilege_command] = value
323
-
324
- # 声明特权命令完成后的提示符。
325
- def prompt(value) = @values[:privilege_prompt] = value
326
- end
327
-
328
- # SSH 兼容参数的声明对象。
329
- class Ssh
330
- # 将旧版 SSH 参数写入当前档案。
331
- def initialize(values)
332
- @values = values
333
- end
334
-
335
- # 声明连接旧版设备所需的参数序列。
336
- def legacy_arguments(*arguments) = @values[:legacy_ssh_arguments] = arguments.flatten
337
- end
338
- end
339
-
340
113
  private
341
114
 
342
115
  # 校验静态接口,不构造策略实例或触发设备 I/O。
@@ -444,3 +217,5 @@ module Net
444
217
  end
445
218
  end
446
219
  end
220
+
221
+ require_relative "profile/builder"
@@ -6,10 +6,13 @@ module Net
6
6
  module Connector
7
7
  class RunningConfig
8
8
  class Strategy
9
+ # 保存策略所属设备,供提示符和厂商校验使用。
9
10
  def initialize(device) = @device = device
10
11
 
12
+ # 默认保留设备响应原文。
11
13
  def clean(text) = text
12
14
 
15
+ # 默认取脚本最后一个完成步骤作为运行配置。
13
16
  def result_step(result) = result.steps.last
14
17
 
15
18
  # 普通采集命令保持视图不变;需要切换视图的采集策略显式调整。
@@ -17,11 +20,13 @@ module Net
17
20
  TerminalRenderer.render(@device.current_prompt.to_s).lines.last.to_s.strip
18
21
  end
19
22
 
23
+ # 公共策略不增加逐命令响应检查。
20
24
  def check_response(_command, _response, _execution) end
21
25
  end
22
26
 
23
27
  # 使用终端行编辑规则渲染 H3C、Huawei 和 Radware 配置。
24
28
  class Rendered < Strategy
29
+ # 应用终端行编辑规则,还原设备最终显示的配置文本。
25
30
  def clean(text) = TerminalRenderer.render(text)
26
31
  end
27
32
  end
@@ -12,6 +12,7 @@ module Net
12
12
  @device = device
13
13
  end
14
14
 
15
+ # 从设备档案选定本次采集策略;无厂商规则时使用公共策略。
15
16
  def self.strategy(device)
16
17
  (device.profile.running_config_strategy || Strategy).new(device)
17
18
  end
@@ -21,6 +22,8 @@ module Net
21
22
  script = Script.new(@device.config_commands)
22
23
  return Result.new(error: incomplete("configuration collection has no commands")) if script.empty?
23
24
 
25
+ # 所有厂商的采集步骤都收紧输出边界,包括候选差异、模式切换及扩展查询。
26
+ script = Script.new(script.map(&:with_output_sensitive))
24
27
  strategy = self.class.strategy(@device)
25
28
  prompt = ->(command) { prompt_for(command, strategy) }
26
29
  @device.execute_operation(script, name: :running_config, prompt: prompt,
@@ -33,6 +36,7 @@ module Net
33
36
 
34
37
  private
35
38
 
39
+ # 只从完成的配置步骤提取非空内容,并保留此前所有步骤。
36
40
  def finish(result)
37
41
  step = @device.send(:config_result_step, result)
38
42
  raise incomplete("configuration collection has no completed configuration step") unless step
@@ -44,6 +48,7 @@ module Net
44
48
  Result.new(steps: result.steps, config: content)
45
49
  end
46
50
 
51
+ # 用当前完整提示符构建结束匹配,避免配置正文中的单个 # 提前结束采集。
47
52
  def prompt_for(command, strategy)
48
53
  return command.prompt if command.prompt
49
54
 
@@ -54,6 +59,7 @@ module Net
54
59
  /(?:\A|(?<=[\r\n]))[ \t\x00]*#{Regexp.escape(prompt)}[ \t\x00]*[\r\n]*\z/
55
60
  end
56
61
 
62
+ # 排除命令回显和提示符,确认响应确实包含配置正文。
57
63
  def content?(step)
58
64
  body = TerminalRenderer.render(step.output.delete_suffix(step.prompt.to_s)).strip
59
65
  lines = body.lines
@@ -61,6 +67,7 @@ module Net
61
67
  !lines.join.strip.empty?
62
68
  end
63
69
 
70
+ # 将缺少配置的情况统一映射为可识别的设备错误码。
64
71
  def incomplete(message)
65
72
  DeviceError.new(message, code: :incomplete_configuration, host: @device.host, phase: :collect)
66
73
  end
@@ -11,7 +11,8 @@ module Net
11
11
  attr_reader :text, :timeout, :interactions, :prompt, :source, :line
12
12
 
13
13
  # 在任何输入输出前校验命令、提示、交互和敏感标记,并冻结命令。
14
- def initialize(text, timeout: nil, interactions: [], prompt: nil, sensitive: false, source: nil, line: nil)
14
+ def initialize(text, timeout: nil, interactions: [], prompt: nil, sensitive: false, output_sensitive: false,
15
+ source: nil, line: nil)
15
16
  unless text.is_a?(String) && !text.strip.empty? && !text.match?(/[\r\n\x00]/)
16
17
  raise ScriptError.new("a command must contain exactly one nonempty CLI line", phase: :parse,
17
18
  source: source, line: line)
@@ -24,12 +25,14 @@ module Net
24
25
  raise ArgumentError, "prompt must be a nonempty Regexp or nil"
25
26
  end
26
27
  raise ArgumentError, "sensitive must be true or false" unless [true, false].include?(sensitive)
28
+ raise ArgumentError, "output_sensitive must be true or false" unless [true, false].include?(output_sensitive)
27
29
 
28
30
  @text = text.dup.freeze
29
31
  @timeout = Expect.duration(timeout)
30
32
  @interactions = interactions.dup.freeze
31
33
  @prompt = prompt
32
34
  @sensitive = sensitive
35
+ @output_sensitive = output_sensitive
33
36
  @source = source&.dup&.freeze
34
37
  @line = line
35
38
  freeze
@@ -38,10 +41,19 @@ module Net
38
41
  # 判断命令文本是否需要脱敏。
39
42
  def sensitive? = @sensitive
40
43
 
44
+ # 响应正文可能包含尚未登记的秘密,与命令文本是否敏感无关。
45
+ def output_sensitive? = @output_sensitive
46
+
41
47
  # 复制命令元数据,只替换命令文本。
42
48
  def with_text(text)
43
49
  self.class.new(text, timeout: timeout, interactions: interactions, prompt: prompt,
44
- sensitive: sensitive?, source: source, line: line)
50
+ sensitive: sensitive?, output_sensitive: output_sensitive?, source: source, line: line)
51
+ end
52
+
53
+ # 配置采集保留调用方的全部命令元数据,只收紧输出的诊断边界。
54
+ def with_output_sensitive
55
+ self.class.new(text, timeout: timeout, interactions: interactions, prompt: prompt,
56
+ sensitive: sensitive?, output_sensitive: true, source: source, line: line)
45
57
  end
46
58
 
47
59
  # 返回命令来源摘要,不展开命令内容。