net-connector 0.4.1 → 0.4.2
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 +34 -0
- data/CONTRIBUTING.md +40 -0
- data/README.md +61 -9
- data/SECURITY.md +11 -0
- data/docs/VERIFICATION.md +129 -6
- data/docs/architecture.md +184 -7
- data/lib/net/connector/device/base.rb +11 -4
- data/lib/net/connector/device/running_config.rb +2 -0
- data/lib/net/connector/engine/command.rb +14 -2
- data/lib/net/connector/engine/configuration.rb +10 -7
- data/lib/net/connector/engine/dialogue.rb +52 -28
- data/lib/net/connector/engine/errors.rb +40 -55
- data/lib/net/connector/engine/execution.rb +32 -5
- data/lib/net/connector/engine/log.rb +11 -9
- data/lib/net/connector/engine/session.rb +38 -7
- data/lib/net/connector/engine/terminal_renderer.rb +7 -4
- data/lib/net/connector/netdisco/batch.rb +31 -3
- data/lib/net/connector/netdisco/cli.rb +73 -31
- data/lib/net/connector/netdisco/client.rb +206 -73
- data/lib/net/connector/netdisco/config_file.rb +26 -4
- data/lib/net/connector/netdisco/diagnostic.rb +91 -0
- data/lib/net/connector/netdisco/fleet.rb +103 -60
- data/lib/net/connector/netdisco/inventory_budget.rb +49 -0
- data/lib/net/connector/netdisco/report.rb +94 -0
- data/lib/net/connector/netdisco/rules.rb +28 -5
- data/lib/net/connector/netdisco/settings.rb +194 -85
- data/lib/net/connector/netdisco/worker.rb +26 -17
- data/lib/net/connector/operations/backup_lock.rb +116 -0
- data/lib/net/connector/operations/local_backup.rb +49 -9
- data/lib/net/connector/operations/parse_output.rb +20 -3
- data/lib/net/connector/operations/private_file.rb +94 -5
- data/lib/net/connector/operations/safe_file.rb +62 -0
- data/lib/net/connector/operations/saved_config/legacy_index.rb +109 -0
- data/lib/net/connector/operations/saved_config.rb +40 -10
- data/lib/net/connector/operations/tftp/file_upload.rb +14 -1
- data/lib/net/connector/operations/tftp_backup.rb +78 -15
- data/lib/net/connector/operations/tftp_receipt.rb +73 -0
- data/lib/net/connector/operations/topology/immediate_strategy.rb +45 -0
- data/lib/net/connector/operations/topology/strategy.rb +20 -0
- data/lib/net/connector/operations/topology.rb +97 -29
- data/lib/net/connector/operations.rb +6 -0
- data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +9 -2
- data/lib/net/connector/vendor/cisco_ios/topology.rb +10 -4
- data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +8 -1
- data/lib/net/connector/vendor/h3c/tftp_backup.rb +5 -0
- data/lib/net/connector/vendor/h3c/topology.rb +9 -4
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +12 -3
- data/lib/net/connector/vendor/hillstone/topology.rb +9 -4
- data/lib/net/connector/vendor/huawei/tftp_backup.rb +6 -0
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +14 -5
- data/lib/net/connector/vendor/palo_alto/topology.rb +7 -2
- data/lib/net/connector/vendor/radware/tftp_backup.rb +9 -2
- data/lib/net/connector/vendor/radware/topology.rb +1 -0
- data/lib/net/connector/version.rb +1 -1
- metadata +35 -5
data/docs/architecture.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
| 接口文本 | 名称匹配、简称、描述和公共接口视图命令 | `device/interface_name.rb`、`device/interface_description.rb` |
|
|
13
13
|
| TextFSM 解析 | 选择模板,将命令或配置转为记录 | `operations/parse_output.rb`、`templates/` |
|
|
14
14
|
| 拓扑与描述计划 | 读取邻居及旧描述、生成命令、重验后下发 | `operations/topology.rb` |
|
|
15
|
-
| 本地备份 |
|
|
15
|
+
| 本地备份 | 路径所有权、采集、比较哈希、保存与完成回执 | `operations/local_backup.rb`、`backup_lock.rb` |
|
|
16
16
|
| 已存配置导出 | 不访问清单或设备,读取已有备份 | `operations/saved_config.rb` |
|
|
17
|
-
|
|
|
17
|
+
| 私有文件读写 | 同 FD 验证读取;0600 替换、文件及目录同步 | `operations/safe_file.rb`、`private_file.rb` |
|
|
18
18
|
| TFTP 备份 | 校验目标、执行导出、核对成功证据 | `operations/tftp_backup.rb` |
|
|
19
19
|
| TFTP 厂商策略 | 命令、提示、源文件、成功证据和目标名称 | `vendor/<厂商>/tftp_backup.rb` |
|
|
20
20
|
| 拓扑厂商策略 | 发现命令、解析证据、配置视图及特殊命令 | `vendor/<厂商>/topology.rb` |
|
|
@@ -25,15 +25,76 @@
|
|
|
25
25
|
| 设备集合 | 读取清单、执行本地或 TFTP 任务、写报告 | `netdisco/fleet.rb` |
|
|
26
26
|
| 设置 | 读取环境变量及 YAML 覆盖项 | `netdisco/settings.rb` |
|
|
27
27
|
|
|
28
|
+
## 公开业务方法与确认范围
|
|
29
|
+
|
|
30
|
+
下表区分执行结果、业务产物和直接抛出的前置错误;调用方不能假设所有失败都在
|
|
31
|
+
`result.error`。参数/脚本预检失败不代表产生过 Result,`Result#value!` 则会重新抛出
|
|
32
|
+
其中的领域错误。错误诊断已脱敏,返回的原始输出、配置和文件内容仍是敏感业务数据。
|
|
33
|
+
|
|
34
|
+
| 入口 | 正常返回 | 失败和前置约束 |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `execute`、`execute_script` / `run` | `Result`,`steps` 保留完整已完成响应 | 参数/脚本构造错误可直接抛出;执行、钩子和预算错误通常进入 Result,不自动重放 |
|
|
37
|
+
| `running_config` / `collect_config` | `Result`,`config` 为通过完整性检查的清理副本 | 执行或清理失败保留步骤;自定义档案/策略构造错误仍可能直接抛出 |
|
|
38
|
+
| `save_config` | `Result` | 不支持时为失败 Result;该入口确认命令响应,不提供拓扑读回或设备介质验证 |
|
|
39
|
+
| `backup(path:)` | `Backup`,含 path、bytes、sha256、collected_at、change、previous_sha256 | 采集/文件错误直接抛出;已 rename 后的持久性错误携带完成回执,不能视为未写入 |
|
|
40
|
+
| `tftp_backup` | 原三成员 `TftpBackup(server, path, completed_at)` | 参数先校验;失败/未确认直接抛出。完成后故障为 `TftpCompletionError`,保留回执 |
|
|
41
|
+
| `tftp_backup_receipt` | 包含原 transfer 的 `TftpReceipt` | 与旧入口相同;当前执行路径只报告 `device_reported`,无服务端摘要 |
|
|
42
|
+
| `parse_command`、`parse_config` | TextFSM 记录数组 | 命令失败、模板错误、严格 UTF-8/解析错误直接抛出,不用空数组代替未知输出 |
|
|
43
|
+
| `neighbors`、`interface_descriptions` | `Neighbor` 数组、接口到描述的 Hash | 不支持、未知/部分输出或读取失败直接抛出;明确空表才允许空结果 |
|
|
44
|
+
| `plan_interface_descriptions` | 冻结的 `Topology::Plan` | 只读证据和能力检查;PAN-OS 当前拒绝自动改写计划,无隐式 commit |
|
|
45
|
+
| `apply_interface_descriptions(plan, confirmed:)` | `Result`,包括变更后已完成的读回/保存步骤 | 未确认、错误设备/旧命令序列、能力不足及重验失败在写入前直接抛出;`stale_plan` 是 `DeviceError`。开始变更后的领域失败保留步骤 |
|
|
46
|
+
| `SavedConfig#read/#parse/#export` | 原始字节、记录数组;export 返回目标路径或 stdout 模式的 nil | 纯本地;缺失/歧义、不安全文件、快照失效、解析或写入错误直接抛出 |
|
|
47
|
+
| `Fleet#backup_all/#tftp_backup_all` | 默认 `Batch`;显式 schema 2 为 `Report` | 设置、外部计划和清单错误在派发前抛出;设备/关闭/回调/报告普通故障保留在批次中,线程中断仍清理后传播 |
|
|
48
|
+
|
|
49
|
+
确认分为三个独立层次:
|
|
50
|
+
|
|
51
|
+
1. 命令响应完成:匹配本次响应的结束提示,或收到厂商明确完成行;不等于业务状态正确。
|
|
52
|
+
2. 业务状态读回:目标描述与批准计划一致;不排除其他设备会话此后改写,也不等于已持久化。
|
|
53
|
+
3. 持久化/服务端证据:本地文件完成 fsync/rename/目录 fsync,或设备明确报告保存完成。
|
|
54
|
+
TFTP 目前只有设备报告,不能据此证明服务器文件、SHA-256 或本批版本归属;设备保存
|
|
55
|
+
完成行也不是断电测试。读取、核验、持久化任何阶段失败都不触发自动回放/回滚。
|
|
56
|
+
|
|
28
57
|
## 业务约束
|
|
29
58
|
|
|
30
59
|
连接器独占一个会话。`Session` 串行执行登录和脚本,失败时关闭传输,不自动重放设备命令。`Result` 在后续步骤失败时仍保存已完成步骤。`RunningConfig` 每次采集创建一个新策略,同一策略负责响应检查、结果选择和清理。选择与清理在会话锁内经过现有设备钩子,子类覆盖后可调用 `super`。清理失败时也会解除临时绑定,其他 Fiber 的离线清理不能借用该策略。
|
|
31
60
|
|
|
32
61
|
采集命令匹配当前会话的完整提示符行,不以末尾单个 `#`、`>` 或 `]` 判断完成。PAN-OS 切换视图时保留已认证的设备身份。缺少最终提示符会使采集失败,旧备份保持不变;只有提示符或命令回显的响应属于 `:incomplete_configuration`,不是成功的空配置。PAN-OS 在 `show` 前后都检查候选配置差异。
|
|
33
62
|
|
|
34
|
-
`LocalBackup`
|
|
63
|
+
`LocalBackup` 在采集前取得 `BackupLock`,持有到摘要比较、替换与结果构造结束。锁名由 realpath 父目录与归一化文件名决定;Unicode NFC、大小写折叠后的摘要同时覆盖尚未创建的目标。区分大小写的文件系统也保守合并这些锁,目标名称本身不变。锁文件用 NOFOLLOW/0600 打开,再验证普通文件、所有者、单硬链接和 inode;释放仅关闭 FD,不删除锁文件。默认 `flock(LOCK_EX | LOCK_NB)`,可选等待共用有限单调期限,超时均为 `BackupBusy`。
|
|
64
|
+
|
|
65
|
+
Fleet 在旧命名基线读取、凭据解析及连接器构造之前加同一把锁,再向下层备份授权一次同进程、同 Fiber 的借用。借用在采集前消费,回调递归备份不能重复使用;没有全局路径缓存。路径锁在外、会话锁在内,`Base#backup` 拒绝已有会话操作中的嵌套调用。独立进程必须采用同一 flock 协议;调用方保护目录及祖先,锁不约束不合作的写入者。
|
|
66
|
+
|
|
67
|
+
`SafeFile` 对一次 NOFOLLOW/NONBLOCK 打开的 FD 做 fstat 和读取,拒绝非普通文件。`SavedConfig#read/#fingerprint`、离线导出和解析复用该边界;`find` 仅兼容返回当时已验证的路径,不保证调用方日后重新打开时身份不变。直接备份继续安全替换末级符号链接,Fleet/SavedConfig 则保留拒绝契约。内容不变时再核对目录项身份,避免因路径替换跳过必要写入;正常未变文件保持 mtime。
|
|
68
|
+
|
|
69
|
+
`PrivateFile.write` 仍返回传入路径,内部 `write_receipt` 执行临时文件写入、flush/fsync、rename、父目录 fsync,并跟踪 not_committed/committed/durable 和出错阶段。rename 前失败保留旧文件;之后失败保留新文件。目录 fsync 的 EINVAL/ENOSYS/ENOTSUP/EOPNOTSUPP/NotImplementedError 单独表示不支持;其他错误不能降级为成功。durable 只表示同步调用成功,未变内容不补验过去写入,断电恢复也未在本地测试中验证。
|
|
70
|
+
|
|
71
|
+
提交后的 `BackupPersistenceError` 只携带原 `Backup` 元数据和受控写入回执,Fleet 保留产物并使用已有 saved_with_error 分类。只接受库定义的具体完成错误及匹配产物类型,第三方异常的 backup 属性或自定义 WriteError 子类不作为完成证据。报告保留已提交位置,导出错误说明 committed;旧 Data 成员、JSON schema 和 strict 策略不变,设备命令不会自动重放。
|
|
72
|
+
|
|
73
|
+
TFTP 只确认设备报告的上传结果:策略去掉命令、应答和提示符回显后,检查明确的完成行;文件名或表示“即将上传”的进度文字不算成功。原始输出或终端渲染文本中的失败证据优先于成功文字。
|
|
74
|
+
|
|
75
|
+
TFTP 内置策略的 `validate_options!` 是纯参数校验,执行于 H3C 源文件探测之前。整个探测、脚本、证据核对、回执和事件记录由原有 `with_operation(:tftp_backup)` 独占;其他线程、Fiber 及回调重入均不能插入设备命令。租约只保护当前会话,不能隔离其他设备连接或服务器上的同名文件。
|
|
76
|
+
|
|
77
|
+
`tftp_backup` 继续返回原三字段 `TftpBackup`;`tftp_backup_receipt` 以不可变 `TftpReceipt` 组合来源、格式、请求与实际路径、核验等级。只有设备完成证据时为 `device_reported`,没有服务器 SHA-256。H3C 自动源探测为 startup,显式文件为 saved_file/unknown;山石为 startup/dat,Cisco 为 running/cfg,PAN-OS 为 running/xml,Radware 为 native_archive/tgz。扩展策略未声明时为 unknown,不能据文件扩展名或字节数推断来源或服务器内容。
|
|
78
|
+
|
|
79
|
+
明确完成证据先生成最小回执,再执行路径提取、元数据钩子及事件记录。已完成步骤之后的清理错误同样保留回执;路径不可用时保留 nil,不把请求文件名冒充实际目标。`TftpCompletionError` 只含受控码、类型和回执,不带原始消息、输出或 cause。Fleet 仅信任库定义的精确错误类及匹配产物;第三方异常上的 transfer 字段和错误子类不能供应完成事实。默认报告成员及 strict 成功规则不变,已上传但收尾失败使用现有 reported_with_error/partial 分类。
|
|
80
|
+
|
|
81
|
+
`Topology` 读取邻居和旧描述,冻结计划,要求显式确认,并在写入前重验全部证据和重建的命令。`ImmediateStrategy` 将即时生效设备的修改/退出视图、运行配置读回、保存拆开:读回未确认或有未识别接口块时停止,不发送保存。重验、下发、读回、保存及阶段间隙都持有原有会话租约;只有所属线程和 Fiber 能顺序执行脚本,其他调用、关闭请求和脚本回调重入均返回 `SessionBusy`。租约不隔离其他设备会话或管理员。
|
|
35
82
|
|
|
36
|
-
|
|
83
|
+
```mermaid
|
|
84
|
+
flowchart LR
|
|
85
|
+
A[计划及证据重验] --> B[修改并退出配置视图]
|
|
86
|
+
B --> C[按批准命令读回]
|
|
87
|
+
C -->|全部目标描述确认| D[发送保存命令]
|
|
88
|
+
C -->|不完整或不匹配| E[保留已完成步骤并返回错误]
|
|
89
|
+
D -->|明确保存完成行| F[设备报告持久化完成]
|
|
90
|
+
D -->|失败或缺少完成证据| G[persistence_unconfirmed]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`Plan` 的 Data 成员和 evidence 结构保持不变,commands 的字符串序列新增真正执行的读回命令,因此旧序列或篡改计划在修改前被拒绝,须重新生成审批。读回执行前检查配置命令仍与审批相同,执行后再检查实际完成步骤;不一致为 verification_plan_changed。修改开始后的 Result 保留修改、读回及保存中的完成步骤;写入前 stale_plan 仍直接抛 DeviceError。保存异常只附带经过屏蔽的底层类型,统一标为 persistence_unconfirmed,不声称修改未发生,不重放、不自动回滚。
|
|
94
|
+
|
|
95
|
+
保存确认仅代表设备的明确完成消息,单独的命令提示符或进度不够。IOS 使用 `[OK]`,NX-OS 使用最终 `Copy complete.`,H3C 使用已记录的主板保存完成行;原文或终端渲染文本中有失败证据时均拒绝确认。山石识别文档中的 `Saving configuration is finished`,但该行来自重启前保存示例,对现有 save all 的复用是推断,其他现场格式仍返回未确认。已知格式与合成夹具来源记录在源码的 `test/fixtures/topology/README.md`;本库不提供设备存储介质断电验证。
|
|
96
|
+
|
|
97
|
+
PAN-OS 使用候选配置模型。[官方提交说明](https://docs.paloaltonetworks.com/ngfw/pan-os-cli-quick-start/use-the-cli/commit-configuration-changes)明确变更在提交后才生效,[锁说明](https://docs.paloaltonetworks.com/ngfw/administration/firewall-administration/launch-the-web-interface/manage-locks-for-restricting-configuration-changes)区分阻止编辑与阻止提交。现有前后 diff 不能证明变更归属;缺少目标固件的锁、候选验证和 commit 完成实验时,自动改写能力关闭,以 candidate_isolation_unavailable 在 I/O 前拒绝计划及下发,保留只读解析。没有增加或猜测锁/commit job 命令。
|
|
37
98
|
|
|
38
99
|
`Session` 使用 `Mutex#try_lock`,让竞争调用立即失败而不等待长时间设备命令。租约同时记录线程与 Fiber;同线程的另一个 Fiber 不能借用。`@performing` 还阻止命令回调嵌套执行脚本。可重入锁本身无法区分“租约内顺序执行脚本”和“命令回调嵌套执行脚本”;替换锁实现时仍须保留这层业务判断。
|
|
39
100
|
|
|
@@ -41,8 +102,49 @@
|
|
|
41
102
|
|
|
42
103
|
`Worker` 按线程完成顺序接收终止通知,设备结果仍写入原清单槽位。任一线程中断时,调用方无需等待先创建的慢线程;创建后续线程失败时,也会停止并等待已启动的任务清理资源。普通设备故障继续转换为逐台结果,回调故障单独记录。
|
|
43
104
|
|
|
105
|
+
设备耗时由 CLOCK_MONOTONIC 度量,包含 on_start、设备任务及关闭,截止于 on_result 前。批次 v2 的总耗时覆盖目录准备和全部 worker/callback,截止于报告写入前;两者不以 UTC 时间相减。Outcome 以非成员元数据保留耗时和安全 Diagnostic,with 在 Worker 时间赋值和产物迁移时保留它们;显式替换审计时间会清除旧计时,替换错误字段会清除旧诊断。手工创建的旧 Outcome 没有单调计时,继续按墙钟差计算并将负值限制为零。Data 的 members/deconstruct/to_h 不添加字段。
|
|
106
|
+
|
|
107
|
+
默认 Fleet 返回 Batch、保存 schema 1,旧 success? 和 status 不变。显式 report_schema: 2 返回组合式 Report;success_policy: :selected 自动选择 v2,selected 配合 schema 1 在清单读取前被拒绝。Report 委托旧 Batch 的业务访问器和严格 success?/status,单独提供 policy 和 policy_success?;原批次可通过 report.batch 访问。selected 必须至少包含一个成功结果,其他状态只能为 filtered/sample_limit,且 callback_errors 为空、report_error 为 nil。部分成功、任何其他跳过和未知状态都阻止策略成功。
|
|
108
|
+
|
|
109
|
+
v2 summary 增加 schema_version、policy、policy_success、coverage、单调 duration_ms、逐台 diagnostic 及 report_diagnostic。coverage 仅按已尝试状态计数,失败和部分成功也算尝试,不能替代成功判定。批次报告使用调用方原有 write(result, directory:) 签名;默认仍传 Batch,v2 传提供相同业务访问器的 Report。已存文件是写入前快照;写入自身失败时,返回对象与 CLI 才能携带最终报告故障。只对已有 Batch 调用 report 不重跑任务,也不能补出原先未记录的批次总耗时或报告写入阶段。
|
|
110
|
+
|
|
111
|
+
Diagnostic 只保存固定词表中的码、类型、阶段和受控产物状态;不调用异常 inspect/to_h,不保留异常引用、正文、消息、回溯、命令、source 或 line。未知错误码/阶段为 nil,未知类型归为 StandardError。v2 会重新筛选旧 Batch 的 error_code/error_type、callback_errors 和 report_error,不能因旧字段名看似安全就直接扩充它。产物阶段仅来自与实际 Backup/TftpBackup 匹配的库内精确错误类;通用文件回执只在报告写入边界使用,不能冒充设备备份完成。默认旧 JSON 的成员与自定义错误字段值保持原契约。
|
|
112
|
+
|
|
44
113
|
`Planner` 先按厂商采样,再按实际 TFTP 文件名排除覆盖冲突,保留采样顺序中的首台设备。未入选设备仍标记为 `sample_limit`,只有入选后目标重名才标记为 `remote_filename_collision`。`Plan#validate!` 校验任务与清单的对应关系;调用方传入或通过 `with` 修改的计划也必须在读取凭据、创建目录及设备 I/O 前通过校验。冲突规则适用于全部厂商,包括不同地址规范化后产生相同文件名的情况。
|
|
45
114
|
|
|
115
|
+
`Settings` 保留动态秘密来源,`snapshot(mode:)` 只复制允许公开的非敏感字段,并在副作用前复用 `Configuration`、`Planner` 和 `Client.options` 校验。Fleet 的一次规划/执行只使用该份冻结策略;CLI 通过 `for_run` 将规划与执行绑定到同一策略。每台设备的凭据读取与策略解析分开,自定义凭据解析器给出的显式连接选项仍保留原优先级。带 `plan:` 的执行不再查询清单;以后新建的批次可以读取新策略和新的 API 凭据。
|
|
116
|
+
|
|
117
|
+
`Client#devices` 每次持有独立 `InventoryBudget`,认证、分页、旧式查询共同累计正文字节和去重前记录数,并共享单调时钟 deadline。默认 HTTP 使用 `read_body`,先检查实际接收字节再追加,不信任 Content-Length。每阶段缩短原生连接/读/写超时;总期限还覆盖慢速响应头。期限观察线程只关闭本次拥有的 HTTP 连接,防止 chunked 的收尾读取拖延返回,调用结束即唤醒并 join。默认 HTTP 不做隐式 GET 重试;这一机制不进入设备命令路径。
|
|
118
|
+
|
|
119
|
+
注入的旧式 `requester` 仍只接收 URI 和 request。它返回后才接受长度和期限检查,不在任意用户 Ruby 回调中注入异步异常。解析和集合操作完成后也检查期限,但这不构成任意 CPU 回调的抢占保证。预算失败仅报告错误码与安全类型,没有部分清单、响应正文或底层 cause。HTTPS 的标准证书检查不变;明文 HTTP 默认兼容,可通过显式策略禁止。
|
|
120
|
+
|
|
121
|
+
## 迁移与尚未启用的能力
|
|
122
|
+
|
|
123
|
+
| 变化 | 调用方需要保留的约定 |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| 配置采集默认输出敏感 | 所有日志模式及错误正文隐藏配置;Result.config/steps 和备份仍保留原数据。自定义敏感业务需声明 output_sensitive,不以注册登录密码代替正文保护 |
|
|
126
|
+
| 共享脱敏依赖 | 安装 expect-pty 0.5.x;字节匹配使用公开 Expect::Redactor,连接器只维护作用域及输出策略,不再需要本地补丁 |
|
|
127
|
+
| 拓扑分阶段计划 | 新计划包含真实读回命令;旧/篡改序列重新生成并批准。PAN-OS 暂拒绝自动改写,不引入未经验证的锁或 commit job 命令 |
|
|
128
|
+
| 文件锁及持久性 | 备份前获取稳定的私有锁文件,调用方不要按目录文件总数推断备份数或在运行中删除锁文件;已提交但收尾失败仍有产物,不自动覆盖重试 |
|
|
129
|
+
| 清单和脚本预算 | 清单预算默认有界;max_script_output_bytes 默认为 nil、显式启用。两者都不是整批设备执行的硬 deadline,累计脚本超限不表示命令未执行 |
|
|
130
|
+
| HTTP | HTTPS 校验保持;默认允许既有明文 HTTP,部署可显式禁止。设置快照不冻结逐设备动态凭据解析 |
|
|
131
|
+
| 报告和成功策略 | 默认 strict/schema 1 保持;selected 自动使用 schema 2,只有预期过滤/采样跳过可忽略。设备部分成功、回调/报告错误仍阻止策略成功 |
|
|
132
|
+
| 解析 | 只在解析副本上严格检查 UTF-8;非法数据直接报错。原始备份/导出不转码,其他设备编码等待真实样本再扩展 |
|
|
133
|
+
|
|
134
|
+
任务书 NC-12(协作取消和整批 deadline)为后置可选项,本轮 **deferred**:没有新增
|
|
135
|
+
取消令牌、取消状态或“停止后不派发”的公开保证。现有 Worker 的异常 kill/join、
|
|
136
|
+
任务 ensure 和 Session 超时仍保留;任意用户回调应自行保证返回,不能承诺非合作
|
|
137
|
+
回调的硬期限。清单获取 deadline 只约束 Client.devices,不覆盖整个 Fleet 任务。
|
|
138
|
+
|
|
139
|
+
NC-07C(TFTP 服务端验证适配器)同为可选扩展,本轮 **deferred**:尚无调用方存储、
|
|
140
|
+
时间/版本关联和目标隔离协议,当前业务执行不生成 server_verified。只检查文件
|
|
141
|
+
存在或只把固定名上传串行化均不足以实现验证,因此 PAN-OS 同名冲突拒绝规则不变。
|
|
142
|
+
后续适配器必须关联设备、实际目标、时间/版本、摘要及本次任务;验证失败也不能重传。
|
|
143
|
+
|
|
144
|
+
本地租约不阻止其他设备会话/管理员写入;flock 不隔离未合作的进程,也不能使祖先目录
|
|
145
|
+
或网络文件系统成为可信事务。目录同步不等于断电恢复认证,TFTP 的同批检查不解决外部
|
|
146
|
+
任务同名覆盖。当前合成夹具没有提供目标固件支持范围,现场验收仍须独立完成。
|
|
147
|
+
|
|
46
148
|
## Expect 语义与 Ruby 边界
|
|
47
149
|
|
|
48
150
|
[Tcl Expect 手册](https://core.tcl-lang.org/expect/doc/trunk/expect.man)定义有序匹配、`exp_continue -continue_timer`、缓冲区消费、EOF,以及分离的关闭和等待职责。[匹配循环](https://github.com/tcltk-depot/expect/blob/main/expect.c)在继续匹配时保留截止时间,[进程处理](https://github.com/tcltk-depot/expect/blob/main/exp_command.c)负责等待子进程并重试中断。这些是参考语义;`net-connector` 是设备操作库,不实现 Tcl 解释器或完整 Expect API。
|
|
@@ -52,6 +154,7 @@
|
|
|
52
154
|
| 匹配 | 先识别连接失败,再处理交互,最后匹配提示符;提示符匹配必须消费字节 | `ResponseReader` |
|
|
53
155
|
| 时间 | 命令写入和提示应答共用单调时钟截止时间,进度与分页不延长它 | `Session`、`ResponseReader` |
|
|
54
156
|
| 缓冲区 | 输出上限由 `max_output_bytes` 控制,流式匹配保留 32 KiB 未匹配尾部 | `Transports::Pty`、`ResponseReader` |
|
|
157
|
+
| 脚本累计输出 | 可选 `max_script_output_bytes`,默认 nil;主命令及追加查询共用计数 | `Configuration`、`Execution` |
|
|
55
158
|
| EOF | 返回 `ConnectionClosed`,保留先前步骤,关闭会话 | `ResponseReader`、`Execution`、`Session` |
|
|
56
159
|
| 资源 | 通过 `expect-pty#hard_close` 管理子进程;设置或刷新失败仍释放日志文件 | `Transports::Pty`、`Log` |
|
|
57
160
|
| 人工交互 | 人工接管结束自动会话,后续操作需重新连接 | `Session` |
|
|
@@ -59,8 +162,46 @@
|
|
|
59
162
|
|
|
60
163
|
提示符和交互标记是 Ruby 正则表达式,应短到能放入未匹配尾部;流式适配器不支持需要无限长历史的模式。收集输出的上限与匹配窗口上限不同。终端渲染器只处理常见行编辑控制符,不是完整屏幕终端模拟器。`Profile#terminal_size` 使用 `[宽, 高]`,PTY 适配器转换为 Ruby 的 `[行, 列]`。
|
|
61
164
|
|
|
165
|
+
`max_script_output_bytes` 为正 Integer 或 nil。计数属于每个 Execution,主命令以及准备、提示符和后处理钩子的 `Execution#query` 共用计数,包含原始分页标记和最终提示符,不能用 `capture: false` 绕过。登录和提权认证保留各自原有单响应上限。每次脚本新建计数器;一次租约中的多个脚本、多个设备和多个批次不共用它。
|
|
166
|
+
|
|
167
|
+
累计等于上限时当前脚本可正常结束,但下一条查询在发送前失败;超过上限的完整主响应先进入 steps,再返回 `ScriptOutputLimitExceeded`,后处理和下一命令不会继续。追加查询沿用不进入公开 steps 的旧契约,但计入预算并在错误中保留实际查询命令。吞掉查询预算异常的钩子不能让超额脚本报告成功。已执行命令不重放;确认的 TFTP 完成行仍可构造带收尾错误的回执。
|
|
168
|
+
|
|
169
|
+
累计检查不在读取中途切断当前命令,最多还会接收一个受 `max_output_bytes` 限制的响应;已完成步骤不截断或丢弃。配置清理、解析、用户回调和反复 `Result#output` 的副本不计入该字节预算,因此它不是 RSS 或整批内存硬上限。保持默认 nil 是兼容性决定,合成基准不足以确定适合所有设备的默认阈值。设置通过 YAML `ssh.max_script_output_bytes`、`NET_CONNECTOR_MAX_SCRIPT_OUTPUT_BYTES` 和 CLI 同名选项进入批次策略快照,优先级为 CLI > ENV > YAML;默认不向旧连接器工厂添加新键,显式凭据 resolver 仍可覆盖连接参数。
|
|
170
|
+
|
|
62
171
|
Ruby 对象显式拥有资源并使用关键字参数。`Profile` 提供有限声明入口,厂商策略负责差异行为。公开方法、厂商钩子、结果对象和 CLI JSON 字段延续现有契约;内部不做运行时方法注入,也没有工作流 DSL。
|
|
63
172
|
|
|
173
|
+
## 关键依赖的维护状态与替代方案
|
|
174
|
+
|
|
175
|
+
2026-09-27 核对 RubyGems 发布元数据:[`expect-pty`](https://rubygems.org/gems/expect-pty)
|
|
176
|
+
最新为 0.5.0(2026-09-27 发布),[`textfsm`](https://rubygems.org/gems/textfsm)
|
|
177
|
+
最新为 0.2.0(2026-09-12 发布)。源码分别位于
|
|
178
|
+
[`gatework/expect-ruby`](https://github.com/gatework/expect-ruby) 和
|
|
179
|
+
[`gatework/textfsm`](https://github.com/gatework/textfsm),与本项目同属 gatework。
|
|
180
|
+
这是发布状态快照,不代表独立安全审计或未来维护承诺;集中维护带来的人员和
|
|
181
|
+
发布权限风险仍需关注,不应以下载量或作者知名度代替代码及行为验证。
|
|
182
|
+
|
|
183
|
+
当前使用 `expect-pty ~> 0.5.0`(允许 `>= 0.5.0, < 0.6.0`)和
|
|
184
|
+
`textfsm ~> 0.2.0`(允许 `>= 0.2.0, < 0.3.0`),没有锁死补丁版本。
|
|
185
|
+
共享脱敏使用 0.5.0 起公开的 `Expect::Redactor`:完整文本、字节流匹配及分片
|
|
186
|
+
缓冲由依赖负责,连接器保留秘密作用域、`[REDACTED]` 标记与输出敏感性策略。
|
|
187
|
+
缺少公共接口时明确拒绝初始化脱敏作用域。
|
|
188
|
+
本地 lockfile 固定本地实际解析结果;本项目不提交该文件,CI 按各 Ruby 版本
|
|
189
|
+
重新解析依赖,应用使用者则应提交自己的 lockfile。升级前检查上游变更和
|
|
190
|
+
安全通告,并运行本项目的本地 PTY、错误脱敏、模板样本及隔离安装检查。
|
|
191
|
+
|
|
192
|
+
| 依赖 | 已有隔离边界 | 替代方案与验收条件 |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `expect-pty` | `Transports::Pty` 包装信道,设备构造支持 `transport:` 注入 | 可维护受控分支,或实现 Ruby `PTY` / `IO.select` 适配器;必须通过有序匹配、分片、写入超时、EOF、中断、子进程回收及日志脱敏测试 |
|
|
195
|
+
| `textfsm` | `Operations::ParseOutput` 集中构造解析器和映射异常 | 可维护受控分支,或在该入口接入另一解析实现;必须保持索引选择、模板语义、输出字段及错误码,并验证现有和新增模板样本 |
|
|
196
|
+
|
|
197
|
+
传输替换还不是完全即插即用:`Session`、`ResponseReader`、`Authentication`、
|
|
198
|
+
`Execution` 等直接使用 `Expect.monotonic`,配置及命令校验使用
|
|
199
|
+
`Expect.duration`,会话映射 `Expect` 的写入与启动异常。若决定迁移,先将
|
|
200
|
+
时间和异常转换收敛到引擎边界,再用同一套契约测试对比两个实现。直接改用
|
|
201
|
+
`PTY` 并不能自动获得匹配、背压和进程清理能力;这些成本属于迁移评估。
|
|
202
|
+
跨进程接入其他语言的 TextFSM 实现还需处理编码、超时、部署和错误映射,
|
|
203
|
+
目前没有内置这样的适配器。
|
|
204
|
+
|
|
64
205
|
## 厂商能力
|
|
65
206
|
|
|
66
207
|
`device.supports?(capability)` 读取档案与基于方法的采集命令,不建立传输连接或策略实例。接受 `:running_config`、`:save_config`、`:backup`、`:tftp_backup`、`:neighbors`、`:interface_descriptions`、`:interface_description_changes`;未知名称返回 `false`。它只表明实现了能力,不验证设备授权或固件兼容性。
|
|
@@ -72,7 +213,7 @@ Ruby 对象显式拥有资源并使用关键字参数。`Profile` 提供有限
|
|
|
72
213
|
| Cisco IOS / IOS XE | 是 | 是 | 是 | 是 | 是 | 是 |
|
|
73
214
|
| Cisco NX-OS | 是 | 是 | 是 | 是 | 是 | 是 |
|
|
74
215
|
| Radware Alteon | 是 | 是 | 是 | 否 | 是 | 否 |
|
|
75
|
-
| PAN-OS | 是 | 否 | 是 | 是 | 是 |
|
|
216
|
+
| PAN-OS | 是 | 否 | 是 | 是 | 是 | 否,候选隔离未验证 |
|
|
76
217
|
| 华为 | 是 | 是 | 是 | 否 | 否 | 否 |
|
|
77
218
|
| 山石 | 是 | 是 | 是 | 是 | 是 | 是 |
|
|
78
219
|
|
|
@@ -111,18 +252,40 @@ router.supports?(:tftp_backup) # => true,不连接设备
|
|
|
111
252
|
|
|
112
253
|
配置采集可把 `running_config_strategy` 绑定到 `Net::Connector::RunningConfig::Strategy` 子类。策略定义清理、结果步骤、视图提示符和逐响应检查。PAN-OS 候选差异只影响配置采集;直接 `execute("show config diff")` 仍返回该命令输出。静态命令列表留在档案中。已有的 `collect_config`、`clean_config` 和受保护的 `config_result_step` 钩子仍可覆盖并调用 `super`。`Base` 负责通用脚本与锁内回调,不决定配置是否完整。
|
|
113
254
|
|
|
114
|
-
拓扑策略通过 `topology_strategy YourStrategy` 绑定。类方法 `supports?` 声明三种拓扑能力,实例方法提供命令、模板、输出完整性和接口拼写。厂商使用自定义清单标签时,还须通过 `neighbor_template` 提供 TextFSM
|
|
255
|
+
拓扑策略通过 `topology_strategy YourStrategy` 绑定。类方法 `supports?` 声明三种拓扑能力,实例方法提供命令、模板、输出完整性和接口拼写。厂商使用自定义清单标签时,还须通过 `neighbor_template` 提供 TextFSM 模板,因为内置索引只匹配已有厂商键。变更策略必须声明 `leave_configuration`、`verification_commands`、`persistence_commands` 和 `persistence_confirmed?`;仅实现旧 finish_commands 的策略仍可读取,但不能据此猜测保存边界,计划返回 description_stages_unsupported。即时生效且使用 interface 块的设备可继承 `Topology::ImmediateStrategy`;自定义采集必须执行已列入计划的命令并返回完成步骤。
|
|
115
256
|
|
|
116
257
|
业务操作针对单个连接器构造,并提供 `call`。TFTP 策略只处理厂商传输差异,公共操作统一成功和失败规则。生成的文件名经过 `TftpTarget` 校验及长度限制;带作用域的 IPv6 地址会转为安全 ASCII 标记,特别长的地址使用稳定 SHA-256 标记。原本合法的文件名保留拼写,只有整体过长才缩短清单名称。TFTP 返回值表示设备报告上传完成,服务器文件核对仍由调用方负责。
|
|
117
258
|
|
|
118
259
|
TFTP 策略的类方法 `filename(host, label: nil)` 是不产生 I/O 的命名接口,默认使用 `file_extension` 声明的扩展名。Netdisco 只清理清单名称,不再维护厂商扩展名或固定文件名的分支。Radware 和山石分别声明 `tgz`、`dat`,PAN-OS 返回固定名称。旧自定义策略未实现该类方法时仍使用通用 `cfg` 名称。H3C、华为继承公共 `Tftp::FileUpload`,共用上传脚本、默认源文件名和完成证据;各自只负责取得并校验源文件。
|
|
119
260
|
|
|
261
|
+
自定义 TFTP 策略可增加 `validate_options!(target, source_file:, vrf:)`,要求不访问设备;可增加 `receipt_metadata(target, source_file:, explicit_source:)`,只返回 configuration_kind/source_file/format/requested_path 字段。重写 script 的子类必须同时重写相应钩子,才启用自身的预检及元数据契约;继承的厂商限制不会偷偷施加到旧扩展脚本。缺少钩子时仍做通用安全字符串校验,保持旧执行接口,但不宣称厂商参数全部已在 I/O 前验证,也不继承父脚本的配置来源声明。
|
|
262
|
+
|
|
263
|
+
服务器核验适配器尚未接入;TFTP API 不会自动生成 server_verified 结果。PAN-OS 固定名仍按现有计划拒绝同批碰撞,串行运行不改变该限制,也不保证跨进程或跨批次隔离。
|
|
264
|
+
|
|
120
265
|
验收入口是 `script/ci`,也可用 `bundle exec rake release:check`。它检查源码、可用 Git 历史和 gem 内容中的敏感数据,执行 Ruby 与工作流 lint、完整测试,并在隔离 gem 目录及最小 Bundler 应用中安装。真实本地 PTY 烟测覆盖厂商加载、配置采集、打包模板和 CLI,不接触网络设备。初次下载依赖和工具需要联网,详见[验证文档](VERIFICATION.md)及[发布文档](RELEASING.md)。
|
|
121
266
|
|
|
267
|
+
## 后续工作边界
|
|
268
|
+
|
|
269
|
+
以下是按实际使用需求选择的候选工作,不表示已支持或已承诺交付日期。
|
|
270
|
+
|
|
271
|
+
| 方向 | 首个可独立验收的范围 | 进入实现前需要的证据 |
|
|
272
|
+
| --- | --- | --- |
|
|
273
|
+
| Juniper、Arista、F5、Fortinet | 每次增加一个设备系列的登录、配置采集与本地备份,再按需扩展 TFTP / 拓扑 | 指定型号和固件的脱敏会话样本、分页和失败回显、现场验证条件;不能仅靠厂商名称声明支持 |
|
|
274
|
+
| TextFSM 模板 | 按业务需要增加接口、路由等具体命令及字段 | 样本来源和许可、正常 / 空表 / 部分输出测试、模板随 gem 安装的证据;`parse_config` 仍是指定模板提取,不是完整配置模型 |
|
|
275
|
+
| 持久调度与重试 | 先由调用应用在持久队列中运行单设备任务 | 设备互斥、任务标识、幂等策略、重试上限及取消语义;连接器不自动重放可能已执行的配置命令 |
|
|
276
|
+
| 英文文档 | 优先增加英文 README,提供安装、主要能力、限制和验证入口 | 确定与中文文档同步更新的维护方式;CLI 消息国际化另行评估,避免静默改变现有输出 |
|
|
277
|
+
|
|
278
|
+
几千台设备的调度应由应用限制总并发及同设备并发,并持久保存逐台结果。
|
|
279
|
+
只读采集可在重新建连后按应用策略重试;配置下发中断后应先回读设备状态,
|
|
280
|
+
不能把不确定的执行结果当成“尚未执行”。跨进程租约、断点恢复和持久队列
|
|
281
|
+
属于调用平台的职责,当前 `Worker` 只负责一个进程内的有界并发与资源清理。
|
|
282
|
+
|
|
122
283
|
## 加载与兼容路径
|
|
123
284
|
|
|
124
285
|
`require "net/connector"` 只加载设备 API 和引擎,不预先加载厂商规则或 TextFSM。每个厂商入口只组装自身规则和公共父类;解析操作按需加载。更底层的调用方可用 `require "net/connector/engine/core"`,不加载设备定义、业务操作或厂商规则。`engine/base`、`engine/profile`、`engine` 保留为旧入口的转发路径。
|
|
125
286
|
|
|
287
|
+
`require "net/connector/netdisco"`、`SavedConfig#find/#read/#export` 和 CLI 离线导出也不加载 TextFSM;`SavedConfig#parse` 才加载解析入口。每次解析构造新的有状态 Parser。`ParseOutput` 将输入副本按 UTF-8 字节校验,再复用终端渲染器的严格模式;原文中的非法字节不能被控制符擦除后绕过检查,渲染过程中损坏的多字节字符也会被拒绝。错误统一为不含正文和底层 cause 的 `invalid_output_encoding`。拓扑在厂商表头匹配、计数前先校验原文,保留原有完整性规则。日志渲染仍转义非法字节;备份字节不变。当前不自动推断或转码其他编码。
|
|
288
|
+
|
|
126
289
|
旧的 `Operations::RunningConfig`、`Operations::RunningConfig::<Vendor>`、`Operations::Tftp::<Vendor>`、`Operations::Topology::<Vendor>` 常量和 require 路径都转发到同一实现类,不维护两份逻辑。已有公开结果常量也通过 autoload 保留。新增厂商代码应直接使用厂商目录下的类。
|
|
127
290
|
|
|
128
291
|
## 接口描述规则
|
|
@@ -133,18 +296,32 @@ TFTP 策略的类方法 `filename(host, label: nil)` 是不产生 I/O 的命名
|
|
|
133
296
|
|
|
134
297
|
默认建议因此从 `To peer Ethernet1/2` 变为 `To peer Eth1/2`。`abbreviate: false` 保留原拼写,`lowercase: true` 仅将对端接口改为小写。自定义代码块拿到原始邻居,可生成完整描述。输出仍必须经过 80 字节及字符校验、证据复核、确认和回读。
|
|
135
298
|
|
|
136
|
-
`InterfaceDescription.commands(interface:, description:, leave: "exit")` 为 IOS/NX-OS 和山石分别生成 `interface`、`description`、退出命令;H3C 使用 `leave: "quit"
|
|
299
|
+
`InterfaceDescription.commands(interface:, description:, leave: "exit")` 为 IOS/NX-OS 和山石分别生成 `interface`、`description`、退出命令;H3C 使用 `leave: "quit"`。进入、退出全局配置视图和保存完成证据由厂商负责。旧 finish_commands 及 PAN-OS 的纯命令构造/超时辅助方法保留,公共执行流程不会借此启用未经验证的候选提交。命令构造器本身不连接设备,也不能绕开已审核的拓扑计划。
|
|
137
300
|
|
|
138
301
|
## 敏感命令的生命周期
|
|
139
302
|
|
|
140
303
|
一个临时脱敏范围贯穿命令准备、收发、厂商后处理、用户回调和错误归一化。后续查询复用外层范围,`ensure` 在结束时清除临时秘密。敏感错误保留类型、代码、阶段和已完成步骤,但隐藏可能包含部分秘密的消息、输出及底层回溯。显式结果数据仍是原始数据。直接与流式脱敏先匹配真实秘密,包括含字面量 `[REDACTED]` 的秘密,再保留已有标记。
|
|
141
304
|
|
|
305
|
+
`Net::Connector::Redactor` 仅维护秘密作用域和输出敏感性,完整文本及日志分片均委托给共享 `Expect::Redactor`;不保留第二套正则匹配、区间合并或尾部缓冲算法。日志目标各自持有一个过滤流,规则随当前作用域同步;`[REDACTED]` 作为可配置替换标记保留。连接器使用显式完整词边界保持原日志收尾契约;依赖库的独立流接口默认仍隐藏疑似秘密前缀。连续或重叠的隐藏区间可以合并成一个标记,业务输出字节不参与过滤。
|
|
306
|
+
|
|
307
|
+
`Command#sensitive?` 控制命令文本,`Command#output_sensitive?` 独立标记响应正文;两者默认都为 `false`,`with_text` 保留两项标记。`with_output_sensitive` 复制命令的文本、超时、提示、交互、来源及行号,只将输出标记设为 `true`。输出敏感时仍可记录安全命令文字、响应字节数、耗时和错误类型/阶段;消息、错误输出、底层消息和回溯可能包含未知秘密,统一屏蔽。异常的原始 cause 不向调用方传递。
|
|
308
|
+
|
|
309
|
+
`RunningConfig` 将每个厂商采集步骤标记为输出敏感,包括 PAN-OS 的前后候选差异查询。`Base#perform_script` 在已持有会话锁、已登录后保护批次准备,在执行完命令后保护结果选择和清理;两处都在离开范围前归一化异常。命令范围覆盖厂商准备、追加查询和回调,即使钩子用新命令替换原命令也继承原有保护。敏感标记不改变锁的 Thread/Fiber 所有权,不开放回调重入,不触发自动重放。
|
|
310
|
+
|
|
311
|
+
这改变了配置采集的日志默认值:text/debug/raw 文件及注入的 logger 不再得到配置正文,没有关闭该保护的采集开关。显式的 `execute`/`Script` 保持原默认值,调用方编写读取配置的命令时应指定 `output_sensitive: true`;命令文本含秘密时仍须同时使用 `sensitive: true`。普通脚本下一步及下一次操作恢复各自的诊断规则。配置正文不会加入长期脱敏词表,也不会为了日志而清洗 `Result.steps`、`Result.config` 或备份字节。
|
|
312
|
+
|
|
313
|
+
保护覆盖连接器管理的日志与异常边界。调用方自行将 `result.output`、`value!` 或钩子收到的原始响应写入其他系统,仍是在显式处理敏感业务数据。脱敏不表示设备命令未执行;失败结果中的已完成步骤必须保留,不能据此自动重试命令。
|
|
314
|
+
|
|
142
315
|
## 本地备份标识与旧文件
|
|
143
316
|
|
|
144
317
|
批量备份只用规范化管理地址命名为 `<IP>.txt`,IPv6 的 `:` 改为 `_`。清单名称仍保留在结果元数据及 TFTP 文件名中。`SavedConfig` 优先使用规范文件;缺失时只接受唯一的旧版 `<名称>-<IP>.txt`。匹配多个旧文件时明确失败,不按修改时间随意选择。
|
|
145
318
|
|
|
146
319
|
首次成功的规范备份会与唯一旧文件的哈希比较,旧文件保持不变,并报告 `changed` 或 `unchanged`。采集失败不会创建规范文件。连接设备前先拒绝符号链接和非普通文件。旧文件存在歧义时,应保留原件,由操作人员核对后把当前正确配置放到规范路径,再恢复备份或导出。
|
|
147
320
|
|
|
321
|
+
Fleet 每次 `backup_all` 创建共享的 `SavedConfig(indexed: true)`,首次规范文件缺失时在互斥锁内构建旧命名索引,成功或失败均只扫描一次。文件按规范化地址分组,IPv6 zone 不参与下划线还原;匹配多个等价地址文件仍拒绝。索引只保存冻结的文件名及 dev/ino/mode/size/mtime/ctime,不缓存正文。读取前后检查同一 FD 的身份,并核对最终目录项;快照后改变或不可读时报 `SavedConfigChanged`,不使用失效基线连接设备。索引构建失败返回安全的 IOError,不发布部分表。
|
|
322
|
+
|
|
323
|
+
快照期间新增的旧名称文件留给下一批识别;每次查找仍先检查规范路径。直接创建的 `SavedConfig` 默认保持逐次实时查找。元数据检查不是文件系统事务,也不能在时间戳精度不足或非合作写入者持续修改时证明内容不可变;调用方仍须保护父目录及旧文件。索引不保留在 Fleet 实例或进程全局缓存中。
|
|
324
|
+
|
|
148
325
|
## 公开契约与 Rails 接入
|
|
149
326
|
|
|
150
327
|
应用代码优先使用 `Net::Connector.open/build`、`Base` 的公开设备方法、`Command`、`Script`、结果对象,以及 `Netdisco::Fleet`。厂商扩展使用 `Profile` DSL 和文档中列出的策略接口;`Profile::Builder`、`Session` 的状态字段、工作线程调度及解析辅助方法属于内部实现。兼容 require 路径只转发到当前实现,新增扩展使用厂商目录下的类。项目仍处于 0.x,公开契约的变更会在更新记录中说明。
|
|
@@ -172,8 +172,9 @@ module Net
|
|
|
172
172
|
|
|
173
173
|
# 采集配置并以原子方式保存为私有文件。
|
|
174
174
|
# 采集失败时保留已有备份文件。
|
|
175
|
-
def backup(path:)
|
|
176
|
-
|
|
175
|
+
def backup(path:, lock_timeout: 0)
|
|
176
|
+
@session.assert_path_lock_order!(:backup)
|
|
177
|
+
Operations::LocalBackup.new(self).call(path: path, lock_timeout: lock_timeout)
|
|
177
178
|
end
|
|
178
179
|
|
|
179
180
|
# 要求设备直接向 TFTP 服务器导出原生配置。
|
|
@@ -182,6 +183,11 @@ module Net
|
|
|
182
183
|
Operations::TftpBackup.new(self).call(host: host, path: path, source_file: source_file, vrf: vrf)
|
|
183
184
|
end
|
|
184
185
|
|
|
186
|
+
# 明确请求来源、格式及设备报告等级;旧入口继续返回原有三字段对象。
|
|
187
|
+
def tftp_backup_receipt(host:, path: nil, source_file: nil, vrf: nil)
|
|
188
|
+
Operations::TftpBackup.new(self).call_receipt(host: host, path: path, source_file: source_file, vrf: vrf)
|
|
189
|
+
end
|
|
190
|
+
|
|
185
191
|
# 配置采集是设备的基础能力,两个公共入口共享同一流程。
|
|
186
192
|
def collect_config = RunningConfig.new(self).call
|
|
187
193
|
|
|
@@ -356,10 +362,11 @@ module Net
|
|
|
356
362
|
prepare: method(:prepare_command), after_command: finish_step, prompt: prompt)
|
|
357
363
|
execution.context[:operation] = operation if operation
|
|
358
364
|
execution.context[:privilege] = privilege
|
|
365
|
+
output_sensitive = script.any?(&:output_sensitive?)
|
|
359
366
|
@session.perform(:script) do
|
|
360
|
-
before_batch(execution)
|
|
367
|
+
@session.output_scope(output_sensitive) { before_batch(execution) }
|
|
361
368
|
result = execution.execute(script, &on_step)
|
|
362
|
-
finalize ? finalize.call(result) : result
|
|
369
|
+
@session.output_scope(output_sensitive) { finalize ? finalize.call(result) : result }
|
|
363
370
|
end
|
|
364
371
|
rescue Error => error
|
|
365
372
|
Result.new(steps: execution ? execution.steps : [], error: error)
|
|
@@ -22,6 +22,8 @@ module Net
|
|
|
22
22
|
script = Script.new(@device.config_commands)
|
|
23
23
|
return Result.new(error: incomplete("configuration collection has no commands")) if script.empty?
|
|
24
24
|
|
|
25
|
+
# 所有厂商的采集步骤都收紧输出边界,包括候选差异、模式切换及扩展查询。
|
|
26
|
+
script = Script.new(script.map(&:with_output_sensitive))
|
|
25
27
|
strategy = self.class.strategy(@device)
|
|
26
28
|
prompt = ->(command) { prompt_for(command, strategy) }
|
|
27
29
|
@device.execute_operation(script, name: :running_config, prompt: prompt,
|
|
@@ -11,7 +11,8 @@ module Net
|
|
|
11
11
|
attr_reader :text, :timeout, :interactions, :prompt, :source, :line
|
|
12
12
|
|
|
13
13
|
# 在任何输入输出前校验命令、提示、交互和敏感标记,并冻结命令。
|
|
14
|
-
def initialize(text, timeout: nil, interactions: [], prompt: nil, sensitive: false,
|
|
14
|
+
def initialize(text, timeout: nil, interactions: [], prompt: nil, sensitive: false, output_sensitive: false,
|
|
15
|
+
source: nil, line: nil)
|
|
15
16
|
unless text.is_a?(String) && !text.strip.empty? && !text.match?(/[\r\n\x00]/)
|
|
16
17
|
raise ScriptError.new("a command must contain exactly one nonempty CLI line", phase: :parse,
|
|
17
18
|
source: source, line: line)
|
|
@@ -24,12 +25,14 @@ module Net
|
|
|
24
25
|
raise ArgumentError, "prompt must be a nonempty Regexp or nil"
|
|
25
26
|
end
|
|
26
27
|
raise ArgumentError, "sensitive must be true or false" unless [true, false].include?(sensitive)
|
|
28
|
+
raise ArgumentError, "output_sensitive must be true or false" unless [true, false].include?(output_sensitive)
|
|
27
29
|
|
|
28
30
|
@text = text.dup.freeze
|
|
29
31
|
@timeout = Expect.duration(timeout)
|
|
30
32
|
@interactions = interactions.dup.freeze
|
|
31
33
|
@prompt = prompt
|
|
32
34
|
@sensitive = sensitive
|
|
35
|
+
@output_sensitive = output_sensitive
|
|
33
36
|
@source = source&.dup&.freeze
|
|
34
37
|
@line = line
|
|
35
38
|
freeze
|
|
@@ -38,10 +41,19 @@ module Net
|
|
|
38
41
|
# 判断命令文本是否需要脱敏。
|
|
39
42
|
def sensitive? = @sensitive
|
|
40
43
|
|
|
44
|
+
# 响应正文可能包含尚未登记的秘密,与命令文本是否敏感无关。
|
|
45
|
+
def output_sensitive? = @output_sensitive
|
|
46
|
+
|
|
41
47
|
# 复制命令元数据,只替换命令文本。
|
|
42
48
|
def with_text(text)
|
|
43
49
|
self.class.new(text, timeout: timeout, interactions: interactions, prompt: prompt,
|
|
44
|
-
sensitive: sensitive?, source: source, line: line)
|
|
50
|
+
sensitive: sensitive?, output_sensitive: output_sensitive?, source: source, line: line)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 配置采集保留调用方的全部命令元数据,只收紧输出的诊断边界。
|
|
54
|
+
def with_output_sensitive
|
|
55
|
+
self.class.new(text, timeout: timeout, interactions: interactions, prompt: prompt,
|
|
56
|
+
sensitive: sensitive?, output_sensitive: true, source: source, line: line)
|
|
45
57
|
end
|
|
46
58
|
|
|
47
59
|
# 返回命令来源摘要,不展开命令内容。
|
|
@@ -8,13 +8,13 @@ module Net
|
|
|
8
8
|
# 显式且不可变的连接设置;环境变量读取由调用库的可执行程序负责。
|
|
9
9
|
class Configuration
|
|
10
10
|
attr_reader :host, :username, :password, :enable_password, :protocol, :port, :login_timeout,
|
|
11
|
-
:command_timeout, :write_timeout, :max_output_bytes, :log_file, :log_format, :log_level,
|
|
11
|
+
:command_timeout, :write_timeout, :max_output_bytes, :max_script_output_bytes, :log_file, :log_format, :log_level,
|
|
12
12
|
:known_hosts, :host_key_policy, :challenges, :logger
|
|
13
13
|
|
|
14
14
|
# 校验端点、凭据、超时、日志、主机密钥和挑战配置,然后冻结设置。
|
|
15
15
|
def initialize(host: nil, username: nil, password: nil, enable_password: nil, protocol: :ssh, port: nil,
|
|
16
16
|
login_timeout: 10, command_timeout: nil, write_timeout: 10,
|
|
17
|
-
max_output_bytes: 32 * 1024 * 1024, log_file: nil, logger: nil,
|
|
17
|
+
max_output_bytes: 32 * 1024 * 1024, max_script_output_bytes: nil, log_file: nil, logger: nil,
|
|
18
18
|
log_format: :text, log_level: :info,
|
|
19
19
|
known_hosts: nil, host_key_policy: :strict, telnet_fallback: false, legacy_ssh: false,
|
|
20
20
|
challenges: [])
|
|
@@ -32,11 +32,8 @@ module Net
|
|
|
32
32
|
@login_timeout = timeout_seconds(login_timeout, :login_timeout)
|
|
33
33
|
@command_timeout = command_timeout.nil? ? nil : timeout_seconds(command_timeout, :command_timeout)
|
|
34
34
|
@write_timeout = timeout_seconds(write_timeout, :write_timeout)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
@max_output_bytes = max_output_bytes
|
|
35
|
+
@max_output_bytes = positive_integer(max_output_bytes, :max_output_bytes)
|
|
36
|
+
@max_script_output_bytes = positive_integer(max_script_output_bytes, :max_script_output_bytes) unless max_script_output_bytes.nil?
|
|
40
37
|
@log_file = absolute_path(log_file)
|
|
41
38
|
if logger && (!logger.respond_to?(:debug) || !logger.respond_to?(:info) ||
|
|
42
39
|
!logger.respond_to?(:warn) || !logger.respond_to?(:error) ||
|
|
@@ -86,6 +83,12 @@ module Net
|
|
|
86
83
|
|
|
87
84
|
private
|
|
88
85
|
|
|
86
|
+
def positive_integer(value, name)
|
|
87
|
+
raise ArgumentError, "#{name} must be a positive Integer" unless value.is_a?(Integer) && value.positive?
|
|
88
|
+
|
|
89
|
+
value
|
|
90
|
+
end
|
|
91
|
+
|
|
89
92
|
# 将可选值校验为字符串副本。
|
|
90
93
|
def frozen_string(value)
|
|
91
94
|
return nil if value.nil?
|
|
@@ -130,47 +130,24 @@ module Net
|
|
|
130
130
|
counts = Hash.new(0)
|
|
131
131
|
bytes = 0
|
|
132
132
|
loop do
|
|
133
|
-
event =
|
|
133
|
+
event = read_event(patterns, deadline)
|
|
134
134
|
raw << event.before unless event.before.empty?
|
|
135
135
|
output << event.before unless event.before.empty?
|
|
136
136
|
bytes += event.before.bytesize + event.match.bytesize
|
|
137
|
-
|
|
138
|
-
raise @session.error(OutputLimitExceeded, "device output exceeded max_output_bytes",
|
|
139
|
-
phase: phase, command: command, output: (raw + [event.match]).join), cause: nil
|
|
140
|
-
end
|
|
141
|
-
raise read_error(event, phase, command, raw.join), cause: nil unless event.matched?
|
|
137
|
+
validate_event(event, bytes, raw, phase, command)
|
|
142
138
|
|
|
143
139
|
if event.index == patterns.size
|
|
144
140
|
raw << event.match
|
|
145
141
|
output << event.match
|
|
146
142
|
elsif event.index == patterns.size - 1
|
|
147
|
-
|
|
148
|
-
raise @session.error(PromptError, "prompt pattern did not consume any output",
|
|
149
|
-
phase: phase, command: command, output: raw.join), cause: nil
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
raw << event.match
|
|
153
|
-
output << event.match
|
|
154
|
-
raw_text = raw.join
|
|
155
|
-
return Response.new(raw: raw_text, output: (output == raw) ? raw_text : output.join, prompt: event.match)
|
|
143
|
+
return finish_response(event.match, raw, output, phase, command)
|
|
156
144
|
elsif event.index < failures.size
|
|
157
|
-
|
|
158
|
-
raise @session.error(klass, "device connection failed (#{code})", phase: phase,
|
|
159
|
-
code: code, command: command), cause: nil
|
|
145
|
+
raise connection_error(failures.values.fetch(event.index), phase, command), cause: nil
|
|
160
146
|
else
|
|
161
147
|
reply = interactions.fetch(event.index - failures.size)
|
|
162
148
|
raw << event.match
|
|
163
149
|
output << event.match if reply.capture?
|
|
164
|
-
|
|
165
|
-
if event.match.empty? || (reply.limit && counts[reply] > reply.limit)
|
|
166
|
-
raise interaction_error("response rejected or repeated", phase, command), cause: nil
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
value = interaction_response(reply, event.match, phase, command)
|
|
170
|
-
raise interaction_error("response unavailable", phase, command), cause: nil unless value.is_a?(String)
|
|
171
|
-
|
|
172
|
-
@session.redactor.remember(value.chomp) if reply.sensitive?
|
|
173
|
-
@session.write(value, deadline: deadline, phase: phase, command: command)
|
|
150
|
+
respond(reply, event.match, counts, deadline, phase, command)
|
|
174
151
|
end
|
|
175
152
|
next if Expect.monotonic < deadline
|
|
176
153
|
|
|
@@ -180,6 +157,53 @@ module Net
|
|
|
180
157
|
|
|
181
158
|
private
|
|
182
159
|
|
|
160
|
+
# 分页和流式输出只消耗剩余时间,不为下一次读取重新计时。
|
|
161
|
+
def read_event(patterns, deadline)
|
|
162
|
+
@session.transport.read(patterns, timeout: [deadline - Expect.monotonic, 0].max)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# 连接失败模式携带稳定错误码,供连接级恢复规则选择处理方式。
|
|
166
|
+
def connection_error(failure, phase, command)
|
|
167
|
+
klass, code = failure
|
|
168
|
+
@session.error(klass, "device connection failed (#{code})", phase: phase, code: code, command: command)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# 先限制原始字节数,再处理传输失败,避免错误输出绕过总量限制。
|
|
172
|
+
def validate_event(event, bytes, raw, phase, command)
|
|
173
|
+
if bytes > @session.configuration.max_output_bytes
|
|
174
|
+
raise @session.error(OutputLimitExceeded, "device output exceeded max_output_bytes",
|
|
175
|
+
phase: phase, command: command, output: (raw + [event.match]).join), cause: nil
|
|
176
|
+
end
|
|
177
|
+
raise read_error(event, phase, command, raw.join), cause: nil unless event.matched?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# 完整提示符必须消费字节;交互标记可从业务输出排除,但原始响应始终保留。
|
|
181
|
+
def finish_response(prompt, raw, output, phase, command)
|
|
182
|
+
if prompt.empty?
|
|
183
|
+
raise @session.error(PromptError, "prompt pattern did not consume any output",
|
|
184
|
+
phase: phase, command: command, output: raw.join), cause: nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
raw << prompt
|
|
188
|
+
output << prompt
|
|
189
|
+
raw_text = raw.join
|
|
190
|
+
Response.new(raw: raw_text, output: (output == raw) ? raw_text : output.join, prompt: prompt)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# 重复提示和缺失应答必须在写入前失败;所有应答共用读取操作的截止时间。
|
|
194
|
+
def respond(reply, prompt, counts, deadline, phase, command)
|
|
195
|
+
counts[reply] += 1
|
|
196
|
+
if prompt.empty? || (reply.limit && counts[reply] > reply.limit)
|
|
197
|
+
raise interaction_error("response rejected or repeated", phase, command), cause: nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
value = interaction_response(reply, prompt, phase, command)
|
|
201
|
+
raise interaction_error("response unavailable", phase, command), cause: nil unless value.is_a?(String)
|
|
202
|
+
|
|
203
|
+
@session.redactor.remember(value.chomp) if reply.sensitive?
|
|
204
|
+
@session.write(value, deadline: deadline, phase: phase, command: command)
|
|
205
|
+
end
|
|
206
|
+
|
|
183
207
|
# 动态口令尚未返回时无法加入词表,须在敏感范围内先归一化回调异常。
|
|
184
208
|
# 成功后由调用方登记响应,临时敏感标记不会影响后续普通命令的诊断。
|
|
185
209
|
def interaction_response(reply, prompt, phase, command)
|