net-connector 0.4.0 → 0.4.1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -65
  3. data/README.md +88 -95
  4. data/docs/VERIFICATION.md +2 -1
  5. data/docs/architecture.md +144 -248
  6. data/lib/net/connector/device/base.rb +1 -0
  7. data/lib/net/connector/device/interface_description.rb +3 -0
  8. data/lib/net/connector/device/profile/builder.rb +236 -0
  9. data/lib/net/connector/device/profile.rb +2 -227
  10. data/lib/net/connector/device/running_config/strategy.rb +5 -0
  11. data/lib/net/connector/device/running_config.rb +5 -0
  12. data/lib/net/connector/engine/dialogue.rb +1 -0
  13. data/lib/net/connector/engine/errors.rb +2 -0
  14. data/lib/net/connector/engine/log.rb +13 -9
  15. data/lib/net/connector/engine/session.rb +1 -0
  16. data/lib/net/connector/netdisco/cli.rb +20 -20
  17. data/lib/net/connector/netdisco/device.rb +4 -8
  18. data/lib/net/connector/netdisco/fleet.rb +2 -48
  19. data/lib/net/connector/netdisco/plan.rb +77 -0
  20. data/lib/net/connector/netdisco/planner.rb +12 -23
  21. data/lib/net/connector/netdisco/worker.rb +31 -24
  22. data/lib/net/connector/operations/tftp/file_upload.rb +42 -0
  23. data/lib/net/connector/operations/tftp/strategy.rb +9 -11
  24. data/lib/net/connector/operations/topology/strategy.rb +14 -0
  25. data/lib/net/connector/operations/topology.rb +1 -0
  26. data/lib/net/connector/vendor/cisco_ios/running_config.rb +1 -0
  27. data/lib/net/connector/vendor/cisco_ios/topology.rb +9 -0
  28. data/lib/net/connector/vendor/cisco_nxos/running_config.rb +1 -0
  29. data/lib/net/connector/vendor/h3c/tftp_backup.rb +2 -18
  30. data/lib/net/connector/vendor/h3c/topology.rb +9 -0
  31. data/lib/net/connector/vendor/hillstone/running_config.rb +1 -0
  32. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +4 -0
  33. data/lib/net/connector/vendor/hillstone/topology.rb +9 -0
  34. data/lib/net/connector/vendor/huawei/tftp_backup.rb +2 -18
  35. data/lib/net/connector/vendor/palo_alto/running_config.rb +4 -0
  36. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +7 -5
  37. data/lib/net/connector/vendor/palo_alto/topology.rb +10 -0
  38. data/lib/net/connector/vendor/radware/tftp_backup.rb +2 -2
  39. data/lib/net/connector/vendor/radware/topology.rb +2 -0
  40. data/lib/net/connector/version.rb +1 -1
  41. metadata +26 -24
data/docs/architecture.md CHANGED
@@ -1,148 +1,102 @@
1
- # Device operations model
2
-
3
- The public connector object represents one device session. It owns connection state,
4
- command execution, and the vendor profile. Callers continue to use
5
- `device.running_config`, `device.backup(path:)`, and `device.tftp_backup(...)`.
6
- Running configuration is a basic device capability: `device/` owns the facade,
7
- immutable profile, and shared collection flow. `Net::Connector::Operations`
8
- groups optional workflows over that device: backup, parsing, neighbor discovery,
9
- and interface description plans. Saved-file export is a local operation and
10
- does not open a device session.
11
-
12
- | Layer | Responsibility | Location |
1
+ # 设备操作架构
2
+
3
+ 一个公开连接器对象对应一台设备的一段会话,负责连接状态、命令执行和厂商档案。调用方仍使用 `device.running_config`、`device.backup(path:)`、`device.tftp_backup(...)`。运行配置是设备的基本能力,因此设备入口、不可变档案和公共采集流程放在 `device/`。`Operations` 承载备份、解析、邻居发现和接口描述计划等业务;已有文件的导出只读取本地备份,不建立设备连接。
4
+
5
+ | 层次 | 职责 | 位置 |
13
6
  | --- | --- | --- |
