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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +82 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/docs/RELEASING.md +81 -0
- data/docs/VERIFICATION.md +73 -0
- data/docs/architecture.md +268 -0
- data/exe/net-connector-backup +6 -0
- data/lib/net/connector/device/base.rb +368 -0
- data/lib/net/connector/device/interface_description.rb +41 -0
- data/lib/net/connector/device/interface_name.rb +56 -0
- data/lib/net/connector/device/profile.rb +446 -0
- data/lib/net/connector/device/running_config/strategy.rb +29 -0
- data/lib/net/connector/device/running_config.rb +69 -0
- data/lib/net/connector/engine/authentication.rb +77 -0
- data/lib/net/connector/engine/base.rb +4 -0
- data/lib/net/connector/engine/command.rb +101 -0
- data/lib/net/connector/engine/configuration.rb +131 -0
- data/lib/net/connector/engine/core.rb +6 -0
- data/lib/net/connector/engine/dialogue.rb +223 -0
- data/lib/net/connector/engine/errors.rb +183 -0
- data/lib/net/connector/engine/execution.rb +64 -0
- data/lib/net/connector/engine/log.rb +227 -0
- data/lib/net/connector/engine/log_messages.rb +47 -0
- data/lib/net/connector/engine/profile.rb +4 -0
- data/lib/net/connector/engine/recovery.rb +57 -0
- data/lib/net/connector/engine/result.rb +67 -0
- data/lib/net/connector/engine/session.rb +333 -0
- data/lib/net/connector/engine/terminal_renderer.rb +158 -0
- data/lib/net/connector/engine/transport.rb +184 -0
- data/lib/net/connector/engine.rb +4 -0
- data/lib/net/connector/netdisco/batch.rb +71 -0
- data/lib/net/connector/netdisco/cli.rb +142 -0
- data/lib/net/connector/netdisco/client.rb +148 -0
- data/lib/net/connector/netdisco/config_file.rb +87 -0
- data/lib/net/connector/netdisco/device.rb +75 -0
- data/lib/net/connector/netdisco/fleet.rb +258 -0
- data/lib/net/connector/netdisco/planner.rb +77 -0
- data/lib/net/connector/netdisco/result_store.rb +43 -0
- data/lib/net/connector/netdisco/rules.rb +131 -0
- data/lib/net/connector/netdisco/settings.rb +183 -0
- data/lib/net/connector/netdisco/worker.rb +75 -0
- data/lib/net/connector/netdisco.rb +14 -0
- data/lib/net/connector/operations/local_backup.rb +49 -0
- data/lib/net/connector/operations/parse_output.rb +60 -0
- data/lib/net/connector/operations/private_file.rb +25 -0
- data/lib/net/connector/operations/running_config/cisco.rb +14 -0
- data/lib/net/connector/operations/running_config/hillstone.rb +12 -0
- data/lib/net/connector/operations/running_config/palo_alto.rb +12 -0
- data/lib/net/connector/operations/running_config/strategy.rb +3 -0
- data/lib/net/connector/operations/running_config.rb +16 -0
- data/lib/net/connector/operations/saved_config.rb +72 -0
- data/lib/net/connector/operations/tftp/cisco_ios.rb +11 -0
- data/lib/net/connector/operations/tftp/cisco_nxos.rb +11 -0
- data/lib/net/connector/operations/tftp/h3c.rb +11 -0
- data/lib/net/connector/operations/tftp/hillstone.rb +11 -0
- data/lib/net/connector/operations/tftp/huawei.rb +11 -0
- data/lib/net/connector/operations/tftp/palo_alto.rb +11 -0
- data/lib/net/connector/operations/tftp/radware.rb +11 -0
- data/lib/net/connector/operations/tftp/strategy.rb +77 -0
- data/lib/net/connector/operations/tftp_backup.rb +120 -0
- data/lib/net/connector/operations/topology/cisco.rb +11 -0
- data/lib/net/connector/operations/topology/h3c.rb +11 -0
- data/lib/net/connector/operations/topology/hillstone.rb +11 -0
- data/lib/net/connector/operations/topology/palo_alto.rb +11 -0
- data/lib/net/connector/operations/topology/radware.rb +11 -0
- data/lib/net/connector/operations/topology/strategy.rb +53 -0
- data/lib/net/connector/operations/topology.rb +192 -0
- data/lib/net/connector/operations.rb +20 -0
- data/lib/net/connector/templates/cisco_cdp_neighbors_detail.textfsm +7 -0
- data/lib/net/connector/templates/cisco_ios_running_config_interfaces.textfsm +12 -0
- data/lib/net/connector/templates/cisco_ios_show_ip_interface_brief.textfsm +7 -0
- data/lib/net/connector/templates/h3c_interface_descriptions.textfsm +11 -0
- data/lib/net/connector/templates/h3c_lldp_local_first.textfsm +8 -0
- data/lib/net/connector/templates/h3c_lldp_name_first.textfsm +8 -0
- data/lib/net/connector/templates/hillstone_interface_descriptions.textfsm +11 -0
- data/lib/net/connector/templates/hillstone_lldp_neighbors.textfsm +7 -0
- data/lib/net/connector/templates/index +8 -0
- data/lib/net/connector/templates/palo_alto_interface_descriptions.textfsm +6 -0
- data/lib/net/connector/templates/palo_alto_lldp_neighbors.textfsm +11 -0
- data/lib/net/connector/templates/radware_port_names.textfsm +13 -0
- data/lib/net/connector/vendor/cisco_ios/running_config.rb +19 -0
- data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +32 -0
- data/lib/net/connector/vendor/cisco_ios/topology.rb +33 -0
- data/lib/net/connector/vendor/cisco_ios.rb +48 -0
- data/lib/net/connector/vendor/cisco_nxos/running_config.rb +22 -0
- data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +34 -0
- data/lib/net/connector/vendor/cisco_nxos.rb +55 -0
- data/lib/net/connector/vendor/h3c/tftp_backup.rb +43 -0
- data/lib/net/connector/vendor/h3c/topology.rb +56 -0
- data/lib/net/connector/vendor/h3c.rb +77 -0
- data/lib/net/connector/vendor/h3c_wireless.rb +14 -0
- data/lib/net/connector/vendor/hillstone/running_config.rb +13 -0
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +40 -0
- data/lib/net/connector/vendor/hillstone/topology.rb +48 -0
- data/lib/net/connector/vendor/hillstone.rb +43 -0
- data/lib/net/connector/vendor/huawei/tftp_backup.rb +34 -0
- data/lib/net/connector/vendor/huawei.rb +64 -0
- data/lib/net/connector/vendor/palo_alto/running_config.rb +70 -0
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +36 -0
- data/lib/net/connector/vendor/palo_alto/topology.rb +63 -0
- data/lib/net/connector/vendor/palo_alto.rb +43 -0
- data/lib/net/connector/vendor/radware/tftp_backup.rb +40 -0
- data/lib/net/connector/vendor/radware/topology.rb +14 -0
- data/lib/net/connector/vendor/radware.rb +62 -0
- data/lib/net/connector/version.rb +7 -0
- data/lib/net/connector.rb +48 -0
- data/lib/net-connector.rb +3 -0
- 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
|