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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -1
  3. data/README.md +92 -20
  4. data/docs/VERIFICATION.md +17 -7
  5. data/docs/architecture.md +102 -64
  6. data/examples/netdisco_database.yml +16 -0
  7. data/lib/net/connector/device/base.rb +27 -107
  8. data/lib/net/connector/device/local_backup.rb +96 -0
  9. data/lib/net/connector/device/profile.rb +8 -3
  10. data/lib/net/connector/device/running_config/strategy.rb +1 -1
  11. data/lib/net/connector/device/running_config.rb +44 -5
  12. data/lib/net/connector/device/save_config.rb +21 -0
  13. data/lib/net/connector/device/tftp/file_upload.rb +53 -0
  14. data/lib/net/connector/{operations/tftp_receipt.rb → device/tftp/receipt.rb} +18 -20
  15. data/lib/net/connector/device/tftp/strategy.rb +67 -0
  16. data/lib/net/connector/device/tftp/target.rb +63 -0
  17. data/lib/net/connector/device/tftp.rb +117 -0
  18. data/lib/net/connector/device/topology/immediate_strategy.rb +40 -0
  19. data/lib/net/connector/device/topology/interface_description.rb +46 -0
  20. data/lib/net/connector/device/topology/interface_name.rb +58 -0
  21. data/lib/net/connector/device/topology/strategy.rb +75 -0
  22. data/lib/net/connector/device/topology.rb +277 -0
  23. data/lib/net/connector/engine/authentication.rb +2 -2
  24. data/lib/net/connector/engine/configuration.rb +1 -1
  25. data/lib/net/connector/engine/dialogue.rb +21 -21
  26. data/lib/net/connector/engine/error_metadata.rb +50 -0
  27. data/lib/net/connector/engine/errors.rb +15 -1
  28. data/lib/net/connector/engine/execution.rb +17 -10
  29. data/lib/net/connector/engine/log/event.rb +66 -0
  30. data/lib/net/connector/engine/log/formatter.rb +16 -0
  31. data/lib/net/connector/engine/log/messages.rb +52 -0
  32. data/lib/net/connector/engine/log/stream.rb +56 -0
  33. data/lib/net/connector/engine/log.rb +105 -84
  34. data/lib/net/connector/engine/session.rb +83 -42
  35. data/lib/net/connector/engine/terminal_text.rb +33 -0
  36. data/lib/net/connector/engine/transport.rb +2 -2
  37. data/lib/net/connector/netdisco/batch.rb +9 -45
  38. data/lib/net/connector/netdisco/cli.rb +17 -10
  39. data/lib/net/connector/netdisco/client.rb +4 -3
  40. data/lib/net/connector/netdisco/config_file.rb +6 -3
  41. data/lib/net/connector/netdisco/database_client.rb +197 -0
  42. data/lib/net/connector/netdisco/device.rb +4 -5
  43. data/lib/net/connector/netdisco/diagnostic.rb +9 -39
  44. data/lib/net/connector/netdisco/fleet.rb +26 -39
  45. data/lib/net/connector/netdisco/report.rb +37 -16
  46. data/lib/net/connector/netdisco/result_store.rb +2 -2
  47. data/lib/net/connector/netdisco/settings.rb +26 -9
  48. data/lib/net/connector/netdisco.rb +1 -0
  49. data/lib/net/connector/{operations → storage}/backup_lock.rb +2 -2
  50. data/lib/net/connector/{operations → storage}/private_file.rb +2 -7
  51. data/lib/net/connector/{operations → storage}/safe_file.rb +1 -1
  52. data/lib/net/connector/{operations → storage}/saved_config.rb +10 -35
  53. data/lib/net/connector/storage.rb +15 -0
  54. data/lib/net/connector/textfsm.rb +72 -0
  55. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +3 -3
  56. data/lib/net/connector/vendor/cisco_ios/topology.rb +5 -5
  57. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +3 -3
  58. data/lib/net/connector/vendor/h3c/tftp_backup.rb +4 -4
  59. data/lib/net/connector/vendor/h3c/topology.rb +5 -5
  60. data/lib/net/connector/vendor/h3c.rb +3 -3
  61. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +3 -3
  62. data/lib/net/connector/vendor/hillstone/topology.rb +5 -5
  63. data/lib/net/connector/vendor/huawei/tftp_backup.rb +3 -3
  64. data/lib/net/connector/vendor/palo_alto/running_config.rb +1 -1
  65. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +3 -3
  66. data/lib/net/connector/vendor/palo_alto/topology.rb +3 -6
  67. data/lib/net/connector/vendor/radware/tftp_backup.rb +3 -3
  68. data/lib/net/connector/vendor/radware/topology.rb +2 -2
  69. data/lib/net/connector/version.rb +1 -1
  70. data/lib/net/connector.rb +1 -6
  71. metadata +47 -39
  72. data/lib/net/connector/device/interface_description.rb +0 -44
  73. data/lib/net/connector/device/interface_name.rb +0 -56
  74. data/lib/net/connector/engine/base.rb +0 -4
  75. data/lib/net/connector/engine/log_messages.rb +0 -47
  76. data/lib/net/connector/engine/profile.rb +0 -4
  77. data/lib/net/connector/engine.rb +0 -4
  78. data/lib/net/connector/operations/local_backup.rb +0 -89
  79. data/lib/net/connector/operations/parse_output.rb +0 -77
  80. data/lib/net/connector/operations/running_config/cisco.rb +0 -14
  81. data/lib/net/connector/operations/running_config/hillstone.rb +0 -12
  82. data/lib/net/connector/operations/running_config/palo_alto.rb +0 -12
  83. data/lib/net/connector/operations/running_config/strategy.rb +0 -3
  84. data/lib/net/connector/operations/running_config.rb +0 -16
  85. data/lib/net/connector/operations/saved_config/legacy_index.rb +0 -109
  86. data/lib/net/connector/operations/tftp/cisco_ios.rb +0 -11
  87. data/lib/net/connector/operations/tftp/cisco_nxos.rb +0 -11
  88. data/lib/net/connector/operations/tftp/file_upload.rb +0 -55
  89. data/lib/net/connector/operations/tftp/h3c.rb +0 -11
  90. data/lib/net/connector/operations/tftp/hillstone.rb +0 -11
  91. data/lib/net/connector/operations/tftp/huawei.rb +0 -11
  92. data/lib/net/connector/operations/tftp/palo_alto.rb +0 -11
  93. data/lib/net/connector/operations/tftp/radware.rb +0 -11
  94. data/lib/net/connector/operations/tftp/strategy.rb +0 -75
  95. data/lib/net/connector/operations/tftp_backup.rb +0 -183
  96. data/lib/net/connector/operations/topology/cisco.rb +0 -11
  97. data/lib/net/connector/operations/topology/h3c.rb +0 -11
  98. data/lib/net/connector/operations/topology/hillstone.rb +0 -11
  99. data/lib/net/connector/operations/topology/immediate_strategy.rb +0 -45
  100. data/lib/net/connector/operations/topology/palo_alto.rb +0 -11
  101. data/lib/net/connector/operations/topology/radware.rb +0 -11
  102. data/lib/net/connector/operations/topology/strategy.rb +0 -87
  103. data/lib/net/connector/operations/topology.rb +0 -261
  104. 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