14
- | Device session | Login, command dialogue, script execution, logging | `engine/` |
15
- | Device | Public facade, immutable profile, collection and interface naming helpers | `device/` |
16
- | Vendor assembly | Static CLI prompts, commands, interactions, strategy bindings and session hooks | `vendor/<name>.rb` |
17
- | Configuration collection | Shared execution, completeness checks and hook adaptation | `device/running_config.rb`, `device/running_config/strategy.rb` |
18
- | Vendor collection | Configuration cleanup, view transitions and response checks | `vendor/<name>/running_config.rb` |
19
- | Interface text | Name matching, optional abbreviations, description formatting and common interface-view commands | `device/interface_name.rb`, `device/interface_description.rb` |
20
- | TextFSM parsing | Select a template and parse command output or saved configuration into records | `operations/parse_output.rb`, `templates/` |
21
- | Topology and description plan | Read CDP/LLDP neighbors and current descriptions, prepare commands, recheck evidence before confirmed execution | `operations/topology.rb` |
22
- | Local backup | Collect, compare hashes, atomically save, report change | `operations/local_backup.rb` |
23
- | Saved configuration export | Find and export an existing local backup without inventory access | `operations/saved_config.rb` |
24
- | Private file write | Atomically replace local backup and export files with mode `0600` | `operations/private_file.rb` |
25
- | TFTP backup | Validate target, run export, check transfer evidence, report result | `operations/tftp_backup.rb` |
26
- | TFTP vendor strategy | Export command, prompts, source file, success evidence, remote name | `vendor/<name>/tftp_backup.rb` |
27
- | Topology vendor strategy | Discovery commands, parsing evidence, configuration views and command exceptions | `vendor/<name>/topology.rb` |
28
- | Inventory plan | Select ready devices, apply per-vendor limits, record skip reasons | `netdisco/planner.rb` |
29
- | Batch worker | Dispatch independent device jobs and isolate callback failures | `netdisco/worker.rb` |
30
- | Batch result | Summarize outcomes and preserve report failures | `netdisco/batch.rb` |
31
- | Fleet | Load inventory, run local or TFTP tasks, write reports | `netdisco/fleet.rb` |
32
- | Settings | Read environment and YAML overrides for CLI and fleet | `netdisco/settings.rb` |
33
-
34
- ## Business contracts
35
-
36
- The connector facade owns one session. `Session` serializes login and scripts,
37
- closes the transport on failures, and never replays a device command. Each
38
- `Result` retains completed steps on failure. `RunningConfig` executes one
39
- collection script with one fresh strategy per call. Response checks, step
40
- selection and cleanup share that strategy. Selection and cleanup run through
41
- the existing facade hooks while the session lock is held; overridden methods
42
- can call `super`. The temporary binding is released even when cleanup fails,
43
- and is not available to other Fibers performing offline cleanup. Collection
44
- commands use the current session's complete prompt line, not a generic trailing
45
- `#`, `>` or `]`. PAN-OS view transitions preserve the authenticated prompt identity.
46
- A missing final prompt fails collection and leaves the previous backup untouched. Missing or blank configuration, including a response containing
47
- only a prompt or command echo, is `:incomplete_configuration`,
48
- not a successful empty backup. PAN-OS checks the candidate diff both before and
49
- after its `show` command.
50
-
51
- `LocalBackup` collects before replacing a private file and preserves the old
52
- file on failure. TFTP reports only device-side completion. Its strategies check
53
- explicit completion lines after removing command, reply and prompt echoes;
54
- filenames and future-tense progress do not establish success. Failure evidence
55
- from raw and rendered output takes precedence over completion messages. `Topology` reads
56
- neighbors and descriptions, freezes a plan, requires explicit confirmation,
57
- rechecks evidence, rebuilds the commands, and reads the configuration back.
58
- The complete recheck/write/readback sequence holds a session operation lease.
59
- Sequential scripts from its owning thread and Fiber may execute; other callers,
60
- close attempts and reentrant script callbacks receive `SessionBusy`. This lease
61
- is local to one connector instance, not a device-side or cross-process lock.
62
- `Fleet` keeps skipped, failed, successful, and saved-with-close-error outcomes
63
- distinct; callback and report errors remain visible without discarding device
64
- results. Neighbor table headers alone can establish an empty table, but unknown
65
- or partly parsed rows cannot establish a complete discovery result. Diagnostics
66
- are checked against both original output and terminal-rendered text so that
67
- color sequences cannot hide failures and carriage returns cannot erase them.
68
- Plan revalidation compares the complete discovered neighbor, including chassis
69
- ID when available, without changing the public evidence hash shape. PAN-OS
70
- configuration collection preserves multiline quoted values, but the interface
71
- description template supports single-line comments only; incomplete quoted
72
- comments raise `ParsingError` instead of becoming truncated plan evidence.
73
-
74
- ## Expect semantics and Ruby boundaries
75
-
76
- The [Tcl Expect manual](https://core.tcl-lang.org/expect/doc/trunk/expect.man)
77
- defines ordered matching, `exp_continue -continue_timer`, buffer consumption,
78
- EOF, and separate close/wait responsibilities. Its
79
- [matching loop](https://github.com/tcltk-depot/expect/blob/main/expect.c)
80
- keeps the deadline when `EXP_CONTINUE_TIMER` is returned; its
81
- [process handling](https://github.com/tcltk-depot/expect/blob/main/exp_command.c)
82
- waits for the owned child and retries interrupted waits. These are useful
83
- reference semantics; `net-connector` is a device operations library, not a Tcl
84
- interpreter or a complete Expect API port.
85
-
86
- | Concern | Connector contract | Owner |
7
+ | 设备会话 | 登录、命令交互、脚本和日志 | `engine/` |
8
+ | 设备 | 公开入口、不可变档案、配置采集和接口命名 | `device/` |
9
+ | 厂商组装 | 提示符、命令、交互、策略绑定及会话钩子 | `vendor/<厂商>.rb` |
10
+ | 公共配置采集 | 执行、完整性判断和旧钩子适配 | `device/running_config.rb`、`device/running_config/strategy.rb` |
11
+ | 厂商配置采集 | 清理文本、切换视图和检查响应 | `vendor/<厂商>/running_config.rb` |
12
+ | 接口文本 | 名称匹配、简称、描述和公共接口视图命令 | `device/interface_name.rb`、`device/interface_description.rb` |
13
+ | TextFSM 解析 | 选择模板,将命令或配置转为记录 | `operations/parse_output.rb`、`templates/` |
14
+ | 拓扑与描述计划 | 读取邻居及旧描述、生成命令、重验后下发 | `operations/topology.rb` |
15
+ | 本地备份 | 采集、比较哈希、原子保存并报告变化 | `operations/local_backup.rb` |
16
+ | 已存配置导出 | 不访问清单或设备,读取已有备份 | `operations/saved_config.rb` |
17
+ | 私有文件写入 | 以 `0600` 权限原子替换文件 | `operations/private_file.rb` |
18
+ | TFTP 备份 | 校验目标、执行导出、核对成功证据 | `operations/tftp_backup.rb` |
19
+ | TFTP 厂商策略 | 命令、提示、源文件、成功证据和目标名称 | `vendor/<厂商>/tftp_backup.rb` |
20
+ | 拓扑厂商策略 | 发现命令、解析证据、配置视图及特殊命令 | `vendor/<厂商>/topology.rb` |
21
+ | 清单规划 | 选择就绪设备、限制厂商数量、记录跳过原因 | `netdisco/planner.rb` |
22
+ | 清单计划 | 保存不可变快照,校验任务槽位、跳过原因和目标冲突 | `netdisco/plan.rb` |
23
+ | 批量执行 | 分派独立设备任务,隔离回调故障 | `netdisco/worker.rb` |
24
+ | 批量结果 | 汇总设备状态并保留报告故障 | `netdisco/batch.rb` |
25
+ | 设备集合 | 读取清单、执行本地或 TFTP 任务、写报告 | `netdisco/fleet.rb` |
26
+ | 设置 | 读取环境变量及 YAML 覆盖项 | `netdisco/settings.rb` |
27
+
28
+ ## 业务约束
29
+
30
+ 连接器独占一个会话。`Session` 串行执行登录和脚本,失败时关闭传输,不自动重放设备命令。`Result` 在后续步骤失败时仍保存已完成步骤。`RunningConfig` 每次采集创建一个新策略,同一策略负责响应检查、结果选择和清理。选择与清理在会话锁内经过现有设备钩子,子类覆盖后可调用 `super`。清理失败时也会解除临时绑定,其他 Fiber 的离线清理不能借用该策略。
31
+
32
+ 采集命令匹配当前会话的完整提示符行,不以末尾单个 `#`、`>` 或 `]` 判断完成。PAN-OS 切换视图时保留已认证的设备身份。缺少最终提示符会使采集失败,旧备份保持不变;只有提示符或命令回显的响应属于 `:incomplete_configuration`,不是成功的空配置。PAN-OS 在 `show` 前后都检查候选配置差异。
33
+
34
+ `LocalBackup` 先采集再替换私有文件。TFTP 只确认设备报告的上传结果:策略去掉命令、应答和提示符回显后,检查明确的完成行;文件名或表示“即将上传”的进度文字不算成功。原始输出或终端渲染文本中的失败证据优先于成功文字。
35
+
36
+ `Topology` 读取邻居和旧描述,冻结计划,要求显式确认,下发前重验全部证据,重建命令,并在执行后回读。重验、下发、回读期间持有会话操作租约;只有所属线程和 Fiber 能顺序执行脚本,其他调用、关闭请求和命令回调中的重入均返回 `SessionBusy`。租约只保护一个连接器实例,不是设备端或跨进程锁。
37
+
38
+ `Session` 使用 `Mutex#try_lock`,让竞争调用立即失败而不等待长时间设备命令。租约同时记录线程与 Fiber;同线程的另一个 Fiber 不能借用。`@performing` 还阻止命令回调嵌套执行脚本。可重入锁本身无法区分“租约内顺序执行脚本”和“命令回调嵌套执行脚本”;替换锁实现时仍须保留这层业务判断。
39
+
40
+ `Fleet` 区分跳过、失败、成功和保存成功但关闭失败的结果;回调和报告故障不会丢弃设备结果。邻居表头可以证明空表,但未知或部分解析的数据行不能证明完整发现。诊断同时检查原始输出与终端渲染文本,防止颜色控制符隐藏错误或回车覆盖失败信息。计划重验比较完整邻居身份,包括可用的机箱 ID,公开证据哈希结构不变。PAN-OS 配置采集保留引号内的多行文本;接口描述模板只支持单行备注,未闭合引号会返回 `ParsingError`。
41
+
42
+ `Worker` 按线程完成顺序接收终止通知,设备结果仍写入原清单槽位。任一线程中断时,调用方无需等待先创建的慢线程;创建后续线程失败时,也会停止并等待已启动的任务清理资源。普通设备故障继续转换为逐台结果,回调故障单独记录。
43
+
44
+ `Planner` 先按厂商采样,再按实际 TFTP 文件名排除覆盖冲突,保留采样顺序中的首台设备。未入选设备仍标记为 `sample_limit`,只有入选后目标重名才标记为 `remote_filename_collision`。`Plan#validate!` 校验任务与清单的对应关系;调用方传入或通过 `with` 修改的计划也必须在读取凭据、创建目录及设备 I/O 前通过校验。冲突规则适用于全部厂商,包括不同地址规范化后产生相同文件名的情况。
45
+
46
+ ## Expect 语义与 Ruby 边界
47
+
48
+ [Tcl Expect 手册](https://core.tcl-lang.org/expect/doc/trunk/expect.man)定义有序匹配、`exp_continue -continue_timer`、缓冲区消费、EOF,以及分离的关闭和等待职责。[匹配循环](https://github.com/tcltk-depot/expect/blob/main/expect.c)在继续匹配时保留截止时间,[进程处理](https://github.com/tcltk-depot/expect/blob/main/exp_command.c)负责等待子进程并重试中断。这些是参考语义;`net-connector` 是设备操作库,不实现 Tcl 解释器或完整 Expect API。
49
+
50
+ | 关注点 | 连接器约束 | 负责对象 |
87
51
  | --- | --- | --- |
88
- | Matching | Connection failures precede interactions, then the final prompt; prompt matches must consume bytes | `ResponseReader` |
89
- | Time | One monotonic deadline includes command writes and prompt replies; progress and pagination never extend it | `Session`, `ResponseReader` |
90
- | Buffers | Preserve collected output up to `max_output_bytes`; retain 32 KiB of unmatched tail while streaming larger output | `Transports::Pty`, `ResponseReader` |
91
- | EOF | Return `ConnectionClosed`, preserve earlier completed steps, close the session | `ResponseReader`, `Execution`, `Session` |
92
- | Resources | Own the child through `expect-pty#hard_close`; release log files even after setup or flush errors | `Transports::Pty`, `Log` |
93
- | Interactive use | A manual interaction ends the automated session; subsequent work creates a fresh connection | `Session` |
94
- | Concurrency | One script or multi-script operation owns a device session; callbacks cannot reenter it; independent jobs use a bounded worker pool | `Session`, `Netdisco::Worker` |
95
-
96
- Patterns are Ruby regular expressions. Keep custom prompts and interaction
97
- markers short enough to fit the unmatched tail; a pattern requiring an
98
- arbitrarily long transcript is not supported by the streaming adapter. Output
99
- collection and the matching window have different limits. The terminal renderer
100
- handles common line-editing controls, not a full screen terminal emulator.
101
- `Profile#terminal_size` uses `[width, height]`; the PTY adapter converts it to
102
- Ruby's `[rows, columns]` convention.
103
-
104
- Ruby objects retain explicit resource ownership and keyword arguments.
105
- `Profile` provides finite declarations; strategies contain vendor behavior.
106
- Private guards use direct names such as `check_block!` and
107
- `check_change_support!`. Public methods, vendor hooks, result objects, and CLI
108
- JSON fields remain stable. There is no runtime method injection or workflow DSL.
109
-
110
- ## Vendor capabilities
111
-
112
- `device.supports?(capability)` reads the profile and method-based collection
113
- commands without opening a transport or constructing a strategy. Accepted
114
- capabilities are `:running_config`, `:save_config`, `:backup`, `:tftp_backup`,
115
- `:neighbors`, `:interface_descriptions`, and
116
- `:interface_description_changes`; unknown names return `false`. This reports
117
- implementation support, not device authorization or firmware compatibility.
118
-
119
- | Vendor | Collect / local backup | Save | TFTP | Neighbors | Descriptions | Plan and apply descriptions |
52
+ | 匹配 | 先识别连接失败,再处理交互,最后匹配提示符;提示符匹配必须消费字节 | `ResponseReader` |
53
+ | 时间 | 命令写入和提示应答共用单调时钟截止时间,进度与分页不延长它 | `Session`、`ResponseReader` |
54
+ | 缓冲区 | 输出上限由 `max_output_bytes` 控制,流式匹配保留 32 KiB 未匹配尾部 | `Transports::Pty`、`ResponseReader` |
55
+ | EOF | 返回 `ConnectionClosed`,保留先前步骤,关闭会话 | `ResponseReader`、`Execution`、`Session` |
56
+ | 资源 | 通过 `expect-pty#hard_close` 管理子进程;设置或刷新失败仍释放日志文件 | `Transports::Pty`、`Log` |
57
+ | 人工交互 | 人工接管结束自动会话,后续操作需重新连接 | `Session` |
58
+ | 并发 | 单个脚本或多脚本操作独占会话,回调不能重入;独立设备由限量线程池执行 | `Session`、`Netdisco::Worker` |
59
+
60
+ 提示符和交互标记是 Ruby 正则表达式,应短到能放入未匹配尾部;流式适配器不支持需要无限长历史的模式。收集输出的上限与匹配窗口上限不同。终端渲染器只处理常见行编辑控制符,不是完整屏幕终端模拟器。`Profile#terminal_size` 使用 `[宽, 高]`,PTY 适配器转换为 Ruby 的 `[行, 列]`。
61
+
62
+ Ruby 对象显式拥有资源并使用关键字参数。`Profile` 提供有限声明入口,厂商策略负责差异行为。公开方法、厂商钩子、结果对象和 CLI JSON 字段延续现有契约;内部不做运行时方法注入,也没有工作流 DSL。
63
+
64
+ ## 厂商能力
65
+
66
+ `device.supports?(capability)` 读取档案与基于方法的采集命令,不建立传输连接或策略实例。接受 `:running_config`、`:save_config`、`:backup`、`:tftp_backup`、`:neighbors`、`:interface_descriptions`、`:interface_description_changes`;未知名称返回 `false`。它只表明实现了能力,不验证设备授权或固件兼容性。
67
+
68
+ | 厂商 | 采集及本地备份 | 保存 | TFTP | 邻居 | 描述读取 | 描述计划及下发 |
120
69
  | --- | --- | --- | --- | --- | --- | --- |
121
- | H3C | Yes | Yes | Yes | Yes | Yes | Yes |
122
- | H3C wireless | Yes | Yes | Yes | Yes | Yes | Yes |
123
- | Cisco IOS / IOS XE | Yes | Yes | Yes | Yes | Yes | Yes |
124
- | Cisco NX-OS | Yes | Yes | Yes | Yes | Yes | Yes |
125
- | Radware Alteon | Yes | Yes | Yes | No | Yes | No |
126
- | PAN-OS | Yes | No | Yes | Yes | Yes | Yes |
127
- | Huawei | Yes | Yes | Yes | No | No | No |
128
- | Hillstone | Yes | Yes | Yes | Yes | Yes | Yes |
129
-
130
- The `Profile` DSL declares static command, prompt, interaction, and strategy
131
- bindings. Configuration, TFTP and topology rules live beside their vendor under
132
- `vendor/<name>/`. The shared collection flow and operations own validation,
133
- execution and result semantics. Identical rules are reused directly: NX-OS uses
134
- IOS topology rules, H3C wireless inherits H3C, and H3C/Huawei/Radware use the common
135
- rendered configuration strategy. A directory is not a reason to copy a rule. A subclass inherits and may replace these bindings.
136
- Setting a TFTP or topology binding to `nil` disables that capability. A `nil`
137
- collection binding uses the default strategy; an empty collection command list
138
- disables collection. For example, a model variant can replace its transfer flow:
70
+ | H3C | 是 | 是 | 是 | 是 | 是 | 是 |
71
+ | H3C 无线 | 是 | 是 | 是 | 是 | 是 | 是 |
72
+ | Cisco IOS / IOS XE | 是 | 是 | 是 | 是 | 是 | 是 |
73
+ | Cisco NX-OS | 是 | 是 | 是 | 是 | 是 | 是 |
74
+ | Radware Alteon | 是 | 是 | 是 | 否 | 是 | 否 |
75
+ | PAN-OS | 是 | 否 | 是 | 是 | 是 | 是 |
76
+ | 华为 | 是 | 是 | 是 | 否 | 否 | 否 |
77
+ | 山石 | 是 | 是 | 是 | 是 | 是 | 是 |
78
+
79
+ `Profile` DSL 声明静态命令、提示符、交互和策略绑定。配置、TFTP、拓扑规则位于各自的 `vendor/<厂商>/` 下;公共业务负责校验、执行和结果语义。相同规则直接复用:NX-OS 使用 IOS 拓扑规则,H3C 无线继承 H3C,H3C、华为、Radware 使用公共终端渲染配置策略。目录存在不意味着必须复制一份规则,子类可只覆盖需要变化的绑定。
80
+
81
+ ```mermaid
82
+ flowchart LR
83
+ A[厂商 profile 声明块] --> B[Profile.define]
84
+ B --> C[Profile::Builder]
85
+ C --> D[命令、提示符、分页和策略字段]
86
+ D --> E[Builder#build]
87
+ E --> F[Profile.new 校验并冻结]
88
+ ```
89
+
90
+ `device/profile.rb` 中的 `Profile` 是不可变值对象;`device/profile/builder.rb` 保存可变声明 DSL 及字段构造器。`Profile.define` 复制父档案,运行一次厂商声明块,再由 `build` 校验并冻结新档案。子类覆盖规则不会污染父类。TFTP 或拓扑绑定设为 `nil` 会关闭对应能力;配置采集绑定为 `nil` 时使用默认策略,空采集命令列表则关闭采集。
91
+
92
+ 例如,型号变体只替换 TFTP 流程:
139
93
 
140
94
  ```ruby
141
95
  require "net/connector"
142
96
  require "net/connector/vendor/cisco_ios"
143
97
 
144
98
  class VariantTransfer < Net::Connector::CiscoIos::TftpBackup
145
- # Override only the methods needed by this model; preserve the Strategy API.
99
+ # 只覆盖该型号需要变化的方法,并遵守 Strategy 接口。
146
100
  end
147
101
 
148
102
  class VariantRouter < Net::Connector.vendor_class(:cisco_ios)
@@ -152,117 +106,59 @@ class VariantRouter < Net::Connector.vendor_class(:cisco_ios)
152
106
  end
153
107
 
154
108
  router = VariantRouter.new(host: "192.0.2.10", username: "operator")
155
- router.supports?(:tftp_backup) # => true, no connection attempted
109
+ router.supports?(:tftp_backup) # => true,不连接设备
110
+ ```
111
+
112
+ 配置采集可把 `running_config_strategy` 绑定到 `Net::Connector::RunningConfig::Strategy` 子类。策略定义清理、结果步骤、视图提示符和逐响应检查。PAN-OS 候选差异只影响配置采集;直接 `execute("show config diff")` 仍返回该命令输出。静态命令列表留在档案中。已有的 `collect_config`、`clean_config` 和受保护的 `config_result_step` 钩子仍可覆盖并调用 `super`。`Base` 负责通用脚本与锁内回调,不决定配置是否完整。
113
+
114
+ 拓扑策略通过 `topology_strategy YourStrategy` 绑定。类方法 `supports?` 声明三种拓扑能力,实例方法提供命令、模板、输出完整性和接口拼写。厂商使用自定义清单标签时,还须通过 `neighbor_template` 提供 TextFSM 模板,因为内置索引只匹配已有厂商键。确认、证据复核及回读仍由公共 `Topology` 完成。
115
+
116
+ 业务操作针对单个连接器构造,并提供 `call`。TFTP 策略只处理厂商传输差异,公共操作统一成功和失败规则。生成的文件名经过 `TftpTarget` 校验及长度限制;带作用域的 IPv6 地址会转为安全 ASCII 标记,特别长的地址使用稳定 SHA-256 标记。原本合法的文件名保留拼写,只有整体过长才缩短清单名称。TFTP 返回值表示设备报告上传完成,服务器文件核对仍由调用方负责。
117
+
118
+ TFTP 策略的类方法 `filename(host, label: nil)` 是不产生 I/O 的命名接口,默认使用 `file_extension` 声明的扩展名。Netdisco 只清理清单名称,不再维护厂商扩展名或固定文件名的分支。Radware 和山石分别声明 `tgz`、`dat`,PAN-OS 返回固定名称。旧自定义策略未实现该类方法时仍使用通用 `cfg` 名称。H3C、华为继承公共 `Tftp::FileUpload`,共用上传脚本、默认源文件名和完成证据;各自只负责取得并校验源文件。
119
+
120
+ 验收入口是 `script/ci`,也可用 `bundle exec rake release:check`。它检查源码、可用 Git 历史和 gem 内容中的敏感数据,执行 Ruby 与工作流 lint、完整测试,并在隔离 gem 目录及最小 Bundler 应用中安装。真实本地 PTY 烟测覆盖厂商加载、配置采集、打包模板和 CLI,不接触网络设备。初次下载依赖和工具需要联网,详见[验证文档](VERIFICATION.md)及[发布文档](RELEASING.md)。
121
+
122
+ ## 加载与兼容路径
123
+
124
+ `require "net/connector"` 只加载设备 API 和引擎,不预先加载厂商规则或 TextFSM。每个厂商入口只组装自身规则和公共父类;解析操作按需加载。更底层的调用方可用 `require "net/connector/engine/core"`,不加载设备定义、业务操作或厂商规则。`engine/base`、`engine/profile`、`engine` 保留为旧入口的转发路径。
125
+
126
+ 旧的 `Operations::RunningConfig`、`Operations::RunningConfig::<Vendor>`、`Operations::Tftp::<Vendor>`、`Operations::Topology::<Vendor>` 常量和 require 路径都转发到同一实现类,不维护两份逻辑。已有公开结果常量也通过 autoload 保留。新增厂商代码应直接使用厂商目录下的类。
127
+
128
+ ## 接口描述规则
129
+
130
+ 原始 `Neighbor` 字段和计划证据完整保留发现值。`InterfaceName.key` 用于匹配本机接口别名与运行配置名称;`InterfaceName.configuration` 保留配置命令的接口展开规则。`InterfaceName.short` 只决定描述里对端接口的显示形式,绝不替换本机下发命令中的接口名。
131
+
132
+ `InterfaceDescription.format(neighbor, abbreviate: true, lowercase: false)` 是各厂商默认计划共用的纯函数,生成 `To <名称> <接口>`。已知接口族会缩写,并保留原有大小写:Ethernet/Eth 为 Eth,GigabitEthernet/GE/Gi 为 Gi,Ten-GigabitEthernet/TenGigabitEthernet/XGE/Te 为 Te,FastEthernet/Fa 为 Fa,port-channel/Po 为 Po。端口编号与子接口后缀保留。`ge-0/0/1`、`100GE1/0/1`、`Port 12` 等未知形式默认不变;这是有限映射,不声称识别全部厂商命名。
133
+
134
+ 默认建议因此从 `To peer Ethernet1/2` 变为 `To peer Eth1/2`。`abbreviate: false` 保留原拼写,`lowercase: true` 仅将对端接口改为小写。自定义代码块拿到原始邻居,可生成完整描述。输出仍必须经过 80 字节及字符校验、证据复核、确认和回读。
135
+
136
+ `InterfaceDescription.commands(interface:, description:, leave: "exit")` 为 IOS/NX-OS 和山石分别生成 `interface`、`description`、退出命令;H3C 使用 `leave: "quit"`。进入配置视图和保存配置仍由厂商负责。PAN-OS 保留 `set network interface ... comment`、`commit` 及延长的提交超时。公共命令构造器不连接设备,也不能绕开已审核的拓扑计划。
137
+
138
+ ## 敏感命令的生命周期
139
+
140
+ 一个临时脱敏范围贯穿命令准备、收发、厂商后处理、用户回调和错误归一化。后续查询复用外层范围,`ensure` 在结束时清除临时秘密。敏感错误保留类型、代码、阶段和已完成步骤,但隐藏可能包含部分秘密的消息、输出及底层回溯。显式结果数据仍是原始数据。直接与流式脱敏先匹配真实秘密,包括含字面量 `[REDACTED]` 的秘密,再保留已有标记。
141
+
142
+ ## 本地备份标识与旧文件
143
+
144
+ 批量备份只用规范化管理地址命名为 `<IP>.txt`,IPv6 的 `:` 改为 `_`。清单名称仍保留在结果元数据及 TFTP 文件名中。`SavedConfig` 优先使用规范文件;缺失时只接受唯一的旧版 `<名称>-<IP>.txt`。匹配多个旧文件时明确失败,不按修改时间随意选择。
145
+
146
+ 首次成功的规范备份会与唯一旧文件的哈希比较,旧文件保持不变,并报告 `changed` 或 `unchanged`。采集失败不会创建规范文件。连接设备前先拒绝符号链接和非普通文件。旧文件存在歧义时,应保留原件,由操作人员核对后把当前正确配置放到规范路径,再恢复备份或导出。
147
+
148
+ ## 公开契约与 Rails 接入
149
+
150
+ 应用代码优先使用 `Net::Connector.open/build`、`Base` 的公开设备方法、`Command`、`Script`、结果对象,以及 `Netdisco::Fleet`。厂商扩展使用 `Profile` DSL 和文档中列出的策略接口;`Profile::Builder`、`Session` 的状态字段、工作线程调度及解析辅助方法属于内部实现。兼容 require 路径只转发到当前实现,新增扩展使用厂商目录下的类。项目仍处于 0.x,公开契约的变更会在更新记录中说明。
151
+
152
+ gem 不依赖 Rails,也不自动创建数据库模型或管理应用的连接池。`logger: Rails.logger` 由应用持有,连接器只追加设备标记,不修改或关闭它。数据库报告通过 `ResultStore::Database` 注入仓储,写报告发生在调用线程中;逐设备凭据解析、连接器工厂和回调则运行在工作线程中。
153
+
154
+ 在 Rails 内调用涉及模型、自动加载或应用状态的线程代码时,应由应用用 `Rails.application.executor.wrap` 包住相应调用。仅在外层包住 `fleet.backup_all` 不会覆盖子线程。例如,凭据解析器可以写为:
155
+
156
+ ```ruby
157
+ credentials = lambda do |device|
158
+ Rails.application.executor.wrap do
159
+ DeviceCredential.for_host(device.host).connection_settings
160
+ end
161
+ end
156
162
  ```
157
163
 
158
- For collection, bind `running_config_strategy` to a subclass of
159
- `Net::Connector::RunningConfig::Strategy`. The strategy defines cleaning, result-step
160
- selection, expected view transitions and per-response validation. PAN-OS candidate
161
- diff checks apply only to collection; a direct `execute("show config diff")` still
162
- returns the requested output. Static command lists stay in the profile. The
163
- facade's `collect_config`, `clean_config` and protected `config_result_step` hooks
164
- remain available for existing subclasses. `Base` runs generic scripts and locked
165
- callbacks; it does not decide whether a configuration is complete.
166
-
167
- For topology, bind a subclass of `Operations::Topology::Strategy` (or an
168
- existing vendor strategy) with `topology_strategy YourStrategy`. Its static
169
- `supports?` declares which of the three topology operations it implements;
170
- the instance methods provide commands, templates, output completeness, and
171
- interface spelling. A vendor using a custom inventory label must also provide
172
- a TextFSM template through `neighbor_template`, because the bundled index
173
- matches the existing vendor keys. Keep confirmation, evidence checks, and
174
- readback in `Topology`.
175
-
176
- Device operations are constructed for one connector and have a `call` method. TFTP
177
- strategies contain only device-specific transfer behavior; the operation owns
178
- the shared success and failure rules. Each vendor binds its strategies in its
179
- profile, so adding a backup operation does not add methods to every vendor
180
- connector. Generated TFTP filenames share `TftpTarget` validation and length
181
- limits. Scoped IPv6 addresses become safe ASCII filename tokens; an unusually
182
- long address uses a stable SHA-256 token. Existing valid filenames retain their
183
- spelling, and inventory labels are shortened only when the complete name would
184
- exceed the target limit.
185
-
186
- The facade keeps the existing device API while allowing batch workers and
187
- single-device callers to share the same operations. The TFTP result means the
188
- device reported upload completion; checking the server file remains the
189
- caller's responsibility.
190
-
191
- The acceptance gate is `script/ci` (also `bundle exec rake release:check`). It
192
- checks source, available Git history and gem contents for sensitive data; runs
193
- Ruby and workflow lint and the full test suite; and installs the gem into both
194
- an isolated gem home and a minimal Bundler application. A local PTY smoke checks
195
- vendor loading, configuration collection, packaged TextFSM templates and the CLI.
196
- RuboCop checks Ruby lint, security, whitespace, frozen string comments and the
197
- project's double-quoted string convention. No real device is contacted. Initial
198
- dependency and tool downloads require internet access; see
199
- [verification](VERIFICATION.md) and [release instructions](RELEASING.md).
200
-
201
- ## Loading and compatibility
202
-
203
- `require "net/connector"` loads the device API and engine, without loading vendor
204
- rules or TextFSM. Each vendor entry point assembles only its own rules and shared
205
- parents. Parsing loads when a parsing or topology workflow is used. Lower-level
206
- callers can load `net/connector/engine/core` without device definitions, business
207
- operations or vendor rules. `engine/base`, `engine/profile`, and `engine` remain
208
- forwarding entry points for existing callers.
209
-
210
- Old `Operations::RunningConfig`, `Operations::RunningConfig::<Vendor>`,
211
- `Operations::Tftp::<Vendor>` and `Operations::Topology::<Vendor>` constants and
212
- require paths forward to the same classes, rather than maintaining duplicate
213
- implementations. Existing public result constants also remain available through
214
- autoload. New vendor code uses the vendor-owned classes directly.
215
-
216
- ## Interface description policy
217
-
218
- Raw `Neighbor` fields and plan evidence retain the exact discovered values.
219
- `InterfaceName.key` matches local aliases to running configuration names;
220
- `InterfaceName.configuration` preserves the established CLI expansion rules.
221
- `InterfaceName.short` is exclusively a display policy for the remote port in a
222
- description. It never replaces the local interface used in commands.
223
-
224
- `InterfaceDescription.format(neighbor, abbreviate: true, lowercase: false)` is
225
- shared by every vendor's default plan. It retains the neighbor device name and
226
- produces `To <name> <port>`. Known families shorten as follows, preserving lower,
227
- upper or initial-capital case: Ethernet/Eth → Eth, GigabitEthernet/GE/Gi → Gi,
228
- Ten-GigabitEthernet/TenGigabitEthernet/XGE/Te → Te, FastEthernet/Fa → Fa, and
229
- port-channel/Po → Po. Port numbers and subinterface suffixes stay intact. Unknown
230
- forms such as `ge-0/0/1`, `100GE1/0/1` and `Port 12` stay unchanged by default;
231
- this is a finite mapping, not a claim to recognize all vendors or interface types.
232
-
233
- This changes the default proposal from `To peer Ethernet1/2` to
234
- `To peer Eth1/2`. Use `plan_interface_descriptions(abbreviate: false)` to keep the
235
- previous spelling, or `lowercase: true` to lowercase only the remote interface.
236
- An explicit formatter block receives the original neighbor and controls the
237
- complete description. All outputs still pass the same 80-byte and character
238
- validation, evidence recheck, confirmation and readback requirements.
239
-
240
- `InterfaceDescription.commands(interface:, description:, leave: "exit")` builds
241
- separate `interface`, `description` and exit commands for IOS/NX-OS and Hillstone;
242
- H3C supplies `leave: "quit"`. Entering configuration mode and saving remain
243
- vendor responsibilities. PAN-OS retains `set network interface ... comment`,
244
- `commit`, and its extended commit timeout. The common builder is pure: it does
245
- not send commands or bypass the reviewed topology plan.
246
-
247
- ## Sensitive command lifetime
248
-
249
- One temporary redaction scope spans preparation, exchange, vendor postprocessing,
250
- user callbacks and error normalization. Follow-up queries share the enclosing
251
- scope; `ensure` removes temporary secrets afterward. Sensitive errors preserve
252
- type, code, phase and completed steps but hide arbitrary messages, output and
253
- underlying backtraces that could contain partial secrets. Explicit result data
254
- remains raw. Direct and streaming redaction prioritize actual secrets, including
255
- secrets containing the literal `[REDACTED]`, before preserving existing markers.
256
-
257
- ## Local backup identity and legacy files
258
-
259
- Fleet backups use the normalized management IP alone (`<IP>.txt`, with IPv6 `:`
260
- replaced by `_`). Inventory names remain result metadata and TFTP labels.
261
- `SavedConfig` prefers this canonical file. If absent, it accepts exactly one
262
- legacy `<name>-<IP>.txt` file; multiple matches fail rather than choosing by mtime.
263
- The first successful canonical backup compares against the unique legacy digest,
264
- retains the legacy file, and reports `changed` or `unchanged` accordingly. Failed
265
- collection creates no canonical file. Symlinks and non-regular candidates are
266
- rejected before connecting. Existing ambiguous legacy sets need operator review:
267
- retain the originals and place the verified current configuration at the canonical
268
- path before resuming backup/export.
164
+ 这里的 `DeviceCredential` 是应用自己的模型。涉及 Rails 的工厂、回调或自定义连接器方法也遵循同一规则;Rails 的 Executor 负责应用代码的执行边界及数据库连接回收。不要在工作线程中使用 Reloader 包住整批任务。需要每台设备独立重试和调度时,可在应用的 Active Job 中调用单设备 API。具体规则见 [Rails 线程与代码执行指南](https://guides.rubyonrails.org/threading_and_code_execution.html)。
@@ -325,6 +325,7 @@ module Net
325
325
  @config_strategy_scope = previous
326
326
  end
327
327
 
328
+ # 只允许当前 Fiber 复用采集中的策略;离线调用创建独立策略。
328
329
  def config_strategy
329
330
  scope = @config_strategy_scope
330
331
  scope && scope.first.equal?(Fiber.current) ? scope.last : RunningConfig.strategy(self)
@@ -6,6 +6,7 @@ module Net
6
6
  module Connector
7
7
  # 描述文本与常见接口视图命令的纯构造方法,不连接或修改设备。
8
8
  module InterfaceDescription
9
+ # 将邻居名称和接口拼成默认描述;只缩写邻居接口,保留原始大小写。
9
10
  def self.format(neighbor, abbreviate: true, lowercase: false)
10
11
  port = if abbreviate
11
12
  InterfaceName.short(neighbor.neighbor_interface, lowercase: lowercase)
@@ -15,6 +16,7 @@ module Net
15
16
  validate!("To #{neighbor.neighbor_name} #{port}")
16
17
  end
17
18
 
19
+ # 生成进入接口、设置描述和退出视图的命令,不执行设备操作。
18
20
  def self.commands(interface:, description:, leave: "exit")
19
21
  validate_interface!(interface)
20
22
  validate!(description)
@@ -31,6 +33,7 @@ module Net
31
33
  raise ArgumentError, "description must be 1-80 bytes of plain interface text"
32
34
  end
33
35
 
36
+ # 拒绝可能把描述命令拆成多条命令的不安全接口名。
34
37
  def self.validate_interface!(value)
35
38
  return value if value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9.\/-]*\z/)
36
39