net-connector 0.8.0 → 0.10.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -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 +3 -3
  8. data/docs/architecture.md +25 -9
  9. data/docs/netdisco.md +229 -0
  10. data/docs/netdisco.zh-CN.md +291 -0
  11. data/docs/usage.md +141 -0
  12. data/docs/usage.zh-CN.md +204 -0
  13. data/examples/backup.yml +5 -4
  14. data/examples/inventory_sql.yml +5 -1
  15. data/lib/net/connector/background_task.rb +69 -0
  16. data/lib/net/connector/device/base.rb +5 -4
  17. data/lib/net/connector/device/local_backup.rb +3 -0
  18. data/lib/net/connector/device/running_config/strategy.rb +2 -1
  19. data/lib/net/connector/device/running_config.rb +2 -0
  20. data/lib/net/connector/device/tftp/receipt.rb +1 -0
  21. data/lib/net/connector/device/tftp/strategy.rb +28 -9
  22. data/lib/net/connector/device/tftp.rb +9 -8
  23. data/lib/net/connector/device/topology/immediate_strategy.rb +2 -1
  24. data/lib/net/connector/device/topology/interface_description.rb +3 -3
  25. data/lib/net/connector/device/topology/interface_name.rb +10 -19
  26. data/lib/net/connector/device/topology/stages.rb +50 -0
  27. data/lib/net/connector/device/topology.rb +65 -24
  28. data/lib/net/connector/device_session.rb +38 -0
  29. data/lib/net/connector/engine/core.rb +1 -0
  30. data/lib/net/connector/engine/dialogue.rb +5 -2
  31. data/lib/net/connector/engine/known_hosts.rb +20 -4
  32. data/lib/net/connector/engine/log.rb +14 -9
  33. data/lib/net/connector/engine/recovery.rb +1 -1
  34. data/lib/net/connector/engine/session.rb +14 -5
  35. data/lib/net/connector/engine/transport.rb +36 -15
  36. data/lib/net/connector/execution_pool.rb +108 -0
  37. data/lib/net/connector/netdisco/backup_run.rb +27 -7
  38. data/lib/net/connector/netdisco/cli/options.rb +18 -8
  39. data/lib/net/connector/netdisco/cli.rb +20 -9
  40. data/lib/net/connector/netdisco/client.rb +5 -6
  41. data/lib/net/connector/netdisco/config_file.rb +22 -14
  42. data/lib/net/connector/netdisco/database_client.rb +73 -39
  43. data/lib/net/connector/netdisco/device.rb +3 -0
  44. data/lib/net/connector/netdisco/diagnostic.rb +4 -0
  45. data/lib/net/connector/netdisco/fleet.rb +70 -38
  46. data/lib/net/connector/netdisco/inventory_cache.rb +218 -0
  47. data/lib/net/connector/netdisco/operation_batch.rb +154 -0
  48. data/lib/net/connector/netdisco/operations.rb +268 -0
  49. data/lib/net/connector/netdisco/plan.rb +12 -3
  50. data/lib/net/connector/netdisco/planner.rb +26 -17
  51. data/lib/net/connector/netdisco/progress.rb +31 -10
  52. data/lib/net/connector/netdisco/report/files.rb +1 -0
  53. data/lib/net/connector/netdisco/result_store.rb +1 -0
  54. data/lib/net/connector/netdisco/rules.rb +7 -5
  55. data/lib/net/connector/netdisco/settings.rb +111 -23
  56. data/lib/net/connector/netdisco/tftp_archive.rb +6 -1
  57. data/lib/net/connector/netdisco/tftp_verification.rb +36 -11
  58. data/lib/net/connector/netdisco/worker.rb +7 -46
  59. data/lib/net/connector/storage/private_file.rb +6 -0
  60. data/lib/net/connector/vendor/cisco_ios/running_config.rb +1 -0
  61. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +2 -0
  62. data/lib/net/connector/vendor/cisco_ios/topology.rb +2 -0
  63. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +2 -0
  64. data/lib/net/connector/vendor/h3c/tftp_backup.rb +1 -0
  65. data/lib/net/connector/vendor/h3c/topology.rb +3 -1
  66. data/lib/net/connector/vendor/hillstone/running_config.rb +1 -0
  67. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +2 -0
  68. data/lib/net/connector/vendor/hillstone/topology.rb +3 -1
  69. data/lib/net/connector/vendor/huawei/tftp_backup.rb +2 -1
  70. data/lib/net/connector/vendor/palo_alto/running_config.rb +1 -0
  71. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +2 -0
  72. data/lib/net/connector/vendor/palo_alto/topology.rb +16 -16
  73. data/lib/net/connector/vendor/radware/running_config.rb +1 -0
  74. data/lib/net/connector/vendor/radware/tftp_backup.rb +2 -0
  75. data/lib/net/connector/vendor/radware/topology.rb +1 -0
  76. data/lib/net/connector/version.rb +2 -1
  77. data/lib/net/connector.rb +1 -0
  78. data/lib/net-connector.rb +1 -0
  79. metadata +14 -16
