net-connector 0.9.0 → 0.10.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/docs/VERIFICATION.md +1 -1
  4. data/docs/architecture.md +18 -5
  5. data/docs/netdisco.md +54 -1
  6. data/docs/netdisco.zh-CN.md +46 -2
  7. data/docs/usage.md +24 -7
  8. data/docs/usage.zh-CN.md +22 -8
  9. data/examples/backup.yml +1 -1
  10. data/lib/net/connector/background_task.rb +69 -0
  11. data/lib/net/connector/device/base.rb +5 -11
  12. data/lib/net/connector/device/running_config.rb +5 -3
  13. data/lib/net/connector/device/topology/interface_description.rb +3 -3
  14. data/lib/net/connector/device/topology/interface_name.rb +10 -19
  15. data/lib/net/connector/device/topology/stages.rb +50 -0
  16. data/lib/net/connector/device/topology.rb +65 -24
  17. data/lib/net/connector/device_session.rb +43 -0
  18. data/lib/net/connector/engine/known_hosts.rb +31 -3
  19. data/lib/net/connector/engine/log.rb +28 -21
  20. data/lib/net/connector/engine/transport.rb +74 -32
  21. data/lib/net/connector/execution_pool.rb +109 -0
  22. data/lib/net/connector/netdisco/backup_run.rb +14 -2
  23. data/lib/net/connector/netdisco/cli/options.rb +1 -0
  24. data/lib/net/connector/netdisco/cli.rb +18 -7
  25. data/lib/net/connector/netdisco/client.rb +5 -6
  26. data/lib/net/connector/netdisco/config_file.rb +5 -2
  27. data/lib/net/connector/netdisco/database_client.rb +33 -15
  28. data/lib/net/connector/netdisco/fleet.rb +68 -36
  29. data/lib/net/connector/netdisco/inventory_cache.rb +218 -0
  30. data/lib/net/connector/netdisco/operation_batch.rb +154 -0
  31. data/lib/net/connector/netdisco/operations.rb +268 -0
  32. data/lib/net/connector/netdisco/planner.rb +26 -17
  33. data/lib/net/connector/netdisco/progress.rb +22 -10
  34. data/lib/net/connector/netdisco/settings.rb +36 -6
  35. data/lib/net/connector/netdisco/tftp_archive.rb +2 -1
  36. data/lib/net/connector/netdisco/tftp_verification.rb +36 -11
  37. data/lib/net/connector/netdisco/worker.rb +7 -46
  38. data/lib/net/connector/storage/backup_lock.rb +29 -3
  39. data/lib/net/connector/storage/safe_file.rb +30 -4
  40. data/lib/net/connector/vendor/h3c/topology.rb +1 -1
  41. data/lib/net/connector/vendor/hillstone/topology.rb +1 -1
  42. data/lib/net/connector/version.rb +1 -1
  43. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 920ff1b5e80a66f6616db8640d4f18adf5b151641ca5ffdadb26e9e5e9aeb703
4
- data.tar.gz: df797d8c9c074dff2ee9fae3c141f519f6ee03dca788ebfc7b967f8051a7aef6
3
+ metadata.gz: 9891d74f563f6d1383bf3a5a41e1eeb51e607b1395ec4794a42ed45027ce3c34
4
+ data.tar.gz: 950f56fcf3428c4a9a33c2176768f5cab3bdb4f19e649dd6f8e89547810dcea7
5
5
  SHA512:
6
- metadata.gz: c79c8f6f1bf7bb5c7588ccf6343213b34deb0acbeaee6c0be6276186937c0a563084b5ce99867d8579ab079ef7820386de0d2233a56f2ee75e6e005db9866a09
7
- data.tar.gz: d27a5f585e0a2f18ad3957648600d5504c27da66bb01625613cc3f8b9a8f8bd17ccb8159a50a459f009d2da2949602d53bb82db23bba65ffa9662c7d2acd80c4
6
+ metadata.gz: a785501bd50b61f542b9222ae24597025a20d3aa1d10fd2a042d20a7653f5fbe8ec2f6ccaf3ab6ea34db0cd11553fb918a836c34fd21dda9b4cb8b0dec83ed81
7
+ data.tar.gz: 5e1ef923eb2d603595d6225ed7cb6f08c36dbfc12c760d5b9aa69c94bbf812ef7a288586a48ff1b75947fc4886eb7dfbf830a5038bda2bfd6a43205aca18848e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # 更新记录
2
2
 
3
- ## Unreleased
3
+ ## 0.10.1 - 2026-10-11
4
+
5
+ - 设备 `open`、PTY、SSH 信任快照、日志与本地文件统一保护资源交接和关闭边界;异步取消在资源登记或回收后交付,业务等待仍可取消,收尾故障不覆盖首个操作错误。
6
+ - 修复线程回收同时发生二次取消时的循环异常原因,保留原始失败。
7
+ - 配置正文检查提前结束扫描,减少大配置的对象分配和字符串复制,保持原有命令回显与空配置判定。
8
+
9
+ ## 0.10.0 - 2026-10-11
10
+
11
+ - 新增 `sync_interface_descriptions`:一次调用、每台设备一个连接,完成邻居发现、描述修改、读回与保存;无变化时不写入或保存,后端仍支持定制格式和分阶段审批。
12
+ - 行为变更:默认接口描述改为 `To_<设备名>_<接口简称>`。H3C、Cisco、Hillstone 的聚合口及 10/25/40/100G 接口别名统一为简称,保留端口、拆分通道和子接口编号。
13
+ - 批量配置与备份共用有限并发调度,`NC_CONCURRENCY` 设置默认并发数;单台设备和回调的普通故障独立记录,报告提供清单总数、成功、失败及细分类别,中断回收本批资源,不自动重放命令。
14
+ - Netdisco 清单默认缓存三天,支持 `ND_CACHE_TTL` 和 YAML 调整、强制刷新、过期刷新失败时回退历史。缓存按来源和身份隔离,只保存白名单字段,缓存写入失败仍交付新清单。
15
+ - 修复设备连接、后台线程、PostgreSQL 连接和传输超时恢复的收尾边界:取消时释放已取得资源,后续清理故障不覆盖原始失败或已完成结果。
16
+ - TFTP 核验在目标锁内采集上传前基线;未变化的旧文件不能凭未来时间戳冒充本次上传。回调与报告使用同一核验结果,后续故障保留上传回执。
17
+ - 拓扑计划按规范接口判重,在连接或新增 I/O 前拒绝重复变更。全量清单规划减少采样副本和索引,保持设备选择顺序及 TFTP 目标仲裁。
4
18
 
5
19
  ## 0.9.0 - 2026-10-11
6
20
 
data/docs/VERIFICATION.md CHANGED
@@ -97,7 +97,7 @@ debug Logger 计数目标样本;不关闭协议检查、丢弃步骤或共享
97
97
  | --- | --- | --- |
98
98
  | 核心引擎 | `lib/net/connector/engine/**/*.rb` | 80% / 80% |
99
99
  | 脱敏与错误处理 | `lib/net/connector/engine/errors.rb`,包含 `Redactor` | 80% / 80% |
100
- | 批量并发 | `lib/net/connector/netdisco/worker.rb` | 80% / 80% |
100
+ | 批量并发 | `lib/net/connector/background_task.rb`、`execution_pool.rb`、`device_session.rb`、`netdisco/worker.rb` | 80% / 80% |
101
101
 
102
102
  关键组为空、任一关键文件未测量或组内没有可执行行时均失败。没有分支的组
103
103
  不需要分支命中,但仍须通过行覆盖率及文件加载检查。
data/docs/architecture.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 设备操作架构
2
2
 
3
- 一个连接器对应一台设备的一段会话。`Base` 组合设备能力,提供统一的命令执行和会话入口;各项能力的公开方法、流程和策略放在同一功能目录。公共层不反向依赖厂商,也不保留另一套 Operations 入口。
3
+ 一个连接器对应一台设备的一段会话。`Base` 组合设备能力,提供统一的命令执行和会话入口;各项能力的公开方法、流程和策略放在同一功能目录。设备公共层不直接加载厂商实现,也不额外包装同一套单设备业务;批次编排由 Netdisco 层调用这些公开能力。
4
4
 
