net-connector 0.4.2 → 0.6.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 +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +92 -20
- data/docs/VERIFICATION.md +17 -7
- data/docs/architecture.md +102 -64
- data/examples/netdisco_database.yml +16 -0
- data/lib/net/connector/device/base.rb +27 -107
- data/lib/net/connector/device/local_backup.rb +96 -0
- data/lib/net/connector/device/profile.rb +8 -3
- data/lib/net/connector/device/running_config/strategy.rb +1 -1
- data/lib/net/connector/device/running_config.rb +44 -5
- data/lib/net/connector/device/save_config.rb +21 -0
- data/lib/net/connector/device/tftp/file_upload.rb +53 -0
- data/lib/net/connector/{operations/tftp_receipt.rb → device/tftp/receipt.rb} +18 -20
- data/lib/net/connector/device/tftp/strategy.rb +67 -0
- data/lib/net/connector/device/tftp/target.rb +63 -0
- data/lib/net/connector/device/tftp.rb +117 -0
- data/lib/net/connector/device/topology/immediate_strategy.rb +40 -0
- data/lib/net/connector/device/topology/interface_description.rb +46 -0
- data/lib/net/connector/device/topology/interface_name.rb +58 -0
- data/lib/net/connector/device/topology/strategy.rb +75 -0
- data/lib/net/connector/device/topology.rb +277 -0
- data/lib/net/connector/engine/authentication.rb +2 -2
- data/lib/net/connector/engine/configuration.rb +1 -1
- data/lib/net/connector/engine/dialogue.rb +21 -21
- data/lib/net/connector/engine/error_metadata.rb +50 -0
- data/lib/net/connector/engine/errors.rb +15 -1
- data/lib/net/connector/engine/execution.rb +17 -10
- data/lib/net/connector/engine/log/event.rb +66 -0
- data/lib/net/connector/engine/log/formatter.rb +16 -0
- data/lib/net/connector/engine/log/messages.rb +52 -0
- data/lib/net/connector/engine/log/stream.rb +56 -0
- data/lib/net/connector/engine/log.rb +105 -84
- data/lib/net/connector/engine/session.rb +83 -42
- data/lib/net/connector/engine/terminal_text.rb +33 -0
- data/lib/net/connector/engine/transport.rb +2 -2
- data/lib/net/connector/netdisco/batch.rb +9 -45
- data/lib/net/connector/netdisco/cli.rb +17 -10
- data/lib/net/connector/netdisco/client.rb +4 -3
- data/lib/net/connector/netdisco/config_file.rb +6 -3
- data/lib/net/connector/netdisco/database_client.rb +197 -0
- data/lib/net/connector/netdisco/device.rb +4 -5
- data/lib/net/connector/netdisco/diagnostic.rb +9 -39
- data/lib/net/connector/netdisco/fleet.rb +26 -39
- data/lib/net/connector/netdisco/report.rb +37 -16
- data/lib/net/connector/netdisco/result_store.rb +2 -2
- data/lib/net/connector/netdisco/settings.rb +26 -9
- data/lib/net/connector/netdisco.rb +1 -0
- data/lib/net/connector/{operations → storage}/backup_lock.rb +2 -2
- data/lib/net/connector/{operations → storage}/private_file.rb +2 -7
- data/lib/net/connector/{operations → storage}/safe_file.rb +1 -1
- data/lib/net/connector/{operations → storage}/saved_config.rb +10 -35
- data/lib/net/connector/storage.rb +15 -0
- data/lib/net/connector/textfsm.rb +72 -0
- data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/cisco_ios/topology.rb +5 -5
- data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/h3c/tftp_backup.rb +4 -4
- data/lib/net/connector/vendor/h3c/topology.rb +5 -5
- data/lib/net/connector/vendor/h3c.rb +3 -3
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/hillstone/topology.rb +5 -5
- data/lib/net/connector/vendor/huawei/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/palo_alto/running_config.rb +1 -1
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/palo_alto/topology.rb +3 -6
- data/lib/net/connector/vendor/radware/tftp_backup.rb +3 -3
- data/lib/net/connector/vendor/radware/topology.rb +2 -2
- data/lib/net/connector/version.rb +1 -1
- data/lib/net/connector.rb +1 -6
- metadata +47 -39
- data/lib/net/connector/device/interface_description.rb +0 -44
- data/lib/net/connector/device/interface_name.rb +0 -56
- data/lib/net/connector/engine/base.rb +0 -4
- data/lib/net/connector/engine/log_messages.rb +0 -47
- data/lib/net/connector/engine/profile.rb +0 -4
- data/lib/net/connector/engine.rb +0 -4
- data/lib/net/connector/operations/local_backup.rb +0 -89
- data/lib/net/connector/operations/parse_output.rb +0 -77
- data/lib/net/connector/operations/running_config/cisco.rb +0 -14
- data/lib/net/connector/operations/running_config/hillstone.rb +0 -12
- data/lib/net/connector/operations/running_config/palo_alto.rb +0 -12
- data/lib/net/connector/operations/running_config/strategy.rb +0 -3
- data/lib/net/connector/operations/running_config.rb +0 -16
- data/lib/net/connector/operations/saved_config/legacy_index.rb +0 -109
- data/lib/net/connector/operations/tftp/cisco_ios.rb +0 -11
- data/lib/net/connector/operations/tftp/cisco_nxos.rb +0 -11
- data/lib/net/connector/operations/tftp/file_upload.rb +0 -55
- data/lib/net/connector/operations/tftp/h3c.rb +0 -11
- data/lib/net/connector/operations/tftp/hillstone.rb +0 -11
- data/lib/net/connector/operations/tftp/huawei.rb +0 -11
- data/lib/net/connector/operations/tftp/palo_alto.rb +0 -11
- data/lib/net/connector/operations/tftp/radware.rb +0 -11
- data/lib/net/connector/operations/tftp/strategy.rb +0 -75
- data/lib/net/connector/operations/tftp_backup.rb +0 -183
- data/lib/net/connector/operations/topology/cisco.rb +0 -11
- data/lib/net/connector/operations/topology/h3c.rb +0 -11
- data/lib/net/connector/operations/topology/hillstone.rb +0 -11
- data/lib/net/connector/operations/topology/immediate_strategy.rb +0 -45
- data/lib/net/connector/operations/topology/palo_alto.rb +0 -11
- data/lib/net/connector/operations/topology/radware.rb +0 -11
- data/lib/net/connector/operations/topology/strategy.rb +0 -87
- data/lib/net/connector/operations/topology.rb +0 -261
- data/lib/net/connector/operations.rb +0 -26
|
@@ -63,6 +63,13 @@ module Net
|
|
|
63
63
|
|
|
64
64
|
def client(policy: snapshot(mode: :inventory))
|
|
65
65
|
options = policy.client_options
|
|
66
|
+
if policy.inventory_source == :postgres
|
|
67
|
+
connection = DatabaseClient::CONNECTION_ENV.each_with_object({}) do |(key, name), values|
|
|
68
|
+
item = secret_value(name)
|
|
69
|
+
values[key] = item if item
|
|
70
|
+
end
|
|
71
|
+
return DatabaseClient.new(connection_options: connection, **options)
|
|
72
|
+
end
|
|
66
73
|
raise ArgumentError, "NETDISCO_URL is required" unless options.fetch(:url)
|
|
67
74
|
|
|
68
75
|
authentication = if (token = secret_value("NETDISCO_API_KEY"))
|
|
@@ -73,9 +80,17 @@ module Net
|
|
|
73
80
|
Client.new(**options, **authentication)
|
|
74
81
|
end
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
def inventory_source
|
|
84
|
+
source = value("NETDISCO_SOURCE") || "http"
|
|
85
|
+
raise ArgumentError, "NETDISCO_SOURCE must be http or postgres" unless %w[http postgres].include?(source)
|
|
86
|
+
|
|
87
|
+
source.to_sym
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 原生客户端和 --show-config 共享预算校验;只验证所选来源的查询设置。
|
|
77
91
|
def client_options
|
|
78
|
-
|
|
92
|
+
defaults = inventory_source == :postgres ? DatabaseClient::DEFAULTS : Client::DEFAULTS
|
|
93
|
+
options = defaults.to_h do |name, default|
|
|
79
94
|
key = "NETDISCO_#{name.to_s.upcase}"
|
|
80
95
|
parsed = case name
|
|
81
96
|
when :inventory_timeout then number(key, default)
|
|
@@ -84,6 +99,13 @@ module Net
|
|
|
84
99
|
end
|
|
85
100
|
[name, parsed]
|
|
86
101
|
end
|
|
102
|
+
if inventory_source == :postgres
|
|
103
|
+
query = value("NETDISCO_QUERY") || (raise ArgumentError, "NETDISCO_QUERY is required for postgres")
|
|
104
|
+
return DatabaseClient.options(query: query, query_params: json("NETDISCO_QUERY_PARAMS", []), **options)
|
|
105
|
+
end
|
|
106
|
+
if value("NETDISCO_QUERY") || value("NETDISCO_QUERY_PARAMS")
|
|
107
|
+
raise ArgumentError, "SQL query settings require NETDISCO_SOURCE=postgres"
|
|
108
|
+
end
|
|
87
109
|
options = Client.options(**options)
|
|
88
110
|
url = value("NETDISCO_URL")
|
|
89
111
|
Client.validate_url!(url, allow_insecure_http: options.fetch(:allow_insecure_http)) if url
|
|
@@ -164,12 +186,6 @@ module Net
|
|
|
164
186
|
options.freeze
|
|
165
187
|
end
|
|
166
188
|
|
|
167
|
-
# 兼容原入口;Fleet 传入本批策略,独立调用则读取当前策略和当前秘密。
|
|
168
|
-
def credentials_for(device, policy: snapshot(mode: :backup))
|
|
169
|
-
credentials = device_credentials_for(device)
|
|
170
|
-
policy.connection_options(device.vendor).merge(credentials) if credentials
|
|
171
|
-
end
|
|
172
|
-
|
|
173
189
|
def device_credentials_for(device)
|
|
174
190
|
prefix = "NET_CONNECTOR_#{device.vendor.to_s.upcase}_"
|
|
175
191
|
username = secret_value(prefix + "USERNAME") || secret_value("NET_CONNECTOR_DEVICE_USERNAME")
|
|
@@ -195,7 +211,7 @@ module Net
|
|
|
195
211
|
|
|
196
212
|
def config_hash
|
|
197
213
|
{
|
|
198
|
-
netdisco: client_options,
|
|
214
|
+
netdisco: client_options.merge(source: inventory_source),
|
|
199
215
|
backup: { directory: backup_directory, concurrency: concurrency, limit_per_vendor: limit_per_vendor },
|
|
200
216
|
inventory: { include_hosts: list("NET_CONNECTOR_INCLUDE_HOSTS"),
|
|
201
217
|
exclude_hosts: list("NET_CONNECTOR_EXCLUDE_HOSTS"),
|
|
@@ -285,6 +301,7 @@ module Net
|
|
|
285
301
|
end
|
|
286
302
|
|
|
287
303
|
def required_secret(key) = secret_value(key) || (raise ArgumentError, "#{key} is required")
|
|
304
|
+
|
|
288
305
|
def present?(value) = value.is_a?(String) && !value.strip.empty?
|
|
289
306
|
end
|
|
290
307
|
end
|
|
@@ -7,7 +7,7 @@ module Net
|
|
|
7
7
|
module Connector
|
|
8
8
|
class BackupBusy < Error; end
|
|
9
9
|
|
|
10
|
-
module
|
|
10
|
+
module Storage
|
|
11
11
|
# 路径锁在设备采集之前取得;锁文件长期保留,避免等待者落在已经删除的 inode 上。
|
|
12
12
|
class BackupLock
|
|
13
13
|
STORAGE_KEY = :net_connector_backup_delegation
|
|
@@ -61,7 +61,7 @@ module Net
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# 仅持有实际 flock 的同一 Fiber 可授权一次内层采集,避免把路径锁变成任意可重入锁。
|
|
64
|
-
def
|
|
64
|
+
def with_delegated_lock
|
|
65
65
|
raise ThreadError, "backup path delegation requires lock ownership" unless @owner == [Process.pid, Fiber.current]
|
|
66
66
|
|
|
67
67
|
previous = Thread.current[STORAGE_KEY]
|
|
@@ -4,7 +4,7 @@ require "tempfile"
|
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
|
-
module
|
|
7
|
+
module Storage
|
|
8
8
|
# 文件同步和目录项同步分别记录;原子可见不等于断电后仍能恢复新目录项。
|
|
9
9
|
module PrivateFile
|
|
10
10
|
Receipt = Data.define(:path, :state, :phase) do
|
|
@@ -62,13 +62,8 @@ module Net
|
|
|
62
62
|
[WriteError, PersistenceError, DirectorySyncUnsupported].any? { |type| error.instance_of?(type) }
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
#
|
|
65
|
+
# 统一返回写入回执;失败时从异常 receipt 判断目录项是否已经替换。
|
|
66
66
|
def self.write(path, contents)
|
|
67
|
-
write_receipt(path, contents)
|
|
68
|
-
path
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.write_receipt(path, contents)
|
|
72
67
|
raise ArgumentError, "path must be a nonempty String" unless path.is_a?(String) && !path.empty?
|
|
73
68
|
|
|
74
69
|
receipt = Receipt.new(path: File.expand_path(path), state: :not_committed, phase: :resolve_parent)
|
|
@@ -4,11 +4,10 @@ require "fileutils"
|
|
|
4
4
|
require "ipaddr"
|
|
5
5
|
require_relative "private_file"
|
|
6
6
|
require_relative "safe_file"
|
|
7
|
-
require_relative "saved_config/legacy_index"
|
|
8
7
|
|
|
9
8
|
module Net
|
|
10
9
|
module Connector
|
|
11
|
-
module
|
|
10
|
+
module Storage
|
|
12
11
|
# 直接导出已有本地配置,无需访问 Netdisco。
|
|
13
12
|
class SavedConfig
|
|
14
13
|
# 只用规范化的管理地址标识配置,避免设备改名改变备份身份。
|
|
@@ -20,16 +19,14 @@ module Net
|
|
|
20
19
|
raise ArgumentError, "host must be an IP address"
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
#
|
|
24
|
-
def initialize(directory
|
|
22
|
+
# 只读取规范设备地址对应的文件,不维护第二套历史文件命名。
|
|
23
|
+
def initialize(directory:)
|
|
25
24
|
raise ArgumentError, "directory must be a nonempty String" unless directory.is_a?(String) && !directory.empty?
|
|
26
|
-
raise ArgumentError, "indexed must be true or false" unless [true, false].include?(indexed)
|
|
27
25
|
|
|
28
26
|
@directory = File.expand_path(directory).freeze
|
|
29
|
-
@legacy_index = LegacyIndex.new(@directory) if indexed
|
|
30
27
|
end
|
|
31
28
|
|
|
32
|
-
#
|
|
29
|
+
# 从同一已验证文件描述符取得路径或内容。
|
|
33
30
|
def find(host, required: true)
|
|
34
31
|
with_file(host, required: required) { |path, _file, _stat| path }
|
|
35
32
|
end
|
|
@@ -43,34 +40,12 @@ module Net
|
|
|
43
40
|
with_file(host) { |_path, file, _stat| file.read }
|
|
44
41
|
end
|
|
45
42
|
|
|
46
|
-
def resolve_path(host, required:)
|
|
47
|
-
filename = self.class.filename(host)
|
|
48
|
-
path = File.join(@directory, filename)
|
|
49
|
-
unless File.exist?(path) || File.symlink?(path)
|
|
50
|
-
entries = if @legacy_index
|
|
51
|
-
@legacy_index.candidates(filename)
|
|
52
|
-
else
|
|
53
|
-
Dir.children(@directory).select { |name| name.end_with?("-#{filename}") }
|
|
54
|
-
end
|
|
55
|
-
return nil if entries.empty? && !required
|
|
56
|
-
|
|
57
|
-
raise ArgumentError, "no saved configuration for #{host}" if entries.empty?
|
|
58
|
-
raise ArgumentError, "multiple saved configurations for #{host}" if entries.size > 1
|
|
59
|
-
|
|
60
|
-
path = @legacy_index ? entries.first : File.join(@directory, entries.first)
|
|
61
|
-
end
|
|
62
|
-
path
|
|
63
|
-
end
|
|
64
|
-
|
|
65
43
|
def with_file(host, required: true)
|
|
66
|
-
path =
|
|
67
|
-
|
|
68
|
-
return @legacy_index.open(path) { |name, file, stat| yield name, file, stat } if path.is_a?(LegacyIndex::Entry)
|
|
69
|
-
|
|
70
|
-
SafeFile.open(path) { |file, stat| yield path, file, stat }
|
|
44
|
+
path = File.join(@directory, self.class.filename(host))
|
|
45
|
+
SafeFile.open(path, missing: !required) { |file, stat| yield path, file, stat }
|
|
71
46
|
end
|
|
72
47
|
|
|
73
|
-
private :
|
|
48
|
+
private :with_file
|
|
74
49
|
|
|
75
50
|
# 将已保存配置写入目标文件或标准输出。
|
|
76
51
|
def export(host:, output: nil, io: $stdout)
|
|
@@ -81,7 +56,7 @@ module Net
|
|
|
81
56
|
destination = File.expand_path(output)
|
|
82
57
|
directory = File.dirname(destination)
|
|
83
58
|
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
84
|
-
PrivateFile.write(destination, contents)
|
|
59
|
+
PrivateFile.write(destination, contents).path
|
|
85
60
|
else
|
|
86
61
|
io.binmode if io.respond_to?(:binmode)
|
|
87
62
|
io.write(contents)
|
|
@@ -91,9 +66,9 @@ module Net
|
|
|
91
66
|
|
|
92
67
|
# 使用指定模板解析已有配置,无需访问设备或 Netdisco。
|
|
93
68
|
def parse(host:, template:, template_dir: nil)
|
|
94
|
-
require_relative "
|
|
69
|
+
require_relative "../textfsm"
|
|
95
70
|
|
|
96
|
-
parser =
|
|
71
|
+
parser = TextFSM.new(template_dir: template_dir)
|
|
97
72
|
parser.call(read(host), template: template, host: host)
|
|
98
73
|
end
|
|
99
74
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
# 文件存储不依赖设备连接;备份、离线导出和批量报告共享这些原语。
|
|
6
|
+
module Storage
|
|
7
|
+
autoload :BackupLock, File.expand_path("storage/backup_lock", __dir__)
|
|
8
|
+
autoload :PrivateFile, File.expand_path("storage/private_file", __dir__)
|
|
9
|
+
autoload :SafeFile, File.expand_path("storage/safe_file", __dir__)
|
|
10
|
+
autoload :SavedConfig, File.expand_path("storage/saved_config", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
autoload :BackupBusy, File.expand_path("storage/backup_lock", __dir__)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "engine/errors"
|
|
4
|
+
require_relative "engine/terminal_text"
|
|
5
|
+
|
|
6
|
+
module Net
|
|
7
|
+
module Connector
|
|
8
|
+
# 将设备命令回显或配置文本按 TextFSM 模板转换为结构化记录。
|
|
9
|
+
class TextFSM
|
|
10
|
+
# 设备入口与功能实现共置,Base 只组合能力,不重复业务流程。
|
|
11
|
+
module Capability
|
|
12
|
+
# 执行命令并按厂商、命令或显式 TextFSM 模板返回结构化记录。
|
|
13
|
+
def parse_command(command, template: nil, template_dir: nil)
|
|
14
|
+
parser = TextFSM.new(template_dir: template_dir)
|
|
15
|
+
parser.call(execute_command(command).value!, template: template, vendor: vendor, command: command, host: host)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 采集运行配置并使用指定 TextFSM 模板提取结构化记录。
|
|
19
|
+
def parse_config(template:, template_dir: nil)
|
|
20
|
+
parser = TextFSM.new(template_dir: template_dir)
|
|
21
|
+
parser.call(running_config.value!, template: template, host: host)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
DEFAULT_TEMPLATE_DIR = File.expand_path("templates", __dir__).freeze
|
|
26
|
+
|
|
27
|
+
# 配置模板目录和 Netmiko 风格的命令索引文件。
|
|
28
|
+
def initialize(template_dir: nil, index: "index")
|
|
29
|
+
template_dir ||= DEFAULT_TEMPLATE_DIR
|
|
30
|
+
raise ArgumentError, "template_dir must be a nonempty String" unless template_dir.is_a?(String) && !template_dir.empty?
|
|
31
|
+
raise ArgumentError, "index must be a nonempty String" unless index.is_a?(String) && !index.empty?
|
|
32
|
+
|
|
33
|
+
@template_dir = File.expand_path(template_dir)
|
|
34
|
+
@index = index
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 显式模板按路径读取;未指定时按厂商和命令从索引选择模板。
|
|
38
|
+
def call(text, template: nil, vendor: nil, command: nil, host: nil)
|
|
39
|
+
require "textfsm"
|
|
40
|
+
|
|
41
|
+
text = TerminalText.render(text, host: host)
|
|
42
|
+
if template
|
|
43
|
+
raise ArgumentError, "template must be a nonempty String" unless template.is_a?(String) && !template.empty?
|
|
44
|
+
|
|
45
|
+
::TextFSM::Parser.from_file(File.expand_path(template, @template_dir)).parse_hashes(text)
|
|
46
|
+
else
|
|
47
|
+
raise ArgumentError, "vendor and command are required for template lookup" unless vendor && command.is_a?(String) && !command.empty?
|
|
48
|
+
|
|
49
|
+
attributes = { "Vendor" => vendor.to_s, "Command" => command }
|
|
50
|
+
table = ::TextFSM::CliTable.new(index: @index, template_dir: @template_dir)
|
|
51
|
+
unless table.index.match(attributes)
|
|
52
|
+
raise ParsingError.new("no TextFSM template matches this vendor and command",
|
|
53
|
+
code: :template_missing, host: host, phase: :parse)
|
|
54
|
+
end
|
|
55
|
+
table.parse(text, attributes: attributes).to_hashes
|
|
56
|
+
end
|
|
57
|
+
rescue ::TextFSM::IndexError
|
|
58
|
+
raise ParsingError.new("TextFSM template index is invalid",
|
|
59
|
+
code: :template_invalid, host: host, phase: :parse), cause: nil
|
|
60
|
+
rescue ::TextFSM::TemplateError
|
|
61
|
+
raise ParsingError.new("TextFSM template is invalid",
|
|
62
|
+
code: :template_invalid, host: host, phase: :parse), cause: nil
|
|
63
|
+
rescue ::TextFSM::ParseError
|
|
64
|
+
raise ParsingError.new("TextFSM could not parse the device output",
|
|
65
|
+
code: :parse_failed, host: host, phase: :parse), cause: nil
|
|
66
|
+
rescue IOError, SystemCallError
|
|
67
|
+
raise ParsingError.new("unable to read TextFSM template",
|
|
68
|
+
code: :template_unreadable, host: host, phase: :parse), cause: nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/tftp/strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module CiscoIos
|
|
8
|
-
class TftpBackup <
|
|
8
|
+
class TftpBackup < Tftp::Strategy
|
|
9
9
|
# 在连接前拒绝当前厂商不支持的参数组合。
|
|
10
10
|
def validate_options!(_target, source_file:, vrf: nil)
|
|
11
11
|
raise ArgumentError, "Cisco IOS does not use source_file" unless source_file.nil?
|
|
@@ -28,7 +28,7 @@ module Net
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# 检查设备回显是否确认 TFTP 传输完成。
|
|
31
|
-
def
|
|
31
|
+
def device_reported_complete?(result)
|
|
32
32
|
completion_lines(result).any? do |line|
|
|
33
33
|
copied_bytes?(line) || line.match?(/\A\[OK(?:\s*-\s*[1-9]\d*\s+bytes)?\]\z/i)
|
|
34
34
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/topology/immediate_strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module CiscoIos
|
|
8
|
-
class Topology <
|
|
8
|
+
class Topology < Topology::ImmediateStrategy
|
|
9
9
|
# IOS 提供 CDP 邻居、配置描述读取和描述变更。
|
|
10
10
|
def self.supports?(capability)
|
|
11
11
|
%i[neighbors interface_descriptions interface_description_changes].include?(capability)
|
|
@@ -21,10 +21,10 @@ module Net
|
|
|
21
21
|
def protocol = :cdp
|
|
22
22
|
|
|
23
23
|
# 进入全局配置视图。
|
|
24
|
-
def
|
|
24
|
+
def enter_configuration_command = "configure terminal"
|
|
25
25
|
|
|
26
26
|
# 先回到执行视图读回,随后才执行档案中的保存命令。
|
|
27
|
-
def
|
|
27
|
+
def leave_configuration_commands = ["end"]
|
|
28
28
|
|
|
29
29
|
def persistence_pattern = /\A(?:\[OK\]|Copy complete\.)\z/i
|
|
30
30
|
|
|
@@ -40,7 +40,7 @@ module Net
|
|
|
40
40
|
|
|
41
41
|
# 复用公共接口描述命令,保留本机接口原名。
|
|
42
42
|
def change_commands(change)
|
|
43
|
-
InterfaceDescription.commands(interface: change.interface, description: change.new_description)
|
|
43
|
+
Net::Connector::Topology::InterfaceDescription.commands(interface: change.interface, description: change.new_description)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/tftp/strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module CiscoNxos
|
|
8
|
-
class TftpBackup <
|
|
8
|
+
class TftpBackup < Tftp::Strategy
|
|
9
9
|
# 在连接前拒绝当前厂商不支持的参数组合。
|
|
10
10
|
def validate_options!(_target, source_file:, **)
|
|
11
11
|
raise ArgumentError, "Cisco NX-OS does not use source_file" unless source_file.nil?
|
|
@@ -27,7 +27,7 @@ module Net
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# 检查设备回显是否确认 TFTP 传输完成。
|
|
30
|
-
def
|
|
30
|
+
def device_reported_complete?(result)
|
|
31
31
|
completion_lines(result).any? do |line|
|
|
32
32
|
line.match?(/\A
|
|
33
33
|
(?:transfer|copy)(?:\s+operation)?\s+
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/tftp/file_upload"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module H3c
|
|
8
|
-
class TftpBackup <
|
|
8
|
+
class TftpBackup < Tftp::FileUpload
|
|
9
9
|
# 自动探测选择的是启动配置;显式文件仍不能推断为运行或启动配置。
|
|
10
10
|
def receipt_metadata(target, source_file:, explicit_source:)
|
|
11
11
|
super.merge(configuration_kind: explicit_source ? :saved_file : :startup)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# 选取并校验 H3C 启动配置文件路径。
|
|
15
|
-
def
|
|
15
|
+
def resolve_source_file(value)
|
|
16
16
|
return TftpTarget.validate_source_file!(value) if value
|
|
17
17
|
|
|
18
|
-
output = @device.
|
|
18
|
+
output = @device.execute_command("display startup").value!
|
|
19
19
|
rendered = TerminalRenderer.render(output)
|
|
20
20
|
path = rendered[/^\s*Next main startup saved-configuration file:\s*(\S+)/i, 1]
|
|
21
21
|
path ||= rendered[/^\s*Current startup saved-configuration file:\s*(\S+)/i, 1]
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/topology/immediate_strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module H3c
|
|
8
|
-
class Topology <
|
|
8
|
+
class Topology < Topology::ImmediateStrategy
|
|
9
9
|
TABLE_HEADER = /^\s*(?:System Name\s+Local Interface\b|(?:Local Interface|LocalIf)\s+)/i
|
|
10
10
|
# H3C 提供 LLDP 邻居、配置描述读取和描述变更。
|
|
11
11
|
def self.supports?(capability)
|
|
@@ -19,10 +19,10 @@ module Net
|
|
|
19
19
|
def description_template = "h3c_interface_descriptions.textfsm"
|
|
20
20
|
|
|
21
21
|
# 进入系统视图以修改接口描述。
|
|
22
|
-
def
|
|
22
|
+
def enter_configuration_command = "system-view"
|
|
23
23
|
|
|
24
24
|
# 返回用户视图读回;保存交给后续独立阶段。
|
|
25
|
-
def
|
|
25
|
+
def leave_configuration_commands = ["return"]
|
|
26
26
|
|
|
27
27
|
def persistence_pattern = /\ASaved the current configuration to mainboard device successfully\.\z/i
|
|
28
28
|
|
|
@@ -52,7 +52,7 @@ module Net
|
|
|
52
52
|
|
|
53
53
|
# 在接口视图设置描述后用 quit 返回系统视图。
|
|
54
54
|
def change_commands(change)
|
|
55
|
-
InterfaceDescription.commands(interface: change.interface, description: change.new_description, leave: "quit")
|
|
55
|
+
Net::Connector::Topology::InterfaceDescription.commands(interface: change.interface, description: change.new_description, leave: "quit")
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
private
|
|
@@ -56,7 +56,7 @@ module Net
|
|
|
56
56
|
return command unless /\A[ \t]*rule[ \t]+XXX[ \t]+append\b/i.match?(command.text)
|
|
57
57
|
|
|
58
58
|
rule_id = execution.context[:rule_id]
|
|
59
|
-
raise execution.
|
|
59
|
+
raise execution.build_error("new rule ID has not been acquired") unless rule_id
|
|
60
60
|
|
|
61
61
|
command.with_text(command.text.sub(/XXX/i, rule_id))
|
|
62
62
|
end
|
|
@@ -65,9 +65,9 @@ module Net
|
|
|
65
65
|
def after_command(command, _response, execution)
|
|
66
66
|
return unless /\A[ \t]*rule[ \t]+pass\b/i.match?(command.text)
|
|
67
67
|
|
|
68
|
-
response = execution.
|
|
68
|
+
response = execution.execute_command("dis this")
|
|
69
69
|
rule_id = response.raw[/rule\s+(\d+)\s+pass[^\n]+\n\s*#/i, 1]
|
|
70
|
-
raise execution.
|
|
70
|
+
raise execution.build_error("device did not return the new rule ID") unless rule_id
|
|
71
71
|
|
|
72
72
|
execution.context[:rule_id] = rule_id
|
|
73
73
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/tftp/strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module Hillstone
|
|
8
|
-
class TftpBackup <
|
|
8
|
+
class TftpBackup < Tftp::Strategy
|
|
9
9
|
# 山石原生导出的配置文件使用 dat 扩展名。
|
|
10
10
|
def self.file_extension = "dat"
|
|
11
11
|
|
|
@@ -31,7 +31,7 @@ module Net
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# 检查设备回显是否确认 TFTP 传输完成。
|
|
34
|
-
def
|
|
34
|
+
def device_reported_complete?(result)
|
|
35
35
|
!completion_line(result).nil?
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/topology/immediate_strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module Hillstone
|
|
8
|
-
class Topology <
|
|
8
|
+
class Topology < Topology::ImmediateStrategy
|
|
9
9
|
# 山石提供 LLDP 邻居、配置描述读取和描述变更。
|
|
10
10
|
def self.supports?(capability)
|
|
11
11
|
%i[neighbors interface_descriptions interface_description_changes].include?(capability)
|
|
@@ -18,10 +18,10 @@ module Net
|
|
|
18
18
|
def description_template = "hillstone_interface_descriptions.textfsm"
|
|
19
19
|
|
|
20
20
|
# 进入配置视图。
|
|
21
|
-
def
|
|
21
|
+
def enter_configuration_command = "configure"
|
|
22
22
|
|
|
23
23
|
# 退出配置视图后先读回,保存动作随后单独执行。
|
|
24
|
-
def
|
|
24
|
+
def leave_configuration_commands = ["exit"]
|
|
25
25
|
|
|
26
26
|
def persistence_pattern = /\ASaving configuration is finished\.?\z/i
|
|
27
27
|
|
|
@@ -43,7 +43,7 @@ module Net
|
|
|
43
43
|
|
|
44
44
|
# 复用公共接口描述命令。
|
|
45
45
|
def change_commands(change)
|
|
46
|
-
InterfaceDescription.commands(interface: change.interface, description: change.new_description)
|
|
46
|
+
Net::Connector::Topology::InterfaceDescription.commands(interface: change.interface, description: change.new_description)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
private
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/tftp/file_upload"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module Huawei
|
|
8
|
-
class TftpBackup <
|
|
8
|
+
class TftpBackup < Tftp::FileUpload
|
|
9
9
|
# 缺少源文件也必须在首次设备 I/O 之前拒绝。
|
|
10
10
|
def validate_options!(target, source_file:, vrf: nil)
|
|
11
11
|
super
|
|
@@ -13,7 +13,7 @@ module Net
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# 要求调用方提供合法的华为启动配置文件路径。
|
|
16
|
-
def
|
|
16
|
+
def resolve_source_file(value)
|
|
17
17
|
raise ArgumentError, "Huawei TFTP backup requires source_file" unless value
|
|
18
18
|
|
|
19
19
|
TftpTarget.validate_source_file!(value)
|
|
@@ -23,7 +23,7 @@ module Net
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# 采集前后都检查候选差异,拒绝把未提交配置误作运行配置。
|
|
26
|
-
def
|
|
26
|
+
def validate_response!(command, response, _execution)
|
|
27
27
|
return unless command.text == "show config diff"
|
|
28
28
|
return if body(response.output, command.text).empty?
|
|
29
29
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/tftp/strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module PaloAlto
|
|
8
|
-
class TftpBackup <
|
|
8
|
+
class TftpBackup < Tftp::Strategy
|
|
9
9
|
FILENAME = "running-config.xml"
|
|
10
10
|
|
|
11
11
|
# 返回 PAN-OS 固定使用的运行配置文件名。
|
|
@@ -33,7 +33,7 @@ module Net
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# 检查设备回显是否确认 TFTP 传输完成。
|
|
36
|
-
def
|
|
36
|
+
def device_reported_complete?(result)
|
|
37
37
|
completion_lines(result).any? do |line|
|
|
38
38
|
line.match?(/\ASent\s+[1-9]\d*\s+bytes(?:\s+in\s+[\d.]+\s+(?:secs?|seconds?))?[.!]?\z/i)
|
|
39
39
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/topology/strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module PaloAlto
|
|
8
|
-
class Topology <
|
|
8
|
+
class Topology < Topology::Strategy
|
|
9
9
|
# 候选配置缺少经固件实验确认的所有权/commit 协议,暂只提供读取能力。
|
|
10
10
|
def self.supports?(capability)
|
|
11
11
|
%i[neighbors interface_descriptions].include?(capability)
|
|
@@ -20,10 +20,7 @@ module Net
|
|
|
20
20
|
def description_template = "palo_alto_interface_descriptions.textfsm"
|
|
21
21
|
|
|
22
22
|
# 进入候选配置视图。
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
# 提交候选配置后退出配置视图。
|
|
26
|
-
def finish_commands = ["commit", "exit"]
|
|
23
|
+
def enter_configuration_command = "configure"
|
|
27
24
|
|
|
28
25
|
# 只计入非空邻居块,跳过没有对端的本机接口。
|
|
29
26
|
def expected_neighbor_count(output, _template)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../device/tftp/strategy"
|
|
4
4
|
|
|
5
5
|
module Net
|
|
6
6
|
module Connector
|
|
7
7
|
module Radware
|
|
8
|
-
class TftpBackup <
|
|
8
|
+
class TftpBackup < Tftp::Strategy
|
|
9
9
|
# Radware 原生导出生成压缩配置归档。
|
|
10
10
|
def self.file_extension = "tgz"
|
|
11
11
|
|
|
@@ -32,7 +32,7 @@ module Net
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# 检查设备回显是否确认 TFTP 传输完成。
|
|
35
|
-
def
|
|
35
|
+
def device_reported_complete?(result)
|
|
36
36
|
completion_lines(result).any? do |line|
|
|
37
37
|
line.match?(/\A
|
|
38
38
|
(?:Current\s+)?(?:configuration|config)\s+
|