net-connector 0.9.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -1
- data/docs/VERIFICATION.md +1 -1
- data/docs/architecture.md +16 -5
- data/docs/netdisco.md +54 -1
- data/docs/netdisco.zh-CN.md +46 -2
- data/docs/usage.md +24 -7
- data/docs/usage.zh-CN.md +22 -8
- data/examples/backup.yml +1 -1
- data/lib/net/connector/background_task.rb +69 -0
- data/lib/net/connector/device/topology/interface_description.rb +3 -3
- data/lib/net/connector/device/topology/interface_name.rb +10 -19
- data/lib/net/connector/device/topology/stages.rb +50 -0
- data/lib/net/connector/device/topology.rb +65 -24
- data/lib/net/connector/device_session.rb +38 -0
- data/lib/net/connector/engine/transport.rb +15 -5
- data/lib/net/connector/execution_pool.rb +108 -0
- data/lib/net/connector/netdisco/backup_run.rb +14 -2
- data/lib/net/connector/netdisco/cli/options.rb +1 -0
- data/lib/net/connector/netdisco/cli.rb +18 -7
- data/lib/net/connector/netdisco/client.rb +5 -6
- data/lib/net/connector/netdisco/config_file.rb +5 -2
- data/lib/net/connector/netdisco/database_client.rb +33 -15
- data/lib/net/connector/netdisco/fleet.rb +68 -36
- data/lib/net/connector/netdisco/inventory_cache.rb +218 -0
- data/lib/net/connector/netdisco/operation_batch.rb +154 -0
- data/lib/net/connector/netdisco/operations.rb +268 -0
- data/lib/net/connector/netdisco/planner.rb +26 -17
- data/lib/net/connector/netdisco/progress.rb +22 -10
- data/lib/net/connector/netdisco/settings.rb +36 -6
- data/lib/net/connector/netdisco/tftp_archive.rb +2 -1
- data/lib/net/connector/netdisco/tftp_verification.rb +36 -11
- data/lib/net/connector/netdisco/worker.rb +7 -46
- data/lib/net/connector/vendor/h3c/topology.rb +1 -1
- data/lib/net/connector/vendor/hillstone/topology.rb +1 -1
- data/lib/net/connector/version.rb +1 -1
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6ba09a377ed95488c9ecd62788732557564339638dec3ae596bbce0226369fa
|
|
4
|
+
data.tar.gz: '0876aa9703bdbb2e46db010fcf263a2e1f1e7549e0f63d1bcf60f8529193e3ed'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6636cacf21a401c68a6b6dd596e3c4ab11eb153879e6e6e9f74339cf72bea7f8e71e6937745300fc9e513c7b4521b1eecdb249a1cf3abe7b6c3a15fc253b34a4
|
|
7
|
+
data.tar.gz: f97ff17b82418d78a1c959f420d4c17a53efc7ce4101694719e394e7d50d42f1ae2e86167a3b470850e598eba9becc42cb6364133fe2e6888eca426342534129
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.10.0 - 2026-10-11
|
|
4
|
+
|
|
5
|
+
- 新增 `sync_interface_descriptions`:一次调用、每台设备一个连接,完成邻居发现、描述修改、读回与保存;无变化时不写入或保存,后端仍支持定制格式和分阶段审批。
|
|
6
|
+
- 行为变更:默认接口描述改为 `To_<设备名>_<接口简称>`。H3C、Cisco、Hillstone 的聚合口及 10/25/40/100G 接口别名统一为简称,保留端口、拆分通道和子接口编号。
|
|
7
|
+
- 批量配置与备份共用有限并发调度,`NC_CONCURRENCY` 设置默认并发数;单台设备和回调的普通故障独立记录,报告提供清单总数、成功、失败及细分类别,中断回收本批资源,不自动重放命令。
|
|
8
|
+
- Netdisco 清单默认缓存三天,支持 `ND_CACHE_TTL` 和 YAML 调整、强制刷新、过期刷新失败时回退历史。缓存按来源和身份隔离,只保存白名单字段,缓存写入失败仍交付新清单。
|
|
9
|
+
- 修复设备连接、后台线程、PostgreSQL 连接和传输超时恢复的收尾边界:取消时释放已取得资源,后续清理故障不覆盖原始失败或已完成结果。
|
|
10
|
+
- TFTP 核验在目标锁内采集上传前基线;未变化的旧文件不能凭未来时间戳冒充本次上传。回调与报告使用同一核验结果,后续故障保留上传回执。
|
|
11
|
+
- 拓扑计划按规范接口判重,在连接或新增 I/O 前拒绝重复变更。全量清单规划减少采样副本和索引,保持设备选择顺序及 TFTP 目标仲裁。
|
|
4
12
|
|
|
5
13
|
## 0.9.0 - 2026-10-11
|
|
6
14
|
|
data/docs/VERIFICATION.md
CHANGED
|
@@ -97,7 +97,7 @@ debug Logger 计数目标样本;不关闭协议检查、丢弃步骤或共享
|
|
|
97
97
|
| --- | --- | --- |
|
|
98
98
|
| 核心引擎 | `lib/net/connector/engine/**/*.rb` | 80% / 80% |
|
|
99
99
|
| 脱敏与错误处理 | `lib/net/connector/engine/errors.rb`,包含 `Redactor` | 80% / 80% |
|
|
100
|
-
| 批量并发 | `lib/net/connector/netdisco/worker.rb` | 80% / 80% |
|
|
100
|
+
| 批量并发 | `lib/net/connector/background_task.rb`、`execution_pool.rb`、`device_session.rb`、`netdisco/worker.rb` | 80% / 80% |
|
|
101
101
|
|
|
102
102
|
关键组为空、任一关键文件未测量或组内没有可执行行时均失败。没有分支的组
|
|
103
103
|
不需要分支命中,但仍须通过行覆盖率及文件加载检查。
|
data/docs/architecture.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 设备操作架构
|
|
2
2
|
|
|
3
|
-
一个连接器对应一台设备的一段会话。`Base`
|
|
3
|
+
一个连接器对应一台设备的一段会话。`Base` 组合设备能力,提供统一的命令执行和会话入口;各项能力的公开方法、流程和策略放在同一功能目录。设备公共层不直接加载厂商实现,也不额外包装同一套单设备业务;批次编排由 Netdisco 层调用这些公开能力。
|
|
4
4
|
|
|
5
5
|
| 层次 | 职责 | 位置 |
|
|
6
6
|
| --- | --- | --- |
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
| `tftp_backup` | `TftpReceipt`,直接包含 server、path、时间、来源、格式及核验等级 | 参数先校验;失败/未确认直接抛出。完成后故障为 `TftpCompletionError`,保留回执 |
|
|
39
39
|
| `parse_command`、`parse_config` | TextFSM 记录数组 | 命令失败、模板错误、严格 UTF-8/解析错误直接抛出,不用空数组代替未知输出 |
|
|
40
40
|
| `neighbors`、`interface_descriptions` | `Neighbor` 数组、接口到描述的 Hash | 不支持、未知/部分输出或读取失败直接抛出;明确空表才允许空结果 |
|
|
41
|
+
| `sync_interface_descriptions` | `Result` | 显式同步请求在同一租约内完成发现、重验、修改、读回、保存;无变化不写入或保存 |
|
|
41
42
|
| `plan_interface_descriptions` | 冻结的 `Topology::Plan` | 只读证据和能力检查;PAN-OS 当前拒绝自动改写计划,无隐式 commit |
|
|
42
43
|
| `apply_interface_descriptions(plan, confirmed:)` | `Result`,包括变更后已完成的读回/保存步骤 | 未确认、错误设备/旧命令序列、能力不足及重验失败在写入前直接抛出;`stale_plan` 是 `DeviceError`。开始变更后的领域失败保留步骤 |
|
|
43
44
|
| `SavedConfig#read/#parse/#export` | 原始字节、记录数组;export 返回目标路径或 stdout 模式的 nil | 纯本地,只读取规范地址文件;缺失、不安全文件、解析或写入错误直接抛出 |
|
|
@@ -84,7 +85,7 @@ TFTP 内置策略的 `validate_options!` 是纯参数校验,执行于 H3C 源
|
|
|
84
85
|
|
|
85
86
|
明确完成证据先生成最小回执,再执行路径提取、元数据钩子及事件记录。已完成步骤之后的清理错误同样保留回执;路径不可用时保留 nil,不把请求文件名冒充实际目标。`TftpCompletionError` 只含受控码、类型和回执,不带原始消息、输出或 cause。Fleet 仅信任库定义的精确错误类及匹配产物;第三方异常上的 receipt 字段和错误子类不能供应完成事实。已上传但收尾失败使用 reported_with_error/partial 分类。
|
|
86
87
|
|
|
87
|
-
`Topology`
|
|
88
|
+
`Topology` 默认通过显式 `sync_interface_descriptions` 调用在同一连接与租约内完成发现到保存,调用方无须传递计划。独立规划/应用流程仍冻结计划并要求显式确认;两条路径共用写入前证据重验和命令核对。`ImmediateStrategy` 将即时生效设备的修改/退出视图、运行配置读回、保存拆开:读回未确认或有未识别接口块时停止,不发送保存。重验、下发、读回、保存及阶段间隙都持有原有会话租约;只有所属线程和 Fiber 能顺序执行脚本,其他调用、关闭请求和脚本回调重入均返回 `SessionBusy`。租约不隔离其他设备会话或管理员。
|
|
88
89
|
|
|
89
90
|
```mermaid
|
|
90
91
|
flowchart LR
|
|
@@ -355,9 +356,9 @@ RunningConfig 的两处受控 `send` 保留,用于私有策略绑定与 protec
|
|
|
355
356
|
|
|
356
357
|
原始 `Neighbor` 字段和计划证据完整保留发现值。`Topology::InterfaceName.key` 用于匹配本机接口别名与运行配置名称;`Topology::InterfaceName.configuration` 保留配置命令的接口展开规则。`Topology::InterfaceName.short` 只决定描述里对端接口的显示形式,绝不替换本机下发命令中的接口名。
|
|
357
358
|
|
|
358
|
-
`Topology::InterfaceDescription.format(neighbor, abbreviate: true, lowercase: false)`
|
|
359
|
+
`Topology::InterfaceDescription.format(neighbor, abbreviate: true, lowercase: false)` 是各厂商同步与计划共用的纯函数,生成 `To_<名称>_<接口简称>`。接口族的全称、简称和大小写统一,端口、拆分通道与子接口后缀保持不变;H3C 二层和三层聚合身份不合并。完整规则及厂商命名依据见[设备指南](usage.zh-CN.md#邻居发现与接口描述)。
|
|
359
360
|
|
|
360
|
-
|
|
361
|
+
`abbreviate: false` 保留接口原拼写,`lowercase: true` 仅将对端接口改为小写。自定义代码块拿到原始邻居,可生成完整描述。输出仍须经过 80 字节及字符校验、证据复核和回读;同步调用表达写入意图,独立计划应用仍要求确认。
|
|
361
362
|
|
|
362
363
|
`Topology::InterfaceDescription.commands(interface:, description:, leave: "exit")` 为 IOS/NX-OS 和山石分别生成 `interface`、`description`、退出命令;H3C 使用 `leave: "quit"`。进入、退出全局配置视图和保存完成证据由厂商负责。各阶段命令分别由策略声明,PAN-OS 的命令构造/超时辅助方法不会启用未经验证的候选提交。命令构造器本身不连接设备,也不能绕开已审核的拓扑计划。
|
|
363
364
|
|
|
@@ -401,7 +402,7 @@ RunningConfig 的两处受控 `send` 保留,用于私有策略绑定与 protec
|
|
|
401
402
|
|
|
402
403
|
注入的 logger 由调用方持有;连接器不修改级别、formatter、progname,也不关闭它。每次写入同时检查配置与 logger 当前级别。消息对象支持 to_s/inspect 和安全 to_h,应用可自行输出 JSON。自有文本日志使用毫秒时间及逐行事件;raw 文件只写经过敏感保护的字节。上下文退出前先收尾渲染与脱敏缓冲,避免上一命令尾部被标成下一条命令。
|
|
403
404
|
|
|
404
|
-
人类进度显示由 Progress 单独管理:终端 write/flush
|
|
405
|
+
人类进度显示由 Progress 单独管理:终端 write/flush 的普通故障会停用后续显示,并只保留固定词表内的错误类型,不携带原始异常。心跳照常回收,设备任务、events.jsonl 与报告持久化继续进行;BackupRun 将显示故障合并为一次批次 callback_errors。若首次故障发生在报告保存后的最终显示阶段,仅补写一次报告诊断,最终 JSON 与退出码采用更新后的策略结果;已有报告错误保持优先,不重放设备备份或事件。
|
|
405
406
|
|
|
406
407
|
`log_file` 使用 NOFOLLOW/NONBLOCK 打开,再对同一 FD 检查普通文件类型,随后才修改权限或写入。FIFO、设备文件和目录不能用作日志文件;应用拥有的输出流通过 `logger` 接入。日志打开先于登录,不能依赖登录超时为文件打开提供期限。
|
|
407
408
|
|
|
@@ -422,3 +423,13 @@ end
|
|
|
422
423
|
```
|
|
423
424
|
|
|
424
425
|
这里的 `DeviceCredential` 是应用自己的模型。涉及 Rails 的工厂、回调或自定义连接器方法也遵循同一规则;Rails 的 Executor 负责应用代码的执行边界及数据库连接回收。不要在工作线程中使用 Reloader 包住整批任务。需要每台设备独立重试和调度时,可在应用的 Active Job 中调用单设备 API。具体规则见 [Rails 线程与代码执行指南](https://guides.rubyonrails.org/threading_and_code_execution.html)。
|
|
426
|
+
|
|
427
|
+
## 公共并发、配置批次与清单缓存
|
|
428
|
+
|
|
429
|
+
`ExecutionPool` 只持有线程、启动闸门和完成事件,按输入顺序交付块结果。普通设备失败如何转成结果由业务适配负责;创建并登记线程时延迟异步中断,登记完成后恢复,启动失败不执行任何设备块。工作线程捕获异常并由调用线程同步传播,避免宿主全局线程策略影响无关线程。原 `Netdisco::Worker` 保留备份计时、结果槽位和通知适配,不再自行管理线程。
|
|
430
|
+
|
|
431
|
+
`DeviceSession` 持有单台任务连接器,分别交付业务值与普通故障,收尾不覆盖先前错误或中断。`Netdisco::Operations` 为配置脚本与接口描述同步、规划/应用提供全批预检、固定策略、动态凭据和安全摘要;它保留领域 `Result`、计划和完成步骤,使用独立 `OperationBatch`,不借用备份路径或上传状态。应用审批计划必须走原拓扑重验、变更、回读、保存流程;有限并发不代表跨设备事务或自动重试安全。
|
|
432
|
+
|
|
433
|
+
`Settings#client` 为原始清单适配器包裹 `InventoryCache`,默认三天有效。缓存仅保存完整白名单 rows,Rules 和重复地址判定每次重做。身份、SQL 及绑定参数只参与摘要分区,不写入文件;凭据缺失仍在缓存读取前拒绝。过期刷新在有界、按身份的跨进程文件锁内完成,成功后原子替换;失败使用原时间的已验证历史,成功空清单替换旧值。最新查询成功与本地缓存持久化成功是两个事实,不能因写盘失败回退旧数据或重复查询。强刷跳过有效期,不跳过身份、限额、文件校验和失败回退边界。
|
|
434
|
+
|
|
435
|
+
后台心跳和 HTTP 超时观察使用内部 `BackgroundTask` 作用域,前台保留主业务值和原始故障;观察线程的异常同步交付所有者,不受宿主全局线程异常策略影响。创建至登记期间延后中断,停止和等待期间保证线程回收;心跳普通故障转换为显示诊断,正常 HTTP 请求的未知观察故障仍传播。
|
data/docs/netdisco.md
CHANGED
|
@@ -18,6 +18,8 @@ export NC_DEVICE_PASSWORD='replace-me'
|
|
|
18
18
|
|
|
19
19
|
Use `ND_API_KEY` instead of the HTTP username/password pair when available. HTTPS retains standard certificate verification. HTTP is allowed by default for compatibility; set `netdisco.allow_insecure_http: false` to reject it before sending credentials.
|
|
20
20
|
|
|
21
|
+
`NC_CONCURRENCY` sets the default worker count for backups, configuration scripts and interface changes (1–50, default 4). An explicit API argument or CLI option takes precedence over the environment, then YAML.
|
|
22
|
+
|
|
21
23
|
YAML contains only non-sensitive settings. Unknown fields, Ruby object tags, and credentials in YAML are rejected. Settings precedence is CLI, supported environment variables, YAML, then defaults. Files load only through `--config FILE` or `NC_CONFIG`. Start with [backup.yml](../examples/backup.yml):
|
|
22
24
|
|
|
23
25
|
```yaml
|
|
@@ -38,6 +40,25 @@ tftp:
|
|
|
38
40
|
cisco_nxos: management
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
## Inventory cache
|
|
44
|
+
|
|
45
|
+
HTTP and PostgreSQL inventory use a local cache by default, valid for three days (259,200 seconds). The default directory is `.cache/net-connector/inventory`, relative to the current working directory. Configure it through YAML:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
netdisco:
|
|
49
|
+
cache_directory: /var/cache/network/inventory
|
|
50
|
+
cache_ttl: 259200
|
|
51
|
+
cache_enabled: true
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`ND_CACHE_TTL=86400` sets a one-day lifetime. Explicit settings overrides take precedence over the environment, then YAML, then the three-day default. TTL must be finite and positive; each batch fixes its policy snapshot before scheduling.
|
|
55
|
+
|
|
56
|
+
The directory must belong to the current user with mode `0700`; cache and lock files use `0600`. Source, account, database and SQL parameters partition the cache. Only inventory fields are stored; passwords, API keys, SQL and extra response fields are excluded. Each read applies current filtering and vendor rules. A successful empty inventory replaces history.
|
|
57
|
+
|
|
58
|
+
Expired inventory refreshes automatically. A remote inventory failure falls back to validated history without changing its original timestamp. Configuration errors, missing credentials, interruption and unsafe files remain errors. Failed cache writes preserve newly fetched inventory. `fleet.inventory_status` reports `:fresh_cache`, `:refreshed`, `:stale_cache` or `:uncached`, the original `fetched_at`, and safe failure metadata; the CLI warns on history fallback or cache write failure.
|
|
59
|
+
|
|
60
|
+
Use `net-backup --refresh-inventory`, `fleet.devices(force_refresh: true)`, or `fleet.plan_backup(force_refresh: true)` to bypass freshness. TFTP planning accepts the same keyword. A forced refresh also falls back to valid history if the remote request fails. `cache_enabled: false` disables caching. Injected clients stay uncached unless explicitly wrapped in `Netdisco::InventoryCache`; shared wrapper status describes the latest call, so synchronize concurrent access to the same instance. Cache locks bound waiting and coalesce ordinary expired-cache refreshes across processes; independently requested forced refreshes still query the source.
|
|
61
|
+
|
|
41
62
|
## CLI planning and execution
|
|
42
63
|
|
|
43
64
|
```sh
|
|
@@ -89,6 +110,34 @@ Local backups compare SHA-256. `on_change:` fires for created or changed artifac
|
|
|
89
110
|
|
|
90
111
|
`ResultStore::Json` writes private JSON by default. Use `ResultStore::Database.new(repository: YourModel)` with a `create!(attributes)` repository, or `result_store: nil` to persist yourself. Custom stores receive `write(report, directory:)`. Report errors retain known committed locations and prevent success.
|
|
91
112
|
|
|
113
|
+
## Configuration batches and interface changes
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
fleet = Net::Connector::Netdisco::Fleet.new
|
|
117
|
+
batch = fleet.execute_scripts(
|
|
118
|
+
scripts: { "192.0.2.1" => ["system-view", "interface GigabitEthernet1/0/1", "description uplink", "quit", "quit"] },
|
|
119
|
+
confirmed: true,
|
|
120
|
+
concurrency: 4
|
|
121
|
+
)
|
|
122
|
+
puts JSON.pretty_generate(batch.to_h)
|
|
123
|
+
puts "Total=#{batch.summary[:total]} succeeded=#{batch.summary[:succeeded]} failed=#{batch.summary[:failed]}"
|
|
124
|
+
|
|
125
|
+
batch = fleet.sync_interface_descriptions(hosts: ["192.0.2.1"])
|
|
126
|
+
puts "Total=#{batch.summary[:total]} succeeded=#{batch.summary[:succeeded]} failed=#{batch.summary[:failed]}"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Scripts accept `Script`, command arrays or multiline text. The full batch is parsed and target addresses are normalized before credentials or device connections; duplicate, unknown, filtered and unsupported targets are rejected. Scripts contain the vendor's complete commands, including mode transitions and save commands where required. An empty batch reports `:no_devices` and is unsuccessful.
|
|
130
|
+
|
|
131
|
+
Interface application calls the existing topology operation, retaining evidence revalidation, configuration readback and persistence confirmation. It does not execute an approved plan as an ordinary script. Both write operations require `confirmed: true`, and never automatically retry commands. Each task owns and closes its connector. Ordinary failures remain per-device outcomes; `Interrupt` stops and joins all owned workers. Confirmed work is not rolled back across devices.
|
|
132
|
+
|
|
133
|
+
`OperationBatch` is separate from backup reports. `outcomes` retain the original `Result` or plan, completed steps and the first failure; `:partial` preserves work followed by an error, and missing credentials prevent success. `plans` selects only fully successful planning results. `to_h` includes safe diagnostics and counts, excluding commands, outputs, credentials and evidence. `on_start` and `on_result` run on workers and must be thread safe; ordinary callback faults are recorded without repeating device operations. Logging uses the configured `ssh.log_directory`.
|
|
134
|
+
|
|
135
|
+
`sync_interface_descriptions` is the direct update entry: one request and one connection per device, from discovery through readback and save. Matching descriptions cause no writes or saves; a no-op does not confirm an earlier save. The default format is `To_<peer name>_<interface abbreviation>`; formatting options and blocks remain backend choices. Separate preview/approval workflows remain available through `plan_interface_descriptions` and `apply_interface_descriptions(plans: planned.plans, confirmed: true)`.
|
|
136
|
+
|
|
137
|
+
Each device publishes one immutable outcome after execution and connection cleanup. Ordinary credential, connection, execution and cleanup failures affect that device while active and queued siblings continue. Once scheduling completes, `summary` and `to_h[:summary]` report `total`, `succeeded` and `failed`, with total = succeeded + failed. Total counts the devices requested and scheduled in this batch; partial completion and missing credentials count as failures and also appear in `partial` and `missing_credentials`. `callback_errors` counts notification faults separately without changing device outcomes. Empty batches report zero counts and `no_devices`. The library returns the report; callers choose terminal output or JSON export. Whole-batch approval validation and interruption retain their existing boundaries, and completed device changes cannot be rolled back.
|
|
138
|
+
|
|
139
|
+
For other independent tasks, load `net/connector/execution_pool` and use `Net::Connector::ExecutionPool.new(concurrency: 4).map(items) { |item| ... }`. Results preserve input order, all threads must start before any block runs, and uncaught exceptions stop the pool and propagate to its caller. Business code decides which ordinary errors become per-task results.
|
|
140
|
+
|
|
92
141
|
## PostgreSQL inventory
|
|
93
142
|
|
|
94
143
|
Set `netdisco.source: postgres` and provide your own SELECT query. Table names and business filters are not built into the library. See [inventory_sql.yml](../examples/inventory_sql.yml):
|
|
@@ -134,7 +183,9 @@ Explicit host mappings, device rules, and vendor overrides take precedence over
|
|
|
134
183
|
|
|
135
184
|
## TFTP history and verification
|
|
136
185
|
|
|
137
|
-
Direct upload receipts only establish `device_reported`. Fleet can verify a local/mounted TFTP directory via `verification_root:`; the CLI accepts `--tftp-root DIR` or YAML `tftp.verification_root`. A verified receipt requires a current, nonempty regular file matching the actual receipt path, with SHA-256 evidence. Missing evidence prevents `verified` success.
|
|
186
|
+
Direct upload receipts only establish `device_reported`. Fleet can verify a local/mounted TFTP directory via `verification_root:`; the CLI accepts `--tftp-root DIR` or YAML `tftp.verification_root`. A verified receipt requires a current, nonempty regular file matching the actual receipt path, with SHA-256 evidence. Missing evidence prevents `verified` success. A mismatched receipt path is retained as device evidence, but cannot be verified or archived without a baseline for that locked target.
|
|
187
|
+
|
|
188
|
+
Verification holds a target lock across the pre-upload file-state/time baseline, upload and fingerprint check, including batches without history preservation. An unchanged old file cannot qualify through a future timestamp. The server directory must be writable for lock files and the filesystem-time probe. Result callbacks receive the verified outcome, and file evidence never substitutes for a failed device operation or cleanup.
|
|
138
189
|
|
|
139
190
|
Batch examples enable history preservation. With an accessible server directory, uploads use hostname/IP names and are retained in both `<batch>/tftp/` and server `archive/<batch>/`; pre-existing same-name files are archived under `previous/` before upload. File-state and filesystem-time baselines are captured under the upload lock. Verification or archival failure preserves available receipts and committed paths without reporting success.
|
|
140
191
|
|
|
@@ -163,10 +214,12 @@ Examples load the project `.env` through `dotenv` without overriding existing en
|
|
|
163
214
|
| Variables | Purpose / default |
|
|
164
215
|
| --- | --- |
|
|
165
216
|
| `ND_URL` | HTTP inventory base URL |
|
|
217
|
+
| `ND_CACHE_TTL` | Inventory cache lifetime in seconds; finite positive number, default 259200 (three days) |
|
|
166
218
|
| `ND_USERNAME`, `ND_PASSWORD` | HTTP login without API key |
|
|
167
219
|
| `ND_API_KEY` | Existing API key |
|
|
168
220
|
| `ND_DB_HOST`, `ND_DB_NAME`, `ND_DB_USER`, `ND_DB_PASS` | Required database connection fields |
|
|
169
221
|
| `NC_CONFIG` | Non-sensitive YAML file |
|
|
222
|
+
| `NC_CONCURRENCY` | Default batch worker count; integer 1–50, default 4 |
|
|
170
223
|
| `NC_DEVICE_USERNAME`, `NC_DEVICE_PASSWORD` | Default device credentials |
|
|
171
224
|
| `NC_<VENDOR>_USERNAME`, `NC_<VENDOR>_PASSWORD` | Vendor credentials, e.g. `CISCO_IOS` |
|
|
172
225
|
| `NC_ENABLE_PASSWORD`, `NC_<VENDOR>_ENABLE_PASSWORD` | Optional privilege credentials |
|
data/docs/netdisco.zh-CN.md
CHANGED
|
@@ -50,7 +50,7 @@ exit 1 unless batch.success?
|
|
|
50
50
|
|
|
51
51
|
gem 安装后的命令为 `net-backup`;源码入口为 `bin/net-backup`,可在项目目录运行 `bundle exec bin/net-backup --help`。原命令 `net-connector-backup` 已改名。
|
|
52
52
|
|
|
53
|
-
命令行程序 `net-backup` 的 YAML 文件只允许非敏感设置;Netdisco
|
|
53
|
+
命令行程序 `net-backup` 的 YAML 文件只允许非敏感设置;Netdisco 和设备凭据留在环境变量中。`ND_URL` 从环境覆盖 YAML 地址,`NC_CONCURRENCY` 覆盖 YAML 并发数;其他非敏感策略通过 YAML 或 CLI 设置。批量示例也支持 `NC_CONFIG`,可从 [完整配置示例](../examples/backup.yml) 开始。只有传入 `--config FILE` 或设置 `NC_CONFIG` 时才加载文件:
|
|
54
54
|
|
|
55
55
|
```yaml
|
|
56
56
|
netdisco:
|
|
@@ -136,7 +136,7 @@ CLI 的计划与批次摘要使用 JSON。默认 `--success-policy strict` 使
|
|
|
136
136
|
|
|
137
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
138
|
|
|
139
|
-
本地批量示例和 TFTP 批量示例默认全量;显式 `--sample N` 或 YAML `backup.limit_per_vendor` 才限制每厂商数量(示例允许 1 至 5)。优先级为 CLI > YAML。并发默认 4,可用 `--concurrency N` 或 YAML `backup.concurrency`
|
|
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
140
|
|
|
141
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
142
|
|
|
@@ -147,6 +147,8 @@ CLI 的计划与批次摘要使用 JSON。默认 `--success-policy strict` 使
|
|
|
147
147
|
| 环境变量 | 默认值 | 用途 |
|
|
148
148
|
| --- | --- | --- |
|
|
149
149
|
| `ND_URL` | HTTP 模式必填 | Netdisco 服务根地址,可包含租户路径 |
|
|
150
|
+
| `NC_CONCURRENCY` | 默认 4,范围 1 至 50 | 备份、配置批次和接口修正共用的并发数 |
|
|
151
|
+
| `ND_CACHE_TTL` | 默认 259200 秒(三天) | 清单缓存有效期,须为有限正数,优先于 YAML `netdisco.cache_ttl` |
|
|
150
152
|
| `NC_CONFIG` | 未设置 | CLI / 批量示例的非敏感 YAML 配置文件 |
|
|
151
153
|
| `ND_USERNAME`, `ND_PASSWORD` | 未提供 API 密钥时必填 | 清单 API 登录 |
|
|
152
154
|
| `ND_API_KEY` | 未设置 | 直接使用已有 API 密钥 |
|
|
@@ -245,3 +247,45 @@ TFTP 的 `verification` 汇总与设备条目的 `server_file_verified` 区分
|
|
|
245
247
|
使用 `accept_new` / `replace` 时,SSH 在每个会话的临时 known_hosts 中协商,认证后通过独立锁文件合并到共享文件。
|
|
246
248
|
网络登录不占用共享锁;失败登录不删除原信任记录,`replace` 只替换当前主机。该同步协调本库进程,外部手工工具应避免同时改写同一文件。
|
|
247
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
CHANGED
|
@@ -73,18 +73,35 @@ Parsing validates UTF-8 before and after terminal rendering. Invalid bytes raise
|
|
|
73
73
|
## Topology and interface descriptions
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
|
-
|
|
77
|
-
plan = device.plan_interface_descriptions
|
|
78
|
-
puts plan.commands.join("\n")
|
|
79
|
-
# Review the plan and approve the exact commands before applying it.
|
|
80
|
-
device.apply_interface_descriptions(plan, confirmed: true).value!
|
|
76
|
+
device.sync_interface_descriptions.value!
|
|
81
77
|
```
|
|
82
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
|
+
|
|
83
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.
|
|
84
82
|
|
|
85
|
-
The default description is `
|
|
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.
|
|
86
103
|
|
|
87
|
-
|
|
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.
|
|
88
105
|
|
|
89
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.
|
|
90
107
|
|
data/docs/usage.zh-CN.md
CHANGED
|
@@ -111,18 +111,30 @@ rows = saved.parse(host: "192.0.2.10", template: "/path/to/template.textfsm")
|
|
|
111
111
|
```ruby
|
|
112
112
|
Net::Connector.open(:h3c, host: "192.0.2.10", username: ENV.fetch("NC_DEVICE_USERNAME"),
|
|
113
113
|
password: ENV.fetch("NC_DEVICE_PASSWORD")) do |device|
|
|
114
|
-
|
|
115
|
-
plan.changes.each { |change| puts "#{change.interface}: #{change.old_description.inspect} -> #{change.new_description.inspect}" }
|
|
116
|
-
puts plan.commands.join("\n")
|
|
117
|
-
# 核对将要下发的命令,并完成操作审批。
|
|
118
|
-
result = device.apply_interface_descriptions(plan, confirmed: true)
|
|
119
|
-
result.value!
|
|
114
|
+
device.sync_interface_descriptions.value!
|
|
120
115
|
end
|
|
121
116
|
```
|
|
122
117
|
|
|
123
|
-
|
|
118
|
+
显式调用 `sync_interface_descriptions`,在同一连接中完成发现、修改、读回和保存,不需要处理计划或二次确认;已有描述一致时不修改、不保存。需要单独预览审批时,后端仍可使用 `plan_interface_descriptions` 与 `apply_interface_descriptions(plan, confirmed: true)`。
|
|
124
119
|
|
|
125
|
-
|
|
120
|
+
默认描述为 `To_<邻居名称>_<接口简称>`,例如 `To_Core-SW_Gi1/0/1`。同类接口的厂商全称、简称和大小写统一,显示文本中的空格替换为下划线。端口、拆分通道与子接口编号完整保留;原始邻居记录和本机下发接口拼写不改变。`abbreviate: false` 保留对端接口全称,`lowercase: true` 仅将对端接口简称转为小写;后端也可用代码块定制。公共纯函数是 `Net::Connector::Topology::InterfaceDescription.format`。
|
|
121
|
+
|
|
122
|
+
| 接口类型 | 厂商名称与别名 | 描述简称 |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| Ethernet | `Ethernet`、`ethernet` | `Eth` |
|
|
125
|
+
| 1G | `GigabitEthernet`、`GE`、`Gi` | `Gi` |
|
|
126
|
+
| 10G | `Ten-GigabitEthernet`、`TenGigabitEthernet`、`TenGigE`、`XGE`、山石 `xethernet` / `xe` | `Te` |
|
|
127
|
+
| 25G | H3C `Twenty-FiveGigE` / `WGE`、Cisco `TwentyFiveGigE`、`25GE` | `Twe` |
|
|
128
|
+
| 40G | `FortyGigE`、`FortyGigabitEthernet`、`FGE`、`40GE` | `Fo` |
|
|
129
|
+
| 100G | `HundredGigE`、`HundredGigabitEthernet`、`HGE`、`100GE` | `Hu` |
|
|
130
|
+
| H3C 二层聚合 | `Bridge-Aggregation`、`BAGG` | `BAGG` |
|
|
131
|
+
| H3C 三层聚合 | `Route-Aggregation`、`RAGG` | `RAGG` |
|
|
132
|
+
| Cisco 聚合 | `Port-channel`、`Po` | `Po` |
|
|
133
|
+
| 山石聚合 | `aggregate`、`Agg` | `Agg` |
|
|
134
|
+
|
|
135
|
+
命名依据:[H3C 接口名称](https://www.h3c.com/en/d_202307/1892442_294551_0.htm)、[Cisco 接口命令](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)、[山石接口示例](https://kb.hillstonenet.com/en/wp-content/uploads/2016/05/Hillstone-All-Series-Device-Troubleshooting-and-Debug-Guide.pdf)。未知前缀保持原样,不猜测速率。二层与三层聚合身份保持区分;命名规则不代表设备固件已经现场验证。
|
|
136
|
+
|
|
137
|
+
规划会拒绝歧义邻居、缺少对端身份、不安全文本和无法识别的输出。独立计划应用须传 `confirmed: true`;同步和计划应用都会再次读取邻居和旧描述;证据变化在写入前抛出 `DeviceError`,其 `code` 为 `:stale_plan`。IOS/NX-OS、H3C 和山石采用“修改 → 退出配置视图 → 读回 → 保存”的顺序;读回不匹配返回 `:description_unconfirmed`,解析不完整或查询命令不符合审批时也不会保存。
|
|
126
138
|
|
|
127
139
|
`plan.commands` 现在包括读回命令,旧版计划必须重新生成、审核;执行仍返回 `Result`,保留修改、读回及保存阶段已经完成的步骤。只有设备提供明确保存完成行才成功,保存超时、失败或只有提示符会返回 `:persistence_unconfirmed`,此时描述可能已生效,不能自动重放或回滚。现场下发前仍应按设备固件核对命令与回显。
|
|
128
140
|
|
|
@@ -130,6 +142,8 @@ PAN-OS 的自动描述计划及改写暂不提供,`supports?(:interface_descri
|
|
|
130
142
|
|
|
131
143
|
PAN-OS 在导出前后检查候选配置差异,并拒绝 XML 或非 set 格式输出,避免将未提交配置误作运行配置。
|
|
132
144
|
|
|
145
|
+
同一审批计划中的规范接口只能出现一次;接口缩写与全称也按同一接口判重。在新增设备 I/O 前拒绝重复审批;Fleet 应用整批计划时,在建立任一设备会话前执行相同预检。
|
|
146
|
+
|
|
133
147
|
## 命令脚本与自动交互
|
|
134
148
|
|
|
135
149
|
```ruby
|
data/examples/backup.yml
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
# 后台观察线程属于调用作用域;停止、等待和异常交付不能盖掉前台原故障。
|
|
6
|
+
module BackgroundTask
|
|
7
|
+
# task 自行等待停止信号,stop 只负责唤醒;正常前台也必须观察后台失败。
|
|
8
|
+
def self.run(task:, stop:)
|
|
9
|
+
unless task.respond_to?(:call) && stop.respond_to?(:call) && block_given?
|
|
10
|
+
raise ArgumentError, "background task requires task, stop and a foreground block"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
thread = value = failure = nil
|
|
14
|
+
completion = Queue.new
|
|
15
|
+
begin
|
|
16
|
+
# 创建与登记之间不交付异步中断;子线程恢复即时交付,捕获后同步传回所有者。
|
|
17
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
18
|
+
thread = Thread.new { observe(task, completion) }
|
|
19
|
+
end
|
|
20
|
+
value = yield
|
|
21
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 收尾后向所有者重新抛出原故障,包括中断。
|
|
22
|
+
failure = error
|
|
23
|
+
ensure
|
|
24
|
+
begin
|
|
25
|
+
# 二次中断不能跳过 join;恢复交付时也保留已经发生的前台故障。
|
|
26
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
27
|
+
cleanup_error = finish(thread, stop) if thread
|
|
28
|
+
failure ||= cleanup_error
|
|
29
|
+
end
|
|
30
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 保留前台故障,观察延迟到达的取消。
|
|
31
|
+
failure ||= error
|
|
32
|
+
end
|
|
33
|
+
# break 和非局部 return 也会展开作用域;异常交付必须在 ensure 内,不能绕过。
|
|
34
|
+
failure ||= completion.pop unless completion.empty?
|
|
35
|
+
raise failure if failure
|
|
36
|
+
end
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.observe(task, completion)
|
|
41
|
+
failure = nil
|
|
42
|
+
Thread.handle_interrupt(Exception => :immediate) { task.call }
|
|
43
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 不让 global abort_on_exception 将子线程故障传播到无关主线程。
|
|
44
|
+
failure = error
|
|
45
|
+
ensure
|
|
46
|
+
completion << failure
|
|
47
|
+
end
|
|
48
|
+
private_class_method :observe
|
|
49
|
+
|
|
50
|
+
def self.finish(thread, stop)
|
|
51
|
+
failure = nil
|
|
52
|
+
begin
|
|
53
|
+
stop.call
|
|
54
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 唤醒失败时仍必须终止并回收自有线程。
|
|
55
|
+
failure = error
|
|
56
|
+
thread.kill
|
|
57
|
+
ensure
|
|
58
|
+
begin
|
|
59
|
+
thread.join
|
|
60
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 等待失败也不能覆盖先前故障。
|
|
61
|
+
failure ||= error
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
failure
|
|
65
|
+
end
|
|
66
|
+
private_class_method :finish
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -7,14 +7,14 @@ module Net
|
|
|
7
7
|
class Topology
|
|
8
8
|
# 描述文本与常见接口视图命令的纯构造方法,不连接或修改设备。
|
|
9
9
|
module InterfaceDescription
|
|
10
|
-
#
|
|
10
|
+
# 将邻居名称和接口拼成默认描述;统一邻居接口简称,原始发现记录保持不变。
|
|
11
11
|
def self.format(neighbor, abbreviate: true, lowercase: false)
|
|
12
12
|
port = if abbreviate
|
|
13
13
|
InterfaceName.short(neighbor.neighbor_interface, lowercase: lowercase)
|
|
14
14
|
else
|
|
15
15
|
lowercase ? neighbor.neighbor_interface.downcase : neighbor.neighbor_interface
|
|
16
16
|
end
|
|
17
|
-
validate!("
|
|
17
|
+
validate!("To_#{neighbor.neighbor_name.tr(" ", "_")}_#{port.tr(" ", "_")}")
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# 生成进入接口、设置描述和退出视图的命令,不执行设备操作。
|
|
@@ -36,7 +36,7 @@ module Net
|
|
|
36
36
|
|
|
37
37
|
# 拒绝可能把描述命令拆成多条命令的不安全接口名。
|
|
38
38
|
def self.validate_interface!(value)
|
|
39
|
-
return value if value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9
|
|
39
|
+
return value if value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9.:\/-]*\z/)
|
|
40
40
|
|
|
41
41
|
raise ArgumentError, "neighbor interface name is unsafe"
|
|
42
42
|
end
|
|
@@ -6,39 +6,30 @@ module Net
|
|
|
6
6
|
# 接口身份匹配与描述展示共用名称规则;展示简称不能直接用作下发命令。
|
|
7
7
|
module InterfaceName
|
|
8
8
|
RULES = [
|
|
9
|
-
[/\A(?:
|
|
9
|
+
[/\A(?:bridge-aggregation|bagg)(?=\d)/i, "bridge-aggregation", "BAGG"],
|
|
10
|
+
[/\A(?:route-aggregation|ragg)(?=\d)/i, "route-aggregation", "RAGG"],
|
|
11
|
+
[/\A(?:aggregate|agg)(?=\d)/i, "aggregate", "Agg"],
|
|
12
|
+
[/\A(?:hundredgigabitethernet|hundredgige|hge|100ge|hu)(?=\d)/i, "hundredgigabitethernet", "Hu"],
|
|
13
|
+
[/\A(?:fortygigabitethernet|fortygige|fge|40ge|fo)(?=\d)/i, "fortygigabitethernet", "Fo"],
|
|
14
|
+
[/\A(?:twenty-?fivegigabitethernet|twenty-?fivegige|wge|25ge|twe)(?=\d)/i, "twentyfivegigabitethernet", "Twe"],
|
|
15
|
+
[/\A(?:xge|10ge|xgigabitethernet|ten-gigabitethernet|tengigabitethernet|ten-?gige|xethernet|xe|te)(?=\d)/i, "tengigabitethernet", "Te"],
|
|
10
16
|
[/\A(?:gigabitethernet|ge|gi)(?=\d)/i, "gigabitethernet", "Gi"],
|
|
11
17
|
[/\A(?:fastethernet|fa)(?=\d)/i, "fastethernet", "Fa"],
|
|
12
18
|
[/\A(?:ethernet|eth)(?=\d)/i, "ethernet", "Eth"],
|
|
13
19
|
[/\A(?:port-channel|po)(?=\d)/i, "port-channel", "Po"]
|
|
14
20
|
].map(&:freeze).freeze
|
|
15
21
|
|
|
16
|
-
#
|
|
22
|
+
# 用于本机邻居表与配置的关联;同类别名归一,不混淆速率、聚合类型或端口编号。
|
|
17
23
|
def self.key(name)
|
|
18
24
|
rule = RULES.find { |pattern, _key, _short| pattern.match?(name) }
|
|
19
25
|
rule ? name.sub(rule.first, rule[1]).downcase : name.downcase
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
# 仅缩短已识别的前缀,保留端口、子接口编号及未知命名。
|
|
23
|
-
#
|
|
29
|
+
# 厂商全称、缩写与大小写统一展示;原始身份和本机下发拼写不参与替换。
|
|
24
30
|
def self.short(name, lowercase: false)
|
|
25
31
|
rule = RULES.find { |pattern, _key, _short| pattern.match?(name) }
|
|
26
|
-
formatted =
|
|
27
|
-
name.sub(rule.first) do |prefix|
|
|
28
|
-
abbreviation = rule.last
|
|
29
|
-
if prefix == prefix.downcase
|
|
30
|
-
abbreviation.downcase
|
|
31
|
-
elsif prefix == prefix.upcase
|
|
32
|
-
abbreviation.upcase
|
|
33
|
-
elsif prefix.casecmp?(abbreviation)
|
|
34
|
-
prefix
|
|
35
|
-
else
|
|
36
|
-
abbreviation
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
else
|
|
40
|
-
name.dup
|
|
41
|
-
end
|
|
32
|
+
formatted = rule ? name.sub(rule.first, rule.last) : name.dup
|
|
42
33
|
lowercase ? formatted.downcase : formatted
|
|
43
34
|
end
|
|
44
35
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../engine/command"
|
|
4
|
+
require_relative "interface_description"
|
|
5
|
+
|
|
6
|
+
module Net
|
|
7
|
+
module Connector
|
|
8
|
+
class Topology
|
|
9
|
+
# 离线预检与现场执行共用确切阶段,不复制厂商命令或把保存合并到变更阶段。
|
|
10
|
+
module Stages
|
|
11
|
+
def self.build(strategy:, host:, changes:)
|
|
12
|
+
validate_changes!(strategy, changes)
|
|
13
|
+
phases = %i[leave_configuration_commands verification_commands persistence_commands].map do |method|
|
|
14
|
+
strategy.public_send(method)
|
|
15
|
+
end
|
|
16
|
+
unless phases.all? { |commands| commands.is_a?(Array) && !commands.empty? }
|
|
17
|
+
raise UnsupportedOperation.new("topology strategy must declare separate readback and persistence stages",
|
|
18
|
+
code: :description_stages_unsupported, host: host, phase: :plan)
|
|
19
|
+
end
|
|
20
|
+
leave, verify, persist = phases
|
|
21
|
+
commands = changes.flat_map do |change|
|
|
22
|
+
strategy.change_commands(change)
|
|
23
|
+
end
|
|
24
|
+
sequences = { change: [strategy.enter_configuration_command] + commands + leave,
|
|
25
|
+
verify: verify, persist: persist }
|
|
26
|
+
sequences.transform_values do |sequence|
|
|
27
|
+
Script.new(sequence.map { |command| strategy.script_command(command) }, name: "interface descriptions")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 同一物理接口只能出现一次;缩写与全称也不能绕过离线和现场的共同预检。
|
|
32
|
+
def self.validate_changes!(strategy, changes)
|
|
33
|
+
unless changes.is_a?(Array) && changes.all?(Topology::Change)
|
|
34
|
+
raise ArgumentError, "topology changes must be an Array of Changes"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
interfaces = {}
|
|
38
|
+
changes.each do |change|
|
|
39
|
+
InterfaceDescription.validate_interface!(change.interface)
|
|
40
|
+
key = strategy.interface_key(change.interface)
|
|
41
|
+
raise ArgumentError, "topology changes must identify unique interfaces" if interfaces.key?(key)
|
|
42
|
+
|
|
43
|
+
interfaces[key] = true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
private_class_method :validate_changes!
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|