5
5
  | 层次 | 职责 | 位置 |
6
6
  | --- | --- | --- |
@@ -38,6 +38,7 @@
38
38
  | `tftp_backup` | `TftpReceipt`,直接包含 server、path、时间、来源、格式及核验等级 | 参数先校验;失败/未确认直接抛出。完成后故障为 `TftpCompletionError`,保留回执 |
39
39
  | `parse_command`、`parse_config` | TextFSM 记录数组 | 命令失败、模板错误、严格 UTF-8/解析错误直接抛出,不用空数组代替未知输出 |
40
40
  | `neighbors`、`interface_descriptions` | `Neighbor` 数组、接口到描述的 Hash | 不支持、未知/部分输出或读取失败直接抛出;明确空表才允许空结果 |
41
+ | `sync_interface_descriptions` | `Result` | 显式同步请求在同一租约内完成发现、重验、修改、读回、保存;无变化不写入或保存 |
41
42
  | `plan_interface_descriptions` | 冻结的 `Topology::Plan` | 只读证据和能力检查;PAN-OS 当前拒绝自动改写计划,无隐式 commit |
42
43
  | `apply_interface_descriptions(plan, confirmed:)` | `Result`,包括变更后已完成的读回/保存步骤 | 未确认、错误设备/旧命令序列、能力不足及重验失败在写入前直接抛出;`stale_plan` 是 `DeviceError`。开始变更后的领域失败保留步骤 |
43
44
  | `SavedConfig#read/#parse/#export` | 原始字节、记录数组;export 返回目标路径或 stdout 模式的 nil | 纯本地,只读取规范地址文件;缺失、不安全文件、解析或写入错误直接抛出 |
@@ -84,7 +85,7 @@ TFTP 内置策略的 `validate_options!` 是纯参数校验,执行于 H3C 源
84
85
 
85
86
  明确完成证据先生成最小回执,再执行路径提取、元数据钩子及事件记录。已完成步骤之后的清理错误同样保留回执;路径不可用时保留 nil,不把请求文件名冒充实际目标。`TftpCompletionError` 只含受控码、类型和回执,不带原始消息、输出或 cause。Fleet 仅信任库定义的精确错误类及匹配产物;第三方异常上的 receipt 字段和错误子类不能供应完成事实。已上传但收尾失败使用 reported_with_error/partial 分类。
86
87
 
87
- `Topology` 读取邻居和旧描述,冻结计划,要求显式确认,并在写入前重验全部证据和重建的命令。`ImmediateStrategy` 将即时生效设备的修改/退出视图、运行配置读回、保存拆开:读回未确认或有未识别接口块时停止,不发送保存。重验、下发、读回、保存及阶段间隙都持有原有会话租约;只有所属线程和 Fiber 能顺序执行脚本,其他调用、关闭请求和脚本回调重入均返回 `SessionBusy`。租约不隔离其他设备会话或管理员。
88
+ `Topology` 默认通过显式 `sync_interface_descriptions` 调用在同一连接与租约内完成发现到保存,调用方无须传递计划。独立规划/应用流程仍冻结计划并要求显式确认;两条路径共用写入前证据重验和命令核对。`ImmediateStrategy` 将即时生效设备的修改/退出视图、运行配置读回、保存拆开:读回未确认或有未识别接口块时停止,不发送保存。重验、下发、读回、保存及阶段间隙都持有原有会话租约;只有所属线程和 Fiber 能顺序执行脚本,其他调用、关闭请求和脚本回调重入均返回 `SessionBusy`。租约不隔离其他设备会话或管理员。
88
89
 
89
90
  ```mermaid
90
91
  flowchart LR
@@ -298,6 +299,8 @@ TFTP 策略必须实现 `validate_options!(target, source_file:, vrf:)`、`recei
298
299
 
299
300
  ## 加载入口与厂商策略
300
301
 
302
+ 公开设备 `open` 与批量任务共用 `DeviceSession` 的关闭边界。PTY、SSH 信任快照、日志及本地文件在取得资源与登记所有权之间延后异步取消,关闭时保护回收过程;设备命令、文件读取和锁竞争仍可取消。清理保留首个故障,二次取消不能替换主错误或形成循环异常原因。
303
+
301
304
  `require "net/connector"` 加载设备 API 和引擎,厂商由 `vendor_class` 按需加载。TextFSM 能力模块可预先组合,但外部 `textfsm` gem 只在实际解析时加载。仅需会话引擎时使用 `require "net/connector/engine/core"`;离线文件操作使用 `net/connector/storage`,不加载厂商。
302
305
 
303
306
  `engine/dialogue` 只定义 Interaction、Dialogue 与 Response;读取循环位于 `engine/response_reader`。`engine/errors` 不加载脱敏运行时,`engine/redactor` 负责秘密作用域。Session 明确加载并持有读取器与脱敏器。
@@ -355,9 +358,9 @@ RunningConfig 的两处受控 `send` 保留,用于私有策略绑定与 protec
355
358
 
356
359
  原始 `Neighbor` 字段和计划证据完整保留发现值。`Topology::InterfaceName.key` 用于匹配本机接口别名与运行配置名称;`Topology::InterfaceName.configuration` 保留配置命令的接口展开规则。`Topology::InterfaceName.short` 只决定描述里对端接口的显示形式,绝不替换本机下发命令中的接口名。
357
360
 
