net-connector 0.7.3 → 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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/CONTRIBUTING.md +12 -0
  4. data/README.md +27 -424
  5. data/README.zh-CN.md +64 -0
  6. data/{exe/net-connector-backup → bin/net-backup} +1 -0
  7. data/docs/VERIFICATION.md +2 -2
  8. data/docs/architecture.md +27 -5
  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 +12 -5
  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 +23 -16
  22. data/lib/net/connector/device/topology/immediate_strategy.rb +2 -1
  23. data/lib/net/connector/engine/authentication.rb +1 -0
  24. data/lib/net/connector/engine/core.rb +1 -0
  25. data/lib/net/connector/engine/dialogue.rb +5 -134
  26. data/lib/net/connector/engine/errors.rb +0 -92
  27. data/lib/net/connector/engine/known_hosts.rb +20 -4
  28. data/lib/net/connector/engine/log.rb +27 -6
  29. data/lib/net/connector/engine/recovery.rb +1 -1
  30. data/lib/net/connector/engine/redactor.rb +98 -0
  31. data/lib/net/connector/engine/response_reader.rb +139 -0
  32. data/lib/net/connector/engine/session.rb +20 -7
  33. data/lib/net/connector/engine/transport.rb +21 -10
  34. data/lib/net/connector/netdisco/backup_run.rb +35 -8
  35. data/lib/net/connector/netdisco/cli/options.rb +20 -8
  36. data/lib/net/connector/netdisco/cli.rb +14 -11
  37. data/lib/net/connector/netdisco/config_file.rb +19 -14
  38. data/lib/net/connector/netdisco/connection.rb +0 -1
  39. data/lib/net/connector/netdisco/database_client.rb +44 -26
  40. data/lib/net/connector/netdisco/device.rb +5 -0
  41. data/lib/net/connector/netdisco/diagnostic.rb +8 -0
  42. data/lib/net/connector/netdisco/fleet.rb +12 -3
  43. data/lib/net/connector/netdisco/plan.rb +16 -3
  44. data/lib/net/connector/netdisco/planner.rb +2 -1
  45. data/lib/net/connector/netdisco/progress.rb +31 -7
  46. data/lib/net/connector/netdisco/report/files.rb +4 -2
  47. data/lib/net/connector/netdisco/result_store.rb +6 -7
  48. data/lib/net/connector/netdisco/rules.rb +8 -5
  49. data/lib/net/connector/netdisco/settings.rb +83 -19
  50. data/lib/net/connector/netdisco/tftp_archive.rb +6 -3
  51. data/lib/net/connector/netdisco/tftp_verification.rb +1 -0
  52. data/lib/net/connector/netdisco/worker.rb +3 -1
  53. data/lib/net/connector/netdisco.rb +1 -21
  54. data/lib/net/connector/storage/private_file.rb +6 -0
  55. data/lib/net/connector/vendor/cisco_ios/running_config.rb +1 -0
  56. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +2 -0
  57. data/lib/net/connector/vendor/cisco_ios/topology.rb +2 -0
  58. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +2 -0
  59. data/lib/net/connector/vendor/h3c/tftp_backup.rb +1 -0
  60. data/lib/net/connector/vendor/h3c/topology.rb +2 -0
  61. data/lib/net/connector/vendor/hillstone/running_config.rb +1 -0
  62. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +2 -0
  63. data/lib/net/connector/vendor/hillstone/topology.rb +2 -0
  64. data/lib/net/connector/vendor/huawei/tftp_backup.rb +2 -1
  65. data/lib/net/connector/vendor/palo_alto/running_config.rb +27 -6
  66. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +2 -0
  67. data/lib/net/connector/vendor/palo_alto/topology.rb +16 -16
  68. data/lib/net/connector/vendor/radware/running_config.rb +1 -0
  69. data/lib/net/connector/vendor/radware/tftp_backup.rb +2 -0
  70. data/lib/net/connector/vendor/radware/topology.rb +1 -0
  71. data/lib/net/connector/version.rb +2 -1
  72. data/lib/net/connector.rb +1 -0
  73. data/lib/net-connector.rb +1 -0
  74. metadata +12 -20
  75. data/lib/net/connector/netdisco/tftp_history.rb +0 -16
