net-connector 0.8.0 → 0.9.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/CONTRIBUTING.md +12 -0
  4. data/README.md +26 -425
  5. data/README.zh-CN.md +64 -0
  6. data/bin/net-backup +1 -0
  7. data/docs/VERIFICATION.md +2 -2
  8. data/docs/architecture.md +9 -4
  9. data/docs/netdisco.md +176 -0
  10. data/docs/netdisco.zh-CN.md +247 -0
  11. data/docs/usage.md +124 -0
  12. data/docs/usage.zh-CN.md +190 -0
  13. data/examples/backup.yml +5 -4
  14. data/examples/inventory_sql.yml +5 -1
  15. data/lib/net/connector/device/base.rb +5 -4
  16. data/lib/net/connector/device/local_backup.rb +3 -0
  17. data/lib/net/connector/device/running_config/strategy.rb +2 -1
  18. data/lib/net/connector/device/running_config.rb +2 -0
  19. data/lib/net/connector/device/tftp/receipt.rb +1 -0
  20. data/lib/net/connector/device/tftp/strategy.rb +28 -9
  21. data/lib/net/connector/device/tftp.rb +9 -8
  22. data/lib/net/connector/device/topology/immediate_strategy.rb +2 -1
  23. data/lib/net/connector/engine/core.rb +1 -0
  24. data/lib/net/connector/engine/dialogue.rb +5 -2
  25. data/lib/net/connector/engine/known_hosts.rb +20 -4
  26. data/lib/net/connector/engine/log.rb +14 -9
  27. data/lib/net/connector/engine/recovery.rb +1 -1
  28. data/lib/net/connector/engine/session.rb +14 -5
  29. data/lib/net/connector/engine/transport.rb +21 -10
  30. data/lib/net/connector/netdisco/backup_run.rb +13 -5
  31. data/lib/net/connector/netdisco/cli/options.rb +17 -8
  32. data/lib/net/connector/netdisco/cli.rb +3 -3
  33. data/lib/net/connector/netdisco/config_file.rb +19 -14
  34. data/lib/net/connector/netdisco/database_client.rb +40 -24
  35. data/lib/net/connector/netdisco/device.rb +3 -0
  36. data/lib/net/connector/netdisco/diagnostic.rb +4 -0
  37. data/lib/net/connector/netdisco/fleet.rb +2 -2
  38. data/lib/net/connector/netdisco/plan.rb +12 -3
  39. data/lib/net/connector/netdisco/progress.rb +9 -0
  40. data/lib/net/connector/netdisco/report/files.rb +1 -0
  41. data/lib/net/connector/netdisco/result_store.rb +1 -0
  42. data/lib/net/connector/netdisco/rules.rb +7 -5
  43. data/lib/net/connector/netdisco/settings.rb +77 -19
  44. data/lib/net/connector/netdisco/tftp_archive.rb +4 -0
  45. data/lib/net/connector/netdisco/worker.rb +1 -1
  46. data/lib/net/connector/storage/private_file.rb +6 -0
  47. data/lib/net/connector/vendor/cisco_ios/running_config.rb +1 -0
  48. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +2 -0
  49. data/lib/net/connector/vendor/cisco_ios/topology.rb +2 -0
  50. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +2 -0
  51. data/lib/net/connector/vendor/h3c/tftp_backup.rb +1 -0
  52. data/lib/net/connector/vendor/h3c/topology.rb +2 -0
  53. data/lib/net/connector/vendor/hillstone/running_config.rb +1 -0
  54. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +2 -0
  55. data/lib/net/connector/vendor/hillstone/topology.rb +2 -0
  56. data/lib/net/connector/vendor/huawei/tftp_backup.rb +2 -1
  57. data/lib/net/connector/vendor/palo_alto/running_config.rb +1 -0
  58. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +2 -0
  59. data/lib/net/connector/vendor/palo_alto/topology.rb +16 -16
  60. data/lib/net/connector/vendor/radware/running_config.rb +1 -0
  61. data/lib/net/connector/vendor/radware/tftp_backup.rb +2 -0
  62. data/lib/net/connector/vendor/radware/topology.rb +1 -0
  63. data/lib/net/connector/version.rb +2 -1
  64. data/lib/net/connector.rb +1 -0
  65. data/lib/net-connector.rb +1 -0
  66. metadata +7 -16