- # 原生客户端和 --show-config 共享同一预算、地址与 HTTP 策略校验。
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
- options = Client::DEFAULTS.to_h do |name, default|
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../connector"
4
4
  require_relative "netdisco/client"
5
+ require_relative "netdisco/database_client"
5
6
  require_relative "netdisco/rules"
6
7
  require_relative "netdisco/device"
7
8
  require_relative "netdisco/config_file"
@@ -7,7 +7,7 @@ module Net
7
7
  module Connector
8
8
  class BackupBusy < Error; end
9
9
 
10
- module Operations
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 delegate
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 Operations
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
- # 保留原路径返回值;失败时可从异常 receipt 判断目录项是否已经替换。
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,7 +4,7 @@ require "digest"
4
4
 
5
5
  module Net
6
6
  module Connector
7
- module Operations
7
+ module Storage
8
8
  # 对同一 FD 做类型检查和读取;NOFOLLOW 只保护最后一个路径分量,目录由调用方保护。
9
9
  module SafeFile
10
10
  Fingerprint = Data.define(:path, :sha256, :mode, :dev, :ino)
@@ -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 Operations
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
- # 默认每次实时查找;批次显式 indexed 时共享一份惰性的旧命名文件快照。
24
- def initialize(directory:, indexed: false)
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 = resolve_path(host, required: required)
67
- return nil unless path
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 :resolve_path, :with_file
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 "parse_output"
69
+ require_relative "../textfsm"
95
70
 