@@ -0,0 +1,291 @@
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 地址,`NC_CONCURRENCY` 覆盖 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`、环境变量 `NC_CONCURRENCY` 或 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_CONCURRENCY` | 默认 4,范围 1 至 50 | 备份、配置批次和接口修正共用的并发数 |
151
+ | `ND_CACHE_TTL` | 默认 259200 秒(三天) | 清单缓存有效期,须为有限正数,优先于 YAML `netdisco.cache_ttl` |
152
+ | `NC_CONFIG` | 未设置 | CLI / 批量示例的非敏感 YAML 配置文件 |
153
+ | `ND_USERNAME`, `ND_PASSWORD` | 未提供 API 密钥时必填 | 清单 API 登录 |
154
+ | `ND_API_KEY` | 未设置 | 直接使用已有 API 密钥 |
155
+ | `ND_DB_HOST`, `ND_DB_NAME` | PostgreSQL 模式必填 | 数据库主机或 Unix socket 目录、数据库名 |
156
+ | `ND_DB_USER`, `ND_DB_PASS` | PostgreSQL 模式必填 | 仅从环境注入的数据库用户名、密码 |
157
+ | `NC_DEVICE_USERNAME`, `NC_DEVICE_PASSWORD` | 未设置 | 设备登录默认凭据 |
158
+ | `NC_<VENDOR>_USERNAME`, `NC_<VENDOR>_PASSWORD` | 未设置 | 单厂商凭据,例如 `CISCO_IOS` |
159
+ | `NC_ENABLE_PASSWORD`, `NC_<VENDOR>_ENABLE_PASSWORD` | 未设置 | 可选提权密码 |
160
+
161
+ 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,旧变量和已移除的设置会明确报错,不输出变量值。
162
+
163
+ 将原来的环境配置移到 YAML:清单预算放在 `netdisco`;过滤、厂商/主机映射及规则放在 `inventory`;脚本输出预算与厂商协议放在 `ssh`;VRF 和源文件放在 `tftp`。例如:
164
+
165
+ ```yaml
166
+ inventory:
167
+ include_vendors: [h3c, cisco_nxos]
168
+ device_rules:
169
+ - vendor: Cisco
170
+ model_prefix: N9K
171
+ connector: cisco_nxos
172
+ ssh:
173
+ vendor_protocols:
174
+ h3c: ssh
175
+ max_script_output_bytes: 16777216
176
+ ```
177
+
178
+ `Fleet` 每次规划或执行前通过 `Settings#snapshot(mode:)` 固定非敏感设置,包括筛选规则、目录、并发、协议、主机密钥、日志、TFTP 参数和清单预算。执行中修改 ENV 不改变当批策略;再次调用会读取新值。CLI 一次调用的规划与执行共用策略,优先级为 CLI > 常用环境变量 > YAML > 默认值。`Settings#validate!(mode:)` 复用连接配置及 Planner 的枚举和范围规则;`--show-config` 会拒绝非法设置,`--export` 只验证离线目录,不要求清单地址或认证。
179
+
180
+ 每台任务开始时仍读取设备凭据,`Settings.from_file` 也保留轮换能力。纯策略快照不保存密码、API key 或凭据解析器。需要让多次 API 调用共用策略时,可传 `Settings.new.for_run(mode: :backup)`。自定义 `credentials:` 解析器仍可逐设备返回连接设置,它显式给出的选项优先于批次默认值,由调用方负责一致性;注入的 `client:` 生命周期也由调用方管理。传入 `plan:` 的执行不会重新拉取或筛选已批准清单。`fleet.devices` 可在不连接设备时检查映射。
181
+
182
+ ## 批量示例与进度
183
+
184
+ `examples/backup.rb` 和 `examples/backup_tftp.rb` 默认全量备份符合筛选条件的设备,并在终端原地刷新两行进度(登录、采集、排队、成功和未完成数,以及耗时、平均吞吐、预计剩余时间);失败单独输出,结束后显示汇总和报告路径。重定向时每 25 台输出一次进度,避免刷屏。`--verbose` 显示逐条登录和命令事件,`--json` 才在 STDOUT 输出 JSON;完整计划和结果始终保存在批次目录。`--no-progress` 关闭人类进度。百分比表示任务完成比例,不是成功率。
185
+
186
+ ```sh
187
+ ruby examples/backup.rb # 全量,默认并发 4
188
+ ruby examples/backup.rb --sample 3 # 每厂商最多 3 台
189
+ ruby examples/backup.rb --verbose # 详细命令过程
190
+ ruby examples/backup.rb --json > result.jsonl
191
+ ruby examples/backup.rb --concurrency 10 --username backup-user --ask-password
192
+ ```
193
+
194
+ 批量示例支持 `--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` 的相对路径基于项目根目录。
195
+
196
+ ## 部署、示例与验证
197
+
198
+ ```sh
199
+ bundle install
200
+ script/ci
201
+ ```
202
+
203
+ CI 在 Linux 和 macOS 上覆盖 Ruby 3.2、3.3、3.4、4.0。`script/ci` 扫描源码与可用 Git 历史中的敏感数据,执行 Ruby 与工作流 lint、完整测试,并验证已构建 gem 的隔离安装和本地 PTY 烟测;不连接真实网络设备。首次运行需下载固定校验和的 Gitleaks 与 actionlint。
204
+
205
+ `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/` 下。
206
+
207
+ 参与开发见 [CONTRIBUTING.md](../CONTRIBUTING.md),漏洞报告见 [SECURITY.md](../SECURITY.md)。README 和使用指南提供英文、中文版本;实现注释与架构参考使用中文,欢迎中文或英文的问题与 PR。
208
+
209
+ 真实凭据应放在环境变量和版本库外的本地配置中。检查范围、依赖政策和忽略规则见[验证文档](VERIFICATION.md),发布流程见[发布文档](RELEASING.md)。
210
+
211
+ 备份示例通过 `examples/boot.rb` 读取项目根目录 `.env`,不会自动切换依赖或注入源码路径。直接 `ruby examples/backup.rb`(或在 `examples/` 中执行 `ruby backup.rb`)使用已安装的 gem;修改库后必须重新构建并安装,开发源码验证则使用 `bundle exec ruby examples/backup.rb`。示例中的相对备份、日志路径统一基于项目根目录;进程中显式传入的 `NC_CONFIG` 路径按启动目录解析。
212
+
213
+ 清单自动识别保留显式主机映射、设备规则和厂商覆盖的优先级。若厂商标签陈旧,而 `os: Comware` 与 `model: H3C ...` 同时确认 H3C,则自动使用 H3C 连接器;`H3C WX...` / `H3C AC...` 使用无线连接器。仅型号片段或操作系统单项不会覆盖其他厂商。
214
+
215
+ 备份部署示例 `examples/backup.yml` 使用 `ssh.host_key_policy: accept_new`:首次连接自动登记到 OpenSSH known_hosts,已有密钥变化仍拒绝连接。`--host-key-policy strict|accept_new` 可覆盖本次策略,`--known-hosts FILE` 指定持久保存位置;不应删除密钥文件,否则会丢失历史身份记录。库本身默认仍为 strict。
216
+
217
+ 设备密钥变化也需自动更新时,使用 `--host-key-policy replace --known-hosts FILE`,或在 YAML 设置 `ssh.host_key_policy: replace` 与 `ssh.known_hosts`。文件必须显式指定;只在主机密钥变化错误时删除该设备旧记录并重连一次,不重放设备命令。建议使用备份任务专用的持久密钥文件。
218
+
219
+ 进度每秒更新一次心跳;设备无新输出时仍显示耗时,超过 10 秒的最久任务显示地址、阶段与任务耗时。预计剩余时间按已完成任务的平均吞吐计算,前五台完成前显示“估算中”,异构设备和末尾慢任务会使估算波动。终端事件刷新最多约每秒五次,退出或异常时回收显示线程;结束按错误代码汇总未完成任务。
220
+
221
+ 本地批量示例将配置写为 `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`,该模式设备改名会改变文件路径与比较基线。
222
+
223
+ 公共备份辅助接口随 gem 分发,不依赖 examples 文件:
224
+
225
+ ```ruby
226
+ require "net/connector/netdisco"
227
+ netdisco = Net::Connector::Netdisco
228
+ options = netdisco::CLI::Options.parse(argv: ["--concurrency", "10"])
229
+ settings = netdisco::CLI::Options.settings(options)
230
+ client, credentials = netdisco::Connection.build(settings)
231
+ directory = Net::Connector::Storage::BatchDirectory.create("./backups")
232
+ # 获得 report 和 plan 后:
233
+ # report = netdisco::Report::Files.write(report, directory: directory, plan: plan, concurrency: 10)
234
+ ```
235
+
236
+ `CLI::Options.parse` 接受 `argv/input/output/error/program`,不修改传入参数数组;帮助通过返回值 `:help` 通知调用方,非法输入抛出 `ArgumentError`。`Connection.build` 接受 `input:`,标准输入凭据仅消费一行,RUN 确认由脚本编排负责。模块不加载 `.env` 或切换目录。
237
+
238
+ 批量示例由 `Netdisco::BackupRun` 统一编排,`CLI::Options.settings` 为示例和正式 CLI 共用配置覆盖入口。
239
+ 示例默认 `selected`,正式 CLI 保留 `strict` 默认;两者均支持显式 `--success-policy`,退出码与报告的 `policy_success` 一致。
240
+ 新示例 `summary.json` 使用标准 schema 2 的 `devices`,不再另造 `outcomes`;旧报告可继续用 `review_tftp.rb` 读取。
241
+ TFTP 的 `verification` 汇总与设备条目的 `server_file_verified` 区分上传回执和服务器文件核验,核验成功还包含 `local_file`、`server_archive_file`、`bytes`、`sha256`。
242
+ 报告通过私有原子写入保存;报告保存失败会阻止成功退出,终端最终结果在报告保存之后输出。
243
+
244
+ `--stdin-credentials` 的一行 JSON 保留 `netdisco_username`、`netdisco_password`、`device_username`、`device_password` 四个字段。
245
+ 清单来源为 PostgreSQL 时,前两个字段覆盖数据库用户名和密码,主机和数据库名仍由 `ND_DB_HOST`、`ND_DB_NAME` 提供,TLS 参数来自 YAML;第二行 `RUN` 仍是执行设备任务的确认。
246
+
247
+ 使用 `accept_new` / `replace` 时,SSH 在每个会话的临时 known_hosts 中协商,认证后通过独立锁文件合并到共享文件。
248
+ 网络登录不占用共享锁;失败登录不删除原信任记录,`replace` 只替换当前主机。该同步协调本库进程,外部手工工具应避免同时改写同一文件。
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 ADDED
@@ -0,0 +1,141 @@
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
+ device.sync_interface_descriptions.value!
77
+ ```
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
+
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.
82
+
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.
103
+
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.
105
+
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.
107
+
108
+ ## Scripts and interactions
109
+
110
+ ```ruby
111
+ script = Net::Connector::Script.parse(<<~CLI, name: "change-123")
112
+ configure terminal
113
+ interface GigabitEthernet1/0/1
114
+ description uplink
115
+ end
116
+ CLI
117
+ result = device.execute_script(script)
118
+ warn "#{result.error.code}: #{result.steps.size} completed commands" if result.failure?
119
+ ```
120
+
121
+ `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.
122
+
123
+ 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.
124
+
125
+ 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.
126
+
127
+ ## Connections, host keys, and logging
128
+
129
+ `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.
130
+
131
+ `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.
132
+
133
+ `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.
134
+
135
+ `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.
136
+
137
+ ## Dependencies and extensions
138
+
139
+ 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.
140
+
141
+ 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.