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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +110 -68
- data/CONTRIBUTING.md +40 -0
- data/README.md +140 -95
- data/SECURITY.md +11 -0
- data/docs/VERIFICATION.md +129 -5
- data/docs/architecture.md +321 -248
- data/lib/net/connector/device/base.rb +12 -4
- 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 +7 -0
- data/lib/net/connector/engine/command.rb +14 -2
- data/lib/net/connector/engine/configuration.rb +10 -7
- data/lib/net/connector/engine/dialogue.rb +53 -28
- data/lib/net/connector/engine/errors.rb +41 -54
- data/lib/net/connector/engine/execution.rb +32 -5
- data/lib/net/connector/engine/log.rb +23 -17
- data/lib/net/connector/engine/session.rb +39 -7
- data/lib/net/connector/engine/terminal_renderer.rb +7 -4
- data/lib/net/connector/netdisco/batch.rb +31 -3
- data/lib/net/connector/netdisco/cli.rb +92 -50
- data/lib/net/connector/netdisco/client.rb +206 -73
- data/lib/net/connector/netdisco/config_file.rb +26 -4
- data/lib/net/connector/netdisco/device.rb +4 -8
- data/lib/net/connector/netdisco/diagnostic.rb +91 -0
- data/lib/net/connector/netdisco/fleet.rb +105 -108
- data/lib/net/connector/netdisco/inventory_budget.rb +49 -0
- data/lib/net/connector/netdisco/plan.rb +77 -0
- data/lib/net/connector/netdisco/planner.rb +12 -23
- data/lib/net/connector/netdisco/report.rb +94 -0
- data/lib/net/connector/netdisco/rules.rb +28 -5
- data/lib/net/connector/netdisco/settings.rb +194 -85
- data/lib/net/connector/netdisco/worker.rb +43 -27
- data/lib/net/connector/operations/backup_lock.rb +116 -0
- data/lib/net/connector/operations/local_backup.rb +49 -9
- data/lib/net/connector/operations/parse_output.rb +20 -3
- data/lib/net/connector/operations/private_file.rb +94 -5
- data/lib/net/connector/operations/safe_file.rb +62 -0
- data/lib/net/connector/operations/saved_config/legacy_index.rb +109 -0
- data/lib/net/connector/operations/saved_config.rb +40 -10
- data/lib/net/connector/operations/tftp/file_upload.rb +55 -0
- data/lib/net/connector/operations/tftp/strategy.rb +9 -11
- data/lib/net/connector/operations/tftp_backup.rb +78 -15
- data/lib/net/connector/operations/tftp_receipt.rb +73 -0
- data/lib/net/connector/operations/topology/immediate_strategy.rb +45 -0
- data/lib/net/connector/operations/topology/strategy.rb +34 -0
- data/lib/net/connector/operations/topology.rb +98 -29
- data/lib/net/connector/operations.rb +6 -0
- data/lib/net/connector/vendor/cisco_ios/running_config.rb +1 -0
- data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +9 -2
- data/lib/net/connector/vendor/cisco_ios/topology.rb +18 -3
- data/lib/net/connector/vendor/cisco_nxos/running_config.rb +1 -0
- data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +8 -1
- data/lib/net/connector/vendor/h3c/tftp_backup.rb +7 -18
- data/lib/net/connector/vendor/h3c/topology.rb +17 -3
- data/lib/net/connector/vendor/hillstone/running_config.rb +1 -0
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +16 -3
- data/lib/net/connector/vendor/hillstone/topology.rb +17 -3
- data/lib/net/connector/vendor/huawei/tftp_backup.rb +8 -18
- data/lib/net/connector/vendor/palo_alto/running_config.rb +4 -0
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +19 -8
- data/lib/net/connector/vendor/palo_alto/topology.rb +16 -1
- data/lib/net/connector/vendor/radware/tftp_backup.rb +11 -4
- data/lib/net/connector/vendor/radware/topology.rb +3 -0
- data/lib/net/connector/version.rb +1 -1
- metadata +60 -28
|
@@ -172,8 +172,9 @@ module Net
|
|
|
172
172
|
|
|
173
173
|
# 采集配置并以原子方式保存为私有文件。
|
|
174
174
|
# 采集失败时保留已有备份文件。
|
|
175
|
-
def backup(path:)
|
|
176
|
-
|
|
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,
|
|
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
|
# 返回命令来源摘要,不展开命令内容。
|