net-connector 0.4.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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE +21 -0
  4. data/README.md +277 -0
  5. data/docs/RELEASING.md +81 -0
  6. data/docs/VERIFICATION.md +73 -0
  7. data/docs/architecture.md +268 -0
  8. data/exe/net-connector-backup +6 -0
  9. data/lib/net/connector/device/base.rb +368 -0
  10. data/lib/net/connector/device/interface_description.rb +41 -0
  11. data/lib/net/connector/device/interface_name.rb +56 -0
  12. data/lib/net/connector/device/profile.rb +446 -0
  13. data/lib/net/connector/device/running_config/strategy.rb +29 -0
  14. data/lib/net/connector/device/running_config.rb +69 -0
  15. data/lib/net/connector/engine/authentication.rb +77 -0
  16. data/lib/net/connector/engine/base.rb +4 -0
  17. data/lib/net/connector/engine/command.rb +101 -0
  18. data/lib/net/connector/engine/configuration.rb +131 -0
  19. data/lib/net/connector/engine/core.rb +6 -0
  20. data/lib/net/connector/engine/dialogue.rb +223 -0
  21. data/lib/net/connector/engine/errors.rb +183 -0
  22. data/lib/net/connector/engine/execution.rb +64 -0
  23. data/lib/net/connector/engine/log.rb +227 -0
  24. data/lib/net/connector/engine/log_messages.rb +47 -0
  25. data/lib/net/connector/engine/profile.rb +4 -0
  26. data/lib/net/connector/engine/recovery.rb +57 -0
  27. data/lib/net/connector/engine/result.rb +67 -0
  28. data/lib/net/connector/engine/session.rb +333 -0
  29. data/lib/net/connector/engine/terminal_renderer.rb +158 -0
  30. data/lib/net/connector/engine/transport.rb +184 -0
  31. data/lib/net/connector/engine.rb +4 -0
  32. data/lib/net/connector/netdisco/batch.rb +71 -0
  33. data/lib/net/connector/netdisco/cli.rb +142 -0
  34. data/lib/net/connector/netdisco/client.rb +148 -0
  35. data/lib/net/connector/netdisco/config_file.rb +87 -0
  36. data/lib/net/connector/netdisco/device.rb +75 -0
  37. data/lib/net/connector/netdisco/fleet.rb +258 -0
  38. data/lib/net/connector/netdisco/planner.rb +77 -0
  39. data/lib/net/connector/netdisco/result_store.rb +43 -0
  40. data/lib/net/connector/netdisco/rules.rb +131 -0
  41. data/lib/net/connector/netdisco/settings.rb +183 -0
  42. data/lib/net/connector/netdisco/worker.rb +75 -0
  43. data/lib/net/connector/netdisco.rb +14 -0
  44. data/lib/net/connector/operations/local_backup.rb +49 -0
  45. data/lib/net/connector/operations/parse_output.rb +60 -0
  46. data/lib/net/connector/operations/private_file.rb +25 -0
  47. data/lib/net/connector/operations/running_config/cisco.rb +14 -0
  48. data/lib/net/connector/operations/running_config/hillstone.rb +12 -0
  49. data/lib/net/connector/operations/running_config/palo_alto.rb +12 -0
  50. data/lib/net/connector/operations/running_config/strategy.rb +3 -0
  51. data/lib/net/connector/operations/running_config.rb +16 -0
  52. data/lib/net/connector/operations/saved_config.rb +72 -0
  53. data/lib/net/connector/operations/tftp/cisco_ios.rb +11 -0
  54. data/lib/net/connector/operations/tftp/cisco_nxos.rb +11 -0
  55. data/lib/net/connector/operations/tftp/h3c.rb +11 -0
  56. data/lib/net/connector/operations/tftp/hillstone.rb +11 -0
  57. data/lib/net/connector/operations/tftp/huawei.rb +11 -0
  58. data/lib/net/connector/operations/tftp/palo_alto.rb +11 -0
  59. data/lib/net/connector/operations/tftp/radware.rb +11 -0
  60. data/lib/net/connector/operations/tftp/strategy.rb +77 -0
  61. data/lib/net/connector/operations/tftp_backup.rb +120 -0
  62. data/lib/net/connector/operations/topology/cisco.rb +11 -0
  63. data/lib/net/connector/operations/topology/h3c.rb +11 -0
  64. data/lib/net/connector/operations/topology/hillstone.rb +11 -0
  65. data/lib/net/connector/operations/topology/palo_alto.rb +11 -0
  66. data/lib/net/connector/operations/topology/radware.rb +11 -0
  67. data/lib/net/connector/operations/topology/strategy.rb +53 -0
  68. data/lib/net/connector/operations/topology.rb +192 -0
  69. data/lib/net/connector/operations.rb +20 -0
  70. data/lib/net/connector/templates/cisco_cdp_neighbors_detail.textfsm +7 -0
  71. data/lib/net/connector/templates/cisco_ios_running_config_interfaces.textfsm +12 -0
  72. data/lib/net/connector/templates/cisco_ios_show_ip_interface_brief.textfsm +7 -0
  73. data/lib/net/connector/templates/h3c_interface_descriptions.textfsm +11 -0
  74. data/lib/net/connector/templates/h3c_lldp_local_first.textfsm +8 -0
  75. data/lib/net/connector/templates/h3c_lldp_name_first.textfsm +8 -0
  76. data/lib/net/connector/templates/hillstone_interface_descriptions.textfsm +11 -0
  77. data/lib/net/connector/templates/hillstone_lldp_neighbors.textfsm +7 -0
  78. data/lib/net/connector/templates/index +8 -0
  79. data/lib/net/connector/templates/palo_alto_interface_descriptions.textfsm +6 -0
  80. data/lib/net/connector/templates/palo_alto_lldp_neighbors.textfsm +11 -0
  81. data/lib/net/connector/templates/radware_port_names.textfsm +13 -0
  82. data/lib/net/connector/vendor/cisco_ios/running_config.rb +19 -0
  83. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +32 -0
  84. data/lib/net/connector/vendor/cisco_ios/topology.rb +33 -0
  85. data/lib/net/connector/vendor/cisco_ios.rb +48 -0
  86. data/lib/net/connector/vendor/cisco_nxos/running_config.rb +22 -0
  87. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +34 -0
  88. data/lib/net/connector/vendor/cisco_nxos.rb +55 -0
  89. data/lib/net/connector/vendor/h3c/tftp_backup.rb +43 -0
  90. data/lib/net/connector/vendor/h3c/topology.rb +56 -0
  91. data/lib/net/connector/vendor/h3c.rb +77 -0
  92. data/lib/net/connector/vendor/h3c_wireless.rb +14 -0
  93. data/lib/net/connector/vendor/hillstone/running_config.rb +13 -0
  94. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +40 -0
  95. data/lib/net/connector/vendor/hillstone/topology.rb +48 -0
  96. data/lib/net/connector/vendor/hillstone.rb +43 -0
  97. data/lib/net/connector/vendor/huawei/tftp_backup.rb +34 -0
  98. data/lib/net/connector/vendor/huawei.rb +64 -0
  99. data/lib/net/connector/vendor/palo_alto/running_config.rb +70 -0
  100. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +36 -0
  101. data/lib/net/connector/vendor/palo_alto/topology.rb +63 -0
  102. data/lib/net/connector/vendor/palo_alto.rb +43 -0
  103. data/lib/net/connector/vendor/radware/tftp_backup.rb +40 -0
  104. data/lib/net/connector/vendor/radware/topology.rb +14 -0
  105. data/lib/net/connector/vendor/radware.rb +62 -0
  106. data/lib/net/connector/version.rb +7 -0
  107. data/lib/net/connector.rb +48 -0
  108. data/lib/net-connector.rb +3 -0
  109. metadata +517 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "interface_name"