@@ -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`),关闭会话且保留共享信任记录;此预算只约束锁等待,不是整批任务的期限。
data/docs/usage.md ADDED
@@ -0,0 +1,124 @@
1
+ # Device guide
2
+
3
+ [简体中文](usage.zh-CN.md) | [Home](../README.md) | [Netdisco and fleet backups](netdisco.md)
4
+
5
+ ## Supported devices
6
+
7
+ Use these vendor keys with `Net::Connector.open` or `Net::Connector.build`:
8
+
9
+ | Vendor key | Devices | Configuration command | Save command |
10
+ | --- | --- | --- | --- |
11
+ | `:h3c` | H3C Comware | `dis cur` | `save force` |
12
+ | `:h3c_wireless` | H3C wireless controllers | `dis cur` | `save force` |
13
+ | `:cisco_ios` | Cisco IOS / IOS XE | `show running-config` | `copy running-config startup-config` |
14
+ | `:cisco_nxos` | Cisco NX-OS | `show running-config` | `copy run start` |
15
+ | `:huawei` | Huawei | `dis cur` | `save force` |
16
+ | `:hillstone` | Hillstone StoneOS | `show configuration running` | `save all` |
17
+ | `:radware` | Radware Alteon | `/cfg/dump` | `/cfg/save` |
18
+ | `:palo_alto` | Palo Alto PAN-OS | Set-format export from configuration view | Unsupported |
19
+
20
+ `device.supports?(:tftp_backup)` checks implemented capabilities without connecting. It does not establish firmware compatibility or account permissions. See the [capability matrix](architecture.md#厂商能力) (Chinese).
21
+
22
+ ## Sessions and local backups
23
+
24
+ ```ruby
25
+ require "net/connector"
26
+
27
+ Net::Connector.open(:cisco_ios,
28
+ host: "192.0.2.10", username: ENV.fetch("NC_DEVICE_USERNAME"),
29
+ password: ENV.fetch("NC_DEVICE_PASSWORD"),
30
+ known_hosts: "/etc/net-connector/known_hosts", host_key_policy: :strict) do |device|
31
+ backup = device.backup(path: "/var/backups/network/router.cfg", lock_timeout: 2)
32
+ puts "#{backup.change}: #{backup.sha256}"
33
+ end
34
+ ```
35
+
36
+ Create and protect the parent directory first. `open` closes the session on return, exception, or interruption. One connector owns one session; competing operations fail with `SessionBusy`.
37
+
38
+ `backup` locks the path before collecting, compares SHA-256, and reports `:created`, `:changed`, or `:unchanged`. Unchanged safe files retain their mtime. Default lock acquisition is immediate; contention raises `BackupBusy`. `lock_timeout:` allows a bounded wait. Call backup outside an existing session operation so path ownership precedes session ownership.
39
+
40
+ Writes use mode `0600`, file synchronization, atomic replacement, and parent-directory synchronization. A collection failure leaves the old file intact. Failure after replacement raises `BackupPersistenceError`; `error.backup` retains metadata and `error.receipt` distinguishes `:committed` from `:durable`. Unsupported directory synchronization is reported as `:backup_durability_unsupported`. Do not repeat the device operation to recover file finalization.
41
+
42
+ Private `.net-connector-<digest>.lock` files remain in the directory. All writers must follow the same locking protocol; ancestor directories remain the caller's responsibility.
43
+
44
+ For an in-memory configuration, use `device.running_config.value!`. The `Result` retains completed steps on failure. Collection suppresses configuration content in logs and error diagnostics, including debug/raw and application loggers; returned output and backup files remain sensitive.
45
+
46
+ ## Device-initiated TFTP
47
+
48
+ ```ruby
49
+ receipt = device.tftp_backup(host: "192.0.2.30", source_file: "flash:/startup.cfg",
50
+ path: "site/switch.cfg") # Huawei or H3C
51
+ ```
52
+
53
+ H3C discovers its saved source with `display startup` when omitted; Huawei requires `source_file:`. IOS uses interactive `copy running-config tftp:`. NX-OS defaults to VRF `management`; Hillstone defaults to `mgt-vr`. These two vendors accept `vrf:`. Radware exports `.tgz`, Hillstone exports startup `.dat`, and PAN-OS always uploads `running-config.xml`.
54
+
55
+ The immutable `TftpReceipt` records `server`, actual `path`, `completed_at`, `configuration_kind`, `source_file`, `format`, `requested_path`, and verification fields. Direct calls return `:device_reported` with no server digest: they confirm device output, not the server file. Explicit source files are not assumed to contain running configuration. TFTP carries configuration without encryption; restrict its network access appropriately.
56
+
57
+ Explicit failure raises `:transfer_failed`; missing completion evidence raises `:transfer_unconfirmed`. A failure after confirmed upload raises `TftpCompletionError` with `error.receipt`. A mismatched explicit path is `:transfer_path_mismatch`; an unconfirmed actual path is `:transfer_path_unconfirmed` with receipt path `nil`. No automatic retransmission occurs. See the [single-device example](../examples/device_tftp.rb) and [server verification](netdisco.md#tftp-history-and-verification).
58
+
59
+ ## TextFSM parsing
60
+
61
+ ```ruby
62
+ interfaces = device.parse_command("show ip interface brief") # Cisco IOS
63
+ rows = device.parse_config(template: "cisco_ios_running_config_interfaces.textfsm")
64
+
65
+ saved = Net::Connector::Storage::SavedConfig.new(directory: "/var/backups/network")
66
+ rows = saved.parse(host: "192.0.2.10", template: "/path/to/template.textfsm")
67
+ ```
68
+
69
+ Parsers return arrays of hashes with template field names. `parse_command` uses vendor/command lookup; `parse_config` requires a template. Pass `template:` or `template_dir:` to select external templates. No matches return `[]`; missing or invalid templates raise `ParsingError`, and device failures retain their connector error.
70
+
71
+ Parsing validates UTF-8 before and after terminal rendering. Invalid bytes raise `:invalid_output_encoding`; no encoding is guessed. Raw results, backups, and exports retain their bytes. Each parse has its own parser, and TextFSM loads only when parsing is requested.
72
+
73
+ ## Topology and interface descriptions
74
+
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!
81
+ ```
82
+
83
+ 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
+
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.
86
+
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.
88
+
89
+ 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
+
91
+ ## Scripts and interactions
92
+
93
+ ```ruby
94
+ script = Net::Connector::Script.parse(<<~CLI, name: "change-123")
95
+ configure terminal
96
+ interface GigabitEthernet1/0/1
97
+ description uplink
98
+ end
99
+ CLI
100
+ result = device.execute_script(script)
101
+ warn "#{result.error.code}: #{result.steps.size} completed commands" if result.failure?
102
+ ```
103
+
104
+ `execute_command` runs one command; `execute_script` accepts a `Script` or command array. `Script.load(path)` reads a file. Commands are validated before I/O; completed steps survive later failures. `save_config` explicitly sends save commands; ordinary scripts do not save automatically.
105
+
106
+ Set `max_script_output_bytes:` to bound cumulative raw responses, including additional queries. Default is `nil`. Exceeding it returns `ScriptOutputLimitExceeded` (`:script_output_limit_exceeded`) and stops subsequent commands; the current command may already have executed. `max_output_bytes` separately limits each response. Neither setting is a process-memory limit.
107
+
108
+ Per-command prompts use `interactions: [Net::Connector::Interaction.new(/Token:\z/, ->(_) { "value\n" }, sensitive: true)]`. Mark commands containing secrets `sensitive: true`. For safe command text with sensitive output, use `output_sensitive: true`; `running_config` enables this automatically.
109
+
110
+ ## Connections, host keys, and logging
111
+
112
+ `Configuration` accepts `protocol: :ssh` or `:telnet`, ports, timeouts, output limits, `known_hosts`, `log_file`, `logger`, `log_format`, and `log_level`. Host-key policy defaults to `:strict`. `:accept_new` registers first-use keys and rejects changed keys; `:replace` requires an explicit dedicated `known_hosts` file and replaces only the affected host on a recognized key-change failure. Never delete the persistent trust file to resolve an error.
113
+
114
+ `telnet_fallback` and `legacy_ssh` are disabled by default and apply only to recognized connection failures. Network login does not hold the shared trust-file lock. Authenticated merging has a separate `login_timeout` wait budget; `:known_hosts_busy` closes the session and preserves the shared file.
115
+
116
+ `log_file` requires a regular file, rejecting symlinks and special files. Text logs use Ruby Logger; `:info` records lifecycle events, `:debug` adds permitted output, and `:raw` writes protected device bytes. Injected `logger: Rails.logger` or a standard Logger keeps its ownership, formatting, and current threshold; the stricter of its level and `log_level` applies.
117
+
118
+ `on_event: ->(event) { ... }` receives frozen, redacted `Log::Event` values synchronously. Keep callbacks short; failures are surfaced as logging failures. Events include session/command IDs, operation, phase, duration, byte counts, and controlled diagnostics. `response_received` confirms a response, not device persistence or TFTP server verification. Custom events use `device.log_event("audit", level: :info, count: 2)`; sensitive scopes hide arbitrary custom payloads.
119
+
120
+ ## Dependencies and extensions
121
+
122
+ Load device APIs with `require "net/connector"`; Netdisco has a separate entrypoint. Vendor implementations and TextFSM load on demand. Runtime dependencies declare minimum versions; the host application's lockfile selects compatible versions. Published TextFSM 0.2.0 constrains JSON to 2.x. The explicit JSON 3 source-patch matrix does not establish compatibility of the published dependency.
123
+
124
+ Examples use the development dependency `dotenv` to read the project `.env`; the library and CLI do not. Follow the [loading and migration reference](architecture.md#加载入口与厂商策略) (Chinese) for custom profiles, strategies, and protected hooks.
@@ -0,0 +1,190 @@
1
+ # 设备操作指南
2
+
3
+ [English](usage.md) | [返回首页](../README.zh-CN.md) | [Netdisco 批量备份](netdisco.zh-CN.md)
4
+
5
+ 支持 Ruby 3.2 及以上版本和 POSIX 系统。SSH 调用本机 OpenSSH,Telnet 需要本机安装 `telnet` 并显式选择。主要依赖为 [`expect-pty`](https://rubygems.org/gems/expect-pty) 0.5.0 及以上和 [`textfsm`](https://rubygems.org/gems/textfsm) 0.2.0 及以上。
6
+
7
+ 脱敏直接复用 expect-pty 从 0.5.0 起公开的 `Expect::Redactor` 接口;连接器只管理秘密作用域和配置输出的隐私策略。
8
+
9
+ 运行时依赖只声明所需最低版本,不设置缺少兼容性依据的上限;宿主应用通过自己的 Gemfile/锁文件选择版本。正式发布依赖的安装验证使用 JSON 2。RubyGems 上 textfsm 0.2.0 仍约束 json ~> 2.0;JSON 3 使用固定 TextFSM 源码和显式依赖声明补丁单独验证,不代表公开依赖已经支持 JSON 3。开发工具及兼容测试矩阵的版本约束不影响 gem 使用者。
10
+
11
+ 示例脚本通过开发依赖 `dotenv` 自动读取项目根目录 `.env`(先执行 `bundle install`),已有进程环境变量优先;库和 CLI 本身不会隐式读取 `.env`。
12
+
13
+ ## 支持的设备
14
+
15
+ `device.supports?(:tftp_backup)` 等能力查询不会连接设备,只表示该连接器实现了对应操作,不代表设备权限或固件一定支持。完整能力矩阵和扩展示例见[厂商能力](architecture.md#厂商能力)。
16
+
17
+ | 厂商标识 | 设备系列 | 运行配置命令 | 保存配置命令 |
18
+ | --- | --- | --- | --- |
19
+ | `:h3c` | H3C Comware | `dis cur` | `save force` |
20
+ | `:h3c_wireless` | H3C 无线控制器 | `dis cur` | `save force` |
21
+ | `:cisco_ios` | Cisco IOS / IOS XE | `show running-config` | `copy running-config startup-config` |
22
+ | `:cisco_nxos` | Cisco NX-OS | `show running-config` | `copy run start` |
23
+ | `:radware` | Radware Alteon | `/cfg/dump` | `/cfg/save` |
24
+ | `:palo_alto` | Palo Alto PAN-OS | 候选视图中的 set 格式导出 | 不支持 |
25
+ | `:huawei` | 华为 | `dis cur` | `save force` |
26
+ | `:hillstone` | 山石 StoneOS | `show configuration running` | `save all` |
27
+
28
+ 厂商标识仅使用上表的规范名称。厂商连接器位于 `Net::Connector` 下,例如 `Net::Connector::H3cWireless::Connector`。
29
+
30
+ ## 登录与本地备份
31
+
32
+ ```ruby
33
+ Net::Connector.open(:cisco_ios,
34
+ host: "192.0.2.10", username: "admin", password: ENV.fetch("NC_DEVICE_PASSWORD"),
35
+ known_hosts: "/etc/net-connector/known_hosts", host_key_policy: :strict,
36
+ log_file: "/var/log/net-connector/router.log") do |device|
37
+ backup = device.backup(path: "/var/backups/router-running.cfg")
38
+ puts "#{backup.bytes} 字节,SHA-256 #{backup.sha256}"
39
+ end
40
+ ```
41
+
42
+ `open` 会在代码块结束或出错时关闭会话。`backup` 在采集前取得目标路径锁,持有到比较、保存和生成原有 `Backup` 元数据对象完成;采集失败不会覆盖旧备份。同路径争用默认立即抛出 `BackupBusy`(`code: :backup_busy`),需要有限等待可传 `lock_timeout: 2`,单位为秒。直接调用 `backup`;不要在已有 `with_operation` 会话租约内调用,否则会因锁顺序返回 `SessionBusy`。
43
+
44
+ 写入使用 `0600` 临时文件、文件同步、原子替换及父目录同步。替换后同步或收尾失败会抛出 `BackupPersistenceError`,`error.backup` 保留已写文件的元数据,`error.receipt` 区分 `:committed`(持久性未确认)和 `:durable`(同步已完成)。目录同步不受支持对应 `:backup_durability_unsupported`,不会静默报告持久化成功,也不会自动重做设备采集。
45
+
46
+ 调用方应创建并保护备份目录,运行配置可能含有设备凭据。同目录下的 `.net-connector-<摘要>.lock` 是私有的长期锁文件,正常释放不删除它。所有写入者须使用本库的锁协议;`NOFOLLOW` 不保护被外部替换的祖先目录。锁名保守合并大小写及 Unicode 等价写法,实际备份文件名不变;在区分大小写的文件系统上,这些名称也会串行。
47
+
48
+ ## 设备发起的 TFTP 备份
49
+
50
+ `tftp_backup` 让设备把原生配置直接上传到 TFTP 服务器,命令和提示交互由厂商策略提供。H3C 和华为从设备文件读取源配置,例如:
51
+
52
+ ```ruby
53
+ Net::Connector.open(:huawei, host: "192.0.2.20", username: ENV.fetch("NC_DEVICE_USERNAME"),
54
+ password: ENV.fetch("NC_DEVICE_PASSWORD")) do |device|
55
+ transfer = device.tftp_backup(host: "192.0.2.30", source_file: "flash:/startup.cfg")
56
+ puts "已上传 #{transfer.path} 到 #{transfer.server}"
57
+ end
58
+ ```
59
+
60
+ 示例会下发 `tftp 192.0.2.30 put flash:/startup.cfg`;可传入 `path: "site/switch.cfg"` 指定目标文件名。H3C 默认通过 `display startup` 查找保存配置;华为因型号差异需要显式指定 `source_file:`。Cisco IOS 使用交互式 `copy running-config tftp:`;Nexus 9000 默认使用 `vrf management`,可用 `vrf:` 覆盖。山石导出已保存的启动配置,Radware Alteon 生成 `.tgz` 并处理私钥及 `mansync` 提示,PAN-OS 使用固定的 `running-config.xml` 文件名。其他厂商默认使用 `<管理地址>.cfg`。
61
+
62
+ 只有设备回显确认传输完成,`tftp_backup` 才返回不可变的 `TftpReceipt`。回执直接包含 `server`、实际目标 `path`、`completed_at`、`configuration_kind`、`source_file`、`format`、`requested_path`、`verification` 和 `server_sha256`。明确失败对应 `:transfer_failed`,缺少成功证据对应 `:transfer_unconfirmed`。本方法不读取服务器上的文件。TFTP 不加密配置数据,应限制在合适的管理网络中使用。
63
+
64
+ 回执的当前验证等级为 `:device_reported`,服务器摘要为 `nil`。H3C 自动探测结果标为 `:startup`;H3C/华为显式文件标为 `:saved_file`,格式为 `:unknown`,不凭扩展名推断内容。山石未指定文件名时 `requested_path` 为 `nil`,`path` 保留设备生成名称。
65
+
66
+ 策略先校验参数组合,再在同一次会话租约中完成源探测、上传、证据检查和收尾。上传已确认但日志或清理失败时抛出 `TftpCompletionError`,通过 `error.receipt` 保留完成事实。显式请求与实际路径不同返回 `:transfer_path_mismatch`;实际路径无法安全确认返回 `:transfer_path_unconfirmed`,此时回执的 `path` 为 `nil`。这些错误不会自动重传;Fleet 保留上传事实并标为 `reported_with_error`。
67
+
68
+ 可运行[单设备示例](../examples/device_tftp.rb):`ruby examples/device_tftp.rb --vendor huawei --host 192.0.2.20 --server 192.0.2.30 --source-file flash:/startup.cfg --path site/router.cfg`。凭据使用 `NC_DEVICE_USERNAME`、`NC_DEVICE_PASSWORD`;`--source-file`、`--path`、`--vrf` 指定传输选项。
69
+
70
+ 只需在内存中采集配置时,调用 `device.running_config`;它返回 `Result`,`result.value!` 返回清理后的文本,失败时抛出对应错误。
71
+
72
+ 配置采集统一由 `device/running_config` 提供,厂商差异位于 `vendor/<厂商>/running_config`。
73
+ 旧 `operations/running_config`、TFTP / 拓扑厂商转发路径及 `engine/base` 等设备转发入口已移除;
74
+ 自定义扩展请按[加载入口与迁移表](architecture.md#加载入口与厂商策略)使用当前路径和常量。
75
+
76
+ 子类可覆盖 protected 的 `login_interactions`、`confirmation_interactions`,通过 `super` 取得档案中的交互数组再追加规则。原 `login_dialogues`、`confirmation_dialogues` 已改名,不保留别名;这两个钩子仍不属于应用层公开调用入口。
77
+
78
+ 配置采集默认屏蔽日志和错误诊断中的配置正文,包括 debug/raw 日志和外部 logger;返回的配置、步骤输出和备份内容保持完整。调用方应按敏感数据保管这些业务结果。
79
+
80
+ ## TextFSM 解析
81
+
82
+ `parse_command` 执行一条命令,再按厂商和命令选择 TextFSM 模板。内置索引覆盖 Cisco IOS 的 `show ip interface brief`,以及拓扑发现使用的 CDP/LLDP 命令:
83
+
84
+ ```ruby
85
+ Net::Connector.open(:cisco_ios, host: "192.0.2.10", username: "admin",
86
+ password: ENV.fetch("NC_DEVICE_PASSWORD")) do |device|
87
+ interfaces = device.parse_command("show ip interface brief")
88
+ puts interfaces.first.fetch("INTERFACE")
89
+ end
90
+ ```
91
+
92
+ `parse_config` 采集运行配置,要求显式指定模板。内置 Cisco IOS 模板只提取接口名称和描述,并非完整配置模型:
93
+
94
+ ```ruby
95
+ interfaces = device.parse_config(template: "cisco_ios_running_config_interfaces.textfsm")
96
+ ```
97
+
98
+ 两种方法都返回由模板字段名组成的哈希数组;匹配不到记录时返回 `[]`。模板缺失或无效会抛出 `ParsingError`,设备命令失败则保留原始连接器错误。可用 `template:` 指定外部模板,或用 `template_dir:` 指定含 `index` 的模板目录。每次解析使用独立解析器,批量任务之间不共享状态。已有本地备份也可离线解析:
99
+
100
+ ```ruby
101
+ saved = Net::Connector::Storage::SavedConfig.new(directory: "/var/backups")
102
+ rows = saved.parse(host: "192.0.2.10", template: "/path/to/template.textfsm")
103
+ ```
104
+
105
+ 解析默认严格检查 UTF-8 字节,包括终端控制符处理前的原文和处理后的文本;非法字节会抛出 `ParsingError`(`code: :invalid_output_encoding`),不会转成替换文本继续生成记录或拓扑计划。库不猜测或自动转换其他编码,原始结果、备份和离线导出保持原字节。Netdisco 入口及纯文件导出不加载 TextFSM,实际解析时才加载。
106
+
107
+ ## 邻居发现与接口描述
108
+
109
+ `neighbors` 在 Cisco IOS/NX-OS 上查询 CDP,在 H3C、H3C 无线、山石和 PAN-OS 上查询 LLDP。记录包含 `local_interface`、`neighbor_name`、`neighbor_interface`、`chassis_id` 和 `protocol`。H3C 会按表头选择不同列顺序的模板;未知输出会抛出 `ParsingError`,不会误判为空表。`interface_descriptions` 从运行配置读取当前描述,Radware 则从配置转储读取端口名称。
110
+
111
+ ```ruby
112
+ Net::Connector.open(:h3c, host: "192.0.2.10", username: ENV.fetch("NC_DEVICE_USERNAME"),
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!
120
+ end
121
+ ```
122
+
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`。
124
+
125
+ 规划会拒绝歧义邻居、缺少对端身份、不安全文本和无法识别的输出。下发必须传 `confirmed: true`,操作会再次读取邻居和旧描述;证据变化在写入前抛出 `DeviceError`,其 `code` 为 `:stale_plan`。IOS/NX-OS、H3C 和山石采用“修改 → 退出配置视图 → 读回 → 保存”的顺序;读回不匹配返回 `:description_unconfirmed`,解析不完整或查询命令不符合审批时也不会保存。
126
+
127
+ `plan.commands` 现在包括读回命令,旧版计划必须重新生成、审核;执行仍返回 `Result`,保留修改、读回及保存阶段已经完成的步骤。只有设备提供明确保存完成行才成功,保存超时、失败或只有提示符会返回 `:persistence_unconfirmed`,此时描述可能已生效,不能自动重放或回滚。现场下发前仍应按设备固件核对命令与回显。
128
+
129
+ PAN-OS 的自动描述计划及改写暂不提供,`supports?(:interface_description_changes)` 为 false,入口在设备 I/O 前抛出 `:candidate_isolation_unavailable`。候选配置的归属、验证及提交需要经目标固件实验确认的独立流程;目前可继续只读查询邻居和描述。Radware 支持读取端口名称,但不提供邻居发现或自动改写,其 `neighbors` 返回 `:neighbor_discovery_unsupported`。
130
+
131
+ PAN-OS 在导出前后检查候选配置差异,并拒绝 XML 或非 set 格式输出,避免将未提交配置误作运行配置。
132
+
133
+ ## 命令脚本与自动交互
134
+
135
+ ```ruby
136
+ script = Net::Connector::Script.parse(<<~CLI, name: "change-123")
137
+ # 独立成行的注释会被忽略。
138
+ configure terminal
139
+ interface GigabitEthernet1/0/1
140
+ description uplink
141
+ end
142
+ CLI
143
+
144
+ result = device.execute_script(script) do |step|
145
+ puts "#{step.command.text}: #{step.duration.round(2)} 秒"
146
+ end
147
+
148
+ if result.failure?
149
+ warn "#{result.error.code},阶段 #{result.error.phase}"
150
+ warn "失败前已完成 #{result.steps.size} 条命令"
151
+ end
152
+ ```
153
+
154
+ `execute_command` 执行一条命令;`execute_script` 接收 `Script` 或命令数组;`Script.load(path)` 读取脚本文件。所有命令在设备 I/O 前校验,后续步骤失败时仍保留已完成结果,库不会自动重放命令。`save_config` 显式执行厂商保存命令,普通脚本不会自动保存。
155
+
156
+ 创建连接器时可设置 `max_script_output_bytes: 8 * 1024 * 1024`,限制每个脚本及其追加查询累计收到的原始响应字节;默认 `nil` 保持原有完整输出行为。达到上限后不再发送下一命令,当前响应超过上限时保留刚完成的步骤并返回 `ScriptOutputLimitExceeded`(`code: :script_output_limit_exceeded`)。命令可能已经执行,不会自动重试。原有 `max_output_bytes` 继续限制单次读取响应;累计预算不是进程内存上限,详细计数规则见架构文档。
157
+
158
+ 厂商档案处理分页和常见确认提示。特定命令可给 `execute_command` 传入 `interactions: [Net::Connector::Interaction.new(/Token:\z/, ->(_) { "value\n" }, sensitive: true)]`。敏感命令及交互会暂停回显日志并在错误中脱敏;若普通命令文本包含秘密,必须显式标记 `sensitive: true`。
159
+
160
+ 命令文本安全、输出可能包含秘密时,使用 `device.execute_command("show running-config", output_sensitive: true)`。该标记保护响应及其准备、后处理、回调异常,保留安全命令文字;不会改写 `Result` 的业务输出。`running_config` 自动启用此保护,手写脚本的默认值仍为 `false`。
161
+
162
+ ## 连接与日志设置
163
+
164
+ 连接参数 `on_event: ->(event) { ... }` 接收不可变、已脱敏的 `Log::Event`,可与日志文件或应用 logger 共用;按 `log_level` 过滤,独立于应用 logger 的阈值。回调同步执行,应保持简短;异常按日志故障处理,不会静默吞掉。它不提供未脱敏的配置正文。
165
+
166
+ `Configuration` 支持 `protocol: :ssh`(默认)或 `:telnet`,以及端口、超时、输出大小、`log_file`、`logger`、`log_format`、`log_level` 和 `known_hosts` 等参数。文本日志使用 Ruby 标准库 `Logger`,记录毫秒时间、级别、设备、中文说明和完整事件字段。`:info` 包括连接、登录、命令响应、脚本处理和 TFTP 结果;`:debug` 增加逐行脱敏回显;`:warn`、`:error` 只保留相应级别。`:raw` 文件只写经过现有敏感保护的设备字节,不添加事件字段。
167
+
168
+ `log_file` 只接受普通文件,拒绝符号链接、FIFO 和设备文件;应用管理的输出流可通过 `logger` 注入。命令已经完成后若日志或事件回调失败,失败结果仍保留完成步骤,TFTP 错误仍携带已确认的设备回执;调用方不能据此自动重放命令。
169
+
170
+ 每次连接生成 `session_id`,每条实际发送的命令分配 `command_id`;日志还包含 `operation`、`phase`、脚本 `source` / `line`、`duration_ms`、`response_bytes` 和失败 `code`。`command_complete` 的 `response_received` 只表示收到了提示符;`operation_complete` 覆盖脚本准备、执行及后处理,不替代 TFTP 服务端核验或设备持久化证据。普通自定义事件使用 `device.log_event("audit", level: :info, count: 2)`。
171
+
172
+ 可注入 `logger: Rails.logger` 或普通 `Logger`。有效级别取 `log_level` 与调用方**当前**级别中较严格的一项;连接器不修改它的级别、formatter 或 progname,也不关闭它。消息是已脱敏且冻结的 `Net::Connector::Log::Event`,`to_s` 供文本显示,`to_h` 供应用 formatter 输出 JSON:
173
+
174
+ ```ruby
175
+ require "net/connector"
176
+ require "json"
177
+ require "logger"
178
+ require "time"
179
+
180
+ logger = Logger.new($stdout)
181
+ logger.formatter = lambda do |severity, time, program, message|
182
+ fields = message.is_a?(Net::Connector::Log::Event) ? message.to_h : { message: message.to_s }
183
+ "#{JSON.generate(time: time.iso8601(3), severity: severity, program: program, **fields)}\n"
184
+ end
185
+ # 将 logger: logger 传给 Net::Connector.build / open。
186
+ ```
187
+
188
+ 事件字段接受字符串、符号、整数、有限浮点数、布尔和 nil;复杂对象统一隐藏,不展开对象内容。事件名、字段名和值经过校验/脱敏,会话与命令标识不可由自定义字段覆盖。敏感命令或配置处理期间,自定义事件的名称和载荷整体隐藏,避免钩子把未登记的配置秘密写进日志。更多边界见[架构文档](architecture.md)。
189
+
190
+ 主机密钥策略默认为 `:strict`;`:accept_new` 接受首次连接的密钥;`:replace` 需要显式 `known_hosts` 文件。`telnet_fallback` 和 `legacy_ssh` 默认关闭,只在已识别的连接失败时使用。外部命令以参数数组执行,不经 shell。Telnet 不提供 SSH 加密,只应在可信管理网络启用。设备授权和变更审批由调用方负责。
data/examples/backup.yml CHANGED
@@ -1,9 +1,9 @@
1
- # Non-secret settings only. Credentials belong in environment variables.
2
- # Common NC_* environment variables override their YAML counterparts.
1
+ # 仅保存非敏感策略,凭据使用环境变量。
2
+ # ND_URL 可覆盖 netdisco.url;其他公开策略由 YAML 或 CLI 设置。
3
3
  backup:
4
4
  directory: ./examples/backups
5
5
  concurrency: 4
6
- # limit_per_vendor: 3 # Omit for full inventory.
6
+ # limit_per_vendor: 3 # 省略时使用全量清单。
7
7
  netdisco:
8
8
  source: http
9
9
  page_size: 500
@@ -27,13 +27,14 @@ inventory:
27
27
  ssh:
28
28
  protocol: ssh
29
29
  vendor_protocols: {}
30
- host_key_policy: accept_new # Persist first-seen keys; reject changed keys.
30
+ host_key_policy: accept_new # 保存首次见到的密钥,拒绝已有密钥变化。
31
31
  log_level: info
32
32
  max_script_output_bytes: null
33
33
  tftp:
34
34
  vrfs:
35
35
  cisco_nxos: management
36
36
  hillstone: mgt-vr
37
+ # verification_root: /srv/tftp
37
38
  # server: 192.0.2.10
38
39
  # h3c_source_file: flash:/startup.cfg
39
40
  # h3c_wireless_source_file: flash:/startup.cfg
@@ -1,6 +1,10 @@
1
- # 数据库连接只从 NETDISCO_DB_* 环境变量读取;此文件可直接编辑并纳入版本控制。
1
+ # 数据库身份从 ND_DB_HOST/NAME/USER/PASS 读取;非敏感连接策略在此设置。
2
2
  netdisco:
3
3
  source: postgres
4
+ db_port: 5432
5
+ db_sslmode: verify-full
6
+ # db_sslrootcert: /etc/net-connector/database-ca.crt
7
+ # db_connect_timeout: 10
4
8
  query: |
5
9
  SELECT host(ip) AS ip, name, dns, vendor, os, model, os_ver, serial
6
10
  FROM device