net-connector 0.11.1 → 0.13.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +1 -1
- data/README.zh-CN.md +1 -1
- data/docs/VERIFICATION.md +17 -3
- data/docs/architecture.md +77 -57
- data/docs/netdisco.md +9 -5
- data/docs/netdisco.zh-CN.md +14 -10
- data/docs/templates.md +59 -0
- data/docs/usage.md +6 -1
- data/docs/usage.zh-CN.md +7 -2
- data/docs/vendors.md +61 -0
- data/lib/net/connector/concurrency/observer.rb +71 -0
- data/lib/net/connector/concurrency/pool.rb +116 -0
- data/lib/net/connector/device/base.rb +6 -2
- data/lib/net/connector/device/local_backup.rb +14 -10
- data/lib/net/connector/device/profile/builder.rb +6 -0
- data/lib/net/connector/device/profile.rb +5 -3
- data/lib/net/connector/device/remote_backup/receipt.rb +40 -0
- data/lib/net/connector/device/remote_backup/target.rb +36 -0
- data/lib/net/connector/device/remote_backup.rb +51 -0
- data/lib/net/connector/device/running_config/strategy.rb +3 -3
- data/lib/net/connector/device/running_config.rb +2 -2
- data/lib/net/connector/device/scope.rb +45 -0
- data/lib/net/connector/device/tftp/file_upload.rb +7 -9
- data/lib/net/connector/device/tftp/strategy.rb +20 -6
- data/lib/net/connector/device/topology/change.rb +22 -0
- data/lib/net/connector/device/topology/immediate_strategy.rb +6 -12
- data/lib/net/connector/device/topology/interface_blocks.rb +18 -0
- data/lib/net/connector/device/topology/neighbor.rb +22 -0
- data/lib/net/connector/device/topology/plan.rb +39 -0
- data/lib/net/connector/device/topology/quoted_text.rb +23 -0
- data/lib/net/connector/device/topology/stages.rb +1 -1
- data/lib/net/connector/device/topology/strategy.rb +4 -0
- data/lib/net/connector/device/topology.rb +7 -12
- data/lib/net/connector/engine/command/result.rb +34 -0
- data/lib/net/connector/engine/command.rb +1 -50
- data/lib/net/connector/engine/core.rb +1 -0
- data/lib/net/connector/engine/dialogue.rb +3 -67
- data/lib/net/connector/engine/error/metadata.rb +57 -0
- data/lib/net/connector/engine/errors.rb +3 -3
- data/lib/net/connector/engine/event.rb +27 -0
- data/lib/net/connector/engine/execution.rb +2 -2
- data/lib/net/connector/engine/interaction.rb +57 -0
- data/lib/net/connector/engine/log/event.rb +2 -2
- data/lib/net/connector/engine/log/{stream.rb → redacting_writer.rb} +1 -24
- data/lib/net/connector/engine/log/transcript.rb +32 -0
- data/lib/net/connector/engine/log.rb +9 -8
- data/lib/net/connector/engine/response.rb +21 -0
- data/lib/net/connector/engine/response_reader.rb +8 -2
- data/lib/net/connector/engine/result.rb +3 -27
- data/lib/net/connector/engine/script.rb +57 -0
- data/lib/net/connector/engine/session.rb +1 -1
- data/lib/net/connector/engine/terminal/renderer.rb +186 -0
- data/lib/net/connector/engine/terminal/text.rb +35 -0
- data/lib/net/connector/engine/{transport.rb → transports.rb} +1 -21
- data/lib/net/connector/netdisco/batch.rb +37 -6
- data/lib/net/connector/netdisco/cli/backup.rb +173 -0
- data/lib/net/connector/netdisco/cli/credentials.rb +39 -0
- data/lib/net/connector/netdisco/cli.rb +3 -3
- data/lib/net/connector/netdisco/device/access.rb +31 -0
- data/lib/net/connector/netdisco/device.rb +3 -3
- data/lib/net/connector/netdisco/diagnostic.rb +7 -7
- data/lib/net/connector/netdisco/fleet.rb +32 -41
- data/lib/net/connector/netdisco/inventory/budget.rb +53 -0
- data/lib/net/connector/netdisco/inventory/cache.rb +226 -0
- data/lib/net/connector/netdisco/inventory/http.rb +255 -0
- data/lib/net/connector/netdisco/inventory/postgres.rb +236 -0
- data/lib/net/connector/netdisco/inventory.rb +47 -0
- data/lib/net/connector/netdisco/operations/batch.rb +78 -0
- data/lib/net/connector/netdisco/operations/outcome.rb +76 -0
- data/lib/net/connector/netdisco/operations.rb +28 -53
- data/lib/net/connector/netdisco/progress.rb +4 -4
- data/lib/net/connector/netdisco/report/files.rb +3 -3
- data/lib/net/connector/netdisco/report/store.rb +47 -0
- data/lib/net/connector/netdisco/report/text.rb +2 -2
- data/lib/net/connector/netdisco/report.rb +16 -13
- data/lib/net/connector/netdisco/rules.rb +5 -0
- data/lib/net/connector/netdisco/settings.rb +32 -32
- data/lib/net/connector/netdisco/tftp/archive.rb +214 -0
- data/lib/net/connector/netdisco/tftp/verifier.rb +93 -0
- data/lib/net/connector/netdisco/worker.rb +3 -3
- data/lib/net/connector/netdisco.rb +1 -1
- data/lib/net/connector/storage/{private_file.rb → atomic_file.rb} +1 -1
- data/lib/net/connector/storage/{safe_file.rb → file_reader.rb} +1 -1
- data/lib/net/connector/storage/saved_config.rb +5 -5
- data/lib/net/connector/storage.rb +2 -2
- data/lib/net/connector/templates/{cisco_ios_running_config_interfaces.textfsm → cisco/show_running_config.textfsm} +1 -0
- data/lib/net/connector/templates/cisco_nxos/show_cdp_neighbors_detail.textfsm +11 -0
- data/lib/net/connector/templates/cisco_nxos/show_ip_interface_brief.textfsm +10 -0
- data/lib/net/connector/templates/cisco_nxos/show_running_config.textfsm +13 -0
- data/lib/net/connector/templates/fortinet/diagnose_lldp_rx_neighbor.textfsm +7 -0
- data/lib/net/connector/templates/fortinet/show_full_configuration.textfsm +21 -0
- data/lib/net/connector/templates/{h3c_interface_descriptions.textfsm → h3c/display_current_configuration.textfsm} +1 -0
- data/lib/net/connector/templates/{h3c_lldp_local_first.textfsm → h3c/display_lldp_neighbor_information_list.textfsm} +7 -1
- data/lib/net/connector/templates/{hillstone_interface_descriptions.textfsm → hillstone/show_configuration_running.textfsm} +1 -0
- data/lib/net/connector/templates/huawei/display_current_configuration.textfsm +13 -0
- data/lib/net/connector/templates/huawei/display_lldp_neighbor_brief.textfsm +9 -0
- data/lib/net/connector/templates/index +22 -7
- data/lib/net/connector/templates/juniper/show_configuration_display_set.textfsm +10 -0
- data/lib/net/connector/templates/juniper/show_lldp_neighbors_detail.textfsm +19 -0
- data/lib/net/connector/templates/{palo_alto_interface_descriptions.textfsm → palo_alto/show.textfsm} +1 -0
- data/lib/net/connector/templates/radware/cfg_dump.textfsm +15 -0
- data/lib/net/connector/textfsm.rb +2 -2
- data/lib/net/connector/vendor/cisco/running_config.rb +25 -0
- data/lib/net/connector/vendor/cisco/topology.rb +53 -0
- data/lib/net/connector/vendor/cisco.rb +25 -0
- data/lib/net/connector/vendor/cisco_ios/tftp.rb +40 -0
- data/lib/net/connector/vendor/cisco_ios.rb +5 -14
- data/lib/net/connector/vendor/cisco_nxos/tftp.rb +46 -0
- data/lib/net/connector/vendor/cisco_nxos/topology.rb +22 -0
- data/lib/net/connector/vendor/cisco_nxos.rb +7 -17
- data/lib/net/connector/vendor/fortinet/tftp.rb +30 -0
- data/lib/net/connector/vendor/fortinet/topology.rb +58 -0
- data/lib/net/connector/vendor/fortinet.rb +40 -0
- data/lib/net/connector/vendor/h3c/tftp.rb +36 -0
- data/lib/net/connector/vendor/h3c/topology.rb +49 -47
- data/lib/net/connector/vendor/h3c.rb +5 -5
- data/lib/net/connector/vendor/h3c_wireless.rb +2 -2
- data/lib/net/connector/vendor/hillstone/running_config.rb +8 -5
- data/lib/net/connector/vendor/hillstone/tftp.rb +54 -0
- data/lib/net/connector/vendor/hillstone/topology.rb +44 -41
- data/lib/net/connector/vendor/hillstone.rb +6 -6
- data/lib/net/connector/vendor/huawei/tftp.rb +28 -0
- data/lib/net/connector/vendor/huawei/topology.rb +48 -0
- data/lib/net/connector/vendor/huawei.rb +6 -4
- data/lib/net/connector/vendor/juniper/remote_backup.rb +41 -0
- data/lib/net/connector/vendor/juniper/topology.rb +41 -0
- data/lib/net/connector/vendor/juniper.rb +31 -0
- data/lib/net/connector/vendor/palo_alto/running_config.rb +70 -67
- data/lib/net/connector/vendor/palo_alto/tftp.rb +48 -0
- data/lib/net/connector/vendor/palo_alto/topology.rb +48 -51
- data/lib/net/connector/vendor/palo_alto.rb +6 -6
- data/lib/net/connector/vendor/radware/running_config.rb +11 -8
- data/lib/net/connector/vendor/radware/tftp.rb +50 -0
- data/lib/net/connector/vendor/radware/topology.rb +12 -7
- data/lib/net/connector/vendor/radware.rb +6 -6
- data/lib/net/connector/vendor/topsec.rb +27 -0
- data/lib/net/connector/vendor.rb +24 -0
- data/lib/net/connector/version.rb +1 -1
- data/lib/net/connector.rb +3 -19
- metadata +80 -43
- data/lib/net/connector/background_task.rb +0 -69
- data/lib/net/connector/device_session.rb +0 -43
- data/lib/net/connector/engine/error_metadata.rb +0 -52
- data/lib/net/connector/engine/terminal_renderer.rb +0 -184
- data/lib/net/connector/engine/terminal_text.rb +0 -33
- data/lib/net/connector/execution_pool.rb +0 -114
- data/lib/net/connector/netdisco/backup_run.rb +0 -171
- data/lib/net/connector/netdisco/client.rb +0 -281
- data/lib/net/connector/netdisco/connection.rb +0 -37
- data/lib/net/connector/netdisco/database_client.rb +0 -233
- data/lib/net/connector/netdisco/inventory_budget.rb +0 -49
- data/lib/net/connector/netdisco/inventory_cache.rb +0 -224
- data/lib/net/connector/netdisco/operation_batch.rb +0 -154
- data/lib/net/connector/netdisco/result_store.rb +0 -44
- data/lib/net/connector/netdisco/tftp_archive.rb +0 -212
- data/lib/net/connector/netdisco/tftp_verification.rb +0 -91
- data/lib/net/connector/templates/h3c_lldp_name_first.textfsm +0 -8
- data/lib/net/connector/templates/radware_port_names.textfsm +0 -13
- data/lib/net/connector/vendor/cisco_ios/running_config.rb +0 -22
- data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +0 -41
- data/lib/net/connector/vendor/cisco_ios/topology.rb +0 -50
- data/lib/net/connector/vendor/cisco_nxos/running_config.rb +0 -13
- data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +0 -43
- data/lib/net/connector/vendor/h3c/tftp_backup.rb +0 -33
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +0 -55
- data/lib/net/connector/vendor/huawei/tftp_backup.rb +0 -25
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +0 -49
- data/lib/net/connector/vendor/radware/tftp_backup.rb +0 -49
- /data/lib/net/connector/templates/{cisco_cdp_neighbors_detail.textfsm → cisco/show_cdp_neighbors_detail.textfsm} +0 -0
- /data/lib/net/connector/templates/{cisco_ios_show_ip_interface_brief.textfsm → cisco_ios/show_ip_interface_brief.textfsm} +0 -0
- /data/lib/net/connector/templates/{hillstone_lldp_neighbors.textfsm → hillstone/show_lldp_neighbor_information.textfsm} +0 -0
- /data/lib/net/connector/templates/{palo_alto_lldp_neighbors.textfsm → palo_alto/show_lldp_neighbors_all.textfsm} +0 -0
data/docs/netdisco.zh-CN.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
`Net::Connector::Netdisco` 读取并验证完整清单,再将支持的记录映射到连接器,使用有上限的工作线程执行备份。Netdisco 只提供清单字段;设备凭据来自环境变量或调用方提供的解析器。清单会在连接任何设备前完成校验;不支持、被过滤、重复、缺少凭据、失败,以及保存成功但关闭失败的结果分别保留。
|
|
8
8
|
|
|
9
|
-
每次 `
|
|
9
|
+
每次 `Inventory::HTTP#devices` 默认限制单响应 16 MiB、累计响应 128 MiB、去重前 100,000 条记录、10,000 页和 300 秒总期限。认证、分页及兼容查询共用这些预算;默认 HTTP 客户端逐块计数,超限会关闭连接并抛出带稳定 `code` 的 `Inventory::Error`,Fleet 不会执行半份清单。这些默认值是可调整的设计起点,不是实测容量。可注入 `requester: ->(uri, request)`;该回调只能在回调返回后检查正文和期限,回调自身的阻塞及内存用量由注入方控制。
|
|
10
10
|
|
|
11
11
|
推荐使用 HTTPS,标准证书验证保持开启。`allow_insecure_http` 默认为 `true`;设为 `false` 可在发请求前拒绝 HTTP,YAML 中对应 `netdisco.allow_insecure_http: false`。HTTP 会明文传输登录凭据和 API key,迁移时应先提供可验证的 HTTPS 端点。
|
|
12
12
|
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
本地 `backup(path:)` 用 SHA-256 比较新旧配置,`backup.change` 返回 `:created`、`:changed` 或 `:unchanged`;内容未变且权限、文件身份正常时保留修改时间。这不补验历史写入的断电持久性。`backup_all` 的 `on_change:` 仅在新建或更改文件保存后触发;`on_start:` 和 `on_result:` 观察每台已尝试设备。回调异常记录在 `batch.callback_errors`,不丢弃设备结果。每项结果包含开始、结束和耗时。TFTP 无法比较服务器文件,因此没有 `change`,也不触发变更通知。
|
|
16
16
|
|
|
17
|
-
每批默认由 `
|
|
17
|
+
每批默认由 `Report::Store::Json` 写入私有 JSON 报告,路径见 `batch.report_location`。调用方如有数据库仓储,可传 `Report::Store::Database.new(repository: YourModel)`;仓储需实现 `create!(attributes)`。`report_store: nil` 表示由调用方自行持久化。报告失败保留在 `batch.report_error`,同时使 `batch.success?` 为假。若报告已替换但目录同步失败,仍保留位置;离线 `--export --output` 遇到同类错误返回 2,并说明文件已经提交。
|
|
18
18
|
|
|
19
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
20
|
|
|
21
|
-
`success_policy: :selected` 要求至少一台设备成功,其他记录只因 `filtered` 或 `sample_limit` 跳过,而且没有部分成功、回调或报告错误。缺少凭据、重复地址、无效地址、未知厂商、目标冲突和未知状态均会阻止成功。`coverage.complete` 只表示每条清单记录都已尝试任务;失败任务也计入尝试,不能据此判断配置已保存。自定义 `
|
|
21
|
+
`success_policy: :selected` 要求至少一台设备成功,其他记录只因 `filtered` 或 `sample_limit` 跳过,而且没有部分成功、回调或报告错误。缺少凭据、重复地址、无效地址、未知厂商、目标冲突和未知状态均会阻止成功。`coverage.complete` 只表示每条清单记录都已尝试任务;失败任务也计入尝试,不能据此判断配置已保存。自定义 `Report::Store#write(report, directory:)` 始终接收 Report,并通过 `summary` 获取统一 JSON 结构。
|
|
22
22
|
|
|
23
23
|
外部构造的计划也必须把同一管理地址的全部条目标记为 `duplicate_host` 并跳过,不能通过采样恢复其中一条的执行资格;IPv6 的等价写法视为同一地址。只需汇总时使用 `report.statistics`,它返回 `summary` 除 `devices` 外的字段,不生成设备明细,也不缓存调用方持有的批次容器。
|
|
24
24
|
|
|
@@ -132,6 +132,8 @@ SQL、参数和来源也可分别通过 `--query SQL`、`--query-params JSON`、
|
|
|
132
132
|
|
|
133
133
|
`--plan` 只拉取并验证清单;`--host` 选择一个管理地址;`--tftp` 默认每厂商最多选择五台,`--all` 选择所有就绪设备。本地备份默认选择所有就绪设备,可用 `--limit-per-vendor` 限制。`--show-config` 只输出有效的非敏感设置,不访问 Netdisco。CLI 会拒绝未知 YAML 字段、Ruby 对象标签及配置中的凭据。`--export IP` 只离线读取规范化管理地址对应的 `<IP>.txt`;默认原样写到标准输出,指定 `--output` 后以 `0600` 权限原子写文件。导出的配置仍是敏感数据。
|
|
134
134
|
|
|
135
|
+
这里的 `vendor` 表示连接器平台标识,如 `cisco_ios`、`cisco_nxos`,并非制造商;筛选和采样均按该标识分组。因此限制五台时,IOS 与 NX-OS 可各选五台。`source_vendor` 保留清单中的原始厂商标签。
|
|
136
|
+
|
|
135
137
|
CLI 的计划与批次摘要使用 JSON。默认 `--success-policy strict` 使用严格规则:非空清单且全部成功、回调及报告正常时为 `0`;空清单或有跳过、部分成功、失败时为 `1`;清单或配置错误为 `2`。`--host` 未在清单中找到也返回 `2`。由于其他清单记录会标记为过滤,默认单主机备份成功时批次退出码仍可能是 `1`;应查看 JSON 中的 `succeeded`、`skipped` 和逐台 `status`。
|
|
136
138
|
|
|
137
139
|
显式使用 `--success-policy selected` 后,CLI 按上述 selected 规则决定退出码,保留严格的 `status: incomplete` 与跳过计数,另列 `policy_success`。例如 `net-backup --config config.yml --host 192.0.2.7 --success-policy selected`。成功策略由本次 CLI/API 参数指定,不改变已批准的清单选择,也不触发重试。
|
|
@@ -177,7 +179,7 @@ ssh:
|
|
|
177
179
|
|
|
178
180
|
`Fleet` 每次规划或执行前通过 `Settings#snapshot(mode:)` 固定非敏感设置,包括筛选规则、目录、并发、协议、主机密钥、日志、TFTP 参数和清单预算。执行中修改 ENV 不改变当批策略;再次调用会读取新值。CLI 一次调用的规划与执行共用策略,优先级为 CLI > 常用环境变量 > YAML > 默认值。`Settings#validate!(mode:)` 复用连接配置及 Planner 的枚举和范围规则;`--show-config` 会拒绝非法设置,`--export` 只验证离线目录,不要求清单地址或认证。
|
|
179
181
|
|
|
180
|
-
每台任务开始时仍读取设备凭据,`Settings.from_file` 也保留轮换能力。纯策略快照不保存密码、API key 或凭据解析器。需要让多次 API 调用共用策略时,可传 `Settings.new.for_run(mode: :backup)`。自定义 `credentials:` 解析器仍可逐设备返回连接设置,它显式给出的选项优先于批次默认值,由调用方负责一致性;注入的 `
|
|
182
|
+
每台任务开始时仍读取设备凭据,`Settings.from_file` 也保留轮换能力。纯策略快照不保存密码、API key 或凭据解析器。需要让多次 API 调用共用策略时,可传 `Settings.new.for_run(mode: :backup)`。自定义 `credentials:` 解析器仍可逐设备返回连接设置,它显式给出的选项优先于批次默认值,由调用方负责一致性;注入的 `inventory:` 生命周期也由调用方管理。传入 `plan:` 的执行不会重新拉取或筛选已批准清单。`fleet.devices` 可在不连接设备时检查映射。
|
|
181
183
|
|
|
182
184
|
快照中的备份、缓存、日志、TFTP 核验目录、SSH known-hosts 路径及显式 PostgreSQL CA 证书路径,以创建快照时的工作目录为基准,默认路径和后续相对路径覆盖也沿用这一基准。`~/` 开头的路径及 `~` 使用快照创建时的主目录。PostgreSQL CA 路径保留 libpq 的字面路径语义,不展开波浪号;`sslrootcert: system` 仍表示系统信任库,`./system` 则表示文件。未固定的活设置仍按调用时的当前工作目录和主目录解析路径。
|
|
183
185
|
|
|
@@ -231,15 +233,17 @@ require "net/connector/netdisco"
|
|
|
231
233
|
netdisco = Net::Connector::Netdisco
|
|
232
234
|
options = netdisco::CLI::Options.parse(argv: ["--concurrency", "10"])
|
|
233
235
|
settings = netdisco::CLI::Options.settings(options)
|
|
234
|
-
|
|
236
|
+
inventory, credentials = netdisco::CLI::Credentials.resolve(settings)
|
|
235
237
|
directory = Net::Connector::Storage::BatchDirectory.create("./backups")
|
|
236
238
|
# 获得 report 和 plan 后:
|
|
237
239
|
# report = netdisco::Report::Files.write(report, directory: directory, plan: plan, concurrency: 10)
|
|
238
240
|
```
|
|
239
241
|
|
|
240
|
-
`CLI::Options.parse` 接受 `argv/input/output/error/program`,不修改传入参数数组;帮助通过返回值 `:help` 通知调用方,非法输入抛出 `ArgumentError`。`
|
|
242
|
+
`CLI::Options.parse` 接受 `argv/input/output/error/program`,不修改传入参数数组;帮助通过返回值 `:help` 通知调用方,非法输入抛出 `ArgumentError`。`CLI::Credentials.resolve` 接受 `input:`,标准输入凭据仅消费一行,RUN 确认由脚本编排负责。模块不加载 `.env` 或切换目录。
|
|
243
|
+
|
|
244
|
+
批量示例由 `Netdisco::CLI::Backup` 统一编排,`CLI::Options.settings` 为示例和正式 CLI 共用配置覆盖入口。
|
|
241
245
|
|
|
242
|
-
|
|
246
|
+
终端入口按职责放在 `cli/`:`CLI::Backup` 编排批量备份,`CLI::Credentials.resolve` 解析凭据来源,`CLI::Options` 解析参数和配置覆盖。
|
|
243
247
|
示例默认 `selected`,正式 CLI 保留 `strict` 默认;两者均支持显式 `--success-policy`,退出码与报告的 `policy_success` 一致。
|
|
244
248
|
新示例 `summary.json` 使用标准 schema 2 的 `devices`,不再另造 `outcomes`;旧报告可继续用 `review_tftp.rb` 读取。
|
|
245
249
|
TFTP 的 `verification` 汇总与设备条目的 `server_file_verified` 区分上传回执和服务器文件核验,核验成功还包含 `local_file`、`server_archive_file`、`bytes`、`sha256`。
|
|
@@ -268,7 +272,7 @@ HTTP 和 PostgreSQL 默认启用本地清单缓存,有效期三天(259200
|
|
|
268
272
|
|
|
269
273
|
空文件或格式损坏的缓存按未命中处理,重新查询远端;查询失败时,这类文件不能作为回退历史。
|
|
270
274
|
|
|
271
|
-
强刷使用 `net-backup --refresh-inventory`、`fleet.devices(force_refresh: true)`、`fleet.plan_backup(force_refresh: true)`;TFTP 规划也支持此参数。强刷失败同样回退已验证历史。直接注入客户端时不自动缓存,可显式包裹 `Netdisco::
|
|
275
|
+
强刷使用 `net-backup --refresh-inventory`、`fleet.devices(force_refresh: true)`、`fleet.plan_backup(force_refresh: true)`;TFTP 规划也支持此参数。强刷失败同样回退已验证历史。直接注入客户端时不自动缓存,可显式包裹 `Netdisco::Inventory::Cache`。一个包装器的状态描述最近调用,并发共享实例须由调用方同步。有界跨进程锁合并普通过期刷新,独立强刷请求各自访问远端。
|
|
272
276
|
|
|
273
277
|
## 配置批次与接口修正
|
|
274
278
|
|
|
@@ -292,8 +296,8 @@ puts "清单 #{batch.summary[:total]},成功 #{batch.summary[:succeeded]},
|
|
|
292
296
|
|
|
293
297
|
接口应用复用单设备拓扑操作,保留证据重验、配置回读和持久化确认,不将审批计划当作普通脚本执行。脚本与独立计划应用要求 `confirmed: true`;显式同步调用本身表达修改意图,不自动重放命令。每台任务独占并关闭连接器;普通失败隔离为设备结果,中断停止并等待全部工作线程。跨设备已完成的变更不会自动回滚。
|
|
294
298
|
|
|
295
|
-
`
|
|
299
|
+
`Operations::Batch` 独立于备份报告,`outcomes` 保留原 `Result` 或计划、完成步骤和第一故障;`:partial` 表示已有工作后出错,缺少凭据也使批次不成功。`plans` 只选完整成功的规划结果。`to_h` 仅输出安全诊断和数量,不展开命令、回显、凭据或证据。`on_start` 和 `on_result` 在工作线程执行,回调须线程安全;普通回调故障记录后继续,不重复设备操作。日志使用 `ssh.log_directory`。
|
|
296
300
|
|
|
297
301
|
每台设备独占会话,在操作和关闭完成后发布一个不可变终态。连接、凭据、执行和关闭的普通故障只影响该设备,其他运行中或排队任务继续。整批调度结束后返回完整结果;`summary` 和 `to_h[:summary]` 提供 `total`、`succeeded`、`failed`,满足总数 = 成功 + 失败,并细分 `partial`、`missing_credentials` 和 `callback_errors`。总数是本次请求并调度的设备数量;部分完成与缺少凭据计入失败,回调故障单列,不改写设备执行结果。空批次各项为零且状态为 `no_devices`。库不直接输出终端内容,调用方可将 `to_h` 导出 JSON 报告。设备上的已完成变更不能回滚;全批审批预检和显式中断仍维持原有失败边界。
|
|
298
302
|
|
|
299
|
-
其他独立任务可加载 `net/connector/
|
|
303
|
+
其他独立任务可加载 `net/connector/concurrency/pool`,通过 `Net::Connector::Concurrency::Pool.new(concurrency: 4).map(items) { |item| ... }` 执行。结果按输入顺序返回,全部线程建立成功才运行块,未处理异常会停止线程池并回到调用线程。业务代码负责决定哪些普通错误转换为单任务结果。显式参数优先于 `NC_CONCURRENCY` 环境变量,默认 4、上限 50。
|
data/docs/templates.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# TextFSM 模板组织
|
|
2
|
+
|
|
3
|
+
模板只描述设备输出的记录结构,不负责连接、命令执行、完整性判断或配置变更。厂商策略负责选择模板并核对记录数量,公共 TextFSM 适配器负责严格 UTF-8 与终端文本边界。
|
|
4
|
+
|
|
5
|
+
## 目录与命名
|
|
6
|
+
|
|
7
|
+
`lib/net/connector/templates/` 统一采用 `<平台>/<完整命令名>[_格式变体].textfsm`。命令使用小写,空格、连字符和路径分隔符转为下划线;不使用缩写命令、字段用途名或额外的命令子目录。管道的输出格式选项保留为命令词,纯分页控制 `no-more` 不进入文件名。
|
|
8
|
+
|
|
9
|
+
例如,`show running-config` 对应 `show_running_config.textfsm`,`display current-configuration` 对应 `display_current_configuration.textfsm`,`/cfg/dump` 对应 `cfg_dump.textfsm`,`show configuration | display set` 对应 `show_configuration_display_set.textfsm`。PAN-OS 配置视图的命令是 `show`,其 set 输出对应 `palo_alto/show.textfsm`。H3C 同一命令的列序由同一模板按表头切换状态。
|
|
10
|
+
|
|
11
|
+
| 路径 | 输入与用途 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `cisco/show_cdp_neighbors_detail.textfsm` | IOS 的详细 CDP 邻居输出 |
|
|
14
|
+
| `cisco/show_running_config.textfsm` | IOS 运行配置中的接口和描述 |
|
|
15
|
+
| `cisco_ios/show_ip_interface_brief.textfsm` | IOS 的接口、IP 地址与状态摘要 |
|
|
16
|
+
| `cisco_nxos/show_cdp_neighbors_detail.textfsm` | NX-OS CDP 详细邻居块 |
|
|
17
|
+
| `cisco_nxos/show_running_config.textfsm` | NX-OS 运行配置中的接口和描述 |
|
|
18
|
+
| `cisco_nxos/show_ip_interface_brief.textfsm` | NX-OS 多 VRF 接口、IP 及 protocol/link/admin 状态 |
|
|
19
|
+
| `h3c/display_current_configuration.textfsm` | Comware 运行配置中的接口和描述 |
|
|
20
|
+
| `h3c/display_lldp_neighbor_information_list.textfsm` | 按表头识别名称首列或本地接口首列的 LLDP 列表 |
|
|
21
|
+
| `huawei/display_lldp_neighbor_brief.textfsm` | VRP LLDP 邻居简表,保留端口类型与编号之间的空格 |
|
|
22
|
+
| `huawei/display_current_configuration.textfsm` | VRP 运行配置中的接口和描述 |
|
|
23
|
+
| `hillstone/show_configuration_running.textfsm` | StoneOS 运行配置中的接口和描述 |
|
|
24
|
+
| `hillstone/show_lldp_neighbor_information.textfsm` | StoneOS 的 LLDP 邻居表 |
|
|
25
|
+
| `palo_alto/show.textfsm` | PAN-OS set 格式中的接口和备注 |
|
|
26
|
+
| `palo_alto/show_lldp_neighbors_all.textfsm` | PAN-OS 按接口分块的 LLDP 邻居输出 |
|
|
27
|
+
| `radware/cfg_dump.textfsm` | Alteon 配置转储中的端口名称 |
|
|
28
|
+
| `fortinet/show_full_configuration.textfsm` | 当前视图顶层接口表的描述,跳过嵌套表 |
|
|
29
|
+
| `fortinet/diagnose_lldp_rx_neighbor.textfsm` | FortiOS 改名前后 LLDP 邻居查询的共同输出 |
|
|
30
|
+
| `juniper/show_lldp_neighbors_detail.textfsm` | Junos LLDP 详细邻居块 |
|
|
31
|
+
| `juniper/show_configuration_display_set.textfsm` | Junos 已提交 set 配置中的物理接口与逻辑单元描述 |
|
|
32
|
+
|
|
33
|
+
H3C Wireless 沿用 H3C 模板。H3C 邻居策略与通用命令解析共用按表头切换列序的模板;未知表头直接拒绝,不尝试多个模板后采用碰巧解析出的部分结果。
|
|
34
|
+
|
|
35
|
+
## 索引与显式选择
|
|
36
|
+
|
|
37
|
+
根目录的 `index` 保持 TextFSM 的 `Template, Vendor, Command` 格式。Template 使用相对资源根目录的路径,Vendor 是连接器平台标识,Command 是命令匹配规则。内置索引的 Vendor 与 Command 均使用 `\A...\z` 完整匹配,防止前缀厂商名称及带额外命令片段的输入误选模板。一个共享模板可以被多个平台索引项引用。
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
parser = Net::Connector::TextFSM.new
|
|
41
|
+
rows = parser.call(output, vendor: :cisco_ios, command: "show ip interface brief")
|
|
42
|
+
interfaces = parser.call(config, template: "cisco/show_running_config.textfsm")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
命令索引覆盖内置配置及邻居模板;H3C 列序在模板状态机内选择。模板本身只解析记录,厂商策略仍负责表头、完整性及作用域校验。调用方也可以提供独立的 `template_dir:`,其索引和路径遵循相同规则。
|
|
46
|
+
|
|
47
|
+
## 修改与验证
|
|
48
|
+
|
|
49
|
+
修改模板时保留设备命令和字段语义,使用对应设备输出夹具核对记录。空结果不能代替未知或部分输出;完整性约束仍由拓扑等领域策略维护。`net-connector.gemspec` 递归收录模板,包检查验证每个资源的字节和路径,安装冒烟测试从已安装 gem 执行解析,避免源码目录掩盖资源遗漏。
|
|
50
|
+
|
|
51
|
+
## NX-OS 输出边界
|
|
52
|
+
|
|
53
|
+
NX-OS 模板独立放在 `cisco_nxos/`,CDP 按邻居块清理状态并与设备块数量核对,残缺块不能借用下一条记录的字段。接口描述在空行或下一个顶层配置段结束,防止 route-map 等后续段落的描述覆盖接口描述。IP 接口摘要保留 `VRF`、`INTERFACE`、`IP_ADDRESS`、`PROTOCOL`、`LINK_STATUS`、`ADMIN_STATUS` 六个字段;`show ip interface brief` 与带 `vrf` 参数的命令共用此模板。
|
|
54
|
+
|
|
55
|
+
输出字段参考 [Cisco NX-OS CDP 命令](https://developer.cisco.com/docs/nx-api-cli-reference-for-the-cisco-nexus-7000-series-platform/cdp-commands/)与 [Cisco Nexus IP 接口摘要](https://developer.cisco.com/docs/cisco-nexus-3000-and-9000-series-nx-api-rest-sdk-user-guide-and-api-reference-release-103x/querying-a-summary-of-ip-interface-status-and-configuration-information/)。测试使用脱敏合成输出,不代表真实设备及所有固件均已验证。
|
|
56
|
+
|
|
57
|
+
## 描述字段的词法与业务值
|
|
58
|
+
|
|
59
|
+
带引号的描述字段在模板中保留完整词法文本,`Topology::QuotedText` 负责还原引号与转义,并将不完整字段转为不含原文的领域错误。Junos、FortiOS、PAN-OS 与 Radware 使用同一解码边界;是否拆分无引号文本由厂商策略明确指定。接口不存在描述语句时可返回空描述,`description ""` 等显式空字符串也有明确含义;缺少值的 description/comment/name 语句则是残缺输入,不能被默认空字段掩盖。
|
data/docs/usage.md
CHANGED
|
@@ -16,9 +16,14 @@ Use these vendor keys with `Net::Connector.open` or `Net::Connector.build`:
|
|
|
16
16
|
| `:hillstone` | Hillstone StoneOS | `show configuration running` | `save all` |
|
|
17
17
|
| `:radware` | Radware Alteon | `/cfg/dump` | `/cfg/save` |
|
|
18
18
|
| `:palo_alto` | Palo Alto PAN-OS | Set-format export from configuration view | Unsupported |
|
|
19
|
+
| `:topsec` | Topsec TopOS firewall | `show-running nostop` | Unsupported |
|
|
20
|
+
| `:fortinet` | FortiGate / FortiOS | `show full-configuration` | `execute cfg save` |
|
|
21
|
+
| `:juniper` | Juniper Junos OS | `show configuration \| display set \| no-more` | No implicit candidate commit |
|
|
19
22
|
|
|
20
23
|
`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
24
|
|
|
25
|
+
See [platform details and native Junos FTP/SCP backups](vendors.md). Junos supports `remote_backup(host:, path:, username:, protocol: :scp, password: nil)`; use `supports?(:remote_backup)` to check this separately from TFTP.
|
|
26
|
+
|
|
22
27
|
## Sessions and local backups
|
|
23
28
|
|
|
24
29
|
```ruby
|
|
@@ -62,7 +67,7 @@ Explicit failure raises `:transfer_failed`; missing completion evidence raises `
|
|
|
62
67
|
|
|
63
68
|
```ruby
|
|
64
69
|
interfaces = device.parse_command("show ip interface brief") # Cisco IOS
|
|
65
|
-
rows = device.parse_config(template: "
|
|
70
|
+
rows = device.parse_config(template: "cisco/show_running_config.textfsm")
|
|
66
71
|
|
|
67
72
|
saved = Net::Connector::Storage::SavedConfig.new(directory: "/var/backups/network")
|
|
68
73
|
rows = saved.parse(host: "192.0.2.10", template: "/path/to/template.textfsm")
|
data/docs/usage.zh-CN.md
CHANGED
|
@@ -24,8 +24,13 @@
|
|
|
24
24
|
| `:palo_alto` | Palo Alto PAN-OS | 候选视图中的 set 格式导出 | 不支持 |
|
|
25
25
|
| `:huawei` | 华为 | `dis cur` | `save force` |
|
|
26
26
|
| `:hillstone` | 山石 StoneOS | `show configuration running` | `save all` |
|
|
27
|
+
| `:topsec` | 天融信 TopOS 防火墙 | `show-running nostop` | 未开放 |
|
|
28
|
+
| `:fortinet` | FortiGate / FortiOS | `show full-configuration` | `execute cfg save` |
|
|
29
|
+
| `:juniper` | Juniper Junos OS | `show configuration \| display set \| no-more` | 不隐式提交候选库 |
|
|
27
30
|
|
|
28
|
-
厂商标识仅使用上表的规范名称。厂商连接器位于 `Net::Connector` 下,例如 `Net::Connector::H3cWireless
|
|
31
|
+
厂商标识仅使用上表的规范名称。厂商连接器位于 `Net::Connector` 下,例如 `Net::Connector::Vendor::H3cWireless`。
|
|
32
|
+
|
|
33
|
+
Junos 的 FTP/SCP 原生导出使用 `remote_backup(host:, path:, username:, protocol: :scp, password: nil)`,通过 `supports?(:remote_backup)` 查询。详细能力、视图及命令依据见[新增平台说明](vendors.md)。
|
|
29
34
|
|
|
30
35
|
## 登录与本地备份
|
|
31
36
|
|
|
@@ -94,7 +99,7 @@ end
|
|
|
94
99
|
`parse_config` 采集运行配置,要求显式指定模板。内置 Cisco IOS 模板只提取接口名称和描述,并非完整配置模型:
|
|
95
100
|
|
|
96
101
|
```ruby
|
|
97
|
-
interfaces = device.parse_config(template: "
|
|
102
|
+
interfaces = device.parse_config(template: "cisco/show_running_config.textfsm")
|
|
98
103
|
```
|
|
99
104
|
|
|
100
105
|
两种方法都返回由模板字段名组成的哈希数组;匹配不到记录时返回 `[]`。模板缺失或无效会抛出 `ParsingError`,设备命令失败则保留原始连接器错误。可用 `template:` 指定外部模板,或用 `template_dir:` 指定含 `index` 的模板目录。每次解析使用独立解析器,批量任务之间不共享状态。已有本地备份也可离线解析:
|
data/docs/vendors.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Topsec、Fortinet 与 Juniper
|
|
2
|
+
|
|
3
|
+
三个平台使用 `Vendor` 注册表、Profile 和现有会话执行器,均支持 SSH/Telnet 命令执行、运行配置采集与本地私有备份。能力声明区分设备系统,不能仅凭品牌把 FortiMail、ScreenOS 等产品映射到 FortiOS 或 Junos。
|
|
4
|
+
|
|
5
|
+
| 平台键 | 系统与登录视图 | 采集命令 | 保存 | 原生远端备份 | 拓扑读取 |
|
|
6
|
+
| --- | --- | --- | --- | --- | --- |
|
|
7
|
+
| `topsec` | TopOS 防火墙,`TopsecOS#` 或 `#` 管理视图 | `show-running nostop` | 未开放 | 使用本地采集备份 | 未开放 |
|
|
8
|
+
| `fortinet` | FortiGate/FortiOS,当前账号授权的根或 VDOM 视图 | `show full-configuration` | `execute cfg save` | TFTP `execute backup full-config tftp` | LLDP 邻居、当前视图接口描述 |
|
|
9
|
+
| `juniper` | Junos OS,`user@host>` 操作视图 | `show configuration \| display set \| no-more` | 已提交配置已持久化;不隐式提交候选库 | FTP/SCP `show configuration \| save` | LLDP 邻居、物理接口及逻辑单元描述 |
|
|
10
|
+
|
|
11
|
+
这些能力通过合成会话、解析样例和安装检查验证,尚无真实设备验收记录。Topsec 只实现有检索依据的 TopOS 配置读取方言;其他系列需依据其 CLI 手册接入,不能套用 Cisco 的命令。
|
|
12
|
+
|
|
13
|
+
## Junos 原生 FTP/SCP 导出
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
Net::Connector.open(:juniper, host: "192.0.2.1", username: "audit",
|
|
17
|
+
password: ENV.fetch("NC_DEVICE_PASSWORD")) do |device|
|
|
18
|
+
receipt = device.remote_backup(host: "backup.example", path: "site/srx.cfg",
|
|
19
|
+
username: "archive", protocol: :scp,
|
|
20
|
+
password: ENV["NC_ARCHIVE_PASSWORD"])
|
|
21
|
+
puts "#{receipt.protocol}: #{receipt.server}/#{receipt.path}"
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`protocol:` 接受 `:scp`(默认)或 `:ftp`。`host:` 是 IPv4 地址或 DNS 名称,`path:` 是无空格、无目录穿越的相对路径;账号单独传入,不接受含密码的 URL。`password:` 可省略以使用设备端已有认证配置,提供时仅在密码挑战中发送一次并脱敏。SCP 服务器主机密钥应事先在设备上建立信任,连接器不自动回答接受主机密钥的询问。FTP 为明文协议。
|
|
26
|
+
|
|
27
|
+
导出保持在操作视图,`show configuration` 读取当前已提交配置。只有目标 URL 完全一致且写出行数大于零的 `Wrote ... lines of output to '...'` 完成行才生成 `RemoteBackup::Receipt`。回执标记 `verification: :device_reported`,不声称已读取远端文件或计算摘要。无完成证据时抛出 `transfer_unconfirmed`,设备错误保持原异常;上传确认后的收尾失败抛出 `RemoteBackup::CompletionError`,`error.receipt` 保留上传事实,不自动重传。
|
|
28
|
+
|
|
29
|
+
`device.supports?(:remote_backup)` 可离线检查原生 FTP/SCP 导出能力。目前由 Junos 实现;Fleet/CLI 的本地备份与 TFTP 批次接口保持各自用途。Junos `file copy` 文档列出 FTP、HTTP、HTTPS、SCP,不提供可直接套用的 TFTP 上传接口;`save` 配置模式导出候选内容,`commit` 激活候选库,均不能冒充只读的已提交配置备份。自动接口描述提交暂未开放。
|
|
30
|
+
|
|
31
|
+
## FortiOS 视图、分页与版本
|
|
32
|
+
|
|
33
|
+
采集使用当前账号授权的视图;VDOM 管理账号得到所属 VDOM 的配置,不能把它当成整个设备的全局归档。分页用空格应答,不通过 `config system console` 修改设备的持久设置。`execute cfg save` 为显式保存请求;常见自动保存模式下无需另存。
|
|
34
|
+
|
|
35
|
+
TFTP 导出包括默认值,不接受 `source_file:` 或 `vrf:`。成功必须包含完整的 `Send config file to tftp server OK.` 行;仅出现提示符、命令回显、通用进度或其他传输成功词不算完成。`TftpReceipt` 与已有厂商一致,默认仅为设备报告,服务器文件核验仍由现有 TFTP 核验流程负责。
|
|
36
|
+
|
|
37
|
+
LLDP 查询先读取 `get system status`,明确解析版本后选择命令:7.6.2 及以前使用 `diagnose lldprx neighbor`,7.6.3 起使用 `diagnose lldp rx neighbor`。无法识别版本时直接报解析错误,不猜测命令。接口行数量与解析记录必须一致,空响应不会被当作零邻居。
|
|
38
|
+
|
|
39
|
+
Junos LLDP 使用 `show lldp neighbors detail | no-more`,逐个本地接口块核对记录。残缺记录及只有标题的输出会报错;接口描述从已提交的 set 配置中读取,逻辑单元用 `接口.单元号` 独立返回,保留 `ge-`、`xe-`、`ae` 等原接口身份。
|
|
40
|
+
|
|
41
|
+
## 命令依据
|
|
42
|
+
|
|
43
|
+
- [FireMon TopSec 配置获取接口](https://docs.firemon.com/feature/Content/ADMINISTRATION/DEVICE/Offline_Import_or_Batch_Configs/Additional_Devices.htm):TopSec 的运行配置使用 `show-running nostop`,启动配置使用 `show nostop`,两者不混用。
|
|
44
|
+
- [Fortinet 配置 TFTP 备份](https://community.fortinet.com/fortigate-3/technical-tip-backup-configuration-files-via-tftp-99896):FortiGate `execute backup full-config tftp` 参数。
|
|
45
|
+
- [FortiOS execute cfg](https://docs.fortinet.com/document/fortigate/7.6.5/cli-reference/169686240/execute-cfg):显式保存命令。
|
|
46
|
+
- [Fortinet LLDP 邻居查询](https://community.fortinet.com/fortigate-3/technical-tip-use-lldp-on-fortigate-to-verify-connected-switches-210084):7.6.3 命令改名及邻居输出字段。
|
|
47
|
+
- [Juniper Junos 升级前备份指南](https://www.juniper.net/documentation/us/en/software/junos/srx-upgrade/srx-upgrade.pdf):操作视图 `show configuration | save` 导出已提交配置。
|
|
48
|
+
- [Juniper save 配置模式语义](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/statement/save-cli-config.html):配置模式的层级导出与远端 SCP。
|
|
49
|
+
- [Juniper file copy 协议](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/command/file-copy.html):支持的远端文件传输 URL。
|
|
50
|
+
- [Juniper LLDP 邻居字段](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/command/show-lldp-neighbors.html):接口、机箱、端口和系统名字段,`detail` 选项要求 Junos 19.1R2 或以上。
|
|
51
|
+
|
|
52
|
+
## 拓扑读取能力对齐
|
|
53
|
+
|
|
54
|
+
Huawei VRP 通过 `display lldp neighbor brief` 提供 LLDP 邻居,运行配置提供接口描述。简表不包含机箱 ID,邻居的 `chassis_id` 保留为空;接口类型与编号之间的空格在返回值中保留,在配置身份关联时移除。没有已验证的提交与保存阶段,因此只开放读取。
|
|
55
|
+
|
|
56
|
+
FortiOS 从既有 `show full-configuration` 结果中读取顶层 `config system interface` 的 `description`,不把 `alias` 当作描述,不切换 VDOM 或全局视图。嵌套 VDOM 接口表、多个顶层接口表、缺少 next/end 或未闭合描述会报解析错误。输入按标准 CLI 的四空格缩进组织;不对其他导出格式作静默猜测。
|
|
57
|
+
|
|
58
|
+
- [Huawei LLDP 邻居简表](https://info.support.huawei.com/hedex/api/pages/EDOC1100277650/AZM1016J/04/resources/command/yunshan/LLDPDISPLAYNEIBRIEF%28LLDPOM%29.html):查询视图、字段及端口拼写。
|
|
59
|
+
- [Huawei 当前接口配置](https://info.support.huawei.com/hedex/api/pages/EDOC1100363264/AEN0403J/05/resources/command/yunshan/BUILDRUNINTERFACE1%28CLIOM%29.html):接口块及生效配置的范围。
|
|
60
|
+
- [FortiOS 接口配置结构](https://docs.fortinet.com/document/fortigate/7.4.4/cli-reference/317104469/config-system-interface):description、alias 与 IPv6 等嵌套配置。
|
|
61
|
+
- [Junos 物理接口与逻辑单元描述示例](https://www.juniper.net/documentation/en_US/day-one-books/DayOne-Intro-SRv6.pdf):物理描述与 unit 描述的独立配置层级。
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
module Concurrency
|
|
6
|
+
# 后台观察线程属于调用作用域;停止、等待和异常交付不能盖掉前台原故障。
|
|
7
|
+
module Observer
|
|
8
|
+
# task 自行等待停止信号,stop 只负责唤醒;正常前台也必须观察后台失败。
|
|
9
|
+
def self.run(task:, stop:)
|
|
10
|
+
unless task.respond_to?(:call) && stop.respond_to?(:call) && block_given?
|
|
11
|
+
raise ArgumentError, "background task requires task, stop and a foreground block"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
thread = value = failure = nil
|
|
15
|
+
completion = Queue.new
|
|
16
|
+
begin
|
|
17
|
+
# 创建与登记之间不交付异步中断;子线程恢复即时交付,捕获后同步传回所有者。
|
|
18
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
19
|
+
thread = Thread.new { observe(task, completion) }
|
|
20
|
+
end
|
|
21
|
+
value = yield
|
|
22
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 收尾后向所有者重新抛出原故障,包括中断。
|
|
23
|
+
failure = error
|
|
24
|
+
ensure
|
|
25
|
+
begin
|
|
26
|
+
# 二次中断不能跳过 join;恢复交付时也保留已经发生的前台故障。
|
|
27
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
28
|
+
cleanup_error = finish(thread, stop) if thread
|
|
29
|
+
failure ||= cleanup_error
|
|
30
|
+
end
|
|
31
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 保留前台故障,观察延迟到达的取消。
|
|
32
|
+
failure ||= error
|
|
33
|
+
end
|
|
34
|
+
# break 和非局部 return 也会展开作用域;异常交付必须在 ensure 内,不能绕过。
|
|
35
|
+
failure ||= completion.pop unless completion.empty?
|
|
36
|
+
raise failure, cause: failure.cause if failure
|
|
37
|
+
end
|
|
38
|
+
value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.observe(task, completion)
|
|
42
|
+
failure = nil
|
|
43
|
+
Thread.handle_interrupt(Exception => :immediate) { task.call }
|
|
44
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 不让 global abort_on_exception 将子线程故障传播到无关主线程。
|
|
45
|
+
failure = error
|
|
46
|
+
ensure
|
|
47
|
+
completion << failure
|
|
48
|
+
end
|
|
49
|
+
private_class_method :observe
|
|
50
|
+
|
|
51
|
+
def self.finish(thread, stop)
|
|
52
|
+
failure = nil
|
|
53
|
+
begin
|
|
54
|
+
stop.call
|
|
55
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 唤醒失败时仍必须终止并回收自有线程。
|
|
56
|
+
failure = error
|
|
57
|
+
thread.kill
|
|
58
|
+
ensure
|
|
59
|
+
begin
|
|
60
|
+
thread.join
|
|
61
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 等待失败也不能覆盖先前故障。
|
|
62
|
+
failure ||= error
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
failure
|
|
66
|
+
end
|
|
67
|
+
private_class_method :finish
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
module Concurrency
|
|
6
|
+
# 仅管理有限并发和线程所有权;业务层负责将普通任务故障转换为领域结果。
|
|
7
|
+
class Pool
|
|
8
|
+
DEFAULT_CONCURRENCY = 4
|
|
9
|
+
MAX_CONCURRENCY = 50
|
|
10
|
+
|
|
11
|
+
def self.validate_concurrency!(concurrency)
|
|
12
|
+
unless concurrency.is_a?(Integer) && (1..MAX_CONCURRENCY).cover?(concurrency)
|
|
13
|
+
raise ArgumentError, "concurrency must be an Integer in 1..#{MAX_CONCURRENCY}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
concurrency
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 环境只提供默认并发数;显式构造参数优先,非法值不创建线程。
|
|
20
|
+
def self.environment_concurrency(env: ENV)
|
|
21
|
+
value = env.fetch("NC_CONCURRENCY", DEFAULT_CONCURRENCY.to_s)
|
|
22
|
+
unless value.is_a?(String) && value.match?(/\A[0-9]+\z/)
|
|
23
|
+
raise ArgumentError, "NC_CONCURRENCY must be a positive integer"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
validate_concurrency!(Integer(value, 10))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(concurrency: self.class.environment_concurrency)
|
|
30
|
+
@concurrency = self.class.validate_concurrency!(concurrency)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 保序返回块的值;任何未处理异常均在调用线程抛出,停止并回收本次所有线程。
|
|
34
|
+
# 只复制任务数组,任务内容仍由业务层在下发前校验和固定,不自动重放块。
|
|
35
|
+
def map(items)
|
|
36
|
+
raise ArgumentError, "items must be an Array" unless items.is_a?(Array)
|
|
37
|
+
raise ArgumentError, "map requires a block" unless block_given?
|
|
38
|
+
|
|
39
|
+
items = items.dup
|
|
40
|
+
queue = Queue.new
|
|
41
|
+
items.each_with_index { |item, index| queue << [index, item] }
|
|
42
|
+
count = [items.size, @concurrency].min
|
|
43
|
+
count.times { queue << nil }
|
|
44
|
+
gate = Queue.new
|
|
45
|
+
finished = Queue.new
|
|
46
|
+
results = Array.new(items.size)
|
|
47
|
+
threads = []
|
|
48
|
+
completed = false
|
|
49
|
+
primary_failure = nil
|
|
50
|
+
begin
|
|
51
|
+
count.times do
|
|
52
|
+
# 仅延迟线程创建到登记的异步中断,避免尚未登记的工作线程失去所有者。
|
|
53
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
54
|
+
threads << Thread.new do
|
|
55
|
+
failure = nil
|
|
56
|
+
exhausted = false
|
|
57
|
+
begin
|
|
58
|
+
# 全部线程成功建立后才允许设备操作,创建失败不能留下已发送的变更。
|
|
59
|
+
Thread.handle_interrupt(Exception => :immediate) do
|
|
60
|
+
gate.pop
|
|
61
|
+
while (task = queue.pop)
|
|
62
|
+
index, item = task
|
|
63
|
+
results[index] = yield item
|
|
64
|
+
end
|
|
65
|
+
exhausted = true
|
|
66
|
+
end
|
|
67
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 将中断同步交给调用线程,避免全局 abort_on_exception 波及其他线程。
|
|
68
|
+
failure = error
|
|
69
|
+
ensure
|
|
70
|
+
# Thread.exit/kill 不经过 rescue;不能把尚未完成的任务伪装成合法 nil 结果。
|
|
71
|
+
failure ||= RuntimeError.new("execution worker stopped before completing its tasks") unless exhausted
|
|
72
|
+
finished << [Thread.current, failure]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
count.times { gate << true }
|
|
78
|
+
count.times do
|
|
79
|
+
thread, failure = finished.pop
|
|
80
|
+
thread.join
|
|
81
|
+
raise failure, cause: failure.cause if failure
|
|
82
|
+
end
|
|
83
|
+
completed = true
|
|
84
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 记录取消或原任务故障,收尾中再次取消不能覆盖它。
|
|
85
|
+
primary_failure = error
|
|
86
|
+
raise
|
|
87
|
+
ensure
|
|
88
|
+
stop_threads(threads, primary_failure) unless completed
|
|
89
|
+
end
|
|
90
|
+
results
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# kill 运行任务自身的 ensure;次级关闭故障不能盖掉已观察到的原始异常。
|
|
96
|
+
def stop_threads(threads, primary_failure)
|
|
97
|
+
# 仅回收期间延迟异步取消;交付二次中断之前必须 kill 并 join 全部拥有线程。
|
|
98
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
99
|
+
threads.each(&:kill)
|
|
100
|
+
threads.each do |thread|
|
|
101
|
+
begin
|
|
102
|
+
thread.join
|
|
103
|
+
rescue Exception # rubocop:disable Lint/RescueException -- 等待全部拥有线程收尾,保留调用方原始故障或中断。
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 恢复交付时保留原故障,二次取消不改变批次失败原因。
|
|
109
|
+
# 二次取消可能已以原故障为 cause;不能反向关联形成异常原因环。
|
|
110
|
+
error = primary_failure || error
|
|
111
|
+
raise error, cause: error.cause
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "scope"
|
|
5
5
|
require_relative "../engine/core"
|
|
6
6
|
require_relative "profile"
|
|
7
7
|
require_relative "running_config"
|
|
8
8
|
require_relative "save_config"
|
|
9
9
|
require_relative "local_backup"
|
|
10
|
+
require_relative "remote_backup"
|
|
10
11
|
require_relative "tftp"
|
|
11
12
|
require_relative "topology"
|
|
12
13
|
require_relative "../textfsm"
|
|
@@ -19,6 +20,7 @@ module Net
|
|
|
19
20
|
include RunningConfig::Capability
|
|
20
21
|
include SaveConfig
|
|
21
22
|
include LocalBackup::Capability
|
|
23
|
+
include RemoteBackup::Capability
|
|
22
24
|
include Tftp::Capability
|
|
23
25
|
include Topology::Capability
|
|
24
26
|
include TextFSM::Capability
|
|
@@ -63,7 +65,7 @@ module Net
|
|
|
63
65
|
def self.open(**)
|
|
64
66
|
raise ArgumentError, "open requires a block" unless block_given?
|
|
65
67
|
|
|
66
|
-
value, failure =
|
|
68
|
+
value, failure = Net::Connector::Device::Scope.call(factory: -> { new(**) }) do |device|
|
|
67
69
|
device.connect
|
|
68
70
|
yield device
|
|
69
71
|
end
|
|
@@ -100,6 +102,8 @@ module Net
|
|
|
100
102
|
!config_commands.empty?
|
|
101
103
|
when :save_config
|
|
102
104
|
!save_commands.empty?
|
|
105
|
+
when :remote_backup
|
|
106
|
+
!profile.remote_backup_strategy.nil?
|
|
103
107
|
when :tftp_backup
|
|
104
108
|
!profile.tftp_strategy.nil?
|
|
105
109
|
when :neighbors, :interface_descriptions, :interface_description_changes
|
|
@@ -7,11 +7,15 @@ require_relative "../storage"
|
|
|
7
7
|
module Net
|
|
8
8
|
module Connector
|
|
9
9
|
Backup = Data.define(:path, :bytes, :sha256, :collected_at, :change, :previous_sha256) do
|
|
10
|
-
#
|
|
10
|
+
# 完成回执拥有自己的元数据;调用方和结果回调不能原地改写已保存文件的证据。
|
|
11
11
|
def initialize(path:, bytes:, sha256:, collected_at:, change: nil, previous_sha256: nil)
|
|
12
|
-
super
|
|
12
|
+
super(path: path&.dup&.freeze, bytes: bytes, sha256: sha256&.dup&.freeze,
|
|
13
|
+
collected_at: collected_at&.dup&.freeze, change: change, previous_sha256: previous_sha256&.dup&.freeze)
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
# Ruby 3.2 的原生 Data#with 不调用初始化;更新也须重新复制并冻结元数据。
|
|
17
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
18
|
+
|
|
15
19
|
# 判断本次采集是否产生新的配置内容。
|
|
16
20
|
def changed? = [:created, :changed].include?(change)
|
|
17
21
|
end
|
|
@@ -21,14 +25,14 @@ module Net
|
|
|
21
25
|
attr_reader :backup, :receipt, :underlying_type
|
|
22
26
|
|
|
23
27
|
def initialize(backup:, write_error:, host: nil)
|
|
24
|
-
unless backup.instance_of?(Backup) && Storage::
|
|
28
|
+
unless backup.instance_of?(Backup) && Storage::AtomicFile.receipt_error?(write_error) &&
|
|
25
29
|
write_error.receipt.committed? && write_error.receipt.path == backup.path
|
|
26
30
|
raise ArgumentError, "backup persistence error requires a matching committed backup receipt"
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
@backup, @receipt = backup, write_error.receipt
|
|
30
34
|
@underlying_type = write_error.underlying_type
|
|
31
|
-
code = if write_error.instance_of?(Storage::
|
|
35
|
+
code = if write_error.instance_of?(Storage::AtomicFile::DirectorySyncUnsupported)
|
|
32
36
|
:backup_durability_unsupported
|
|
33
37
|
elsif receipt.durable?
|
|
34
38
|
:backup_finalize_failed
|
|
@@ -47,7 +51,7 @@ module Net
|
|
|
47
51
|
raise ArgumentError, "backup must be a completed Backup" unless backup.instance_of?(Backup)
|
|
48
52
|
|
|
49
53
|
@backup = backup
|
|
50
|
-
@underlying_type =
|
|
54
|
+
@underlying_type = Error::Metadata.type(underlying.class.name) || "StandardError"
|
|
51
55
|
super("backup completed; path lock finalization failed", code: :backup_finalize_failed, host: host, phase: :backup)
|
|
52
56
|
end
|
|
53
57
|
end
|
|
@@ -83,7 +87,7 @@ module Net
|
|
|
83
87
|
def collect_and_write(destination)
|
|
84
88
|
contents = @device.running_config.value!
|
|
85
89
|
digest = Digest::SHA256.hexdigest(contents)
|
|
86
|
-
previous = Storage::
|
|
90
|
+
previous = Storage::FileReader.fingerprint(destination, missing: true, replace_symlink: true)
|
|
87
91
|
previous_digest = previous&.sha256
|
|
88
92
|
change = if previous_digest.nil?
|
|
89
93
|
:created
|
|
@@ -97,7 +101,7 @@ module Net
|
|
|
97
101
|
sha256: digest, collected_at: Time.now.utc, change: change,
|
|
98
102
|
previous_sha256: previous_digest)
|
|
99
103
|
# 内容未变也须核对权限与目录项身份;只在两者仍有效时保留原文件和 mtime。
|
|
100
|
-
if change != :unchanged || previous.mode != 0o600 || !Storage::
|
|
104
|
+
if change != :unchanged || previous.mode != 0o600 || !Storage::FileReader.same_entry?(destination, previous)
|
|
101
105
|
write_backup(backup, contents)
|
|
102
106
|
end
|
|
103
107
|
backup
|
|
@@ -105,9 +109,9 @@ module Net
|
|
|
105
109
|
|
|
106
110
|
# 替换后失败仍交付已提交的备份回执,不能让调用方误判为没有产物。
|
|
107
111
|
def write_backup(backup, contents)
|
|
108
|
-
Storage::
|
|
109
|
-
rescue Storage::
|
|
110
|
-
raise unless Storage::
|
|
112
|
+
Storage::AtomicFile.write(backup.path, contents)
|
|
113
|
+
rescue Storage::AtomicFile::WriteError => error
|
|
114
|
+
raise unless Storage::AtomicFile.receipt_error?(error) && error.receipt.committed?
|
|
111
115
|
|
|
112
116
|
host = @device.host if @device.respond_to?(:host)
|
|
113
117
|
raise BackupPersistenceError.new(backup: backup, write_error: error, host: host), cause: nil
|
|
@@ -85,6 +85,12 @@ module Net
|
|
|
85
85
|
self
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# 指定设备原生 FTP/SCP 导出策略;nil 明确关闭继承能力。
|
|
89
|
+
def remote_backup_strategy(klass)
|
|
90
|
+
@values[:remote_backup_strategy] = klass
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
88
94
|
# 指定运行配置采集策略,覆盖继承的厂商规则。
|
|
89
95
|
def running_config_strategy(klass)
|
|
90
96
|
@values[:running_config_strategy] = klass
|
|
@@ -21,7 +21,7 @@ module Net
|
|
|
21
21
|
:privilege_prompt, :pager_pattern, :pager_response,
|
|
22
22
|
:authentication_error_patterns, :command_error_patterns,
|
|
23
23
|
:login_interactions, :confirmation_interactions, :legacy_ssh_arguments,
|
|
24
|
-
:privilege_command, :command_timeout, :terminal_size, :tftp_strategy, :topology_strategy, :running_config_strategy
|
|
24
|
+
:privilege_command, :command_timeout, :terminal_size, :tftp_strategy, :topology_strategy, :running_config_strategy, :remote_backup_strategy
|
|
25
25
|
|
|
26
26
|
# 使用默认的公共规则创建空设备档案;登录和命令提示符仍由厂商声明。
|
|
27
27
|
def self.default
|
|
@@ -46,7 +46,7 @@ module Net
|
|
|
46
46
|
authentication_error_patterns: DEFAULT_AUTHENTICATION_ERROR_PATTERNS,
|
|
47
47
|
command_error_patterns: [], login_interactions: [], confirmation_interactions: [],
|
|
48
48
|
legacy_ssh_arguments: DEFAULT_LEGACY_SSH_ARGUMENTS, privilege_command: nil,
|
|
49
|
-
command_timeout: 25, terminal_size: nil, tftp_strategy: nil, topology_strategy: nil, running_config_strategy: nil)
|
|
49
|
+
command_timeout: 25, terminal_size: nil, tftp_strategy: nil, topology_strategy: nil, running_config_strategy: nil, remote_backup_strategy: nil)
|
|
50
50
|
@config_commands = command_list(config_commands, :config_commands, allow_nil: true)
|
|
51
51
|
@save_commands = command_list(save_commands, :save_commands)
|
|
52
52
|
@login_prompt = pattern(login_prompt, :login_prompt, allow_nil: true)
|
|
@@ -64,6 +64,7 @@ module Net
|
|
|
64
64
|
@privilege_command = optional_text(privilege_command, :privilege_command)
|
|
65
65
|
@command_timeout = duration(command_timeout, :command_timeout)
|
|
66
66
|
@terminal_size = validate_terminal_size!(terminal_size)
|
|
67
|
+
@remote_backup_strategy = strategy_class(remote_backup_strategy, :remote_backup_strategy, %i[script completed?])
|
|
67
68
|
@running_config_strategy = strategy_class(running_config_strategy, :running_config_strategy,
|
|
68
69
|
%i[clean result_step prompt_text validate_response!])
|
|
69
70
|
@tftp_strategy = strategy_class(tftp_strategy, :tftp_strategy,
|
|
@@ -73,7 +74,7 @@ module Net
|
|
|
73
74
|
end
|
|
74
75
|
@topology_strategy = strategy_class(topology_strategy, :topology_strategy,
|
|
75
76
|
%i[neighbor_command neighbor_template expected_neighbor_count empty_neighbor_output?
|
|
76
|
-
protocol description_template interface_key configuration_interface
|
|
77
|
+
protocol description_template description_key interface_key configuration_interface
|
|
77
78
|
decode_description validate_descriptions! change_error_code supports?
|
|
78
79
|
enter_configuration_command change_commands script_command
|
|
79
80
|
leave_configuration_commands verification_commands persistence_commands persistence_confirmed?])
|
|
@@ -105,6 +106,7 @@ module Net
|
|
|
105
106
|
terminal_size: terminal_size,
|
|
106
107
|
tftp_strategy: tftp_strategy,
|
|
107
108
|
topology_strategy: topology_strategy,
|
|
109
|
+
remote_backup_strategy: remote_backup_strategy,
|
|
108
110
|
running_config_strategy: running_config_strategy
|
|
109
111
|
}
|
|
110
112
|
end
|