net-connector 0.4.2 → 0.5.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -1
  3. data/README.md +42 -19
  4. data/docs/VERIFICATION.md +17 -7
  5. data/docs/architecture.md +96 -64
  6. data/lib/net/connector/device/base.rb +27 -107
  7. data/lib/net/connector/device/local_backup.rb +96 -0
  8. data/lib/net/connector/device/profile.rb +8 -3
  9. data/lib/net/connector/device/running_config/strategy.rb +1 -1
  10. data/lib/net/connector/device/running_config.rb +44 -5
  11. data/lib/net/connector/device/save_config.rb +21 -0
  12. data/lib/net/connector/device/tftp/file_upload.rb +53 -0
  13. data/lib/net/connector/{operations/tftp_receipt.rb → device/tftp/receipt.rb} +18 -20
  14. data/lib/net/connector/device/tftp/strategy.rb +67 -0
  15. data/lib/net/connector/device/tftp/target.rb +63 -0
  16. data/lib/net/connector/device/tftp.rb +117 -0
  17. data/lib/net/connector/device/topology/immediate_strategy.rb +40 -0
  18. data/lib/net/connector/device/topology/interface_description.rb +46 -0
  19. data/lib/net/connector/device/topology/interface_name.rb +58 -0
  20. data/lib/net/connector/device/topology/strategy.rb +75 -0
  21. data/lib/net/connector/device/topology.rb +277 -0
  22. data/lib/net/connector/engine/authentication.rb +1 -1
  23. data/lib/net/connector/engine/configuration.rb +1 -1
  24. data/lib/net/connector/engine/dialogue.rb +17 -17
  25. data/lib/net/connector/engine/error_metadata.rb +50 -0
  26. data/lib/net/connector/engine/errors.rb +15 -1
  27. data/lib/net/connector/engine/execution.rb +15 -8
  28. data/lib/net/connector/engine/log/event.rb +66 -0
  29. data/lib/net/connector/engine/log/formatter.rb +16 -0
  30. data/lib/net/connector/engine/log/messages.rb +52 -0
  31. data/lib/net/connector/engine/log/stream.rb +56 -0
  32. data/lib/net/connector/engine/log.rb +105 -84
  33. data/lib/net/connector/engine/session.rb +83 -42
  34. data/lib/net/connector/engine/terminal_text.rb +33 -0
  35. data/lib/net/connector/engine/transport.rb +2 -2
  36. data/lib/net/connector/netdisco/batch.rb +9 -45
  37. data/lib/net/connector/netdisco/cli.rb +7 -9
  38. data/lib/net/connector/netdisco/client.rb +2 -2
  39. data/lib/net/connector/netdisco/device.rb +4 -5
  40. data/lib/net/connector/netdisco/diagnostic.rb +9 -39
  41. data/lib/net/connector/netdisco/fleet.rb +26 -39
  42. data/lib/net/connector/netdisco/report.rb +37 -16
  43. data/lib/net/connector/netdisco/result_store.rb +2 -2
  44. data/lib/net/connector/netdisco/settings.rb +0 -6
  45. data/lib/net/connector/{operations → storage}/backup_lock.rb +2 -2
  46. data/lib/net/connector/{operations → storage}/private_file.rb +2 -7
  47. data/lib/net/connector/{operations → storage}/safe_file.rb +1 -1
  48. data/lib/net/connector/{operations → storage}/saved_config.rb +10 -35
  49. data/lib/net/connector/storage.rb +15 -0
  50. data/lib/net/connector/textfsm.rb +72 -0
  51. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +3 -3
  52. data/lib/net/connector/vendor/cisco_ios/topology.rb +5 -5
  53. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +3 -3
  54. data/lib/net/connector/vendor/h3c/tftp_backup.rb +4 -4
  55. data/lib/net/connector/vendor/h3c/topology.rb +5 -5
  56. data/lib/net/connector/vendor/h3c.rb +3 -3
  57. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +3 -3
  58. data/lib/net/connector/vendor/hillstone/topology.rb +5 -5
  59. data/lib/net/connector/vendor/huawei/tftp_backup.rb +3 -3
  60. data/lib/net/connector/vendor/palo_alto/running_config.rb +1 -1
  61. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +3 -3
  62. data/lib/net/connector/vendor/palo_alto/topology.rb +3 -6
  63. data/lib/net/connector/vendor/radware/tftp_backup.rb +3 -3
  64. data/lib/net/connector/vendor/radware/topology.rb +2 -2
  65. data/lib/net/connector/version.rb +1 -1
  66. data/lib/net/connector.rb +1 -6
  67. metadata +25 -39
  68. data/lib/net/connector/device/interface_description.rb +0 -44
  69. data/lib/net/connector/device/interface_name.rb +0 -56
  70. data/lib/net/connector/engine/base.rb +0 -4
  71. data/lib/net/connector/engine/log_messages.rb +0 -47
  72. data/lib/net/connector/engine/profile.rb +0 -4
  73. data/lib/net/connector/engine.rb +0 -4
  74. data/lib/net/connector/operations/local_backup.rb +0 -89
  75. data/lib/net/connector/operations/parse_output.rb +0 -77
  76. data/lib/net/connector/operations/running_config/cisco.rb +0 -14
  77. data/lib/net/connector/operations/running_config/hillstone.rb +0 -12
  78. data/lib/net/connector/operations/running_config/palo_alto.rb +0 -12
  79. data/lib/net/connector/operations/running_config/strategy.rb +0 -3
  80. data/lib/net/connector/operations/running_config.rb +0 -16
  81. data/lib/net/connector/operations/saved_config/legacy_index.rb +0 -109
  82. data/lib/net/connector/operations/tftp/cisco_ios.rb +0 -11
  83. data/lib/net/connector/operations/tftp/cisco_nxos.rb +0 -11
  84. data/lib/net/connector/operations/tftp/file_upload.rb +0 -55
  85. data/lib/net/connector/operations/tftp/h3c.rb +0 -11
  86. data/lib/net/connector/operations/tftp/hillstone.rb +0 -11
  87. data/lib/net/connector/operations/tftp/huawei.rb +0 -11
  88. data/lib/net/connector/operations/tftp/palo_alto.rb +0 -11
  89. data/lib/net/connector/operations/tftp/radware.rb +0 -11
  90. data/lib/net/connector/operations/tftp/strategy.rb +0 -75
  91. data/lib/net/connector/operations/tftp_backup.rb +0 -183
  92. data/lib/net/connector/operations/topology/cisco.rb +0 -11
  93. data/lib/net/connector/operations/topology/h3c.rb +0 -11
  94. data/lib/net/connector/operations/topology/hillstone.rb +0 -11
  95. data/lib/net/connector/operations/topology/immediate_strategy.rb +0 -45
  96. data/lib/net/connector/operations/topology/palo_alto.rb +0 -11
  97. data/lib/net/connector/operations/topology/radware.rb +0 -11
  98. data/lib/net/connector/operations/topology/strategy.rb +0 -87
  99. data/lib/net/connector/operations/topology.rb +0 -261
  100. data/lib/net/connector/operations.rb +0 -26
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../vendor/h3c/topology"
4
-
5
- module Net
6
- module Connector
7
- module Operations
8
- Topology::H3c = Net::Connector::H3c::Topology
9
- end
10
- end
11
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../vendor/hillstone/topology"
4
-
5
- module Net
6
- module Connector
7
- module Operations
8
- Topology::Hillstone = Net::Connector::Hillstone::Topology
9
- end
10
- end
11
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "strategy"
4
- require_relative "../../engine/terminal_renderer"
5
-
6
- module Net
7
- module Connector
8
- module Operations
9
- class Topology
10
- # 接口描述立即生效的设备:退出视图、读回、保存是不同阶段,不能在 finish 中提前保存。
11
- class ImmediateStrategy < Strategy
12
- def verification_commands = @device.config_commands
13
- def persistence_commands = @device.profile.save_commands
14
-
15
- # 保留旧扩展查询入口;公共执行流程使用独立阶段,不再执行这份合并列表。
16
- def finish_commands = leave_configuration + persistence_commands
17
-
18
- # TextFSM 不能静默跳过未知接口头;否则目标行正确也不足以证明读回证据完整。
19
- def validate_descriptions!(config, rows)
20
- expected = config.scan(/^interface[ \t]+([^\r\n]*)/).flatten.map(&:strip)
21
- return if expected == rows.map { |row| row.fetch("INTERFACE").strip }
22
-
23
- raise ParsingError.new("interface description output was not fully recognized", code: :unrecognized_output,
24
- host: @device.host, phase: :verify)
25
- end
26
-
27
- # 完成提示必须来自保存步骤;原文和渲染文本中的失败优先,单独提示符/进度不算确认。
28
- def persistence_confirmed?(result)
29
- !result.steps.empty? && result.steps.all? do |step|
30
- raw = step.output.delete_suffix(step.prompt.to_s)
31
- rendered = TerminalRenderer.render(raw)
32
- failure = /\b(?:error|failed|failure|denied|cannot|unable|unsuccessful|aborted)\b/i
33
- next false if raw.match?(failure) || rendered.match?(failure)
34
-
35
- rendered.lines.any? { |line| line.strip.match?(persistence_pattern) }
36
- end
37
- end
38
-
39
- # 没有明确完成证据的策略不能借用一个泛化的成功关键词。
40
- def persistence_pattern = /\A\b\B\z/
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../vendor/palo_alto/topology"
4
-
5
- module Net
6
- module Connector
7
- module Operations
8
- Topology::PaloAlto = Net::Connector::PaloAlto::Topology
9
- end
10
- end
11
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../vendor/radware/topology"
4
-
5
- module Net
6
- module Connector
7
- module Operations
8
- Topology::Radware = Net::Connector::Radware::Topology
9
- end
10
- end
11
- end
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "shellwords"
4
- require_relative "../../device/interface_description"
5
-
6
- module Net
7
- module Connector
8
- module Operations
9
- class Topology
10
- autoload :Cisco, File.expand_path("cisco", __dir__)
11
- autoload :H3c, File.expand_path("h3c", __dir__)
12
- autoload :Hillstone, File.expand_path("hillstone", __dir__)
13
- autoload :PaloAlto, File.expand_path("palo_alto", __dir__)
14
- autoload :Radware, File.expand_path("radware", __dir__)
15
-
16
- # 厂商拓扑规则;默认不提供任何设备能力。
17
- class Strategy
18
- # 保存设备供厂商规则查询档案和会话状态。
19
- def initialize(device)
20
- @device = device
21
- end
22
-
23
- # 声明策略类支持的拓扑能力,默认全部关闭。
24
- def self.supports?(_capability) = false
25
-
26
- # 将实例级能力查询交给策略类声明。
27
- def supports?(capability) = self.class.supports?(capability)
28
-
29
- # 返回读取邻居的命令,未定义时不支持发现。
30
- def neighbor_command = nil
31
-
32
- # 根据原始输出选择邻居解析模板。
33
- def neighbor_template(_output) = nil
34
-
35
- # 返回当前接口描述所用的解析模板。
36
- def description_template = nil
37
-
38
- # 返回输出中应解析出的邻居数量,供完整性校验使用。
39
- def expected_neighbor_count(_output, _template) = 0
40
-
41
- # 只有明确的空表证据才允许返回空邻居列表。
42
- def empty_neighbor_output?(_output) = false
43
-
44
- # 返回进入设备配置视图的命令。
45
- def enter_configuration = nil
46
-
47
- # 返回离开配置视图和保存配置所需的命令。
48
- def finish_commands = []
49
-
50
- # 未声明分阶段契约的旧策略仍可读取,但不能推断其退出、验证和保存的安全边界。
51
- def leave_configuration = nil
52
- def verification_commands = nil
53
- def persistence_commands = nil
54
- def persistence_confirmed?(_result) = false
55
- def change_error_code = :description_unsupported
56
-
57
- # 只读策略可以没有完整性扩展;立即生效的接口块模型会进一步校验解析数量。
58
- def validate_descriptions!(_config, _rows) end
59
-
60
- # 为一条接口描述变更生成厂商命令。
61
- def change_commands(_change) = []
62
-
63
- # 标明邻居发现使用的协议。
64
- def protocol = :lldp
65
-
66
- # 在下发前允许厂商调整命令对象及超时。
67
- def script_command(command) = command
68
-
69
- # 将发现接口名归一为查找当前配置所用的键。
70
- def interface_key(name) = InterfaceName.key(name)
71
-
72
- # 将发现协议常用的接口简称展开为配置视图可识别的名称。
73
- def configuration_interface(name) = InterfaceName.configuration(name)
74
-
75
- # 还原 PAN-OS set 配置或 Alteon dump 中的引号描述。
76
- def decode_description(value)
77
- value = value.strip
78
- value.start_with?('"') ? Shellwords.split(value).join(" ") : value
79
- rescue ArgumentError
80
- value
81
- end
82
-
83
- end
84
- end
85
- end
86
- end
87
- end
@@ -1,261 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "topology/strategy"
4
-
5
- module Net
6
- module Connector
7
- module Operations
8
- # 从 CDP/LLDP 回显生成链路邻居,并规划接口描述变更。
9
- class Topology
10
- Neighbor = Data.define(:local_interface, :neighbor_name, :neighbor_interface, :chassis_id, :protocol)
11
- Change = Data.define(:interface, :old_description, :new_description, :neighbor)
12
- Plan = Data.define(:host, :vendor, :evidence, :changes, :commands)
13
-
14
- # 保存设备及其独立的 TextFSM 解析入口。
15
- def initialize(device, template_dir: nil)
16
- require_relative "parse_output"
17
-
18
- @device = device
19
- @parser = ParseOutput.new(template_dir: template_dir)
20
- @strategy = (device.profile.topology_strategy || Strategy).new(device)
21
- end
22
-
23
- # 执行厂商 CDP/LLDP 命令,返回统一的邻居记录。
24
- def neighbors
25
- command = @strategy.neighbor_command
26
- unless @strategy.supports?(:neighbors) && command
27
- raise UnsupportedOperation.new("this device cannot discover LLDP neighbors",
28
- code: :neighbor_discovery_unsupported, host: @device.host, phase: :discover)
29
- end
30
-
31
- output = @parser.utf8(@device.execute(command).value!, host: @device.host)
32
- template = @strategy.neighbor_template(output)
33
- rows = @parser.call(output, template: template, vendor: @device.vendor, command: command, host: @device.host)
34
- if rows.size != @strategy.expected_neighbor_count(output, template) || (rows.empty? && !@strategy.empty_neighbor_output?(output))
35
- raise ParsingError.new("neighbor output was not recognized", code: :unrecognized_output,
36
- host: @device.host, phase: :discover)
37
- end
38
-
39
- rows.map do |row|
40
- Neighbor.new(local_interface: row.fetch("LOCAL_INTERFACE").strip.freeze,
41
- neighbor_name: row.fetch("NEIGHBOR_NAME", "").strip.freeze,
42
- neighbor_interface: row.fetch("NEIGHBOR_INTERFACE").strip.freeze,
43
- chassis_id: row.fetch("CHASSIS_ID", "").strip.freeze,
44
- protocol: @strategy.protocol).freeze
45
- end
46
- end
47
-
48
- # 从运行配置读取已设置的接口描述或端口名称。
49
- def descriptions = read_descriptions
50
-
51
- # 内部读取先交付执行步骤,再处理解析;解析失败也不能抹去已经完成的采集命令。
52
- def read_descriptions
53
- template = @strategy.description_template
54
- unless @strategy.supports?(:interface_descriptions) && template
55
- raise UnsupportedOperation.new("interface descriptions are unsupported for this vendor",
56
- code: :description_unsupported, host: @device.host, phase: :discover)
57
- end
58
- result = @device.running_config
59
- yield result if block_given?
60
- config = result.value!
61
- rows = @parser.call(config, template: template, host: @device.host)
62
- @strategy.validate_descriptions!(config, rows) if @strategy.respond_to?(:validate_descriptions!)
63
- rows.each_with_object({}) do |row, found|
64
- interface = @strategy.interface_key(row.fetch("INTERFACE"))
65
- description = @strategy.decode_description(row.fetch("DESCRIPTION", ""))
66
- previous = found[interface]
67
- if previous && !previous.empty? && !description.empty? && previous != description
68
- raise ParsingError.new("interface has conflicting descriptions", code: :ambiguous_description,
69
- host: @device.host, phase: :parse)
70
- end
71
- found[interface] = description unless description.empty? && previous
72
- end
73
- end
74
-
75
- private :read_descriptions
76
-
77
- # 比对邻居和现有描述,生成包含确切命令的只读变更计划。
78
- def plan_descriptions(abbreviate: true, lowercase: false, &formatter)
79
- check_change_support!(:plan) if @strategy.supports?(:neighbors)
80
- discovered = neighbors
81
- current = descriptions
82
- current.each_value(&:freeze)
83
- evidence = evidence_for(discovered, current)
84
- changes = discovered.filter_map do |neighbor|
85
- old = current.fetch(@strategy.interface_key(neighbor.local_interface))
86
- proposed = if formatter
87
- formatter.call(neighbor)
88
- else
89
- InterfaceDescription.format(neighbor, abbreviate: abbreviate, lowercase: lowercase)
90
- end
91
- InterfaceDescription.validate!(proposed)
92
- next if old == proposed
93
-
94
- Change.new(interface: @strategy.configuration_interface(neighbor.local_interface).freeze, old_description: old.freeze,
95
- new_description: proposed.freeze, neighbor: neighbor).freeze
96
- end.freeze
97
- commands = if changes.empty?
98
- []
99
- else
100
- stage_scripts(changes).values.flat_map { |script| script.map(&:text) }
101
- end
102
- commands.each(&:freeze)
103
- Plan.new(host: @device.host.dup.freeze, vendor: @device.vendor, evidence: evidence,
104
- changes: changes, commands: commands.freeze).freeze
105
- end
106
-
107
- # 只有明确确认且邻居和旧描述未变化时,才下发计划中的命令。
108
- def apply(plan, confirmed: false)
109
- unless confirmed == true
110
- raise UnsupportedOperation.new("description plan requires confirmation", code: :confirmation_required,
111
- host: @device.host, phase: :apply)
112
- end
113
- raise ArgumentError, "plan must be a topology plan for this device" unless plan.is_a?(Plan) &&
114
- plan.host == @device.host &&
115
- plan.vendor == @device.vendor
116
- if plan.changes.empty?
117
- raise ArgumentError, "empty topology plan cannot contain commands" unless plan.commands.empty?
118
-
119
- return Result.new
120
- end
121
-
122
- @strategy.supports?(:neighbors) ? check_change_support!(:apply) : neighbors
123
- @device.with_operation(:apply) { apply_plan(plan) }
124
- end
125
-
126
- private
127
-
128
- # 同一租约覆盖重验、修改、读回与保存;有完成步骤后发生的错误必须带回这些步骤。
129
- def apply_plan(plan)
130
- stages = stage_scripts(plan.changes)
131
- unless stages.values.flat_map { |script| script.map(&:text) } == plan.commands
132
- raise ArgumentError, "plan commands were modified or use an older execution sequence; regenerate the plan"
133
- end
134
- revalidate_plan!(plan)
135
- steps = []
136
- result = execute_stage(stages.fetch(:change), steps)
137
- return Result.new(steps: steps, error: result.error) if result.failure?
138
-
139
- verify_descriptions!(plan.changes, stages.fetch(:verify), steps)
140
- persisting = true
141
- result = execute_stage(stages.fetch(:persist), steps)
142
- if result.failure? || !@strategy.persistence_confirmed?(result)
143
- return Result.new(steps: steps, error: persistence_error(result.error))
144
- end
145
- Result.new(steps: steps)
146
- rescue Error => error
147
- raise unless steps
148
-
149
- Result.new(steps: steps, error: persisting ? persistence_error(error) : error)
150
- end
151
-
152
- # 读回命令与审批一致,并且每个目标描述均得到确认,才允许进入保存阶段。
153
- def verify_descriptions!(changes, script, steps)
154
- unless Script.new(@device.config_commands).map(&:text) == script.map(&:text)
155
- raise DeviceError.new("configuration readback commands changed after approval",
156
- code: :verification_plan_changed, host: @device.host, phase: :verify)
157
- end
158
- updated = read_descriptions do |readback|
159
- steps.concat(readback.steps)
160
- if readback.success? && readback.steps.map { |step| step.command.text } != script.map(&:text)
161
- raise DeviceError.new("configuration readback did not execute the approved commands",
162
- code: :verification_plan_changed, host: @device.host, phase: :verify)
163
- end
164
- end
165
- unless changes.all? { |change| updated[@strategy.interface_key(change.interface)] == change.new_description }
166
- raise DeviceError.new("interface descriptions were not confirmed by readback",
167
- code: :description_unconfirmed, host: @device.host, phase: :verify)
168
- end
169
- end
170
-
171
- # 重验失败仍在写入前直接抛异常,保持 stale_plan 的既有契约。
172
- def revalidate_plan!(plan)
173
- discovered = neighbors
174
- evidence = evidence_for(discovered, descriptions)
175
- current_neighbors = discovered.to_h { |neighbor| [@strategy.interface_key(neighbor.local_interface), neighbor] }
176
- same_neighbors = plan.changes.all? do |change|
177
- current_neighbors[@strategy.interface_key(change.interface)] == change.neighbor
178
- end
179
- unless evidence == plan.evidence && same_neighbors
180
- raise DeviceError.new("neighbors or interface descriptions changed after planning",
181
- code: :stale_plan, host: @device.host, phase: :apply)
182
- end
183
-
184
- plan.changes.each do |change|
185
- InterfaceDescription.validate!(change.new_description)
186
- unless evidence.fetch(@strategy.interface_key(change.interface)) ==
187
- [change.neighbor.neighbor_name, change.neighbor.neighbor_interface, change.old_description]
188
- raise ArgumentError, "plan changes do not match its discovery evidence"
189
- end
190
- end
191
- end
192
-
193
- # 审批列表保留真正执行的读回命令,使旧版“先保存后读回”计划无法静默复用。
194
- def stage_scripts(changes)
195
- phases = %i[leave_configuration verification_commands persistence_commands].map do |method|
196
- @strategy.public_send(method) if @strategy.respond_to?(method)
197
- end
198
- unless phases.all? { |commands| commands.is_a?(Array) && !commands.empty? }
199
- raise UnsupportedOperation.new("topology strategy must declare separate readback and persistence stages",
200
- code: :description_stages_unsupported, host: @device.host, phase: :plan)
201
- end
202
- leave, verify, persist = phases
203
- sequences = {
204
- change: [@strategy.enter_configuration] + changes.flat_map { |change| change_commands(change) } + leave,
205
- verify: verify, persist: persist
206
- }
207
- sequences.transform_values do |commands|
208
- Script.new(commands.map { |command| @strategy.script_command(command) }, name: "interface descriptions")
209
- end
210
- end
211
-
212
- def execute_stage(script, steps)
213
- result = @device.execute_script(script)
214
- steps.concat(result.steps)
215
- result
216
- end
217
-
218
- # 保存可能已执行,超时、失败或缺少完成行均不自动重放;诊断不附带回显正文。
219
- def persistence_error(error = nil)
220
- underlying = UnderlyingError.new(error, nil, sensitive: true) if error
221
- DeviceError.new("interface descriptions were verified; persistence was not confirmed",
222
- code: :persistence_unconfirmed, host: @device.host, phase: :persist, underlying: underlying)
223
- end
224
-
225
- # 下发描述前确认厂商提供配置能力,避免把只读发现当成可写能力。
226
- def check_change_support!(phase)
227
- return if @strategy.supports?(:interface_description_changes)
228
-
229
- code = @strategy.respond_to?(:change_error_code) ? @strategy.change_error_code : :description_unsupported
230
- raise UnsupportedOperation.new("automatic interface description changes are unavailable; use a verified manual workflow",
231
- code: code, host: @device.host, phase: phase)
232
- end
233
-
234
- # 汇总邻居身份和当前描述,并要求每个目标接口都有配置证据。
235
- def evidence_for(discovered, current)
236
- interfaces = discovered.group_by { |neighbor| @strategy.interface_key(neighbor.local_interface) }
237
- if interfaces.any? { |_interface, peers| peers.size != 1 || peers.first.neighbor_name.empty? ||
238
- peers.first.neighbor_interface.empty? }
239
- raise ParsingError.new("neighbor identity is incomplete or ambiguous", code: :ambiguous_neighbor,
240
- host: @device.host, phase: :plan)
241
- end
242
- unless interfaces.keys.all? { |interface| current.key?(interface) }
243
- raise ParsingError.new("neighbor interface was not found in running configuration",
244
- code: :interface_missing, host: @device.host, phase: :plan)
245
- end
246
-
247
- interfaces.transform_values do |peers|
248
- peer = peers.fetch(0)
249
- [peer.neighbor_name, peer.neighbor_interface, current.fetch(@strategy.interface_key(peer.local_interface))].freeze
250
- end.freeze
251
- end
252
-
253
- # 设备标识来自解析证据,仍需在生成命令前验证其安全性。
254
- def change_commands(change)
255
- InterfaceDescription.validate_interface!(change.interface)
256
- @strategy.change_commands(change)
257
- end
258
- end
259
- end
260
- end
261
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Topology 同时是扩展策略的命名空间。显式装配公共定义,确保旧扩展先加载
4
- # topology/strategy 时,已有常量不会阻止工作流加载;TextFSM 仍延迟到实际解析。
5
- require_relative "operations/topology"
6
-
7
- module Net
8
- module Connector
9
- module Operations
10
- autoload :RunningConfig, File.expand_path("operations/running_config", __dir__)
11
- autoload :ParseOutput, File.expand_path("operations/parse_output", __dir__)
12
- autoload :LocalBackup, File.expand_path("operations/local_backup", __dir__)
13
- autoload :BackupLock, File.expand_path("operations/backup_lock", __dir__)
14
- autoload :TftpBackup, File.expand_path("operations/tftp_backup", __dir__)
15
- autoload :Tftp, File.expand_path("operations/tftp/strategy", __dir__)
16
- end
17
- autoload :TftpTarget, File.expand_path("operations/tftp_backup", __dir__)
18
- autoload :TftpBackup, File.expand_path("operations/tftp_backup", __dir__)
19
- autoload :TftpReceipt, File.expand_path("operations/tftp_receipt", __dir__)
20
- autoload :TftpCompletionError, File.expand_path("operations/tftp_receipt", __dir__)
21
- autoload :Backup, File.expand_path("operations/local_backup", __dir__)
22
- autoload :BackupPersistenceError, File.expand_path("operations/local_backup", __dir__)
23
- autoload :BackupBusy, File.expand_path("operations/backup_lock", __dir__)
24
- autoload :SavedConfigChanged, File.expand_path("operations/saved_config/legacy_index", __dir__)
25
- end
26
- end