358
- `Topology::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` 等未知形式默认不变;这是有限映射,不声称识别全部厂商命名。
361
+ `Topology::InterfaceDescription.format(neighbor, abbreviate: true, lowercase: false)` 是各厂商同步与计划共用的纯函数,生成 `To_<名称>_<接口简称>`。接口族的全称、简称和大小写统一,端口、拆分通道与子接口后缀保持不变;H3C 二层和三层聚合身份不合并。完整规则及厂商命名依据见[设备指南](usage.zh-CN.md#邻居发现与接口描述)。
359
362
 
360
- 默认建议因此从 `To peer Ethernet1/2` 变为 `To peer Eth1/2`。`abbreviate: false` 保留原拼写,`lowercase: true` 仅将对端接口改为小写。自定义代码块拿到原始邻居,可生成完整描述。输出仍必须经过 80 字节及字符校验、证据复核、确认和回读。
363
+ `abbreviate: false` 保留接口原拼写,`lowercase: true` 仅将对端接口改为小写。自定义代码块拿到原始邻居,可生成完整描述。输出仍须经过 80 字节及字符校验、证据复核和回读;同步调用表达写入意图,独立计划应用仍要求确认。
361
364
 
362
365
  `Topology::InterfaceDescription.commands(interface:, description:, leave: "exit")` 为 IOS/NX-OS 和山石分别生成 `interface`、`description`、退出命令;H3C 使用 `leave: "quit"`。进入、退出全局配置视图和保存完成证据由厂商负责。各阶段命令分别由策略声明,PAN-OS 的命令构造/超时辅助方法不会启用未经验证的候选提交。命令构造器本身不连接设备,也不能绕开已审核的拓扑计划。
363
366
 
@@ -401,7 +404,7 @@ RunningConfig 的两处受控 `send` 保留,用于私有策略绑定与 protec
401
404
 
402
405
  注入的 logger 由调用方持有;连接器不修改级别、formatter、progname,也不关闭它。每次写入同时检查配置与 logger 当前级别。消息对象支持 to_s/inspect 和安全 to_h,应用可自行输出 JSON。自有文本日志使用毫秒时间及逐行事件;raw 文件只写经过敏感保护的字节。上下文退出前先收尾渲染与脱敏缓冲,避免上一命令尾部被标成下一条命令。
403
406
 
404
- 人类进度显示由 Progress 单独管理:终端 write/flush 的 I/O 故障会停用后续显示,并只保留固定词表内的错误类型,不携带原始异常。心跳照常回收,设备任务、events.jsonl 与报告持久化继续进行;BackupRun 将显示故障合并为一次批次 callback_errors。若首次故障发生在报告保存后的最终显示阶段,仅补写一次报告诊断,最终 JSON 与退出码采用更新后的策略结果;已有报告错误保持优先,不重放设备备份或事件。
407
+ 人类进度显示由 Progress 单独管理:终端 write/flush 的普通故障会停用后续显示,并只保留固定词表内的错误类型,不携带原始异常。心跳照常回收,设备任务、events.jsonl 与报告持久化继续进行;BackupRun 将显示故障合并为一次批次 callback_errors。若首次故障发生在报告保存后的最终显示阶段,仅补写一次报告诊断,最终 JSON 与退出码采用更新后的策略结果;已有报告错误保持优先,不重放设备备份或事件。
405
408
 
406
409
  `log_file` 使用 NOFOLLOW/NONBLOCK 打开,再对同一 FD 检查普通文件类型,随后才修改权限或写入。FIFO、设备文件和目录不能用作日志文件;应用拥有的输出流通过 `logger` 接入。日志打开先于登录,不能依赖登录超时为文件打开提供期限。
407
410
 
@@ -422,3 +425,13 @@ end
422
425
  ```
423
426
 
424
427
  这里的 `DeviceCredential` 是应用自己的模型。涉及 Rails 的工厂、回调或自定义连接器方法也遵循同一规则;Rails 的 Executor 负责应用代码的执行边界及数据库连接回收。不要在工作线程中使用 Reloader 包住整批任务。需要每台设备独立重试和调度时,可在应用的 Active Job 中调用单设备 API。具体规则见 [Rails 线程与代码执行指南](https://guides.rubyonrails.org/threading_and_code_execution.html)。
428
+
429
+ ## 公共并发、配置批次与清单缓存
430
+
431
+ `ExecutionPool` 只持有线程、启动闸门和完成事件,按输入顺序交付块结果。普通设备失败如何转成结果由业务适配负责;创建并登记线程时延迟异步中断,登记完成后恢复,启动失败不执行任何设备块。工作线程捕获异常并由调用线程同步传播,避免宿主全局线程策略影响无关线程。原 `Netdisco::Worker` 保留备份计时、结果槽位和通知适配,不再自行管理线程。
432
+
433
+ `DeviceSession` 持有单台任务连接器,分别交付业务值与普通故障,收尾不覆盖先前错误或中断。`Netdisco::Operations` 为配置脚本与接口描述同步、规划/应用提供全批预检、固定策略、动态凭据和安全摘要;它保留领域 `Result`、计划和完成步骤,使用独立 `OperationBatch`,不借用备份路径或上传状态。应用审批计划必须走原拓扑重验、变更、回读、保存流程;有限并发不代表跨设备事务或自动重试安全。
434
+
435
+ `Settings#client` 为原始清单适配器包裹 `InventoryCache`,默认三天有效。缓存仅保存完整白名单 rows,Rules 和重复地址判定每次重做。身份、SQL 及绑定参数只参与摘要分区,不写入文件;凭据缺失仍在缓存读取前拒绝。过期刷新在有界、按身份的跨进程文件锁内完成,成功后原子替换;失败使用原时间的已验证历史,成功空清单替换旧值。最新查询成功与本地缓存持久化成功是两个事实,不能因写盘失败回退旧数据或重复查询。强刷跳过有效期,不跳过身份、限额、文件校验和失败回退边界。
436
+
437
+ 后台心跳和 HTTP 超时观察使用内部 `BackgroundTask` 作用域,前台保留主业务值和原始故障;观察线程的异常同步交付所有者,不受宿主全局线程异常策略影响。创建至登记期间延后中断,停止和等待期间保证线程回收;心跳普通故障转换为显示诊断,正常 HTTP 请求的未知观察故障仍传播。
data/docs/netdisco.md CHANGED
@@ -18,6 +18,8 @@ export NC_DEVICE_PASSWORD='replace-me'
18
18
 
19
19
  Use `ND_API_KEY` instead of the HTTP username/password pair when available. HTTPS retains standard certificate verification. HTTP is allowed by default for compatibility; set `netdisco.allow_insecure_http: false` to reject it before sending credentials.
20
20
 
21
+ `NC_CONCURRENCY` sets the default worker count for backups, configuration scripts and interface changes (1–50, default 4). An explicit API argument or CLI option takes precedence over the environment, then YAML.
22
+
21
23
  YAML contains only non-sensitive settings. Unknown fields, Ruby object tags, and credentials in YAML are rejected. Settings precedence is CLI, supported environment variables, YAML, then defaults. Files load only through `--config FILE` or `NC_CONFIG`. Start with [backup.yml](../examples/backup.yml):
22
24
 
23
25
  ```yaml
@@ -38,6 +40,25 @@ tftp:
38
40
  cisco_nxos: management
39
41
  ```
40
42
 
43
+ ## Inventory cache
44
+
45
+ HTTP and PostgreSQL inventory use a local cache by default, valid for three days (259,200 seconds). The default directory is `.cache/net-connector/inventory`, relative to the current working directory. Configure it through YAML:
46
+
47
+ ```yaml
48
+ netdisco:
49
+ cache_directory: /var/cache/network/inventory
50
+ cache_ttl: 259200
51
+ cache_enabled: true
52
+ ```
53
+
54
+ `ND_CACHE_TTL=86400` sets a one-day lifetime. Explicit settings overrides take precedence over the environment, then YAML, then the three-day default. TTL must be finite and positive; each batch fixes its policy snapshot before scheduling.
55
+
56
+ The directory must belong to the current user with mode `0700`; cache and lock files use `0600`. Source, account, database and SQL parameters partition the cache. Only inventory fields are stored; passwords, API keys, SQL and extra response fields are excluded. Each read applies current filtering and vendor rules. A successful empty inventory replaces history.
57
+
58
+ Expired inventory refreshes automatically. A remote inventory failure falls back to validated history without changing its original timestamp. Configuration errors, missing credentials, interruption and unsafe files remain errors. Failed cache writes preserve newly fetched inventory. `fleet.inventory_status` reports `:fresh_cache`, `:refreshed`, `:stale_cache` or `:uncached`, the original `fetched_at`, and safe failure metadata; the CLI warns on history fallback or cache write failure.
59
+
60
+ Use `net-backup --refresh-inventory`, `fleet.devices(force_refresh: true)`, or `fleet.plan_backup(force_refresh: true)` to bypass freshness. TFTP planning accepts the same keyword. A forced refresh also falls back to valid history if the remote request fails. `cache_enabled: false` disables caching. Injected clients stay uncached unless explicitly wrapped in `Netdisco::InventoryCache`; shared wrapper status describes the latest call, so synchronize concurrent access to the same instance. Cache locks bound waiting and coalesce ordinary expired-cache refreshes across processes; independently requested forced refreshes still query the source.
61
+
41
62
  ## CLI planning and execution
42
63
 
43
64
  ```sh
@@ -89,6 +110,34 @@ Local backups compare SHA-256. `on_change:` fires for created or changed artifac
89
110
 
90
111
  `ResultStore::Json` writes private JSON by default. Use `ResultStore::Database.new(repository: YourModel)` with a `create!(attributes)` repository, or `result_store: nil` to persist yourself. Custom stores receive `write(report, directory:)`. Report errors retain known committed locations and prevent success.
91
112
 
113
+ ## Configuration batches and interface changes
114
+
115
+ ```ruby
116
+ fleet = Net::Connector::Netdisco::Fleet.new
117
+ batch = fleet.execute_scripts(
118
+ scripts: { "192.0.2.1" => ["system-view", "interface GigabitEthernet1/0/1", "description uplink", "quit", "quit"] },
119
+ confirmed: true,
120
+ concurrency: 4
121
+ )
122
+ puts JSON.pretty_generate(batch.to_h)
123
+ puts "Total=#{batch.summary[:total]} succeeded=#{batch.summary[:succeeded]} failed=#{batch.summary[:failed]}"
124
+
125
+ batch = fleet.sync_interface_descriptions(hosts: ["192.0.2.1"])
126
+ puts "Total=#{batch.summary[:total]} succeeded=#{batch.summary[:succeeded]} failed=#{batch.summary[:failed]}"
127
+ ```
128
+
129
+ Scripts accept `Script`, command arrays or multiline text. The full batch is parsed and target addresses are normalized before credentials or device connections; duplicate, unknown, filtered and unsupported targets are rejected. Scripts contain the vendor's complete commands, including mode transitions and save commands where required. An empty batch reports `:no_devices` and is unsuccessful.
130
+
131
+ Interface application calls the existing topology operation, retaining evidence revalidation, configuration readback and persistence confirmation. It does not execute an approved plan as an ordinary script. Both write operations require `confirmed: true`, and never automatically retry commands. Each task owns and closes its connector. Ordinary failures remain per-device outcomes; `Interrupt` stops and joins all owned workers. Confirmed work is not rolled back across devices.
132
+
133
+ `OperationBatch` is separate from backup reports. `outcomes` retain the original `Result` or plan, completed steps and the first failure; `:partial` preserves work followed by an error, and missing credentials prevent success. `plans` selects only fully successful planning results. `to_h` includes safe diagnostics and counts, excluding commands, outputs, credentials and evidence. `on_start` and `on_result` run on workers and must be thread safe; ordinary callback faults are recorded without repeating device operations. Logging uses the configured `ssh.log_directory`.
134
+
135
+ `sync_interface_descriptions` is the direct update entry: one request and one connection per device, from discovery through readback and save. Matching descriptions cause no writes or saves; a no-op does not confirm an earlier save. The default format is `To_<peer name>_<interface abbreviation>`; formatting options and blocks remain backend choices. Separate preview/approval workflows remain available through `plan_interface_descriptions` and `apply_interface_descriptions(plans: planned.plans, confirmed: true)`.
136
+
137
+ Each device publishes one immutable outcome after execution and connection cleanup. Ordinary credential, connection, execution and cleanup failures affect that device while active and queued siblings continue. Once scheduling completes, `summary` and `to_h[:summary]` report `total`, `succeeded` and `failed`, with total = succeeded + failed. Total counts the devices requested and scheduled in this batch; partial completion and missing credentials count as failures and also appear in `partial` and `missing_credentials`. `callback_errors` counts notification faults separately without changing device outcomes. Empty batches report zero counts and `no_devices`. The library returns the report; callers choose terminal output or JSON export. Whole-batch approval validation and interruption retain their existing boundaries, and completed device changes cannot be rolled back.
138
+
139
+ For other independent tasks, load `net/connector/execution_pool` and use `Net::Connector::ExecutionPool.new(concurrency: 4).map(items) { |item| ... }`. Results preserve input order, all threads must start before any block runs, and uncaught exceptions stop the pool and propagate to its caller. Business code decides which ordinary errors become per-task results.
140
+
92
141
  ## PostgreSQL inventory
93
142
 
94
143
  Set `netdisco.source: postgres` and provide your own SELECT query. Table names and business filters are not built into the library. See [inventory_sql.yml](../examples/inventory_sql.yml):
@@ -134,7 +183,9 @@ Explicit host mappings, device rules, and vendor overrides take precedence over
134
183
 
135
184
  ## TFTP history and verification
136
185
 
137
- Direct upload receipts only establish `device_reported`. Fleet can verify a local/mounted TFTP directory via `verification_root:`; the CLI accepts `--tftp-root DIR` or YAML `tftp.verification_root`. A verified receipt requires a current, nonempty regular file matching the actual receipt path, with SHA-256 evidence. Missing evidence prevents `verified` success.
186
+ Direct upload receipts only establish `device_reported`. Fleet can verify a local/mounted TFTP directory via `verification_root:`; the CLI accepts `--tftp-root DIR` or YAML `tftp.verification_root`. A verified receipt requires a current, nonempty regular file matching the actual receipt path, with SHA-256 evidence. Missing evidence prevents `verified` success. A mismatched receipt path is retained as device evidence, but cannot be verified or archived without a baseline for that locked target.
187
+
188
+ Verification holds a target lock across the pre-upload file-state/time baseline, upload and fingerprint check, including batches without history preservation. An unchanged old file cannot qualify through a future timestamp. The server directory must be writable for lock files and the filesystem-time probe. Result callbacks receive the verified outcome, and file evidence never substitutes for a failed device operation or cleanup.
138
189
 
139
190
  Batch examples enable history preservation. With an accessible server directory, uploads use hostname/IP names and are retained in both `<batch>/tftp/` and server `archive/<batch>/`; pre-existing same-name files are archived under `previous/` before upload. File-state and filesystem-time baselines are captured under the upload lock. Verification or archival failure preserves available receipts and committed paths without reporting success.
140
191
 
@@ -163,10 +214,12 @@ Examples load the project `.env` through `dotenv` without overriding existing en
163
214
  | Variables | Purpose / default |
164
215
  | --- | --- |
165
216
  | `ND_URL` | HTTP inventory base URL |
217
+ | `ND_CACHE_TTL` | Inventory cache lifetime in seconds; finite positive number, default 259200 (three days) |
166
218
  | `ND_USERNAME`, `ND_PASSWORD` | HTTP login without API key |
167
219
  | `ND_API_KEY` | Existing API key |
168
220
  | `ND_DB_HOST`, `ND_DB_NAME`, `ND_DB_USER`, `ND_DB_PASS` | Required database connection fields |
169
221
  | `NC_CONFIG` | Non-sensitive YAML file |
222
+ | `NC_CONCURRENCY` | Default batch worker count; integer 1–50, default 4 |
170
223
  | `NC_DEVICE_USERNAME`, `NC_DEVICE_PASSWORD` | Default device credentials |
171
224
  | `NC_<VENDOR>_USERNAME`, `NC_<VENDOR>_PASSWORD` | Vendor credentials, e.g. `CISCO_IOS` |
172
225
  | `NC_ENABLE_PASSWORD`, `NC_<VENDOR>_ENABLE_PASSWORD` | Optional privilege credentials |
@@ -50,7 +50,7 @@ exit 1 unless batch.success?
50
50
 
51
51
  gem 安装后的命令为 `net-backup`;源码入口为 `bin/net-backup`,可在项目目录运行 `bundle exec bin/net-backup --help`。原命令 `net-connector-backup` 已改名。
52
52
 
53
- 命令行程序 `net-backup` 的 YAML 文件只允许非敏感设置;Netdisco 和设备凭据留在环境变量中。仅 `ND_URL` 可从环境覆盖 YAML 地址;其他非敏感策略通过 YAML 或 CLI 设置。批量示例也支持 `NC_CONFIG`,可从 [完整配置示例](../examples/backup.yml) 开始。只有传入 `--config FILE` 或设置 `NC_CONFIG` 时才加载文件:
53
+ 命令行程序 `net-backup` 的 YAML 文件只允许非敏感设置;Netdisco 和设备凭据留在环境变量中。`ND_URL` 从环境覆盖 YAML 地址,`NC_CONCURRENCY` 覆盖 YAML 并发数;其他非敏感策略通过 YAML 或 CLI 设置。批量示例也支持 `NC_CONFIG`,可从 [完整配置示例](../examples/backup.yml) 开始。只有传入 `--config FILE` 或设置 `NC_CONFIG` 时才加载文件:
54
54
 
55
55
  ```yaml
56
56
  netdisco:
@@ -136,7 +136,7 @@ CLI 的计划与批次摘要使用 JSON。默认 `--success-policy strict` 使
136
136
 
137
137
  显式使用 `--success-policy selected` 后,CLI 按上述 selected 规则决定退出码,保留严格的 `status: incomplete` 与跳过计数,另列 `policy_success`。例如 `net-backup --config config.yml --host 192.0.2.7 --success-policy selected`。成功策略由本次 CLI/API 参数指定,不改变已批准的清单选择,也不触发重试。
138
138
 
139
- 本地批量示例和 TFTP 批量示例默认全量;显式 `--sample N` 或 YAML `backup.limit_per_vendor` 才限制每厂商数量(示例允许 1 至 5)。优先级为 CLI > YAML。并发默认 4,可用 `--concurrency N` 或 YAML `backup.concurrency` 覆盖。两类示例在备份目录下创建唯一批次目录。TFTP 批量示例在可访问服务器目录时使用 hostname-ip 远端文件名并逐批归档;无法访问服务器目录时使用带批次标识的远端文件名,避免覆盖旧文件;上传完成与服务器文件验证是不同状态。
139
+ 本地批量示例和 TFTP 批量示例默认全量;显式 `--sample N` 或 YAML `backup.limit_per_vendor` 才限制每厂商数量(示例允许 1 至 5)。优先级为 CLI > YAML。并发默认 4,可用 `--concurrency N`、环境变量 `NC_CONCURRENCY` 或 YAML `backup.concurrency` 覆盖,优先级依次降低。两类示例在备份目录下创建唯一批次目录。TFTP 批量示例在可访问服务器目录时使用 hostname-ip 远端文件名并逐批归档;无法访问服务器目录时使用带批次标识的远端文件名,避免覆盖旧文件;上传完成与服务器文件验证是不同状态。
140
140
 
141
141
  全量 TFTP 计划保存为 `plan.json`。目标文件名通常为 `<设备名>-<IP>.cfg`,Radware 用 `.tgz`,山石用 `.dat`。可访问本机 TFTP 服务器目录时,批量示例直接使用 `<设备名>-<IP>.<扩展名>` 上传,并在服务器的 `archive/<批次>/` 与本地 `<批次目录>/tftp/` 各保留一份;上传前已有的同名文件先保存到新归档目录的 `previous/`。无法访问服务器目录时,远端文件名增加东八区批次标识及同秒序号,避免覆盖旧文件;实际文件名见 `summary.json` 的 `remote_path`。计划仍检查同批目标名冲突。PAN-OS 固定使用 `running-config.xml`,还要求 `Sent ... bytes` 完成行;为避免下次上传覆盖它,运行批量示例时必须能访问本机 TFTP 服务器目录,否则该设备会明确失败且不会发起上传。H3C 从 `display startup` 发现源文件,可用 YAML `tftp.h3c_source_file` 覆盖;华为默认 `flash:/startup.cfg`。Nexus 9000 默认 VRF 为 `management`,山石为 `mgt-vr`;YAML `tftp.vrfs` 接受按厂商键配置的映射。山石命令在 `vrouter` 参数后追加唯一的 `.dat` 文件名;直接调用山石连接器且不指定 `path:` 时,由设备生成文件名并在结果中返回。通过 `--tftp-root DIR` 或 YAML `tftp.verification_root` 指定本机或已挂载的服务器目录时,批量示例在每台设备完成后核验实际回执路径、非空文件、修改时间和 SHA-256,并立即保存两份历史文件;报告的 `local_file` 和 `server_archive_file` 分别指向本地批次副本和服务器归档副本。核验或归档失败不会报告成功。未提供本机目录时,可命名设备仍使用不覆盖旧文件的远端文件名,回执保留为 `device_reported`,本地批次不包含配置副本。默认示例使用 `selected` 策略;要求服务器文件核验时使用 `--success-policy verified`,并提供本机服务器目录。可运行 `ruby examples/review_tftp.rb <批次目录>`,根据会话日志复核剩余失败,而不改写原始结果。日志、`events.jsonl` 和逐台结果均以私有权限保存。
142
142
 
@@ -147,6 +147,8 @@ CLI 的计划与批次摘要使用 JSON。默认 `--success-policy strict` 使
147
147
  | 环境变量 | 默认值 | 用途 |
148
148
  | --- | --- | --- |
149
149
  | `ND_URL` | HTTP 模式必填 | Netdisco 服务根地址,可包含租户路径 |
150
+ | `NC_CONCURRENCY` | 默认 4,范围 1 至 50 | 备份、配置批次和接口修正共用的并发数 |
151
+ | `ND_CACHE_TTL` | 默认 259200 秒(三天) | 清单缓存有效期,须为有限正数,优先于 YAML `netdisco.cache_ttl` |
150
152
  | `NC_CONFIG` | 未设置 | CLI / 批量示例的非敏感 YAML 配置文件 |
151
153
  | `ND_USERNAME`, `ND_PASSWORD` | 未提供 API 密钥时必填 | 清单 API 登录 |
152
154
  | `ND_API_KEY` | 未设置 | 直接使用已有 API 密钥 |
@@ -245,3 +247,45 @@ TFTP 的 `verification` 汇总与设备条目的 `server_file_verified` 区分
245
247
  使用 `accept_new` / `replace` 时,SSH 在每个会话的临时 known_hosts 中协商,认证后通过独立锁文件合并到共享文件。
246
248
  网络登录不占用共享锁;失败登录不删除原信任记录,`replace` 只替换当前主机。该同步协调本库进程,外部手工工具应避免同时改写同一文件。
247
249
  认证后的合并锁另有最多 `login_timeout` 秒的等待预算,超时返回 `ConnectionError`(`code: :known_hosts_busy`),关闭会话且保留共享信任记录;此预算只约束锁等待,不是整批任务的期限。
250
+
251
+ 提供 `verification_root:` 时,即使不保留历史,TFTP 核验也在目标锁内采集上传前文件状态与文件系统时间、执行上传并检查指纹。未变化的旧文件不能凭未来时间戳获得服务器核验;服务器目录须可写以建立锁和时间探针。结果回调与最终报告采用核验后的结果,文件证据不覆盖设备操作或关闭故障。回执路径与锁定目标不一致时保留设备证据,但缺少该路径的上传前基线,不核验或归档旧文件。
252
+
253
+ ## 清单缓存
254
+
255
+ HTTP 和 PostgreSQL 默认启用本地清单缓存,有效期三天(259200 秒),目录为当前工作目录下的 `.cache/net-connector/inventory`。YAML `netdisco.cache_directory`、`cache_ttl` 和 `cache_enabled` 分别控制目录、秒数和是否启用;TTL 须为有限正数,设置 `cache_enabled: false` 关闭缓存。
256
+
257
+ 环境变量 `ND_CACHE_TTL=86400` 可将有效期改为一天。优先级为显式设置覆盖 > 环境变量 > YAML > 三天默认值;批次启动时固定有效期,修改环境不会改变正在执行的批次。
258
+
259
+ 目录必须属于当前用户且权限为 `0700`,缓存和锁文件为 `0600`。不同来源、地址、账号、数据库和 SQL 参数隔离缓存,只保存白名单清单字段,不保存密码、API key、SQL 或额外响应字段。每次读取仍应用当前筛选和厂商规则,远端成功返回空数组时替换旧清单。
260
+
261
+ 过期后自动刷新;远端请求失败时回退已验证历史,不更新原获取时间。配置错误、缺少凭据、中断和不安全文件不会被回退掩盖。缓存写入失败仍交付刚取得的新清单。`fleet.inventory_status` 提供 `:fresh_cache`、`:refreshed`、`:stale_cache`、`:uncached`、原始 `fetched_at` 和安全故障字段,CLI 对历史回退和缓存写入失败单独提示。
262
+
263
+ 强刷使用 `net-backup --refresh-inventory`、`fleet.devices(force_refresh: true)`、`fleet.plan_backup(force_refresh: true)`;TFTP 规划也支持此参数。强刷失败同样回退已验证历史。直接注入客户端时不自动缓存,可显式包裹 `Netdisco::InventoryCache`。一个包装器的状态描述最近调用,并发共享实例须由调用方同步。有界跨进程锁合并普通过期刷新,独立强刷请求各自访问远端。
264
+
265
+ ## 配置批次与接口修正
266
+
267
+ ```ruby
268
+ fleet = Net::Connector::Netdisco::Fleet.new
269
+ batch = fleet.execute_scripts(
270
+ scripts: { "192.0.2.1" => ["system-view", "interface GigabitEthernet1/0/1", "description uplink", "quit", "quit"] },
271
+ confirmed: true,
272
+ concurrency: 4
273
+ )
274
+ puts JSON.pretty_generate(batch.to_h)
275
+ puts "总数=#{batch.summary[:total]} 成功=#{batch.summary[:succeeded]} 失败=#{batch.summary[:failed]}"
276
+
277
+ batch = fleet.sync_interface_descriptions(hosts: ["192.0.2.1"])
278
+ puts "清单 #{batch.summary[:total]},成功 #{batch.summary[:succeeded]},失败 #{batch.summary[:failed]}"
279
+ ```
280
+
281
+ 脚本接受 `Script`、命令数组或多行文本。全批先解析并固定脚本、规范化地址,再读取凭据或连接设备;重复、未知、过滤和不支持的目标提前拒绝。脚本须包含对应厂商的完整命令,包括进入/退出配置视图及需要的保存命令。空批次为 `:no_devices`,不判为成功。
282
+
283
+ 默认使用 `sync_interface_descriptions` 一次完成发现、修改、读回与保存,每台设备只建立一个连接,描述一致时不修改、不保存。默认格式为 `To_<邻居名称>_<接口简称>`,各厂商规则见[设备指南](usage.zh-CN.md#邻居发现与接口描述);定制规则留给后端参数或代码块。无变化成功不证明先前已保存。需要单独预览审批时,后端仍可使用 `plan_interface_descriptions` 与 `apply_interface_descriptions(plans: planned.plans, confirmed: true)`。
284
+
285
+ 接口应用复用单设备拓扑操作,保留证据重验、配置回读和持久化确认,不将审批计划当作普通脚本执行。脚本与独立计划应用要求 `confirmed: true`;显式同步调用本身表达修改意图,不自动重放命令。每台任务独占并关闭连接器;普通失败隔离为设备结果,中断停止并等待全部工作线程。跨设备已完成的变更不会自动回滚。
286
+
287
+ `OperationBatch` 独立于备份报告,`outcomes` 保留原 `Result` 或计划、完成步骤和第一故障;`:partial` 表示已有工作后出错,缺少凭据也使批次不成功。`plans` 只选完整成功的规划结果。`to_h` 仅输出安全诊断和数量,不展开命令、回显、凭据或证据。`on_start` 和 `on_result` 在工作线程执行,回调须线程安全;普通回调故障记录后继续,不重复设备操作。日志使用 `ssh.log_directory`。
288
+
289
+ 每台设备独占会话,在操作和关闭完成后发布一个不可变终态。连接、凭据、执行和关闭的普通故障只影响该设备,其他运行中或排队任务继续。整批调度结束后返回完整结果;`summary` 和 `to_h[:summary]` 提供 `total`、`succeeded`、`failed`,满足总数 = 成功 + 失败,并细分 `partial`、`missing_credentials` 和 `callback_errors`。总数是本次请求并调度的设备数量;部分完成与缺少凭据计入失败,回调故障单列,不改写设备执行结果。空批次各项为零且状态为 `no_devices`。库不直接输出终端内容,调用方可将 `to_h` 导出 JSON 报告。设备上的已完成变更不能回滚;全批审批预检和显式中断仍维持原有失败边界。
290
+
291
+ 其他独立任务可加载 `net/connector/execution_pool`,通过 `Net::Connector::ExecutionPool.new(concurrency: 4).map(items) { |item| ... }` 执行。结果按输入顺序返回,全部线程建立成功才运行块,未处理异常会停止线程池并回到调用线程。业务代码负责决定哪些普通错误转换为单任务结果。显式参数优先于 `NC_CONCURRENCY` 环境变量,默认 4、上限 50。
data/docs/usage.md CHANGED
@@ -73,18 +73,35 @@ Parsing validates UTF-8 before and after terminal rendering. Invalid bytes raise
73
73
  ## Topology and interface descriptions
74
74
 
75
75
  ```ruby
76
- neighbors = device.neighbors
77
- plan = device.plan_interface_descriptions
78
- puts plan.commands.join("\n")
79
- # Review the plan and approve the exact commands before applying it.
80
- device.apply_interface_descriptions(plan, confirmed: true).value!
76
+ device.sync_interface_descriptions.value!
81
77
  ```
82
78
 
79
+ This explicit update completes neighbor discovery, description changes, readback and save in one session. Matching descriptions cause no writes or saves. Callers do not manage a plan or a second confirmation step. For a separate preview and approval workflow, use `plan_interface_descriptions`, then `apply_interface_descriptions(plan, confirmed: true)`.
80
+
83
81
  IOS/NX-OS use CDP; H3C, H3C wireless, Hillstone, and PAN-OS use LLDP. Neighbor records contain local interface, peer name/interface, chassis ID, and protocol. Unknown or partial output raises an error rather than becoming an empty table. Radware can read port names but cannot discover neighbors or apply descriptions.
84
82
 
85
- The default description is `To <peer name> <peer interface abbreviation>`. Abbreviations preserve case. Use `abbreviate: false`, `lowercase: true`, or a formatter block to customize them. Raw identities and evidence are unchanged.
83
+ The default description is `To_<peer name>_<interface abbreviation>`, for example `To_Core-SW_Gi1/0/1`. Known vendor spellings and case normalize to the same abbreviation; spaces in displayed names become underscores. Use `abbreviate: false`, `lowercase: true`, or a formatter block to customize them. Raw identities and evidence are unchanged.
84
+
85
+ Applying a separately approved plan requires `confirmed: true`. Both workflows recheck neighbor and description evidence. Changed evidence raises `:stale_plan` before writing. Supported immediate-change devices follow change, exit configuration, read back, then save. Failed/incomplete readback prevents save; unconfirmed persistence returns `:persistence_unconfirmed` with completed steps. Changes may already be active, so commands are never replayed or rolled back automatically.
86
+
87
+ Known names use the following shared rules; port numbers, breakout lanes and subinterfaces remain intact. Unknown prefixes retain their spelling.
88
+
89
+ | Interface family | Vendor names / aliases | Description prefix |
90
+ | --- | --- | --- |
91
+ | Ethernet | `Ethernet`, `ethernet` | `Eth` |
92
+ | 1G | `GigabitEthernet`, `GE`, `Gi` | `Gi` |
93
+ | 10G | `Ten-GigabitEthernet`, `TenGigabitEthernet`, `TenGigE`, `XGE`, Hillstone `xethernet` / `xe` | `Te` |
94
+ | 25G | H3C `Twenty-FiveGigE` / `WGE`, Cisco `TwentyFiveGigE`, `25GE` | `Twe` |
95
+ | 40G | `FortyGigE`, `FortyGigabitEthernet`, `FGE`, `40GE` | `Fo` |
96
+ | 100G | `HundredGigE`, `HundredGigabitEthernet`, `HGE`, `100GE` | `Hu` |
97
+ | H3C Layer 2 aggregation | `Bridge-Aggregation`, `BAGG` | `BAGG` |
98
+ | H3C Layer 3 aggregation | `Route-Aggregation`, `RAGG` | `RAGG` |
99
+ | Cisco aggregation | `Port-channel`, `Po` | `Po` |
100
+ | Hillstone aggregation | `aggregate`, `Agg` | `Agg` |
101
+
102
+ Naming references: [H3C CLI interface names](https://www.h3c.com/en/d_202307/1892442_294551_0.htm), [Cisco interface commands](https://www.cisco.com/c/en/us/td/docs/switches/lan/catalyst9600/software/release/17-13/command_reference/b_1713_9600_cr/interface_and_hardware_commands.html), and [Hillstone interface examples](https://kb.hillstonenet.com/en/wp-content/uploads/2016/05/Hillstone-All-Series-Device-Troubleshooting-and-Debug-Guide.pdf). These display rules do not establish firmware support. H3C Layer 2 and Layer 3 aggregate identities remain distinct.
86
103
 
87
- Apply requires `confirmed: true` and rechecks neighbor and description evidence. Changed evidence raises `:stale_plan` before writing. Supported immediate-change devices follow change, exit configuration, read back, then save. Failed/incomplete readback prevents save; unconfirmed persistence returns `:persistence_unconfirmed` with completed steps. Changes may already be active, so commands are never replayed or rolled back automatically.
104
+ An approval may contain only one change per canonical interface. Duplicate interfaces, including abbreviated and full names for the same interface, are rejected before additional device I/O. Fleet application performs the same check before opening any device session.
88
105
 
89
106
  PAN-OS supports read-only discovery and descriptions. Automatic changes are disabled with `:candidate_isolation_unavailable`; collection checks candidate differences before and after export and requires set format. See [topology contracts](architecture.md#业务约束) (Chinese) before extending these flows.
90
107
 
data/docs/usage.zh-CN.md CHANGED
@@ -111,18 +111,30 @@ rows = saved.parse(host: "192.0.2.10", template: "/path/to/template.textfsm")
111
111
  ```ruby
112
112
  Net::Connector.open(:h3c, host: "192.0.2.10", username: ENV.fetch("NC_DEVICE_USERNAME"),
113
113
  password: ENV.fetch("NC_DEVICE_PASSWORD")) do |device|
114
- plan = device.plan_interface_descriptions
115
- plan.changes.each { |change| puts "#{change.interface}: #{change.old_description.inspect} -> #{change.new_description.inspect}" }
116
- puts plan.commands.join("\n")
117
- # 核对将要下发的命令,并完成操作审批。
118
- result = device.apply_interface_descriptions(plan, confirmed: true)
119
- result.value!
114
+ device.sync_interface_descriptions.value!
120
115
  end
121
116
  ```
122
117
 
123
- 默认描述为 `To <邻居名称> <邻居接口简称>`。接口缩写默认开启并保留大小写:`ethernet1/1` 变为 `eth1/1`,`Ethernet1/1` 变为 `Eth1/1`,`GigabitEthernet1/0/1` 变为 `Gi1/0/1`;未知形式保持原样。原始邻居记录、计划证据和本机下发接口名不会被缩写。`abbreviate: false` 关闭缩写,`lowercase: true` 才会转为小写;也可传入代码块自行生成完整描述。公共纯函数是 `Net::Connector::Topology::InterfaceDescription.format`。
118
+ 显式调用 `sync_interface_descriptions`,在同一连接中完成发现、修改、读回和保存,不需要处理计划或二次确认;已有描述一致时不修改、不保存。需要单独预览审批时,后端仍可使用 `plan_interface_descriptions` 与 `apply_interface_descriptions(plan, confirmed: true)`。
124
119
 
125
- 规划会拒绝歧义邻居、缺少对端身份、不安全文本和无法识别的输出。下发必须传 `confirmed: true`,操作会再次读取邻居和旧描述;证据变化在写入前抛出 `DeviceError`,其 `code` 为 `:stale_plan`。IOS/NX-OS、H3C 和山石采用“修改 → 退出配置视图 → 读回 → 保存”的顺序;读回不匹配返回 `:description_unconfirmed`,解析不完整或查询命令不符合审批时也不会保存。
120
+ 默认描述为 `To_<邻居名称>_<接口简称>`,例如 `To_Core-SW_Gi1/0/1`。同类接口的厂商全称、简称和大小写统一,显示文本中的空格替换为下划线。端口、拆分通道与子接口编号完整保留;原始邻居记录和本机下发接口拼写不改变。`abbreviate: false` 保留对端接口全称,`lowercase: true` 仅将对端接口简称转为小写;后端也可用代码块定制。公共纯函数是 `Net::Connector::Topology::InterfaceDescription.format`。
121
+
122
+ | 接口类型 | 厂商名称与别名 | 描述简称 |
123
+ | --- | --- | --- |
124
+ | Ethernet | `Ethernet`、`ethernet` | `Eth` |
125
+ | 1G | `GigabitEthernet`、`GE`、`Gi` | `Gi` |
126
+ | 10G | `Ten-GigabitEthernet`、`TenGigabitEthernet`、`TenGigE`、`XGE`、山石 `xethernet` / `xe` | `Te` |
127
+ | 25G | H3C `Twenty-FiveGigE` / `WGE`、Cisco `TwentyFiveGigE`、`25GE` | `Twe` |
128
+ | 40G | `FortyGigE`、`FortyGigabitEthernet`、`FGE`、`40GE` | `Fo` |
129
+ | 100G | `HundredGigE`、`HundredGigabitEthernet`、`HGE`、`100GE` | `Hu` |
130
+ | H3C 二层聚合 | `Bridge-Aggregation`、`BAGG` | `BAGG` |
131
+ | H3C 三层聚合 | `Route-Aggregation`、`RAGG` | `RAGG` |
132
+ | Cisco 聚合 | `Port-channel`、`Po` | `Po` |
133
+ | 山石聚合 | `aggregate`、`Agg` | `Agg` |
134
+
135
+ 命名依据:[H3C 接口名称](https://www.h3c.com/en/d_202307/1892442_294551_0.htm)、[Cisco 接口命令](https://www.cisco.com/c/en/us/td/docs/switches/lan/catalyst9600/software/release/17-13/command_reference/b_1713_9600_cr/interface_and_hardware_commands.html)、[山石接口示例](https://kb.hillstonenet.com/en/wp-content/uploads/2016/05/Hillstone-All-Series-Device-Troubleshooting-and-Debug-Guide.pdf)。未知前缀保持原样,不猜测速率。二层与三层聚合身份保持区分;命名规则不代表设备固件已经现场验证。
136
+
137
+ 规划会拒绝歧义邻居、缺少对端身份、不安全文本和无法识别的输出。独立计划应用须传 `confirmed: true`;同步和计划应用都会再次读取邻居和旧描述;证据变化在写入前抛出 `DeviceError`,其 `code` 为 `:stale_plan`。IOS/NX-OS、H3C 和山石采用“修改 → 退出配置视图 → 读回 → 保存”的顺序;读回不匹配返回 `:description_unconfirmed`,解析不完整或查询命令不符合审批时也不会保存。
126
138
 
127
139
  `plan.commands` 现在包括读回命令,旧版计划必须重新生成、审核;执行仍返回 `Result`,保留修改、读回及保存阶段已经完成的步骤。只有设备提供明确保存完成行才成功,保存超时、失败或只有提示符会返回 `:persistence_unconfirmed`,此时描述可能已生效,不能自动重放或回滚。现场下发前仍应按设备固件核对命令与回显。
128
140
 
@@ -130,6 +142,8 @@ PAN-OS 的自动描述计划及改写暂不提供,`supports?(:interface_descri
130
142
 
131
143
  PAN-OS 在导出前后检查候选配置差异,并拒绝 XML 或非 set 格式输出,避免将未提交配置误作运行配置。
132
144
 
145
+ 同一审批计划中的规范接口只能出现一次;接口缩写与全称也按同一接口判重。在新增设备 I/O 前拒绝重复审批;Fleet 应用整批计划时,在建立任一设备会话前执行相同预检。
146
+
133
147
  ## 命令脚本与自动交互
134
148
 
135
149
  ```ruby
data/examples/backup.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  # 仅保存非敏感策略,凭据使用环境变量。
2
- # ND_URL 可覆盖 netdisco.url;其他公开策略由 YAML 或 CLI 设置。
2
+ # ND_URL、ND_CACHE_TTL、NC_CONCURRENCY 可覆盖对应策略;其余由 YAML 或 CLI 设置。
3
3
  backup:
4
4
  directory: ./examples/backups
5
5
  concurrency: 4
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Connector
5
+ # 后台观察线程属于调用作用域;停止、等待和异常交付不能盖掉前台原故障。
6
+ module BackgroundTask
7
+ # task 自行等待停止信号,stop 只负责唤醒;正常前台也必须观察后台失败。
8
+ def self.run(task:, stop:)
9
+ unless task.respond_to?(:call) && stop.respond_to?(:call) && block_given?
10
+ raise ArgumentError, "background task requires task, stop and a foreground block"
11
+ end
12
+
13
+ thread = value = failure = nil
14
+ completion = Queue.new
15
+ begin
16
+ # 创建与登记之间不交付异步中断;子线程恢复即时交付,捕获后同步传回所有者。
17
+ Thread.handle_interrupt(Exception => :never) do
18
+ thread = Thread.new { observe(task, completion) }
19
+ end
20
+ value = yield
21
+ rescue Exception => error # rubocop:disable Lint/RescueException -- 收尾后向所有者重新抛出原故障,包括中断。
22
+ failure = error
23
+ ensure
24
+ begin
25
+ # 二次中断不能跳过 join;恢复交付时也保留已经发生的前台故障。
26
+ Thread.handle_interrupt(Exception => :never) do
27
+ cleanup_error = finish(thread, stop) if thread
28
+ failure ||= cleanup_error
29
+ end
30
+ rescue Exception => error # rubocop:disable Lint/RescueException -- 保留前台故障,观察延迟到达的取消。
31
+ failure ||= error
32
+ end
33
+ # break 和非局部 return 也会展开作用域;异常交付必须在 ensure 内,不能绕过。
34
+ failure ||= completion.pop unless completion.empty?
35
+ raise failure if failure
36
+ end
37
+ value
38
+ end
39
+
40
+ def self.observe(task, completion)
41
+ failure = nil
42
+ Thread.handle_interrupt(Exception => :immediate) { task.call }
43
+ rescue Exception => error # rubocop:disable Lint/RescueException -- 不让 global abort_on_exception 将子线程故障传播到无关主线程。
44
+ failure = error
45
+ ensure
46
+ completion << failure
47
+ end
48
+ private_class_method :observe
49
+
50
+ def self.finish(thread, stop)
51
+ failure = nil
52
+ begin
53
+ stop.call
54
+ rescue Exception => error # rubocop:disable Lint/RescueException -- 唤醒失败时仍必须终止并回收自有线程。
55
+ failure = error
56
+ thread.kill
57
+ ensure
58
+ begin
59
+ thread.join
60
+ rescue Exception => error # rubocop:disable Lint/RescueException -- 等待失败也不能覆盖先前故障。
61
+ failure ||= error
62
+ end
63
+ end
64
+ failure
65
+ end
66
+ private_class_method :finish
67
+ end
68
+ end
69
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
+ require_relative "../device_session"
4
5
  require_relative "../engine/core"
5
6
  require_relative "profile"
6
7
  require_relative "running_config"
@@ -62,20 +63,13 @@ module Net
62
63
  def self.open(**)
63
64
  raise ArgumentError, "open requires a block" unless block_given?
64
65
 
65
- device = new(**)
66
- begin
66
+ value, failure = DeviceSession.call(factory: -> { new(**) }) do |device|
67
67
  device.connect
68
68
  yield device
69
- rescue Exception # rubocop:disable Lint/RescueException -- 本次连接或块失败时仍关闭设备,并保留原始故障或中断。
70
- failed = true
71
- raise
72
- ensure
73
- begin
74
- device.close
75
- rescue Exception # rubocop:disable Lint/RescueException -- 只保留本次作用域的原始异常,不受外层已处理异常影响。
76
- raise unless failed
77
- end
78
69
  end
70
+ raise failure if failure
71
+
72
+ value
79
73
  end
80
74
 
81
75
  # 校验连接配置,创建对话语法、传输层、恢复器和会话对象。
@@ -103,9 +103,11 @@ module Net
103
103
  # 排除命令回显和提示符,确认响应确实包含配置正文。
104
104
  def config_body?(step)
105
105
  body = TerminalRenderer.render(step.output.delete_suffix(step.prompt.to_s)).strip
106
- lines = body.lines
107
- lines.shift if lines.first&.strip == step.command.text
108
- !lines.join.strip.empty?
106
+ # 只需确认首个正文行;不为大配置创建全部行对象或重新拼接整份响应。
107
+ body.each_line.with_index.any? do |line, index|
108
+ text = line.strip
109
+ !text.empty? && !(index.zero? && text == step.command.text)
110
+ end
109
111
  end
110
112
 
111
113
  # 将缺少配置的情况统一映射为可识别的设备错误码。
@@ -7,14 +7,14 @@ module Net
7
7
  class Topology
8
8
  # 描述文本与常见接口视图命令的纯构造方法,不连接或修改设备。
9
9
  module InterfaceDescription
10
- # 将邻居名称和接口拼成默认描述;只缩写邻居接口,保留原始大小写。
10
+ # 将邻居名称和接口拼成默认描述;统一邻居接口简称,原始发现记录保持不变。
11
11
  def self.format(neighbor, abbreviate: true, lowercase: false)
12
12
  port = if abbreviate
13
13
  InterfaceName.short(neighbor.neighbor_interface, lowercase: lowercase)
14
14
  else
15
15
  lowercase ? neighbor.neighbor_interface.downcase : neighbor.neighbor_interface
16
16
  end
17
- validate!("To #{neighbor.neighbor_name} #{port}")
17
+ validate!("To_#{neighbor.neighbor_name.tr(" ", "_")}_#{port.tr(" ", "_")}")
18
18
  end
19
19
 
20
20
  # 生成进入接口、设置描述和退出视图的命令,不执行设备操作。
@@ -36,7 +36,7 @@ module Net
36
36
 
37
37
  # 拒绝可能把描述命令拆成多条命令的不安全接口名。
38
38
  def self.validate_interface!(value)
39
- return value if value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9.\/-]*\z/)
39
+ return value if value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9.:\/-]*\z/)
40
40
 
41
41
  raise ArgumentError, "neighbor interface name is unsafe"
42
42
  end
@@ -6,39 +6,30 @@ module Net
6
6
  # 接口身份匹配与描述展示共用名称规则;展示简称不能直接用作下发命令。
7
7
  module InterfaceName
8
8
  RULES = [
9
- [/\A(?:xge|ten-gigabitethernet|tengigabitethernet|te)(?=\d)/i, "tengigabitethernet", "Te"],
9
+ [/\A(?:bridge-aggregation|bagg)(?=\d)/i, "bridge-aggregation", "BAGG"],
10
+ [/\A(?:route-aggregation|ragg)(?=\d)/i, "route-aggregation", "RAGG"],
11
+ [/\A(?:aggregate|agg)(?=\d)/i, "aggregate", "Agg"],
12
+ [/\A(?:hundredgigabitethernet|hundredgige|hge|100ge|hu)(?=\d)/i, "hundredgigabitethernet", "Hu"],
13
+ [/\A(?:fortygigabitethernet|fortygige|fge|40ge|fo)(?=\d)/i, "fortygigabitethernet", "Fo"],
14
+ [/\A(?:twenty-?fivegigabitethernet|twenty-?fivegige|wge|25ge|twe)(?=\d)/i, "twentyfivegigabitethernet", "Twe"],
15
+ [/\A(?:xge|10ge|xgigabitethernet|ten-gigabitethernet|tengigabitethernet|ten-?gige|xethernet|xe|te)(?=\d)/i, "tengigabitethernet", "Te"],
10
16
  [/\A(?:gigabitethernet|ge|gi)(?=\d)/i, "gigabitethernet", "Gi"],
11
17
  [/\A(?:fastethernet|fa)(?=\d)/i, "fastethernet", "Fa"],
12
18
  [/\A(?:ethernet|eth)(?=\d)/i, "ethernet", "Eth"],
13
19
  [/\A(?:port-channel|po)(?=\d)/i, "port-channel", "Po"]
14
20
  ].map(&:freeze).freeze
15
21
 
16
- # 用于本机邻居表与配置的关联;保留已有证据键的拼写契约。
22
+ # 用于本机邻居表与配置的关联;同类别名归一,不混淆速率、聚合类型或端口编号。
17
23
  def self.key(name)
18
24
  rule = RULES.find { |pattern, _key, _short| pattern.match?(name) }
19
25
  rule ? name.sub(rule.first, rule[1]).downcase : name.downcase
20
26
  end
21
27
 
22
28
  # 仅缩短已识别的前缀,保留端口、子接口编号及未知命名。
23
- # 默认延续输入的大写、小写或首字母大写形式;统一小写由调用方选择。
29
+ # 厂商全称、缩写与大小写统一展示;原始身份和本机下发拼写不参与替换。
24
30
  def self.short(name, lowercase: false)
25
31
  rule = RULES.find { |pattern, _key, _short| pattern.match?(name) }
26
- formatted = if rule
27
- name.sub(rule.first) do |prefix|
28
- abbreviation = rule.last
29
- if prefix == prefix.downcase
30
- abbreviation.downcase
31
- elsif prefix == prefix.upcase
32
- abbreviation.upcase
33
- elsif prefix.casecmp?(abbreviation)
34
- prefix
35
- else
36
- abbreviation
37
- end
38
- end
39
- else
40
- name.dup
41
- end
32
+ formatted = rule ? name.sub(rule.first, rule.last) : name.dup
42
33
  lowercase ? formatted.downcase : formatted
43
34
  end
44
35