4
+
5
+ module Net
6
+ module Connector
7
+ # 描述文本与常见接口视图命令的纯构造方法,不连接或修改设备。
8
+ module InterfaceDescription
9
+ def self.format(neighbor, abbreviate: true, lowercase: false)
10
+ port = if abbreviate
11
+ InterfaceName.short(neighbor.neighbor_interface, lowercase: lowercase)
12
+ else
13
+ lowercase ? neighbor.neighbor_interface.downcase : neighbor.neighbor_interface
14
+ end
15
+ validate!("To #{neighbor.neighbor_name} #{port}")
16
+ end
17
+
18
+ def self.commands(interface:, description:, leave: "exit")
19
+ validate_interface!(interface)
20
+ validate!(description)
21
+ raise ArgumentError, "interface exit must be exit or quit" unless %w[exit quit].include?(leave)
22
+
23
+ ["interface #{interface}", "description #{description}", leave]
24
+ end
25
+
26
+ # 共享的输入约束同样适用于 PAN-OS 的独立命令语法。
27
+ def self.validate!(value)
28
+ return value if value.is_a?(String) && value.bytesize.between?(1, 80) &&
29
+ value.match?(/\A[\p{Alnum}_.: \/-]+\z/u)
30
+
31
+ raise ArgumentError, "description must be 1-80 bytes of plain interface text"
32
+ end
33
+
34
+ def self.validate_interface!(value)
35
+ return value if value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9.\/-]*\z/)
36
+
37
+ raise ArgumentError, "neighbor interface name is unsafe"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Connector
5
+ # 接口身份匹配与描述展示共用名称规则;展示简称不能直接用作下发命令。
6
+ module InterfaceName
7
+ RULES = [
8
+ [/\A(?:xge|ten-gigabitethernet|tengigabitethernet|te)(?=\d)/i, "tengigabitethernet", "Te"],
9
+ [/\A(?:gigabitethernet|ge|gi)(?=\d)/i, "gigabitethernet", "Gi"],
10
+ [/\A(?:fastethernet|fa)(?=\d)/i, "fastethernet", "Fa"],
11
+ [/\A(?:ethernet|eth)(?=\d)/i, "ethernet", "Eth"],
12
+ [/\A(?:port-channel|po)(?=\d)/i, "port-channel", "Po"]
13
+ ].map(&:freeze).freeze
14
+
15
+ # 用于本机邻居表与配置的关联;保留已有证据键的拼写契约。
16
+ def self.key(name)
17
+ rule = RULES.find { |pattern, _key, _short| pattern.match?(name) }
18
+ rule ? name.sub(rule.first, rule[1]).downcase : name.downcase
19
+ end
20
+
21
+ # 仅缩短已识别的前缀,保留端口、子接口编号及未知命名。
22
+ # 默认延续输入的大写、小写或首字母大写形式;统一小写由调用方选择。
23
+ def self.short(name, lowercase: false)
24
+ rule = RULES.find { |pattern, _key, _short| pattern.match?(name) }
25
+ formatted = if rule
26
+ name.sub(rule.first) do |prefix|
27
+ abbreviation = rule.last
28
+ if prefix == prefix.downcase
29
+ abbreviation.downcase
30
+ elsif prefix == prefix.upcase
31
+ abbreviation.upcase
32
+ elsif prefix.casecmp?(abbreviation)
33
+ prefix
34
+ else
35
+ abbreviation
36
+ end
37
+ end
38
+ else
39
+ name.dup
40
+ end
41
+ lowercase ? formatted.downcase : formatted
42
+ end
43
+
44
+ # 保留现有配置视图展开规则;厂商策略可覆盖不适用的名称。
45
+ def self.configuration(name)
46
+ name.sub(/\AXGE(?=\d)/i, "Ten-GigabitEthernet")
47
+ .sub(/\AGE(?=\d)/i, "GigabitEthernet")
48
+ .sub(/\AGi(?=\d)/i, "GigabitEthernet")
49
+ .sub(/\ATe(?=\d)/i, "TenGigabitEthernet")
50
+ .sub(/\AFa(?=\d)/i, "FastEthernet")
51
+ .sub(/\AEth(?=\d)/i, "Ethernet")
52
+ .sub(/\APo(?=\d)/i, "port-channel")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,446 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "expect/pty"
4
+ require_relative "../engine/dialogue"
5
+
6
+ module Net
7
+ module Connector
8
+ # 描述设备命令行规则的不可变值对象;只承载声明式配置,不承载运行状态。
9
+ class Profile
10
+ DEFAULT_PAGER_PATTERN = /(?:\A|(?<=[\r\n]))[ \t]*(?:--More--|---- More ----)[ \t\x00\x08]*(?=\r|\n|\z)/i.freeze
11
+ DEFAULT_AUTHENTICATION_ERROR_PATTERNS = [
12
+ /(?:authentication failed|permission denied|access denied|login incorrect)/i
13
+ ].freeze
14
+ DEFAULT_PASSWORD_PROMPT = /password:\s*\z/i.freeze
15
+ DEFAULT_USERNAME_PROMPT = /(?:login|username|name):\s*\z/i.freeze
16
+ DEFAULT_PRIVILEGE_PROMPT = /#\s*\z/.freeze
17
+ DEFAULT_LEGACY_SSH_ARGUMENTS = ["-1"].freeze
18
+
19
+ attr_reader :config_commands, :save_commands,
20
+ :login_prompt, :command_prompt, :username_prompt, :password_prompt,
21
+ :privilege_prompt, :pager_pattern, :pager_response,
22
+ :authentication_error_patterns, :command_error_patterns,
23
+ :login_interactions, :confirmation_interactions, :legacy_ssh_arguments,
24
+ :privilege_command, :command_timeout, :terminal_size, :tftp_strategy, :topology_strategy, :running_config_strategy
25
+
26
+ # 使用默认的公共规则创建空设备档案;登录和命令提示符仍由厂商声明。
27
+ def self.default
28
+ @default ||= new
29
+ end
30
+
31
+ # 在继承档案的基础上执行一次声明块,生成新的不可变设备档案。
32
+ def self.define(parent: nil, &definition)
33
+ raise ArgumentError, "profile requires a block" unless definition
34
+
35
+ builder = Builder.new(parent || default)
36
+ builder.instance_eval(&definition)
37
+ builder.build
38
+ end
39
+
40
+ # 校验并冻结设备命令、提示符、交互和连接参数。
41
+ def initialize(config_commands: nil, save_commands: [],
42
+ login_prompt: nil, command_prompt: nil,
43
+ username_prompt: DEFAULT_USERNAME_PROMPT, password_prompt: DEFAULT_PASSWORD_PROMPT,
44
+ privilege_prompt: DEFAULT_PRIVILEGE_PROMPT,
45
+ pager_pattern: DEFAULT_PAGER_PATTERN, pager_response: " ",
46
+ authentication_error_patterns: DEFAULT_AUTHENTICATION_ERROR_PATTERNS,
47
+ command_error_patterns: [], login_interactions: [], confirmation_interactions: [],
48
+ legacy_ssh_arguments: DEFAULT_LEGACY_SSH_ARGUMENTS, privilege_command: nil,
49
+ command_timeout: 25, terminal_size: nil, tftp_strategy: nil, topology_strategy: nil, running_config_strategy: nil)
50
+ @config_commands = command_list(config_commands, :config_commands, allow_nil: true)
51
+ @save_commands = command_list(save_commands, :save_commands)
52
+ @login_prompt = pattern(login_prompt, :login_prompt, allow_nil: true)
53
+ @command_prompt = pattern(command_prompt, :command_prompt, allow_nil: true)
54
+ @username_prompt = pattern(username_prompt, :username_prompt)
55
+ @password_prompt = pattern(password_prompt, :password_prompt)
56
+ @privilege_prompt = pattern(privilege_prompt, :privilege_prompt)
57
+ @pager_pattern = pattern(pager_pattern, :pager_pattern)
58
+ @pager_response = response_text(pager_response, :pager_response)
59
+ @authentication_error_patterns = patterns(authentication_error_patterns, :authentication_error_patterns)
60
+ @command_error_patterns = patterns(command_error_patterns, :command_error_patterns)
61
+ @login_interactions = interactions(login_interactions, :login_interactions)
62
+ @confirmation_interactions = interactions(confirmation_interactions, :confirmation_interactions)
63
+ @legacy_ssh_arguments = strings(legacy_ssh_arguments, :legacy_ssh_arguments)
64
+ @privilege_command = optional_text(privilege_command, :privilege_command)
65
+ @command_timeout = duration(command_timeout, :command_timeout)
66
+ @terminal_size = validate_terminal_size(terminal_size)
67
+ @running_config_strategy = strategy_class(running_config_strategy, :running_config_strategy,
68
+ %i[clean result_step prompt_text check_response])
69
+ @tftp_strategy = strategy_class(tftp_strategy, :tftp_strategy,
70
+ %i[source_file default_path remote_path script complete?])
71
+ @topology_strategy = strategy_class(topology_strategy, :topology_strategy,
72
+ %i[neighbor_command neighbor_template expected_neighbor_count empty_neighbor_output?
73
+ protocol description_template interface_key configuration_interface
74
+ decode_description enter_configuration change_commands finish_commands script_command])
75
+ if @topology_strategy && !@topology_strategy.respond_to?(:supports?)
76
+ raise ArgumentError, "topology_strategy must expose supports?"
77
+ end
78
+ freeze
79
+ end
80
+
81
+ # 返回一份供子类声明块继续编辑的关键字参数。
82
+ def to_h
83
+ {
84
+ config_commands: config_commands,
85
+ save_commands: save_commands,
86
+ login_prompt: login_prompt,
87
+ command_prompt: command_prompt,
88
+ username_prompt: username_prompt,
89
+ password_prompt: password_prompt,
90
+ privilege_prompt: privilege_prompt,
91
+ pager_pattern: pager_pattern,
92
+ pager_response: pager_response,
93
+ authentication_error_patterns: authentication_error_patterns,
94
+ command_error_patterns: command_error_patterns,
95
+ login_interactions: login_interactions,
96
+ confirmation_interactions: confirmation_interactions,
97
+ legacy_ssh_arguments: legacy_ssh_arguments,
98
+ privilege_command: privilege_command,
99
+ command_timeout: command_timeout,
100
+ terminal_size: terminal_size,
101
+ tftp_strategy: tftp_strategy,
102
+ topology_strategy: topology_strategy,
103
+ running_config_strategy: running_config_strategy
104
+ }
105
+ end
106
+
107
+ # 返回档案中最有用的设备规则摘要。
108
+ def inspect
109
+ "#<#{self.class} commands=#{config_commands&.size || 0} " \
110
+ "save_commands=#{save_commands.size}>"
111
+ end
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
+ private
341
+
342
+ # 校验静态接口,不构造策略实例或触发设备 I/O。
343
+ def strategy_class(value, name, required_methods)
344
+ return nil if value.nil?
345
+ unless value.is_a?(Class) && (required_methods - value.public_instance_methods).empty?
346
+ raise ArgumentError, "#{name} must be a strategy class implementing #{required_methods.join(", ")}"
347
+ end
348
+
349
+ value
350
+ end
351
+
352
+ # 校验单条命令数组并复制为深度不可变值。
353
+ def command_list(value, name, allow_nil: false)
354
+ return nil if allow_nil && value.nil?
355
+ unless value.is_a?(Array)
356
+ raise ArgumentError, "#{name} must be an Array of command strings"
357
+ end
358
+
359
+ value.map do |command|
360
+ unless command.is_a?(String) && !command.strip.empty? && !command.match?(/[\r\n\x00]/)
361
+ raise ArgumentError, "#{name} must contain nonempty single-line commands"
362
+ end
363
+
364
+ command.dup.freeze
365
+ end.freeze
366
+ end
367
+
368
+ # 校验提示模式并复制为不可变正则表达式。
369
+ def pattern(value, name, allow_nil: false)
370
+ return nil if allow_nil && value.nil?
371
+ unless value.is_a?(Regexp) && !value.match?("")
372
+ raise ArgumentError, "#{name} must be a nonempty Regexp"
373
+ end
374
+
375
+ value.dup.freeze
376
+ end
377
+
378
+ # 校验正则表达式列表并冻结每一项。
379
+ def patterns(values, name)
380
+ unless values.is_a?(Array)
381
+ raise ArgumentError, "#{name} must be an Array of Regexp objects"
382
+ end
383
+
384
+ values.map { |value| pattern(value, name) }.freeze
385
+ end
386
+
387
+ # 校验交互对象列表并冻结列表。
388
+ def interactions(values, name)
389
+ unless values.is_a?(Array) && values.all?(Interaction)
390
+ raise ArgumentError, "#{name} must be an Array of Interaction objects"
391
+ end
392
+
393
+ values.dup.freeze
394
+ end
395
+
396
+ # 校验并复制字符串数组。
397
+ def strings(values, name)
398
+ unless values.is_a?(Array) && values.all?(String)
399
+ raise ArgumentError, "#{name} must be an Array of strings"
400
+ end
401
+
402
+ values.map { |value| value.dup.freeze }.freeze
403
+ end
404
+
405
+ # 校验可选单行文本。
406
+ def optional_text(value, name)
407
+ return if value.nil?
408
+ text(value, name)
409
+ end
410
+
411
+ # 校验并复制单行文本。
412
+ def text(value, name)
413
+ unless value.is_a?(String) && !value.match?(/[\r\n\x00]/)
414
+ raise ArgumentError, "#{name} must be a single-line String"
415
+ end
416
+
417
+ value.dup.freeze
418
+ end
419
+
420
+ # 校验并复制分页或交互响应;响应可以包含换行。
421
+ def response_text(value, name)
422
+ raise ArgumentError, "#{name} must be a String" unless value.is_a?(String)
423
+
424
+ value.dup.freeze
425
+ end
426
+
427
+ # 校验默认命令超时。
428
+ def duration(value, name)
429
+ result = Expect.duration(value)
430
+ raise ArgumentError, "#{name} must be finite" unless result
431
+
432
+ result
433
+ end
434
+
435
+ # 校验终端宽高并冻结数组。
436
+ def validate_terminal_size(value)
437
+ return if value.nil?
438
+ unless value.is_a?(Array) && value.size == 2 && value.all? { |dimension| dimension.is_a?(Integer) && dimension.positive? }
439
+ raise ArgumentError, "terminal_size must contain two positive Integers"
440
+ end
441
+
442
+ value.dup.freeze
443
+ end
444
+ end
445
+ end
446
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../engine/terminal_renderer"
4
+
5
+ module Net
6
+ module Connector
7
+ class RunningConfig
8
+ class Strategy
9
+ def initialize(device) = @device = device
10
+
11
+ def clean(text) = text
12
+
13
+ def result_step(result) = result.steps.last
14
+
15
+ # 普通采集命令保持视图不变;需要切换视图的采集策略显式调整。
16
+ def prompt_text(_command)
17
+ TerminalRenderer.render(@device.current_prompt.to_s).lines.last.to_s.strip
18
+ end
19
+
20
+ def check_response(_command, _response, _execution) end
21
+ end
22
+
23
+ # 使用终端行编辑规则渲染 H3C、Huawei 和 Radware 配置。
24
+ class Rendered < Strategy
25
+ def clean(text) = TerminalRenderer.render(text)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../engine/command"
4
+ require_relative "../engine/result"
5
+ require_relative "running_config/strategy"
6
+
7
+ module Net
8
+ module Connector
9
+ class RunningConfig
10
+ # 保存需要读取运行配置的设备对象。
11
+ def initialize(device)
12
+ @device = device
13
+ end
14
+
15
+ def self.strategy(device)
16
+ (device.profile.running_config_strategy || Strategy).new(device)
17
+ end
18
+
19
+ # 每次采集创建独立策略;响应校验、选择与清理共享状态,均在同一脚本锁内完成。
20
+ def call
21
+ script = Script.new(@device.config_commands)
22
+ return Result.new(error: incomplete("configuration collection has no commands")) if script.empty?
23
+
24
+ strategy = self.class.strategy(@device)
25
+ prompt = ->(command) { prompt_for(command, strategy) }
26
+ @device.execute_operation(script, name: :running_config, prompt: prompt,
27
+ after_command: strategy.method(:check_response)) do |result|
28
+ @device.send(:with_config_strategy, strategy) do
29
+ finish(result)
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def finish(result)
37
+ step = @device.send(:config_result_step, result)
38
+ raise incomplete("configuration collection has no completed configuration step") unless step
39
+
40
+ content = @device.clean_config(step.output)
41
+ unless content.is_a?(String) && !content.strip.empty? && content?(step)
42
+ raise incomplete("configuration collection returned empty content")
43
+ end
44
+ Result.new(steps: result.steps, config: content)
45
+ end
46
+
47
+ def prompt_for(command, strategy)
48
+ return command.prompt if command.prompt
49
+
50
+ prompt = strategy.prompt_text(command)
51
+ if prompt.empty?
52
+ raise PromptError.new("configuration collection has no known device prompt", host: @device.host, phase: :collect)
53
+ end
54
+ /(?:\A|(?<=[\r\n]))[ \t\x00]*#{Regexp.escape(prompt)}[ \t\x00]*[\r\n]*\z/
55
+ end
56
+
57
+ def content?(step)
58
+ body = TerminalRenderer.render(step.output.delete_suffix(step.prompt.to_s)).strip
59
+ lines = body.lines
60
+ lines.shift if lines.first&.strip == step.command.text
61
+ !lines.join.strip.empty?
62
+ end
63
+
64
+ def incomplete(message)
65
+ DeviceError.new(message, code: :incomplete_configuration, host: @device.host, phase: :collect)
66
+ end
67
+ end
68
+ end
69
+ end