96
- parser = ParseOutput.new(template_dir: template_dir)
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 "../../operations/tftp/strategy"
3
+ require_relative "../../device/tftp/strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module CiscoIos
8
- class TftpBackup < Operations::Tftp::Strategy
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 complete?(result)
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 "../../operations/topology/immediate_strategy"
3
+ require_relative "../../device/topology/immediate_strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module CiscoIos
8
- class Topology < Operations::Topology::ImmediateStrategy
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 enter_configuration = "configure terminal"
24
+ def enter_configuration_command = "configure terminal"
25
25
 
26
26
  # 先回到执行视图读回,随后才执行档案中的保存命令。
27
- def leave_configuration = ["end"]
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 "../../operations/tftp/strategy"
3
+ require_relative "../../device/tftp/strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module CiscoNxos
8
- class TftpBackup < Operations::Tftp::Strategy
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 complete?(result)
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 "../../operations/tftp/file_upload"
3
+ require_relative "../../device/tftp/file_upload"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module H3c
8
- class TftpBackup < Operations::Tftp::FileUpload
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 source_file(value)
15
+ def resolve_source_file(value)
16
16
  return TftpTarget.validate_source_file!(value) if value
17
17
 
18
- output = @device.execute("display startup").value!
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 "../../operations/topology/immediate_strategy"
3
+ require_relative "../../device/topology/immediate_strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module H3c
8
- class Topology < Operations::Topology::ImmediateStrategy
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 enter_configuration = "system-view"
22
+ def enter_configuration_command = "system-view"
23
23
 
24
24
  # 返回用户视图读回;保存交给后续独立阶段。
25
- def leave_configuration = ["return"]
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.failure("new rule ID has not been acquired") unless rule_id
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.query("dis this")
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.failure("device did not return the new rule ID") unless rule_id
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 "../../operations/tftp/strategy"
3
+ require_relative "../../device/tftp/strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module Hillstone
8
- class TftpBackup < Operations::Tftp::Strategy
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 complete?(result)
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 "../../operations/topology/immediate_strategy"
3
+ require_relative "../../device/topology/immediate_strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module Hillstone
8
- class Topology < Operations::Topology::ImmediateStrategy
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 enter_configuration = "configure"
21
+ def enter_configuration_command = "configure"
22
22
 
23
23
  # 退出配置视图后先读回,保存动作随后单独执行。
24
- def leave_configuration = ["exit"]
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 "../../operations/tftp/file_upload"
3
+ require_relative "../../device/tftp/file_upload"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module Huawei
8
- class TftpBackup < Operations::Tftp::FileUpload
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 source_file(value)
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 check_response(command, response, _execution)
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 "../../operations/tftp/strategy"
3
+ require_relative "../../device/tftp/strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module PaloAlto
8
- class TftpBackup < Operations::Tftp::Strategy
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 complete?(result)
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 "../../operations/topology/strategy"
3
+ require_relative "../../device/topology/strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module PaloAlto
8
- class Topology < Operations::Topology::Strategy
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 enter_configuration = "configure"
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 "../../operations/tftp/strategy"
3
+ require_relative "../../device/tftp/strategy"
4
4
 
5
5
  module Net
6
6
  module Connector
7
7
  module Radware
8
- class TftpBackup < Operations::Tftp::Strategy
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 complete?(result)
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+