data/README.zh-CN.md ADDED
@@ -0,0 +1,64 @@
1
+ # net-connector
2
+
3
+ [English](README.md) | [简体中文](README.zh-CN.md)
4
+
5
+ 通过 SSH 或 Telnet 操作网络设备的 Ruby 库。支持配置采集、私有备份、交互脚本、TextFSM 解析,以及基于 Netdisco 清单的批量备份。
6
+
7
+ 需要 Ruby 3.2+ 和 POSIX 系统。SSH 使用本机 OpenSSH;Telnet 需要本机安装 `telnet` 并显式配置。
8
+
9
+ ## 安装
10
+
11
+ ```sh
12
+ gem install net-connector
13
+ ```
14
+
15
+ ## 快速开始
16
+
17
+ 先创建并保护备份目录、登记设备 SSH 主机密钥,再通过环境变量提供凭据。
18
+
19
+ ```ruby
20
+ require "net/connector"
21
+
22
+ Net::Connector.open(:cisco_ios,
23
+ host: "192.0.2.10", username: ENV.fetch("NC_DEVICE_USERNAME"),
24
+ password: ENV.fetch("NC_DEVICE_PASSWORD")) do |device|
25
+ backup = device.backup(path: "/var/backups/network/router.cfg")
26
+ puts "#{backup.bytes} bytes, SHA-256 #{backup.sha256}"
27
+ end
28
+ ```
29
+
30
+ 代码块退出时关闭会话。备份通过私有文件原子替换,采集失败保留旧备份,设备命令不会自动重放。返回的配置和命令输出仍是敏感数据。
31
+
32
+ ## 支持的设备
33
+
34
+ H3C Comware 及无线控制器、Cisco IOS/IOS XE 与 NX-OS、华为、山石 StoneOS、Radware Alteon 和 Palo Alto PAN-OS。各厂商能力不同,详见[设备操作指南](docs/usage.zh-CN.md#支持的设备)。合成测试不代表所有固件均已现场验证。
35
+
36
+ ## 批量备份
37
+
38
+ ```sh
39
+ net-backup --config config.yml --show-config
40
+ net-backup --config config.yml --plan
41
+ net-backup --config config.yml
42
+ ```
43
+
44
+ 从 [backup.yml](examples/backup.yml) 或 [inventory_sql.yml](examples/inventory_sql.yml) 开始配置。清单与设备凭据放在环境变量中,YAML 只保存非敏感设置。HTTP/PostgreSQL 清单、成功策略、TFTP 核验和批量示例见 [Netdisco 指南](docs/netdisco.zh-CN.md)。
45
+
46
+ ## 文档
47
+
48
+ | 主题 | English | 简体中文 |
49
+ | --- | --- | --- |
50
+ | 设备操作、解析、拓扑与日志 | [Device guide](docs/usage.md) | [设备操作](docs/usage.zh-CN.md) |
51
+ | 清单、配置、CLI 与批量备份 | [Netdisco guide](docs/netdisco.md) | [清单与批量备份](docs/netdisco.zh-CN.md) |
52
+
53
+ 详细参考:[架构](docs/architecture.md)、[验证](docs/VERIFICATION.md)、[发布](docs/RELEASING.md)和[示例](examples/README.md)。
54
+
55
+ ## 开发
56
+
57
+ ```sh
58
+ bundle install
59
+ script/ci
60
+ ```
61
+
62
+ 门禁执行敏感数据扫描、Ruby 与工作流检查、测试、本地 PTY 基准及隔离安装验证。参与开发见 [CONTRIBUTING](CONTRIBUTING.md),漏洞报告见 [SECURITY](SECURITY.md),版本变化见[更新记录](CHANGELOG.md)。问题和 PR 可使用中文或英文。
63
+
64
+ 采用 [MIT](LICENSE) 许可。
data/bin/net-backup CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # 安装后的命令行入口;退出码由批量 CLI 决定,加载时不直接连接设备。
4
5
  require "net/connector/netdisco"
5
6
 
6
7
  exit Net::Connector::Netdisco::CLI.new.run
data/docs/VERIFICATION.md CHANGED
@@ -176,8 +176,8 @@ BUNDLE_GEMFILE=gemfiles/minimum.gemfile bundle exec rake test package:verify
176
176
  当前这两个关键依赖的正常解析与下限恰好相同,报告仍记录实际版本,供以后比较。
177
177
  开发锁文件均忽略;gemspec 保持兼容范围,基准、夹具、兼容 Gemfile 和审阅报告均不进 gem。
178
178
 
179
- gem 只收录库代码、TextFSM 模板、CLI、架构/验证/发布文档、README、LICENSE、
180
- CHANGELOG、SECURITY 和 CONTRIBUTING。测试、示例、发布工具、工作流、本地评审快照、配置和备份不进入包。
179
+ gem 只收录库代码、TextFSM 模板、CLI、双语设备/Netdisco 指南、架构/验证/发布文档、双语 README、LICENSE、
180
+ CHANGELOG、SECURITY 和 CONTRIBUTING。仅两个非敏感 YAML 示例随包分发;测试、示例脚本、发布工具、工作流、本地评审快照、本地配置和备份不进入包。
181
181
  更改 gemspec 后,实际归档仍须通过独立的文件白名单检查。
182
182
 
183
183
  ## 敏感数据
data/docs/architecture.md CHANGED
@@ -48,7 +48,8 @@
48
48
  1. 命令响应完成:匹配本次响应的结束提示,或收到厂商明确完成行;不等于业务状态正确。
49
49
  2. 业务状态读回:目标描述与批准计划一致;不排除其他设备会话此后改写,也不等于已持久化。
50
50
  3. 持久化/服务端证据:本地文件完成 fsync/rename/目录 fsync,或设备明确报告保存完成。
51
- TFTP 目前只有设备报告,不能据此证明服务器文件、SHA-256 或本批版本归属;设备保存
51
+ 单设备 TFTP 入口只有设备报告;批量入口可通过本机服务器文件核验和归档补充
52
+ SHA-256 与本批版本证据。未经核验的设备报告不能证明服务器文件;设备保存
52
53
  完成行也不是断电测试。读取、核验、持久化任何阶段失败都不触发自动回放/回滚。
53
54
 
54
55
  ## 业务约束
@@ -75,6 +76,8 @@ TFTP 归档分别保留本地批次副本与服务器归档副本的提交位置
75
76
 
76
77
  TFTP 只确认设备报告的上传结果:策略去掉命令、应答和提示符回显后,检查明确的完成行;文件名或表示“即将上传”的进度文字不算成功。原始输出或终端渲染文本中的失败证据优先于成功文字。
77
78
 
79
+ 失败证据核对与完成核对共用精确回显过滤。静态应答从已完成步骤的 `Command#interactions` 读取,`Interaction#static_response` 对动态回调返回 nil,核对不会重新执行挑战回调。失败核对逐步读取原文、回车历史和终端画面,不累积全部画面副本;颜色样式只影响显示,不能拆开诊断词或借光标擦除隐藏已经收到的失败。命令诊断及拓扑保存确认同样保留这类失败历史。
80
+
78
81
  TFTP 内置策略的 `validate_options!` 是纯参数校验,执行于 H3C 源文件探测之前。整个探测、脚本、证据核对、回执和事件记录由原有 `with_operation(:tftp_backup)` 独占;其他线程、Fiber 及回调重入均不能插入设备命令。租约只保护当前会话,不能隔离其他设备连接或服务器上的同名文件。
79
82
 
80
83
  `tftp_backup` 只返回不可变 `TftpReceipt`,直接包含来源、格式、请求路径和实际 `path`、完成时间及核验等级。只有设备完成证据时为 device_reported,没有服务器 SHA-256。H3C 自动源探测为 startup,显式文件为 saved_file/unknown;山石为 startup/dat,Cisco 为 running/cfg,PAN-OS 为 running/xml,Radware 为 native_archive/tgz。公共策略的默认来源为 unknown,不能据扩展名或字节数推断服务器内容。
@@ -119,11 +122,13 @@ Diagnostic 只保存固定词表中的码、类型、阶段和受控产物状态
119
122
 
120
123
  `Settings` 保留动态秘密来源,`snapshot(mode:)` 只复制允许公开的非敏感字段,并在副作用前复用 `Configuration`、`Planner` 和 `Client.options` 校验。Fleet 的一次规划/执行只使用该份冻结策略;CLI 通过 `for_run` 将规划与执行绑定到同一策略。每台设备的凭据读取与策略解析分开,自定义凭据解析器给出的显式连接选项仍保留原优先级。带 `plan:` 的执行不再查询清单;以后新建的批次可以读取新策略和新的 API 凭据。
121
124
 
125
+ `BackupRun` 在创建批次目录和读取凭据前固定并验证策略。已固定的 `Settings#with_overrides` 只复制非敏感快照上的覆盖值,不重新读取环境中的策略字段;后续 `snapshot(mode:)` 继续按当前业务模式校验,离线导出不会隐式转成网络备份校验。原凭据来源仍保持动态。
126
+
122
127
  `Client#devices` 每次持有独立 `InventoryBudget`,认证、分页、QueryRequired 查询共同累计正文字节和去重前记录数,并共享单调时钟 deadline。默认 HTTP 使用 `read_body`,先检查实际接收字节再追加,不信任 Content-Length。每阶段缩短原生连接/读/写超时;总期限还覆盖慢速响应头。期限观察线程只关闭本次拥有的 HTTP 连接,防止 chunked 的收尾读取拖延返回,调用结束即唤醒并 join。默认 HTTP 不做隐式 GET 重试;这一机制不进入设备命令路径。
123
128
 
124
129
  注入的 `requester` 接收 URI 和 request。它返回后才接受长度和期限检查,不在任意用户 Ruby 回调中注入异步异常。解析和集合操作完成后也检查期限,但这不构成任意 CPU 回调的抢占保证。预算失败仅报告错误码与安全类型,没有部分清单、响应正文或底层 cause。HTTPS 的标准证书检查不变;默认允许明文 HTTP,可通过显式策略禁止。
125
130
 
126
- `Settings#inventory_source` 选择 HTTP `Client` 或 PostgreSQL `DatabaseClient`,两者只向 Fleet 提供 `devices`,不改变设备映射、计划和执行接口。数据库 SQL 与标量参数由用户配置,不内置业务表名或筛选;结果用列别名适配 `Client::FIELDS`,只保留这份现有字段白名单。连接参数由 `DatabaseClient::CONNECTION_ENV` 集中映射到 Netdisco 的 `NETDISCO_DB_*` 环境变量,全部排除在纯策略快照外;每次新建清单客户端时读取最新值。
131
+ `Settings#inventory_source` 选择 HTTP `Client` 或 PostgreSQL `DatabaseClient`,两者只向 Fleet 提供 `devices`,不改变设备映射、计划和执行接口。数据库 SQL 与标量参数由用户配置,不内置业务表名或筛选;结果用列别名适配 `Client::FIELDS`,只保留这份现有字段白名单。数据库身份使用 `ND_DB_HOST/NAME/USER/PASS`,每次新建客户端时读取;端口、TLS 和超时来自 YAML 并进入不可变策略快照。
127
132
 
128
133
  `DatabaseClient` 每次调用独占 PostgreSQL 连接,在只读事务中用扩展查询协议声明游标,原生解析器拒绝多语句和非查询输入。FETCH 大小及次数复用清单预算,单行模式使 libpq 不缓存整页;每行在追加前计数,额外列也消耗字节预算。驱动必须先解码单行,所以单个超大字段仍可超过 Ruby 预算的瞬时内存。总 deadline 覆盖连接和所有语句,每次 FETCH 前缩短 statement_timeout;正常完成回滚只读事务,异常关闭连接,任何失败均不交付部分清单。NOTICE、原始数据库异常及 cause 不进入日志或 CLI;稳定错误码区分连接失败、查询失败、无效清单和预算超限。SQL/参数可在 show-config 中查看,不能用于传递凭据。
129
134
 
@@ -177,7 +182,7 @@ Fleet 可把回执与本地普通文件的时间、稳定性、摘要及目录
177
182
 
178
183
  累计等于上限时当前脚本可正常结束,但下一条查询在发送前失败;超过上限的完整主响应先进入 steps,再返回 `ScriptOutputLimitExceeded`,后处理和下一命令不会继续。追加查询不进入主脚本的公开 steps,但计入预算并在错误中保留实际查询命令。吞掉查询预算异常的钩子不能让超额脚本报告成功。已执行命令不重放;确认的 TFTP 完成行仍可构造带收尾错误的回执。
179
184
 
180
- 累计检查不在读取中途切断当前命令,最多还会接收一个受 `max_output_bytes` 限制的响应;已完成步骤不截断或丢弃。配置清理、解析、用户回调和反复 `Result#output` 的副本不计入该字节预算,因此它不是 RSS 或整批内存硬上限。合成基准不足以确定适合所有设备的默认阈值,因此默认不限制累计值。设置通过 YAML `ssh.max_script_output_bytes`、`NC_MAX_SCRIPT_OUTPUT_BYTES` 和 CLI 同名选项进入批次策略快照,优先级为 CLI > ENV > YAML;未配置时不传递该可选键,显式凭据 resolver 可覆盖连接参数。
185
+ 累计检查不在读取中途切断当前命令,最多还会接收一个受 `max_output_bytes` 限制的响应;已完成步骤不截断或丢弃。配置清理、解析、用户回调和反复 `Result#output` 的副本不计入该字节预算,因此它不是 RSS 或整批内存硬上限。合成基准不足以确定适合所有设备的默认阈值,因此默认不限制累计值。设置通过 YAML `ssh.max_script_output_bytes` 和 CLI 同名选项进入批次策略快照,优先级为 CLI > YAML;未配置时不传递该可选键,显式凭据 resolver 可覆盖连接参数。
181
186
 
182
187
  Ruby 对象显式拥有资源并使用关键字参数。`Profile` 提供有限声明入口,厂商策略负责差异行为。公开方法、厂商钩子、结果对象和 CLI JSON 字段以当前文档为准;内部不做运行时方法注入,也没有工作流 DSL。
183
188
 
@@ -284,7 +289,7 @@ TFTP 策略必须实现 `validate_options!(target, source_file:, vrf:)`、`recei
284
289
  | Juniper、Arista、F5、Fortinet | 每次增加一个设备系列的登录、配置采集与本地备份,再按需扩展 TFTP / 拓扑 | 指定型号和固件的脱敏会话样本、分页和失败回显、现场验证条件;不能仅靠厂商名称声明支持 |
285
290
  | TextFSM 模板 | 按业务需要增加接口、路由等具体命令及字段 | 样本来源和许可、正常 / 空表 / 部分输出测试、模板随 gem 安装的证据;`parse_config` 仍是指定模板提取,不是完整配置模型 |
286
291
  | 持久调度与重试 | 先由调用应用在持久队列中运行单设备任务 | 设备互斥、任务标识、幂等策略、重试上限及取消语义;连接器不自动重放可能已执行的配置命令 |
287
- | 英文文档 | 优先增加英文 README,提供安装、主要能力、限制和验证入口 | 确定与中文文档同步更新的维护方式;CLI 消息国际化另行评估,避免静默改变现有输出 |
292
+ | 文档翻译 | README 与设备、Netdisco 指南已提供双语;架构、验证和发布参考仍使用中文 | 按 CONTRIBUTING 同步双语使用文档;参考文档翻译与 CLI 消息国际化另行评估 |
288
293
 
289
294
  几千台设备的调度应由应用限制总并发及同设备并发,并持久保存逐台结果。
290
295
  只读采集可在重新建连后按应用策略重试;配置下发中断后应先回读设备状态,
data/docs/netdisco.md ADDED
@@ -0,0 +1,176 @@
1
+ # Netdisco and fleet backups
2
+
3
+ [简体中文](netdisco.zh-CN.md) | [Home](../README.md) | [Device operations](usage.md)
4
+
5
+ ## Configure inventory and credentials
6
+
7
+ Load `require "net/connector/netdisco"` for inventory and batch APIs. Netdisco supplies metadata; device credentials come from the environment or a caller-provided resolver. The full inventory is validated before device tasks start. Invalid addresses, unsupported vendors, filters, duplicates, missing credentials, failures, and partial results remain distinguishable.
8
+
9
+ For HTTP inventory:
10
+
11
+ ```sh
12
+ export ND_URL=https://netdisco.example/netdisco
13
+ export ND_USERNAME=inventory-reader
14
+ export ND_PASSWORD='replace-me'
15
+ export NC_DEVICE_USERNAME=backup-user
16
+ export NC_DEVICE_PASSWORD='replace-me'
17
+ ```
18
+
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
+
21
+ 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
+
23
+ ```yaml
24
+ netdisco:
25
+ url: https://netdisco.example/netdisco
26
+ allow_insecure_http: false
27
+ backup:
28
+ directory: /var/backups/network
29
+ concurrency: 4
30
+ inventory:
31
+ include_vendors: [h3c, huawei, cisco_ios]
32
+ ssh:
33
+ host_key_policy: strict
34
+ max_script_output_bytes: null
35
+ tftp:
36
+ server: 192.0.2.30
37
+ vrfs:
38
+ cisco_nxos: management
39
+ ```
40
+
41
+ ## CLI planning and execution
42
+
43
+ ```sh
44
+ net-backup --config config.yml --show-config
45
+ net-backup --config config.yml --plan
46
+ net-backup --config config.yml --host 192.0.2.7 --success-policy selected
47
+ net-backup --config config.yml --tftp --plan
48
+ net-backup --config config.yml --tftp --all
49
+ net-backup --config config.yml --export 192.0.2.7 --output ./exports/device.cfg
50
+ ```
51
+
52
+ The installed command is `net-backup`; source checkouts use `bundle exec bin/net-backup`. `--show-config` validates public settings without querying inventory. `--plan` queries and validates inventory without connecting devices. `--host` selects one address and fails if absent. Local CLI backups select all ready devices by default; TFTP selects at most five per vendor unless `--all` is given. `--limit-per-vendor` accepts 1 through 5.
53
+
54
+ `--export IP` reads an existing canonical `<IP>.txt` file offline. It writes original bytes to stdout, or atomically writes a private file with `--output`. It does not require inventory or device credentials. Hostname-prefixed batch-example files are not searched by this command.
55
+
56
+ Plans and results are JSON. Exit codes:
57
+
58
+ | Code | Meaning |
59
+ | --- | --- |
60
+ | `0` | The requested success policy passed |
61
+ | `1` | Empty or incomplete batch under that policy |
62
+ | `2` | Configuration, inventory, or other CLI error |
63
+
64
+ The default `strict` policy requires a nonempty inventory, every outcome successful, and no callback/report errors. Filtered records still prevent strict success, so use `selected` when intentionally selecting one host or sampling. `selected` requires at least one success; remaining outcomes may only be `filtered` or `sample_limit`. Missing credentials, duplicates, invalid addresses, unsupported vendors, collisions, partial results, callback errors, and report errors all prevent it from passing.
65
+
66
+ `verified` adds server-file verification to selected success and requires TFTP with a local server directory. Strict `status` remains separate from `policy_success`; `coverage.complete` means every record was attempted, including failed tasks.
67
+
68
+ ## Fleet API and reports
69
+
70
+ ```ruby
71
+ require "net/connector/netdisco"
72
+
73
+ fleet = Net::Connector::Netdisco::Fleet.new
74
+ plan = fleet.plan_backup
75
+ puts plan.selected.map(&:host)
76
+ report = fleet.backup_all(plan: plan, success_policy: :selected, on_change: ->(outcome) {
77
+ puts "#{outcome.device.host}: #{outcome.backup.change}"
78
+ })
79
+ puts report.counts
80
+ puts report.report_location
81
+ exit 1 unless report.policy_success?
82
+ ```
83
+
84
+ `plan_backup` and `plan_tftp_backup` build inventory snapshots. Passing `plan:` preserves the selection and avoids another inventory query; malformed external plans are rejected before credentials, directories, or device I/O. Tasks run with bounded concurrency and retain inventory order. Ordinary device/callback failures do not discard other results; interruption stops workers and cleans resources.
85
+
86
+ Fleet returns `Netdisco::Report`, delegating raw execution fields to `report.batch`. `success?` and `status` remain strict; `policy_success?` evaluates the chosen policy. `summary` uses schema 2, with public settings, coverage, per-device diagnostics, timestamps, monotonic durations, and verification. `statistics` omits device detail. `Batch#build_report` only wraps existing execution data.
87
+
88
+ Local backups compare SHA-256. `on_change:` fires for created or changed artifacts; `on_start:` and `on_result:` observe attempted tasks. Callback faults appear in `callback_errors`. Saved artifacts with later errors remain `saved_with_error`; confirmed uploads with later errors remain `reported_with_error`. Neither result permits automatic replay.
89
+
90
+ `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
+
92
+ ## PostgreSQL inventory
93
+
94
+ 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):
95
+
96
+ ```yaml
97
+ netdisco:
98
+ source: postgres
99
+ db_port: 5432
100
+ db_sslmode: verify-full
101
+ db_sslrootcert: /etc/net-connector/database-ca.crt
102
+ db_connect_timeout: 10
103
+ query: |
104
+ SELECT host(ip) AS ip, name, dns, vendor, os, model, os_ver, serial
105
+ FROM device
106
+ WHERE vendor = $1
107
+ ORDER BY ip
108
+ query_params: [H3C]
109
+ ```
110
+
111
+ The query must return unique column names including `ip`. Optional retained fields are `name`, `dns`, `vendor`, `os`, `model`, `os_ver`, and `serial`; other fields are discarded. Use `host(ip)` for PostgreSQL `inet` fields to omit prefixes. Parameters bind in array order, without interpolation or environment expansion. Use explicit ordering for repeatable selection.
112
+
113
+ ```sh
114
+ export ND_DB_HOST=database.example
115
+ export ND_DB_NAME=netdisco
116
+ export ND_DB_USER=inventory-reader
117
+ export ND_DB_PASS='replace-me'
118
+ net-backup --config examples/inventory_sql.yml --plan
119
+ ```
120
+
121
+ Host, database, user, and password are required. Source, query, and parameters may be overridden by `--source`, `--query`, and `--query-params`. They are public configuration and may appear in `--show-config`; passwords belong only in connection variables. HTTP credentials and device credentials are not prerequisites for a database-only plan.
122
+
123
+ The lazy-loaded `pg` driver owns one connection per query, uses a read-only transaction and parameterized cursor, and reads rows in single-row mode. Multiple statements, writes, and locking queries fail. Still grant the account only necessary SELECT privileges. Failure or timeout closes the connection and discards the entire inventory; raw SQL, parameters, and responses are excluded from diagnostics.
124
+
125
+ ## Inventory budgets and policy snapshots
126
+
127
+ Default budgets cover authentication, pagination, fallback queries, or database FETCH operations: 500 rows per page, 10,000 pages, 100,000 records before deduplication, 16 MiB per response, 128 MiB total, and 300 seconds total. Configure them under `netdisco`. These are adjustable design defaults, not measured deployment capacity.
128
+
129
+ HTTP counts actual chunks and closes on excess. Injected `requester:` callbacks are checked only after returning; their blocking and memory belong to the caller. PostgreSQL counts column names and values, including discarded fields, after libpq decodes each row; a huge field may still consume driver memory before the check. Connection, query, and FETCH share the deadline and use database statement timeouts.
130
+
131
+ `Settings#snapshot(mode:)` freezes public policy before side effects. CLI planning and execution share it; later calls can read new settings. Credentials remain dynamic per task. `Settings.new.for_run(mode: :backup)` shares policy across API calls; non-sensitive overrides preserve an existing snapshot. A custom `credentials:` resolver can supply per-device connection options, taking precedence over defaults. Injected clients remain caller-owned.
132
+
133
+ Explicit host mappings, device rules, and vendor overrides take precedence over automatic detection. Comware plus a branded H3C model can correct stale vendor labels; H3C WX/AC models select the wireless connector.
134
+
135
+ ## TFTP history and verification
136
+
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.
138
+
139
+ 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
+
141
+ Without a server directory, batch examples add a batch identifier to names, retain `device_reported`, and cannot save local configuration copies. PAN-OS uses the fixed `running-config.xml` name, so the history-preserving example refuses its upload without local server access. Plans still prevent same-batch filename collisions.
142
+
143
+ Reports distinguish `verification`, `server_file_verified`, `local_file`, `server_archive_file`, `bytes`, and `sha256`. TFTP has no configuration `change` notification.
144
+
145
+ ## Batch examples and deployment
146
+
147
+ ```sh
148
+ bundle exec ruby examples/backup.rb --json
149
+ bundle exec ruby examples/backup.rb --sample 3 --concurrency 10
150
+ bundle exec ruby examples/backup_tftp.rb --tftp-root /srv/tftp --success-policy verified
151
+ ```
152
+
153
+ Examples default to all filtered devices and `selected` success, unlike the CLI's strict default and TFTP sampling. Sampling comes from `--sample` or YAML `backup.limit_per_vendor`. Examples create unique UTC+8 batch directories named `YYYY-MM-DD_HH-mm-ss`, adding a sequence on collision. Local files use hostname/IP names; generic Fleet/CLI backups default to IP names, replacing IPv6 colons with underscores. `filename_style: :hostname_ip` changes the path and comparison baseline when a device is renamed.
154
+
155
+ Each batch retains `plan.json`, `events.jsonl`, `summary.json`, and `summary.txt`, with private permissions. Human progress goes to stderr; `--json` controls JSON stdout. `--verbose` shows device events; `--no-progress` disables progress. TTY progress has a heartbeat and throughput/ETA, while redirected output reports every 25 devices. Terminal faults are retained as callback diagnostics without skipping tasks or reports.
156
+
157
+ Examples support hidden `--ask-password` and `--ask-netdisco-password` prompts, plus environment credentials. `--stdin-credentials` reads one JSON line with `netdisco_username`, `netdisco_password`, `device_username`, and `device_password`; a second `RUN` line confirms device execution. For PostgreSQL, the first pair overrides database user/password.
158
+
159
+ Examples load the project `.env` through `dotenv` without overriding existing environment values. Direct `ruby examples/backup.rb` uses the installed gem; `bundle exec ruby` tests checkout code. Example directory paths are based on the project root; an environment-provided `NC_CONFIG` resolves from the startup directory. Shared library helpers do not load `.env` or change directories. See [examples](../examples/README.md) (Chinese).
160
+
161
+ ## Environment reference
162
+
163
+ | Variables | Purpose / default |
164
+ | --- | --- |
165
+ | `ND_URL` | HTTP inventory base URL |
166
+ | `ND_USERNAME`, `ND_PASSWORD` | HTTP login without API key |
167
+ | `ND_API_KEY` | Existing API key |
168
+ | `ND_DB_HOST`, `ND_DB_NAME`, `ND_DB_USER`, `ND_DB_PASS` | Required database connection fields |
169
+ | `NC_CONFIG` | Non-sensitive YAML file |
170
+ | `NC_DEVICE_USERNAME`, `NC_DEVICE_PASSWORD` | Default device credentials |
171
+ | `NC_<VENDOR>_USERNAME`, `NC_<VENDOR>_PASSWORD` | Vendor credentials, e.g. `CISCO_IOS` |
172
+ | `NC_ENABLE_PASSWORD`, `NC_<VENDOR>_ENABLE_PASSWORD` | Optional privilege credentials |
173
+
174
+ Only the variables listed above are supported. There are no legacy aliases. Public settings belong in YAML: budgets under `netdisco`, filters and mappings under `inventory`, script output budget and vendor protocols under `ssh`, and source files/VRFs under `tftp`. Database port, TLS mode, CA path, and connection timeout use `netdisco.db_port`, `db_sslmode`, `db_sslrootcert`, and `db_connect_timeout`. TFTP uses `tftp.server` and `tftp.verification_root`; progress uses `--no-progress`. Removed settings and old prefixes are rejected without echoing their values. Vendor source fields are `h3c_source_file`, `h3c_wireless_source_file`, and `huawei_source_file` under `tftp`.
175
+
176
+ Scheduling and durable queues belong to the caller. No device command retries are added by batch orchestration. For development gates and evidence limits, see [verification](VERIFICATION.md); for protected extension interfaces and report helpers, see [architecture](architecture.md) (Chinese).
@@ -0,0 +1,247 @@
1
+ # Netdisco 与批量备份
2
+
3
+ [English](netdisco.md) | [返回首页](../README.zh-CN.md) | [设备操作](usage.zh-CN.md)
4
+
5
+ ## Netdisco 清单与批量备份
6
+
7
+ `Net::Connector::Netdisco` 读取并验证完整清单,再将支持的记录映射到连接器,使用有上限的工作线程执行备份。Netdisco 只提供清单字段;设备凭据来自环境变量或调用方提供的解析器。清单会在连接任何设备前完成校验;不支持、被过滤、重复、缺少凭据、失败,以及保存成功但关闭失败的结果分别保留。
8
+
9
+ 每次 `Client#devices` 默认限制单响应 16 MiB、累计响应 128 MiB、去重前 100,000 条记录、10,000 页和 300 秒总期限。认证、分页及兼容查询共用这些预算;默认 HTTP 客户端逐块计数,超限会关闭连接并抛出带稳定 `code` 的 `Client::Error`,Fleet 不会执行半份清单。这些默认值是可调整的设计起点,不是实测容量。可注入 `requester: ->(uri, request)`;该回调只能在回调返回后检查正文和期限,回调自身的阻塞及内存用量由注入方控制。
10
+
11
+ 推荐使用 HTTPS,标准证书验证保持开启。`allow_insecure_http` 默认为 `true`;设为 `false` 可在发请求前拒绝 HTTP,YAML 中对应 `netdisco.allow_insecure_http: false`。HTTP 会明文传输登录凭据和 API key,迁移时应先提供可验证的 HTTPS 端点。
12
+
13
+ `Fleet#plan_backup` 和 `Fleet#plan_tftp_backup` 从同一份清单生成计划。把计划传给 `backup_all(plan:)` 或 `tftp_backup_all(plan:)`,可使预览与执行选择同一批设备;计划与清单不符时会拒绝执行。单台设备异常或结果回调失败不会阻止其他设备。`batch.summary` 包含总数、成功、失败、部分成功、跳过、具体状态和逐台结果。部分成功包括已保存但关闭失败,以及本地文件已替换但目录同步或收尾失败;后者保留 backup 并标为 `saved_with_error`。TFTP 的 `reported_uploaded` 仅代表设备报告上传,不代表服务器文件已核验。
14
+
15
+ 本地 `backup(path:)` 用 SHA-256 比较新旧配置,`backup.change` 返回 `:created`、`:changed` 或 `:unchanged`;内容未变且权限、文件身份正常时保留修改时间。这不补验历史写入的断电持久性。`backup_all` 的 `on_change:` 仅在新建或更改文件保存后触发;`on_start:` 和 `on_result:` 观察每台已尝试设备。回调异常记录在 `batch.callback_errors`,不丢弃设备结果。每项结果包含开始、结束和耗时。TFTP 无法比较服务器文件,因此没有 `change`,也不触发变更通知。
16
+
17
+ 每批默认由 `ResultStore::Json` 写入私有 JSON 报告,路径见 `batch.report_location`。调用方如有数据库仓储,可传 `ResultStore::Database.new(repository: YourModel)`;仓储需实现 `create!(attributes)`。`result_store: nil` 表示由调用方自行持久化。报告失败保留在 `batch.report_error`,同时使 `batch.success?` 为假。若报告已替换但目录同步失败,仍保留位置;离线 `--export --output` 遇到同类错误返回 2,并说明文件已经提交。
18
+
19
+ Fleet 统一返回 `Netdisco::Report`,JSON 的 `schema_version` 固定为 `2`。报告包含 `policy`、`policy_success`、清单覆盖和受控诊断;`success?` / `status` 表示严格完成情况,`policy_success?` 表示所选成功策略。任务耗时使用单调时钟,UTC 开始/结束时间独立保留。`report.batch` 是原始执行快照;手工构造的 Batch 可用 `batch.build_report(policy: :selected)` 生成报告,不会再次执行设备或重写文件。
20
+
21
+ `success_policy: :selected` 要求至少一台设备成功,其他记录只因 `filtered` 或 `sample_limit` 跳过,而且没有部分成功、回调或报告错误。缺少凭据、重复地址、无效地址、未知厂商、目标冲突和未知状态均会阻止成功。`coverage.complete` 只表示每条清单记录都已尝试任务;失败任务也计入尝试,不能据此判断配置已保存。自定义 `ResultStore#write(report, directory:)` 始终接收 Report,并通过 `summary` 获取统一 JSON 结构。
22
+
23
+ 外部构造的计划也必须把同一管理地址的全部条目标记为 `duplicate_host` 并跳过,不能通过采样恢复其中一条的执行资格;IPv6 的等价写法视为同一地址。只需汇总时使用 `report.statistics`,它返回 `summary` 除 `devices` 外的字段,不生成设备明细,也不缓存调用方持有的批次容器。
24
+
25
+ ```sh
26
+ export ND_URL=https://netdisco.example/netdisco
27
+ export ND_USERNAME=inventory-reader
28
+ export ND_PASSWORD='replace-me'
29
+ export NC_DEVICE_USERNAME=backup-user
30
+ export NC_DEVICE_PASSWORD='replace-me'
31
+ ```
32
+
33
+ ```ruby
34
+ require "net/connector/netdisco"
35
+
36
+ fleet = Net::Connector::Netdisco::Fleet.new
37
+ plan = fleet.plan_backup
38
+ puts plan.selected.map(&:host)
39
+ batch = fleet.backup_all(plan: plan, on_change: ->(outcome) {
40
+ puts "#{outcome.device.host}: #{outcome.backup.change}"
41
+ })
42
+ puts batch.counts
43
+ puts batch.summary.slice(:succeeded, :failed, :partial, :skipped)
44
+ puts batch.report_location
45
+ batch.outcomes.each do |outcome|
46
+ puts "#{outcome.device.source_ip}: #{outcome.status} #{outcome.backup&.path}"
47
+ end
48
+ exit 1 unless batch.success?
49
+ ```
50
+
51
+ gem 安装后的命令为 `net-backup`;源码入口为 `bin/net-backup`,可在项目目录运行 `bundle exec bin/net-backup --help`。原命令 `net-connector-backup` 已改名。
52
+
53
+ 命令行程序 `net-backup` 的 YAML 文件只允许非敏感设置;Netdisco 和设备凭据留在环境变量中。仅 `ND_URL` 可从环境覆盖 YAML 地址;其他非敏感策略通过 YAML 或 CLI 设置。批量示例也支持 `NC_CONFIG`,可从 [完整配置示例](../examples/backup.yml) 开始。只有传入 `--config FILE` 或设置 `NC_CONFIG` 时才加载文件:
54
+
55
+ ```yaml
56
+ netdisco:
57
+ url: https://netdisco.example/netdisco
58
+ page_size: 500
59
+ max_response_bytes: 16777216
60
+ max_inventory_bytes: 134217728
61
+ max_devices: 100000
62
+ max_pages: 10000
63
+ inventory_timeout: 300
64
+ allow_insecure_http: false
65
+ backup:
66
+ directory: /var/backups/network
67
+ concurrency: 4
68
+ inventory:
69
+ include_vendors: [h3c, huawei, cisco_ios]
70
+ host_overrides:
71
+ 192.0.2.7: h3c_wireless
72
+ ssh:
73
+ host_key_policy: strict
74
+ max_script_output_bytes: null # 可选正整数字节数;null 保持不限制累计值。
75
+ tftp:
76
+ server: 192.0.2.10
77
+ vrfs:
78
+ cisco_nxos: management
79
+ ```
80
+
81
+ ```sh
82
+ net-backup --config config.yml --show-config
83
+ net-backup --config config.yml --plan --host 192.0.2.7
84
+ net-backup --config config.yml --host 192.0.2.7
85
+ net-backup --config config.yml --tftp --plan
86
+ net-backup --config config.yml --tftp --all
87
+ net-backup --config config.yml --export 192.0.2.7 --output ./exports/device.cfg
88
+ ```
89
+
90
+ ### PostgreSQL 联机查询
91
+
92
+ 设置 `netdisco.source: postgres` 可以直接从数据库查询清单,继续使用同一套 Fleet、规则、计划和备份流程。库不内置表名、SQL 或业务筛选条件;必须提供查询,可直接修改 [YAML 示例](../examples/inventory_sql.yml):
93
+
94
+ ```yaml
95
+ netdisco:
96
+ source: postgres
97
+ db_port: 5432
98
+ db_sslmode: verify-full
99
+ db_sslrootcert: /etc/net-connector/database-ca.crt
100
+ db_connect_timeout: 10
101
+ query: |
102
+ SELECT host(ip) AS ip, name, dns, vendor, os, model, os_ver, serial
103
+ FROM device
104
+ WHERE vendor = $1
105
+ ORDER BY ip
106
+ query_params: [H3C]
107
+ ```
108
+
109
+ 查询必须返回唯一命名的 `ip` 列;可选列为 `name`、`dns`、`vendor`、`os`、`model`、`os_ver`、`serial`,其他列会丢弃。自定义表、视图、JOIN 和只读 CTE 均可用列别名适配这一契约。PostgreSQL 的 `inet` 字段建议用 `host(ip) AS ip` 去除前缀长度;筛选及排序由 SQL 决定。参数按数组顺序绑定 `$1`、`$2`,支持字符串、数字、布尔和 null,不做字符串插值或环境变量展开。
110
+
111
+ 数据库身份只从 `ND_DB_HOST`、`ND_DB_NAME`、`ND_DB_USER`、`ND_DB_PASS` 读取,四项必填;HTTP 地址、API key 和设备登录凭据不是清单查询的前置条件:
112
+
113
+ ```sh
114
+ export ND_DB_HOST=database.example
115
+ export ND_DB_NAME=netdisco
116
+ export ND_DB_USER=inventory-reader
117
+ export ND_DB_PASS='replace-me'
118
+
119
+ net-backup --config examples/inventory_sql.yml --show-config
120
+ net-backup --config examples/inventory_sql.yml --plan
121
+ # 覆盖查询参数;实际备份仍需设置 NC_DEVICE_* 凭据。
122
+ net-backup --config examples/inventory_sql.yml --plan --query-params '["Cisco"]'
123
+ ```
124
+
125
+ SQL、参数和来源也可分别通过 `--query SQL`、`--query-params JSON`、`--source postgres` 覆盖 YAML,不再从环境变量读取。SQL 和参数属于可公开配置,会出现在 `--show-config` 中;数据库密码只放在连接环境变量中。身份凭据不进入策略快照、计划、报告或 `inspect`;端口、TLS 和超时属于公开策略,进入快照与配置摘要。已有 Fleet 每次重新查询时读取最新连接凭据;传入已有 `plan:` 执行时不会重新查询。
126
+
127
+ 客户端通过 `pg` 驱动执行只读事务,使用参数化游标分批取数,并在每批启用单行读取。PostgreSQL 原生解析拒绝多条语句,写入和锁定查询会失败;查询账户应仅授予所需表/视图的 SELECT 权限,只读事务不能替代账户权限隔离。驱动只在实际查询时加载,HTTP 和离线导出路径不加载它。
128
+
129
+ 数据库查询复用现有清单预算:`page_size` 控制每次 FETCH 的行数,`max_pages` 限制 FETCH 次数,`max_devices` 在去重前计数;字节预算统计返回列名与文本值,包含最终丢弃的列。字节检查发生在 libpq 解码一行之后,不能限制单个超大字段在驱动内部的瞬时内存。连接、查询和全部 FETCH 共用总期限,同时设置数据库 statement_timeout。任一查询错误、超时或超限均关闭连接并丢弃整份清单,不连接设备;错误消息不输出原始 SQL、参数或数据库响应。建议只选所需字段,并提供明确的 ORDER BY 保持采样顺序稳定。
130
+
131
+ ### 计划与执行
132
+
133
+ `--plan` 只拉取并验证清单;`--host` 选择一个管理地址;`--tftp` 默认每厂商最多选择五台,`--all` 选择所有就绪设备。本地备份默认选择所有就绪设备,可用 `--limit-per-vendor` 限制。`--show-config` 只输出有效的非敏感设置,不访问 Netdisco。CLI 会拒绝未知 YAML 字段、Ruby 对象标签及配置中的凭据。`--export IP` 只离线读取规范化管理地址对应的 `<IP>.txt`;默认原样写到标准输出,指定 `--output` 后以 `0600` 权限原子写文件。导出的配置仍是敏感数据。
134
+
135
+ CLI 的计划与批次摘要使用 JSON。默认 `--success-policy strict` 使用严格规则:非空清单且全部成功、回调及报告正常时为 `0`;空清单或有跳过、部分成功、失败时为 `1`;清单或配置错误为 `2`。`--host` 未在清单中找到也返回 `2`。由于其他清单记录会标记为过滤,默认单主机备份成功时批次退出码仍可能是 `1`;应查看 JSON 中的 `succeeded`、`skipped` 和逐台 `status`。
136
+
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
+
139
+ 本地批量示例和 TFTP 批量示例默认全量;显式 `--sample N` 或 YAML `backup.limit_per_vendor` 才限制每厂商数量(示例允许 1 至 5)。优先级为 CLI > YAML。并发默认 4,可用 `--concurrency N` 或 YAML `backup.concurrency` 覆盖。两类示例在备份目录下创建唯一批次目录。TFTP 批量示例在可访问服务器目录时使用 hostname-ip 远端文件名并逐批归档;无法访问服务器目录时使用带批次标识的远端文件名,避免覆盖旧文件;上传完成与服务器文件验证是不同状态。
140
+
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
+
143
+ 批量 TFTP 源文件通过 YAML `tftp.h3c_source_file`、`tftp.h3c_wireless_source_file`、`tftp.huawei_source_file` 设置。源文件是设备上的路径,需符合连接器校验规则。
144
+
145
+ 本地配置备份写到 `<目录>/<IP>.txt`,IPv6 的 `:` 转成 `_`。设备改名不改变文件名或比较基线;只读取该规范路径,缺失时创建新备份。其他名称的文件不参与查找或哈希比较。Fleet 和离线读取拒绝符号链接及非普通文件。文件原子替换为 `0600`,新目录权限为 `0700`。批次在当前进程执行,需要定时任务或持久队列时由调用方安排;失败命令不会自动重试。
146
+
147
+ | 环境变量 | 默认值 | 用途 |
148
+ | --- | --- | --- |
149
+ | `ND_URL` | HTTP 模式必填 | Netdisco 服务根地址,可包含租户路径 |
150
+ | `NC_CONFIG` | 未设置 | CLI / 批量示例的非敏感 YAML 配置文件 |
151
+ | `ND_USERNAME`, `ND_PASSWORD` | 未提供 API 密钥时必填 | 清单 API 登录 |
152
+ | `ND_API_KEY` | 未设置 | 直接使用已有 API 密钥 |
153
+ | `ND_DB_HOST`, `ND_DB_NAME` | PostgreSQL 模式必填 | 数据库主机或 Unix socket 目录、数据库名 |
154
+ | `ND_DB_USER`, `ND_DB_PASS` | PostgreSQL 模式必填 | 仅从环境注入的数据库用户名、密码 |
155
+ | `NC_DEVICE_USERNAME`, `NC_DEVICE_PASSWORD` | 未设置 | 设备登录默认凭据 |
156
+ | `NC_<VENDOR>_USERNAME`, `NC_<VENDOR>_PASSWORD` | 未设置 | 单厂商凭据,例如 `CISCO_IOS` |
157
+ | `NC_ENABLE_PASSWORD`, `NC_<VENDOR>_ENABLE_PASSWORD` | 未设置 | 可选提权密码 |
158
+
159
+ Netdisco 环境变量使用 `ND_`,连接器使用 `NC_`,仅支持上表列出的变量,不保留旧名别名。凭据保持在环境变量中,YAML 不接受密码。端口、TLS、CA 文件、连接超时使用 YAML `netdisco.db_port`、`db_sslmode`、`db_sslrootcert`、`db_connect_timeout`;TFTP 使用 `tftp.server`、`tftp.verification_root`;进度由 `--no-progress` 控制。其他策略也使用 YAML 或 CLI,旧变量和已移除的设置会明确报错,不输出变量值。
160
+
161
+ 将原来的环境配置移到 YAML:清单预算放在 `netdisco`;过滤、厂商/主机映射及规则放在 `inventory`;脚本输出预算与厂商协议放在 `ssh`;VRF 和源文件放在 `tftp`。例如:
162
+
163
+ ```yaml
164
+ inventory:
165
+ include_vendors: [h3c, cisco_nxos]
166
+ device_rules:
167
+ - vendor: Cisco
168
+ model_prefix: N9K
169
+ connector: cisco_nxos
170
+ ssh:
171
+ vendor_protocols:
172
+ h3c: ssh
173
+ max_script_output_bytes: 16777216
174
+ ```
175
+
176
+ `Fleet` 每次规划或执行前通过 `Settings#snapshot(mode:)` 固定非敏感设置,包括筛选规则、目录、并发、协议、主机密钥、日志、TFTP 参数和清单预算。执行中修改 ENV 不改变当批策略;再次调用会读取新值。CLI 一次调用的规划与执行共用策略,优先级为 CLI > 常用环境变量 > YAML > 默认值。`Settings#validate!(mode:)` 复用连接配置及 Planner 的枚举和范围规则;`--show-config` 会拒绝非法设置,`--export` 只验证离线目录,不要求清单地址或认证。
177
+
178
+ 每台任务开始时仍读取设备凭据,`Settings.from_file` 也保留轮换能力。纯策略快照不保存密码、API key 或凭据解析器。需要让多次 API 调用共用策略时,可传 `Settings.new.for_run(mode: :backup)`。自定义 `credentials:` 解析器仍可逐设备返回连接设置,它显式给出的选项优先于批次默认值,由调用方负责一致性;注入的 `client:` 生命周期也由调用方管理。传入 `plan:` 的执行不会重新拉取或筛选已批准清单。`fleet.devices` 可在不连接设备时检查映射。
179
+
180
+ ## 批量示例与进度
181
+
182
+ `examples/backup.rb` 和 `examples/backup_tftp.rb` 默认全量备份符合筛选条件的设备,并在终端原地刷新两行进度(登录、采集、排队、成功和未完成数,以及耗时、平均吞吐、预计剩余时间);失败单独输出,结束后显示汇总和报告路径。重定向时每 25 台输出一次进度,避免刷屏。`--verbose` 显示逐条登录和命令事件,`--json` 才在 STDOUT 输出 JSON;完整计划和结果始终保存在批次目录。`--no-progress` 关闭人类进度。百分比表示任务完成比例,不是成功率。
183
+
184
+ ```sh
185
+ ruby examples/backup.rb # 全量,默认并发 4
186
+ ruby examples/backup.rb --sample 3 # 每厂商最多 3 台
187
+ ruby examples/backup.rb --verbose # 详细命令过程
188
+ ruby examples/backup.rb --json > result.jsonl
189
+ ruby examples/backup.rb --concurrency 10 --username backup-user --ask-password
190
+ ```
191
+
192
+ 批量示例支持 `--concurrency`(1 至 50)、`--username`、`--password`、`--enable-password`、`--netdisco-url`、`--netdisco-username`、`--netdisco-password`、`--directory` 和 `--config`。命令行优先于环境变量与 YAML;显式设备凭据逐字段覆盖对应厂商凭据。指定 Netdisco 用户名或密码时不再沿用环境中的 API key。密码可使用 `--ask-password` / `--ask-netdisco-password` 隐藏输入,避免命令行密码进入 shell 历史或进程参数;自动化仍可用环境变量或原有 `--stdin-credentials`。`--config` 与 `--directory` 的相对路径基于项目根目录。
193
+
194
+ ## 部署、示例与验证
195
+
196
+ ```sh
197
+ bundle install
198
+ script/ci
199
+ ```
200
+
201
+ CI 在 Linux 和 macOS 上覆盖 Ruby 3.2、3.3、3.4、4.0。`script/ci` 扫描源码与可用 Git 历史中的敏感数据,执行 Ruby 与工作流 lint、完整测试,并验证已构建 gem 的隔离安装和本地 PTY 烟测;不连接真实网络设备。首次运行需下载固定校验和的 Gitleaks 与 actionlint。
202
+
203
+ `bundle exec rake test` 报告当前测试进程的行、分支覆盖率和未加载文件清单。核心引擎、脱敏与错误处理(含 `Redactor`)、批量工作线程分别要求行和分支覆盖率均达到 80%,关键文件没有覆盖率数据也会失败;该门槛同时阻止 CI 和发布预检通过。`bundle exec rake lint` 检查 Ruby 代码,并对 `engine/`、`netdisco/` 限制方法长度(40)和 ABC 复杂度(60)。`bundle exec rake security:check` 扫描敏感数据,`bundle exec rake release:check` 执行完整预检。构建产物和脱敏扫描报告保存在被忽略的 `tmp/` 下。
204
+
205
+ 参与开发见 [CONTRIBUTING.md](../CONTRIBUTING.md),漏洞报告见 [SECURITY.md](../SECURITY.md)。README 和使用指南提供英文、中文版本;实现注释与架构参考使用中文,欢迎中文或英文的问题与 PR。
206
+
207
+ 真实凭据应放在环境变量和版本库外的本地配置中。检查范围、依赖政策和忽略规则见[验证文档](VERIFICATION.md),发布流程见[发布文档](RELEASING.md)。
208
+
209
+ 备份示例通过 `examples/boot.rb` 读取项目根目录 `.env`,不会自动切换依赖或注入源码路径。直接 `ruby examples/backup.rb`(或在 `examples/` 中执行 `ruby backup.rb`)使用已安装的 gem;修改库后必须重新构建并安装,开发源码验证则使用 `bundle exec ruby examples/backup.rb`。示例中的相对备份、日志路径统一基于项目根目录;进程中显式传入的 `NC_CONFIG` 路径按启动目录解析。
210
+
211
+ 清单自动识别保留显式主机映射、设备规则和厂商覆盖的优先级。若厂商标签陈旧,而 `os: Comware` 与 `model: H3C ...` 同时确认 H3C,则自动使用 H3C 连接器;`H3C WX...` / `H3C AC...` 使用无线连接器。仅型号片段或操作系统单项不会覆盖其他厂商。
212
+
213
+ 备份部署示例 `examples/backup.yml` 使用 `ssh.host_key_policy: accept_new`:首次连接自动登记到 OpenSSH known_hosts,已有密钥变化仍拒绝连接。`--host-key-policy strict|accept_new` 可覆盖本次策略,`--known-hosts FILE` 指定持久保存位置;不应删除密钥文件,否则会丢失历史身份记录。库本身默认仍为 strict。
214
+
215
+ 设备密钥变化也需自动更新时,使用 `--host-key-policy replace --known-hosts FILE`,或在 YAML 设置 `ssh.host_key_policy: replace` 与 `ssh.known_hosts`。文件必须显式指定;只在主机密钥变化错误时删除该设备旧记录并重连一次,不重放设备命令。建议使用备份任务专用的持久密钥文件。
216
+
217
+ 进度每秒更新一次心跳;设备无新输出时仍显示耗时,超过 10 秒的最久任务显示地址、阶段与任务耗时。预计剩余时间按已完成任务的平均吞吐计算,前五台完成前显示“估算中”,异构设备和末尾慢任务会使估算波动。终端事件刷新最多约每秒五次,退出或异常时回收显示线程;结束按错误代码汇总未完成任务。
218
+
219
+ 本地批量示例将配置写为 `hostname-ip.txt`(名称取 Netdisco name/dns),空名称使用 `unnamed`,特殊字符清理,IPv6 冒号替换为下划线。批次目录固定使用 UTC+8 的 `YYYY-MM-DD_HH-mm-ss`,同秒冲突追加 `_01` 等序号。每批包含 `plan.json`、`summary.json` 和人类可读的 `summary.txt`(时间、计数、失败分类及失败明细),报告权限为 0600。历史目录不改名;通用 Fleet/CLI 仍默认 IP 文件名,API 可显式传入 `filename_style: :hostname_ip`,该模式设备改名会改变文件路径与比较基线。
220
+
221
+ 公共备份辅助接口随 gem 分发,不依赖 examples 文件:
222
+
223
+ ```ruby
224
+ require "net/connector/netdisco"
225
+ netdisco = Net::Connector::Netdisco
226
+ options = netdisco::CLI::Options.parse(argv: ["--concurrency", "10"])
227
+ settings = netdisco::CLI::Options.settings(options)
228
+ client, credentials = netdisco::Connection.build(settings)
229
+ directory = Net::Connector::Storage::BatchDirectory.create("./backups")
230
+ # 获得 report 和 plan 后:
231
+ # report = netdisco::Report::Files.write(report, directory: directory, plan: plan, concurrency: 10)
232
+ ```
233
+
234
+ `CLI::Options.parse` 接受 `argv/input/output/error/program`,不修改传入参数数组;帮助通过返回值 `:help` 通知调用方,非法输入抛出 `ArgumentError`。`Connection.build` 接受 `input:`,标准输入凭据仅消费一行,RUN 确认由脚本编排负责。模块不加载 `.env` 或切换目录。
235
+
236
+ 批量示例由 `Netdisco::BackupRun` 统一编排,`CLI::Options.settings` 为示例和正式 CLI 共用配置覆盖入口。
237
+ 示例默认 `selected`,正式 CLI 保留 `strict` 默认;两者均支持显式 `--success-policy`,退出码与报告的 `policy_success` 一致。
238
+ 新示例 `summary.json` 使用标准 schema 2 的 `devices`,不再另造 `outcomes`;旧报告可继续用 `review_tftp.rb` 读取。
239
+ TFTP 的 `verification` 汇总与设备条目的 `server_file_verified` 区分上传回执和服务器文件核验,核验成功还包含 `local_file`、`server_archive_file`、`bytes`、`sha256`。
240
+ 报告通过私有原子写入保存;报告保存失败会阻止成功退出,终端最终结果在报告保存之后输出。
241
+
242
+ `--stdin-credentials` 的一行 JSON 保留 `netdisco_username`、`netdisco_password`、`device_username`、`device_password` 四个字段。
243
+ 清单来源为 PostgreSQL 时,前两个字段覆盖数据库用户名和密码,主机和数据库名仍由 `ND_DB_HOST`、`ND_DB_NAME` 提供,TLS 参数来自 YAML;第二行 `RUN` 仍是执行设备任务的确认。
244
+
245
+ 使用 `accept_new` / `replace` 时,SSH 在每个会话的临时 known_hosts 中协商,认证后通过独立锁文件合并到共享文件。
246
+ 网络登录不占用共享锁;失败登录不删除原信任记录,`replace` 只替换当前主机。该同步协调本库进程,外部手工工具应避免同时改写同一文件。
247
+ 认证后的合并锁另有最多 `login_timeout` 秒的等待预算,超时返回 `ConnectionError`(`code: :known_hosts_busy`),关闭会话且保留共享信任记录;此预算只约束锁等待,不是整批任务的期限。