net-connector 0.12.0 → 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 +10 -0
- data/docs/VERIFICATION.md +14 -0
- data/docs/architecture.md +13 -3
- data/docs/templates.md +13 -7
- data/docs/vendors.md +14 -3
- data/lib/net/connector/device/profile.rb +1 -1
- data/lib/net/connector/device/remote_backup/receipt.rb +4 -0
- data/lib/net/connector/device/remote_backup/target.rb +3 -0
- data/lib/net/connector/device/topology/change.rb +22 -0
- data/lib/net/connector/device/topology/immediate_strategy.rb +3 -9
- 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/strategy.rb +4 -0
- data/lib/net/connector/device/topology.rb +6 -11
- data/lib/net/connector/netdisco/batch.rb +33 -4
- data/lib/net/connector/netdisco/operations/outcome.rb +0 -14
- data/lib/net/connector/netdisco/operations.rb +15 -33
- data/lib/net/connector/netdisco/report.rb +14 -11
- data/lib/net/connector/templates/cisco/show_running_config.textfsm +1 -0
- data/lib/net/connector/templates/cisco_nxos/show_running_config.textfsm +1 -0
- data/lib/net/connector/templates/fortinet/show_full_configuration.textfsm +21 -0
- data/lib/net/connector/templates/h3c/display_current_configuration.textfsm +1 -0
- data/lib/net/connector/templates/h3c/{display_lldp_neighbor_information_list_local_first.textfsm → display_lldp_neighbor_information_list.textfsm} +7 -1
- data/lib/net/connector/templates/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 -12
- data/lib/net/connector/templates/juniper/show_configuration_display_set.textfsm +4 -0
- data/lib/net/connector/templates/palo_alto/show.textfsm +1 -0
- data/lib/net/connector/templates/radware/cfg_dump.textfsm +5 -3
- data/lib/net/connector/vendor/fortinet/topology.rb +24 -1
- data/lib/net/connector/vendor/h3c/topology.rb +4 -5
- data/lib/net/connector/vendor/huawei/topology.rb +48 -0
- data/lib/net/connector/vendor/huawei.rb +2 -0
- data/lib/net/connector/vendor/juniper/topology.rb +7 -6
- data/lib/net/connector/vendor/palo_alto/topology.rb +1 -7
- data/lib/net/connector/vendor/radware/topology.rb +2 -0
- data/lib/net/connector/version.rb +1 -1
- metadata +12 -4
- data/lib/net/connector/templates/h3c/display_lldp_neighbor_information_list_name_first.textfsm +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f018d69d8d4cf234bd90fcdd4e56a60e4184ef5ddb4f4cf15f4ff49206d5631
|
|
4
|
+
data.tar.gz: f8e0afe1a9131435134e5580dc0f3ffdc9823e55b7fc5665d60b1768f3c223fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 620f62156a11192ae6648f48878ef61cb810830ee3c9dee8c7e8c6bc12b2b88e4c3f8ba420535bbb82b94a423328897c52844a4b7d76c4a404ae4e59b9f54c95
|
|
7
|
+
data.tar.gz: dc0ea681afb4a9a9cbeef79778d1cfebdf009004ee036a61dfcc8e9cb9f53579ad1967088a784bc2ed730874bdb1770207a57603b800caa55545dec8c8089915
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.13.0 - 2026-10-18
|
|
4
|
+
|
|
5
|
+
- 不兼容调整:H3C 的 LLDP 列序变体合并为 `h3c/display_lldp_neighbor_information_list.textfsm`,显式引用旧模板路径的调用方需要更新。
|
|
6
|
+
- Huawei VRP 增加 LLDP 邻居和接口描述读取,FortiOS 增加当前视图的接口描述读取,Junos 分别返回物理接口与逻辑单元描述。
|
|
7
|
+
- 补齐各平台配置解析索引,修复 H3C 本地接口首列时的字段错位,并要求厂商名和命令完整匹配,避免选择错误模板。
|
|
8
|
+
- 接口描述缺少值、引号未闭合或配置作用域不明时拒绝解析,Radware 端口名称中的转义引号可正确还原。
|
|
9
|
+
- 拓扑计划和备份批次固定各自拥有的嵌套数据,调用方修改原始容器不再改变已生成结果,更新需通过 `with` 创建新快照。
|
|
10
|
+
- 批量报告复用状态计数,减少重复统计时对设备结果的遍历,未知状态仍不能满足成功策略。
|
|
11
|
+
- 修复 Ruby 3.2 下远程备份目标及回执的 `with` 绕过校验的问题,拒绝非法协议、路径及没有核验证据的完成声明。
|
|
12
|
+
|
|
3
13
|
## 0.12.0 - 2026-10-17
|
|
4
14
|
|
|
5
15
|
- 不兼容调整:模块命名空间和加载路径按领域重组,厂商类改为 `Vendor::CiscoIos` 等直接入口,清单与报告存储分别使用 `inventory:`、`report_store:` 注入,旧名称及兼容别名已移除。
|
data/docs/VERIFICATION.md
CHANGED
|
@@ -246,3 +246,17 @@ TFTP 远端未核验回执以及自定义日志目录;离线复核同时支持
|
|
|
246
246
|
源码根目录 `RENAMES.md` 记录批准的改名与兼容边界。完整验收仍运行 `bundle exec rake ci`;
|
|
247
247
|
临时 PostgreSQL 使用 `bundle exec rake test:postgres`,可通过 `NC_TEST_PG_BINDIR` 指定服务端工具目录。
|
|
248
248
|
最低 Ruby 3.2 必须在对应解释器中运行 lint/test;RuboCop 的 TargetRubyVersion 和 Ruby 4.0 测试不能替代此项。
|
|
249
|
+
|
|
250
|
+
## 报告统计基准
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
bundle exec ruby script/benchmarks/report_workload.rb 100000 20
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
脚本分别输出批次构造、首次统计及重复统计的单调时钟耗时与对象分配数;合成结果的创建不计入测量。比较两个源码版本时,在同一 Ruby 与主机上各运行至少三次。批次构造会复制结果容器并累计状态,重复统计复用计数;不读取报告的调用方也承担构造成本。TFTP 文件核验与逐设备明细仍有线性成本,此基准不测网络吞吐或现场设备性能。
|
|
257
|
+
|
|
258
|
+
## 厂商能力与模板一致性
|
|
259
|
+
|
|
260
|
+
`test/capabilities_test.rb` 离线核对全部已注册平台的能力;`test/vendor_topology_parity_test.rb` 覆盖 Huawei LLDP 简表与接口描述、FortiOS 顶层接口描述/嵌套表隔离,以及 Junos 物理接口和逻辑单元描述。`test/textfsm_test.rb` 同时检查 H3C 两种列序在通用命令解析入口中的语义、内置厂商/命令的完整匹配,以及各配置方言的索引入口。完整性拒绝路径覆盖缺失表头、残缺记录、未闭合接口表/引号、VDOM 作用域不明和没有值的 Junos description。
|
|
261
|
+
|
|
262
|
+
新增样本来自官方结构说明构造的离线会话,尚无设备联机验收;模板解析成功不能证明固件覆盖或候选配置写入安全。
|
data/docs/architecture.md
CHANGED
|
@@ -247,10 +247,10 @@ Ruby 对象显式拥有资源并使用关键字参数。`Profile` 提供有限
|
|
|
247
247
|
| Cisco NX-OS | 是 | 是 | 是 | 是 | 是 | 是 |
|
|
248
248
|
| Radware Alteon | 是 | 是 | 是 | 否 | 是 | 否 |
|
|
249
249
|
| PAN-OS | 是 | 否 | 是 | 是 | 是 | 否,候选隔离未验证 |
|
|
250
|
-
| 华为 | 是 | 是 | 是 |
|
|
250
|
+
| 华为 VRP | 是 | 是 | 是 | 是,LLDP 简表 | 是 | 否,提交阶段未验证 |
|
|
251
251
|
| 山石 | 是 | 是 | 是 | 是 | 是 | 是 |
|
|
252
252
|
| Topsec TopOS | 是 | 否 | 否 | 否 | 否 | 否 |
|
|
253
|
-
| Fortinet FortiOS | 是 | 是 | 是 | 是 |
|
|
253
|
+
| Fortinet FortiOS | 是 | 是 | 是 | 是 | 是,当前视图顶层接口表 | 否 |
|
|
254
254
|
| Juniper Junos | 是 | 否 | 否,原生 FTP/SCP | 是 | 是 | 否,候选库提交未开放 |
|
|
255
255
|
|
|
256
256
|
Junos 的 `remote_backup` 通过 `RemoteBackup` 协调与厂商策略导出已提交配置;回执区分设备报告与远端核验。其余平台保持明确的能力声明,见[平台说明](vendors.md)。
|
|
@@ -290,7 +290,7 @@ router.supports?(:tftp_backup) # => true,不连接设备
|
|
|
290
290
|
|
|
291
291
|
配置采集可把 `running_config_strategy` 绑定到 `Net::Connector::RunningConfig::Strategy` 子类。策略实现 `clean`、`result_step`、`prompt_text` 和 `validate_response!`;PAN-OS 候选差异只影响配置采集,直接 `execute_command("show config diff")` 返回该命令输出。静态命令列表留在档案中。`clean_config` 和受保护的 `config_result_step` 钩子可覆盖并调用 `super`。Base 负责通用脚本与锁内回调,不决定配置是否完整。
|
|
292
292
|
|
|
293
|
-
拓扑策略通过 `topology_strategy YourStrategy` 绑定。类方法和实例方法 `supports?` 声明三种拓扑能力,其余方法提供命令、模板、输出完整性和接口拼写。Profile 校验完整接口,包括 `validate_descriptions!`、`change_error_code`、`enter_configuration_command`、`leave_configuration_commands`、`verification_commands`、`persistence_commands` 和 `persistence_confirmed?`。只读策略继承默认空阶段;可写策略必须明确每个阶段,不能用合并的收尾命令推断保存边界。即时生效且使用 interface 块的设备可继承 `Topology::ImmediateStrategy`。自定义采集必须执行已列入计划的命令并返回完成步骤;自定义厂商标签通过 `neighbor_template` 提供模板。
|
|
293
|
+
拓扑策略通过 `topology_strategy YourStrategy` 绑定。类方法和实例方法 `supports?` 声明三种拓扑能力,其余方法提供命令、模板、输出完整性和接口拼写。Profile 校验完整接口,包括 `description_key`、`validate_descriptions!`、`change_error_code`、`enter_configuration_command`、`leave_configuration_commands`、`verification_commands`、`persistence_commands` 和 `persistence_confirmed?`。只读策略继承默认空阶段;可写策略必须明确每个阶段,不能用合并的收尾命令推断保存边界。即时生效且使用 interface 块的设备可继承 `Topology::ImmediateStrategy`。自定义采集必须执行已列入计划的命令并返回完成步骤;自定义厂商标签通过 `neighbor_template` 提供模板。
|
|
294
294
|
|
|
295
295
|
业务操作针对单个连接器构造,并提供 `call`。TFTP 策略只处理厂商传输差异,公共操作统一成功和失败规则。生成的文件名经过 `TftpTarget` 校验及长度限制;带作用域的 IPv6 地址会转为安全 ASCII 标记,特别长的地址使用稳定 SHA-256 标记。原本合法的文件名保留拼写,只有整体过长才缩短清单名称。TFTP 返回值表示设备报告上传完成,服务器文件核对仍由调用方负责。
|
|
296
296
|
|
|
@@ -449,3 +449,13 @@ end
|
|
|
449
449
|
`Settings#inventory` 为原始清单适配器包裹 `Inventory::Cache`,默认三天有效。缓存仅保存完整白名单 rows,Rules 和重复地址判定每次重做。身份、SQL 及绑定参数只参与摘要分区,不写入文件;凭据缺失仍在缓存读取前拒绝。过期刷新在有界、按身份的跨进程文件锁内完成,成功后原子替换;失败使用原时间的已验证历史,成功空清单替换旧值。最新查询成功与本地缓存持久化成功是两个事实,不能因写盘失败回退旧数据或重复查询。强刷跳过有效期,不跳过身份、限额、文件校验和失败回退边界。
|
|
450
450
|
|
|
451
451
|
后台心跳和 HTTP 超时观察使用内部 `Concurrency::Observer` 作用域,前台保留主业务值和原始故障;观察线程的异常同步交付所有者,不受宿主全局线程异常策略影响。创建至登记期间延后中断,停止和等待期间保证线程回收;心跳普通故障转换为显示诊断,正常 HTTP 请求的未知观察故障仍传播。
|
|
452
|
+
|
|
453
|
+
## 领域值与更新边界
|
|
454
|
+
|
|
455
|
+
`Topology::Neighbor`、`Topology::Change`、`Topology::Plan` 各有同名文件,构造时复制并冻结自己拥有的字符串与容器。不可变子对象可在单设备、批次和结果之间共享;调用方的原始容器与格式化回调返回的字符串仍归调用方所有。计划构造校验数据形状,执行边界继续负责地址规范化、命令预检、现场证据重验及读回。
|
|
456
|
+
|
|
457
|
+
`Netdisco::Batch` 复制结果数组和回调诊断,在构造时累计状态计数;`counts` 返回可修改的独立 Hash。未知状态不能满足成功策略。更新结果集合须使用 `with`,它重新计算计数。上述领域值和远程备份目标、回执的 `with` 显式经过构造函数,避免依赖不同 Ruby 版本中原生 `Data#with` 的初始化行为。
|
|
458
|
+
|
|
459
|
+
拓扑的 `InterfaceBlocks` 模块集中核对 interface 块与解析记录的完整性,由立即生效的变更策略和 Huawei 的只读策略共同组合,读取能力不必继承写入流程。`description_key(row)` 把描述记录映射为领域身份;Junos 逻辑单元使用 `<物理接口>.<单元号>`,保留物理接口描述。FortiOS 读取当前视图顶层 `config system interface`,接口 `description` 与显示 `alias` 分开处理,嵌套 IPv6 表不产生接口记录;无法确认作用域、闭合或记录数量时拒绝结果。
|
|
460
|
+
|
|
461
|
+
`Topology::QuotedText` 是描述字段的纯解码边界,不接触设备会话;厂商策略选择无引号文本的处理规则,解析异常统一携带 host、parse 阶段和 unrecognized_output 代码,不携带字段原文或底层异常 cause。配置模板保留完整带引号字段供业务层解码,不能先删外层引号而留下未还原的转义字符。
|
data/docs/templates.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
`lib/net/connector/templates/` 统一采用 `<平台>/<完整命令名>[_格式变体].textfsm`。命令使用小写,空格、连字符和路径分隔符转为下划线;不使用缩写命令、字段用途名或额外的命令子目录。管道的输出格式选项保留为命令词,纯分页控制 `no-more` 不进入文件名。
|
|
8
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
|
|
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
10
|
|
|
11
11
|
| 路径 | 输入与用途 |
|
|
12
12
|
| --- | --- |
|
|
@@ -17,22 +17,24 @@
|
|
|
17
17
|
| `cisco_nxos/show_running_config.textfsm` | NX-OS 运行配置中的接口和描述 |
|
|
18
18
|
| `cisco_nxos/show_ip_interface_brief.textfsm` | NX-OS 多 VRF 接口、IP 及 protocol/link/admin 状态 |
|
|
19
19
|
| `h3c/display_current_configuration.textfsm` | Comware 运行配置中的接口和描述 |
|
|
20
|
-
| `h3c/
|
|
21
|
-
| `
|
|
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 运行配置中的接口和描述 |
|
|
22
23
|
| `hillstone/show_configuration_running.textfsm` | StoneOS 运行配置中的接口和描述 |
|
|
23
24
|
| `hillstone/show_lldp_neighbor_information.textfsm` | StoneOS 的 LLDP 邻居表 |
|
|
24
25
|
| `palo_alto/show.textfsm` | PAN-OS set 格式中的接口和备注 |
|
|
25
26
|
| `palo_alto/show_lldp_neighbors_all.textfsm` | PAN-OS 按接口分块的 LLDP 邻居输出 |
|
|
26
27
|
| `radware/cfg_dump.textfsm` | Alteon 配置转储中的端口名称 |
|
|
28
|
+
| `fortinet/show_full_configuration.textfsm` | 当前视图顶层接口表的描述,跳过嵌套表 |
|
|
27
29
|
| `fortinet/diagnose_lldp_rx_neighbor.textfsm` | FortiOS 改名前后 LLDP 邻居查询的共同输出 |
|
|
28
30
|
| `juniper/show_lldp_neighbors_detail.textfsm` | Junos LLDP 详细邻居块 |
|
|
29
|
-
| `juniper/show_configuration_display_set.textfsm` | Junos 已提交 set
|
|
31
|
+
| `juniper/show_configuration_display_set.textfsm` | Junos 已提交 set 配置中的物理接口与逻辑单元描述 |
|
|
30
32
|
|
|
31
|
-
H3C Wireless 沿用 H3C 模板。H3C
|
|
33
|
+
H3C Wireless 沿用 H3C 模板。H3C 邻居策略与通用命令解析共用按表头切换列序的模板;未知表头直接拒绝,不尝试多个模板后采用碰巧解析出的部分结果。
|
|
32
34
|
|
|
33
35
|
## 索引与显式选择
|
|
34
36
|
|
|
35
|
-
根目录的 `index` 保持 TextFSM 的 `Template, Vendor, Command` 格式。Template 使用相对资源根目录的路径,Vendor 是连接器平台标识,Command
|
|
37
|
+
根目录的 `index` 保持 TextFSM 的 `Template, Vendor, Command` 格式。Template 使用相对资源根目录的路径,Vendor 是连接器平台标识,Command 是命令匹配规则。内置索引的 Vendor 与 Command 均使用 `\A...\z` 完整匹配,防止前缀厂商名称及带额外命令片段的输入误选模板。一个共享模板可以被多个平台索引项引用。
|
|
36
38
|
|
|
37
39
|
```ruby
|
|
38
40
|
parser = Net::Connector::TextFSM.new
|
|
@@ -40,7 +42,7 @@ rows = parser.call(output, vendor: :cisco_ios, command: "show ip interface brief
|
|
|
40
42
|
interfaces = parser.call(config, template: "cisco/show_running_config.textfsm")
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
命令索引覆盖内置配置及邻居模板;H3C 列序在模板状态机内选择。模板本身只解析记录,厂商策略仍负责表头、完整性及作用域校验。调用方也可以提供独立的 `template_dir:`,其索引和路径遵循相同规则。
|
|
44
46
|
|
|
45
47
|
## 修改与验证
|
|
46
48
|
|
|
@@ -51,3 +53,7 @@ interfaces = parser.call(config, template: "cisco/show_running_config.textfsm")
|
|
|
51
53
|
NX-OS 模板独立放在 `cisco_nxos/`,CDP 按邻居块清理状态并与设备块数量核对,残缺块不能借用下一条记录的字段。接口描述在空行或下一个顶层配置段结束,防止 route-map 等后续段落的描述覆盖接口描述。IP 接口摘要保留 `VRF`、`INTERFACE`、`IP_ADDRESS`、`PROTOCOL`、`LINK_STATUS`、`ADMIN_STATUS` 六个字段;`show ip interface brief` 与带 `vrf` 参数的命令共用此模板。
|
|
52
54
|
|
|
53
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/vendors.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
| 平台键 | 系统与登录视图 | 采集命令 | 保存 | 原生远端备份 | 拓扑读取 |
|
|
6
6
|
| --- | --- | --- | --- | --- | --- |
|
|
7
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
|
|
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
10
|
|
|
11
11
|
这些能力通过合成会话、解析样例和安装检查验证,尚无真实设备验收记录。Topsec 只实现有检索依据的 TopOS 配置读取方言;其他系列需依据其 CLI 手册接入,不能套用 Cisco 的命令。
|
|
12
12
|
|
|
@@ -36,7 +36,7 @@ TFTP 导出包括默认值,不接受 `source_file:` 或 `vrf:`。成功必须
|
|
|
36
36
|
|
|
37
37
|
LLDP 查询先读取 `get system status`,明确解析版本后选择命令:7.6.2 及以前使用 `diagnose lldprx neighbor`,7.6.3 起使用 `diagnose lldp rx neighbor`。无法识别版本时直接报解析错误,不猜测命令。接口行数量与解析记录必须一致,空响应不会被当作零邻居。
|
|
38
38
|
|
|
39
|
-
Junos LLDP 使用 `show lldp neighbors detail | no-more`,逐个本地接口块核对记录。残缺记录及只有标题的输出会报错;接口描述从已提交的 set
|
|
39
|
+
Junos LLDP 使用 `show lldp neighbors detail | no-more`,逐个本地接口块核对记录。残缺记录及只有标题的输出会报错;接口描述从已提交的 set 配置中读取,逻辑单元用 `接口.单元号` 独立返回,保留 `ge-`、`xe-`、`ae` 等原接口身份。
|
|
40
40
|
|
|
41
41
|
## 命令依据
|
|
42
42
|
|
|
@@ -48,3 +48,14 @@ Junos LLDP 使用 `show lldp neighbors detail | no-more`,逐个本地接口块
|
|
|
48
48
|
- [Juniper save 配置模式语义](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/statement/save-cli-config.html):配置模式的层级导出与远端 SCP。
|
|
49
49
|
- [Juniper file copy 协议](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/command/file-copy.html):支持的远端文件传输 URL。
|
|
50
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 描述的独立配置层级。
|
|
@@ -74,7 +74,7 @@ module Net
|
|
|
74
74
|
end
|
|
75
75
|
@topology_strategy = strategy_class(topology_strategy, :topology_strategy,
|
|
76
76
|
%i[neighbor_command neighbor_template expected_neighbor_count empty_neighbor_output?
|
|
77
|
-
protocol description_template interface_key configuration_interface
|
|
77
|
+
protocol description_template description_key interface_key configuration_interface
|
|
78
78
|
decode_description validate_descriptions! change_error_code supports?
|
|
79
79
|
enter_configuration_command change_commands script_command
|
|
80
80
|
leave_configuration_commands verification_commands persistence_commands persistence_confirmed?])
|
|
@@ -16,6 +16,10 @@ module Net
|
|
|
16
16
|
super(server: server.dup.freeze, path: path.dup.freeze, protocol: protocol, username: username.dup.freeze,
|
|
17
17
|
completed_at: completed_at.dup.freeze, configuration_kind: configuration_kind, verification: verification)
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
# Ruby 3.2 的原生更新跳过构造校验;更新也必须建立经过校验的快照。
|
|
21
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
22
|
+
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
# 完成后出现收尾错误时保留上传事实,避免调用方盲目重传。
|
|
@@ -23,6 +23,9 @@ module Net
|
|
|
23
23
|
super(host: host.dup.freeze, path: path.dup.freeze, username: username.dup.freeze, protocol: protocol)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Ruby 3.2 的原生更新跳过构造校验;更新也必须建立经过校验的快照。
|
|
27
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
28
|
+
|
|
26
29
|
def url
|
|
27
30
|
"#{protocol}://#{username}@#{host}/#{path}"
|
|
28
31
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "neighbor"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
module Connector
|
|
7
|
+
class Topology
|
|
8
|
+
# 变更的描述和接口归本对象所有;邻居值对象可安全共享。
|
|
9
|
+
Change = Data.define(:interface, :old_description, :new_description, :neighbor) do
|
|
10
|
+
def initialize(interface:, old_description:, new_description:, neighbor:)
|
|
11
|
+
fields = { interface: interface, old_description: old_description, new_description: new_description }
|
|
12
|
+
raise ArgumentError, "description change fields must be Strings" unless fields.values.all?(String)
|
|
13
|
+
raise ArgumentError, "description change requires a Neighbor" unless neighbor.instance_of?(Neighbor)
|
|
14
|
+
|
|
15
|
+
super(**fields.transform_values { |value| value.frozen? ? value : value.dup.freeze }, neighbor: neighbor)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "strategy"
|
|
4
|
+
require_relative "interface_blocks"
|
|
4
5
|
require_relative "../../engine/terminal/renderer"
|
|
5
6
|
|
|
6
7
|
module Net
|
|
@@ -8,18 +9,11 @@ module Net
|
|
|
8
9
|
class Topology
|
|
9
10
|
# 接口描述立即生效的设备:退出视图、读回、保存是不同阶段,不能在 finish 中提前保存。
|
|
10
11
|
class ImmediateStrategy < Strategy
|
|
12
|
+
include InterfaceBlocks
|
|
13
|
+
|
|
11
14
|
def verification_commands = @device.config_commands
|
|
12
15
|
def persistence_commands = @device.profile.save_commands
|
|
13
16
|
|
|
14
|
-
# TextFSM 不能静默跳过未知接口头;否则目标行正确也不足以证明读回证据完整。
|
|
15
|
-
def validate_descriptions!(config, rows)
|
|
16
|
-
expected = config.scan(/^interface[ \t]+([^\r\n]*)/).flatten.map(&:strip)
|
|
17
|
-
return if expected == rows.map { |row| row.fetch("INTERFACE").strip }
|
|
18
|
-
|
|
19
|
-
raise ParsingError.new("interface description output was not fully recognized", code: :unrecognized_output,
|
|
20
|
-
host: @device.host, phase: :verify)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
17
|
# 完成提示必须来自保存步骤;原文和渲染文本中的失败优先,单独提示符/进度不算确认。
|
|
24
18
|
def persistence_confirmed?(result)
|
|
25
19
|
!result.steps.empty? && result.steps.all? do |step|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
class Topology
|
|
6
|
+
# interface 块方言共用完整性校验;解析器不能静默遗漏未知接口头。
|
|
7
|
+
module InterfaceBlocks
|
|
8
|
+
def validate_descriptions!(config, rows)
|
|
9
|
+
expected = config.scan(/^interface[ \t]+([^\r\n]*)/).flatten.map(&:strip)
|
|
10
|
+
return if expected == rows.map { |row| row.fetch("INTERFACE").strip }
|
|
11
|
+
|
|
12
|
+
raise ParsingError.new("interface description output was not fully recognized", code: :unrecognized_output,
|
|
13
|
+
host: @device.host, phase: :verify)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
class Topology
|
|
6
|
+
# 邻居身份拥有独立字符串,格式化回调不能改写发现证据。
|
|
7
|
+
Neighbor = Data.define(:local_interface, :neighbor_name, :neighbor_interface, :chassis_id, :protocol) do
|
|
8
|
+
def initialize(local_interface:, neighbor_name:, neighbor_interface:, chassis_id:, protocol:)
|
|
9
|
+
fields = { local_interface: local_interface, neighbor_name: neighbor_name,
|
|
10
|
+
neighbor_interface: neighbor_interface, chassis_id: chassis_id }
|
|
11
|
+
raise ArgumentError, "neighbor fields must be Strings" unless fields.values.all?(String)
|
|
12
|
+
raise ArgumentError, "neighbor protocol must be a Symbol" unless protocol.is_a?(Symbol)
|
|
13
|
+
|
|
14
|
+
super(**fields.transform_values { |value| value.frozen? ? value : value.dup.freeze }, protocol: protocol)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Ruby 3.2 的 Data#with 不调用初始化,更新也必须重新建立快照。
|
|
18
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "change"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
module Connector
|
|
7
|
+
class Topology
|
|
8
|
+
# 审批树在领域边界固定;单设备与批量操作共享同一不可变计划。
|
|
9
|
+
Plan = Data.define(:host, :vendor, :evidence, :changes, :commands) do
|
|
10
|
+
def initialize(host:, vendor:, evidence:, changes:, commands:)
|
|
11
|
+
unless host.is_a?(String) && vendor.is_a?(Symbol) && evidence.is_a?(Hash) &&
|
|
12
|
+
changes.is_a?(Array) && changes.all? { |change| change.instance_of?(Change) } &&
|
|
13
|
+
commands.is_a?(Array) && commands.all?(String)
|
|
14
|
+
raise ArgumentError, "description plan contains invalid fields"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
super(host: host.frozen? ? host : host.dup.freeze, vendor: vendor,
|
|
18
|
+
evidence: copy_evidence(evidence), changes: changes.dup.freeze,
|
|
19
|
+
commands: commands.map { |command| command.frozen? ? command : command.dup.freeze }.freeze)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def copy_evidence(evidence)
|
|
27
|
+
evidence.to_h do |interface, fields|
|
|
28
|
+
unless interface.is_a?(String) && fields.is_a?(Array) && fields.size == 3 && fields.all?(String)
|
|
29
|
+
raise ArgumentError, "description plan contains invalid evidence"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
[interface.frozen? ? interface : interface.dup.freeze,
|
|
33
|
+
fields.map { |field| field.frozen? ? field : field.dup.freeze }.freeze]
|
|
34
|
+
end.freeze
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require_relative "../../engine/errors"
|
|
5
|
+
|
|
6
|
+
module Net
|
|
7
|
+
module Connector
|
|
8
|
+
class Topology
|
|
9
|
+
# 配置字段的引号解码是纯文本边界,不执行 shell,也不把原文写入错误。
|
|
10
|
+
module QuotedText
|
|
11
|
+
def self.decode(value, host:, split_unquoted: false)
|
|
12
|
+
value = value.strip
|
|
13
|
+
return value unless split_unquoted || value.start_with?('"')
|
|
14
|
+
|
|
15
|
+
Shellwords.split(value).join(" ")
|
|
16
|
+
rescue ArgumentError
|
|
17
|
+
raise ParsingError.new("interface description is not a complete single-line value",
|
|
18
|
+
code: :unrecognized_output, host: host, phase: :parse), cause: nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "shellwords"
|
|
4
4
|
require_relative "interface_description"
|
|
5
|
+
require_relative "quoted_text"
|
|
5
6
|
|
|
6
7
|
module Net
|
|
7
8
|
module Connector
|
|
@@ -56,6 +57,9 @@ module Net
|
|
|
56
57
|
# 在下发前允许厂商调整命令对象及超时。
|
|
57
58
|
def script_command(command) = command
|
|
58
59
|
|
|
60
|
+
# 描述记录默认表示物理接口;厂商可保留逻辑单元的独立身份。
|
|
61
|
+
def description_key(row) = interface_key(row.fetch("INTERFACE"))
|
|
62
|
+
|
|
59
63
|
# 将发现接口名归一为查找当前配置所用的键。
|
|
60
64
|
def interface_key(name) = InterfaceName.key(name)
|
|
61
65
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "topology/strategy"
|
|
4
4
|
require_relative "topology/stages"
|
|
5
|
+
require_relative "topology/plan"
|
|
5
6
|
|
|
6
7
|
module Net
|
|
7
8
|
module Connector
|
|
@@ -31,10 +32,6 @@ module Net
|
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
Neighbor = Data.define(:local_interface, :neighbor_name, :neighbor_interface, :chassis_id, :protocol)
|
|
35
|
-
Change = Data.define(:interface, :old_description, :new_description, :neighbor)
|
|
36
|
-
Plan = Data.define(:host, :vendor, :evidence, :changes, :commands)
|
|
37
|
-
|
|
38
35
|
# 仅承载厂商档案的离线上下文,不能连接、查询或下发设备命令。
|
|
39
36
|
PlanningContext = Data.define(:host, :vendor, :profile) do
|
|
40
37
|
def config_commands = profile.config_commands
|
|
@@ -129,7 +126,7 @@ module Net
|
|
|
129
126
|
rows = @parser.call(config, template: template, host: @device.host)
|
|
130
127
|
@strategy.validate_descriptions!(config, rows)
|
|
131
128
|
rows.each_with_object({}) do |row, found|
|
|
132
|
-
interface = @strategy.
|
|
129
|
+
interface = @strategy.description_key(row)
|
|
133
130
|
description = @strategy.decode_description(row.fetch("DESCRIPTION", ""))
|
|
134
131
|
previous = found[interface]
|
|
135
132
|
if previous && !previous.empty? && !description.empty? && previous != description
|
|
@@ -159,7 +156,6 @@ module Net
|
|
|
159
156
|
validate_change_support!(:plan) if @strategy.supports?(:neighbors)
|
|
160
157
|
discovered = neighbors
|
|
161
158
|
current = interface_descriptions
|
|
162
|
-
current.each_value(&:freeze)
|
|
163
159
|
evidence = evidence_for(discovered, current)
|
|
164
160
|
changes = discovered.filter_map do |neighbor|
|
|
165
161
|
old = current.fetch(@strategy.interface_key(neighbor.local_interface))
|
|
@@ -171,17 +167,16 @@ module Net
|
|
|
171
167
|
InterfaceDescription.validate!(proposed)
|
|
172
168
|
next if old == proposed
|
|
173
169
|
|
|
174
|
-
Change.new(interface: @strategy.configuration_interface(neighbor.local_interface)
|
|
175
|
-
new_description: proposed
|
|
170
|
+
Change.new(interface: @strategy.configuration_interface(neighbor.local_interface), old_description: old,
|
|
171
|
+
new_description: proposed, neighbor: neighbor)
|
|
176
172
|
end.freeze
|
|
177
173
|
commands = if changes.empty?
|
|
178
174
|
[]
|
|
179
175
|
else
|
|
180
176
|
stage_scripts(changes).values.flat_map { |script| script.map(&:text) }
|
|
181
177
|
end
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
changes: changes, commands: commands.freeze).freeze
|
|
178
|
+
Plan.new(host: @device.host, vendor: @device.vendor, evidence: evidence,
|
|
179
|
+
changes: changes, commands: commands)
|
|
185
180
|
end
|
|
186
181
|
|
|
187
182
|
# 只有明确确认且邻居和旧描述未变化时,才下发计划中的命令。
|
|
@@ -15,9 +15,10 @@ module Net
|
|
|
15
15
|
unless duration_ms.nil? || (duration_ms.is_a?(Numeric) && duration_ms.real? && duration_ms.finite? && duration_ms >= 0)
|
|
16
16
|
raise ArgumentError, "duration_ms must be nonnegative and finite"
|
|
17
17
|
end
|
|
18
|
+
raise ArgumentError, "status must be a Symbol or String" unless status.is_a?(Symbol) || status.is_a?(String)
|
|
18
19
|
raise ArgumentError, "diagnostic must be a Diagnostic" unless diagnostic.nil? || diagnostic.instance_of?(Diagnostic)
|
|
19
20
|
|
|
20
|
-
super(device: device, status: status, backup: backup, error_code: error_code,
|
|
21
|
+
super(device: device, status: status.is_a?(String) ? status.dup.freeze : status, backup: backup, error_code: error_code,
|
|
21
22
|
error_type: error_type.is_a?(String) ? error_type.dup.freeze : error_type,
|
|
22
23
|
started_at: started_at&.dup&.freeze, finished_at: finished_at&.dup&.freeze,
|
|
23
24
|
duration_ms: duration_ms, diagnostic: diagnostic)
|
|
@@ -46,17 +47,45 @@ module Net
|
|
|
46
47
|
# 汇总一次批量任务的设备结果、回调故障和报告写入状态。
|
|
47
48
|
Batch = Data.define(:mode, :outcomes, :started_at, :finished_at, :callback_errors,
|
|
48
49
|
:report_location, :report_error) do
|
|
49
|
-
#
|
|
50
|
-
def
|
|
50
|
+
# 批次拥有容器与审计元数据;调用方只能通过 with 构造新快照。
|
|
51
|
+
def initialize(mode:, outcomes:, started_at:, finished_at:, callback_errors:, report_location:, report_error:)
|
|
52
|
+
unless outcomes.is_a?(Array) && outcomes.all? { |outcome| outcome.instance_of?(Outcome) } && callback_errors.is_a?(Array)
|
|
53
|
+
raise ArgumentError, "batch requires outcome and callback error Arrays"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
outcomes = outcomes.dup.freeze
|
|
57
|
+
@counts = outcomes.each_with_object({}) { |outcome, found| found[outcome.status] = found.fetch(outcome.status, 0) + 1 }.freeze
|
|
58
|
+
super(mode: mode, outcomes: outcomes,
|
|
59
|
+
started_at: started_at&.dup&.freeze, finished_at: finished_at&.dup&.freeze,
|
|
60
|
+
callback_errors: callback_errors.map { |entry| copy_callback_error(entry) }.freeze,
|
|
61
|
+
report_location: report_location&.dup&.freeze, report_error: report_error&.dup&.freeze)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
65
|
+
|
|
66
|
+
# 只累计最终计数,不创建与清单等长的中间状态数组。
|
|
67
|
+
def counts = @counts.dup
|
|
51
68
|
|
|
52
69
|
# 仅在所有设备成功且回调、报告均正常时判定整批成功。
|
|
53
|
-
def success? = !outcomes.empty? &&
|
|
70
|
+
def success? = !outcomes.empty? && @counts.fetch(:backed_up, 0) + @counts.fetch(:reported_uploaded, 0) == outcomes.size &&
|
|
71
|
+
callback_errors.empty? && report_error.nil?
|
|
54
72
|
|
|
55
73
|
# 空清单与已尝试但未全部成功的批次分别标记。
|
|
56
74
|
def status = outcomes.empty? ? :no_devices : (success? ? :succeeded : :incomplete)
|
|
57
75
|
|
|
58
76
|
# 显式从原始执行结果构造报告。
|
|
59
77
|
def build_report(policy: :strict) = Report.new(self, policy: policy)
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def copy_callback_error(entry)
|
|
82
|
+
unless entry.is_a?(Hash) && (entry[:host].nil? || entry[:host].is_a?(String))
|
|
83
|
+
raise ArgumentError, "callback errors must contain a host and error type"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
{ host: entry[:host]&.dup&.freeze, error_type: Error::Metadata.type(entry[:error_type]) }.freeze
|
|
87
|
+
end
|
|
88
|
+
|
|
60
89
|
end
|
|
61
90
|
|
|
62
91
|
autoload :Report, File.expand_path("report", __dir__)
|
|
@@ -30,7 +30,6 @@ module Net
|
|
|
30
30
|
raise ArgumentError, "operation duration must be nonnegative and finite"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
value = immutable_plan_value(value) if value.is_a?(Topology::Plan)
|
|
34
33
|
super(device: device, status: status, value: value, error: error,
|
|
35
34
|
started_at: started_at.dup.freeze, finished_at: finished_at.dup.freeze, duration_ms: duration_ms)
|
|
36
35
|
end
|
|
@@ -69,19 +68,6 @@ module Net
|
|
|
69
68
|
end
|
|
70
69
|
raise ArgumentError, "operation state contradicts its domain value or error" unless valid && same_error
|
|
71
70
|
end
|
|
72
|
-
|
|
73
|
-
# 外部构造或 with 更新也复制审批树,不仅冻结 Data 的最外层。
|
|
74
|
-
def immutable_plan_value(value)
|
|
75
|
-
case value
|
|
76
|
-
when String then value.frozen? ? value : value.dup.freeze
|
|
77
|
-
when Array then value.map { |item| immutable_plan_value(item) }.freeze
|
|
78
|
-
when Hash then value.to_h { |key, item| [immutable_plan_value(key), immutable_plan_value(item)] }.freeze
|
|
79
|
-
when Topology::Plan, Topology::Change, Topology::Neighbor
|
|
80
|
-
fields = value.to_h.transform_values { |item| immutable_plan_value(item) }
|
|
81
|
-
value.class.new(**fields).freeze
|
|
82
|
-
else value
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
71
|
end
|
|
86
72
|
|
|
87
73
|
end
|
|
@@ -65,14 +65,14 @@ module Net
|
|
|
65
65
|
result = connector.plan_interface_descriptions(abbreviate: abbreviate, lowercase: lowercase, &formatter)
|
|
66
66
|
raise TypeError, "description planning must return a Topology::Plan" unless result.is_a?(Topology::Plan)
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
normalize_plan(result)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
#
|
|
72
|
+
# 审批计划固定并验证;单设备应用仍执行现场重验、读回和保存,不直接重放 commands。
|
|
73
73
|
def apply_interface_descriptions(plans:, confirmed: false, concurrency: DEFAULT_SETTING, on_start: nil, on_result: nil)
|
|
74
74
|
validate_confirmation!(confirmed)
|
|
75
|
-
tasks = normalize_mapping(plans, :plans) { |plan|
|
|
75
|
+
tasks = normalize_mapping(plans, :plans) { |plan| normalize_plan(plan) }
|
|
76
76
|
tasks.each do |host, plan|
|
|
77
77
|
raise ArgumentError, "plan host does not match its target" unless normalize_host(plan.host) == host
|
|
78
78
|
end
|
|
@@ -125,38 +125,20 @@ module Net
|
|
|
125
125
|
end.freeze
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
-
#
|
|
129
|
-
def
|
|
130
|
-
raise ArgumentError, "plans must contain Topology::Plan values" unless plan.
|
|
131
|
-
normalize_host(plan.host)
|
|
132
|
-
unless Net::Connector.vendors.include?(plan.vendor)
|
|
133
|
-
plan.changes.is_a?(Array) && plan.commands.is_a?(Array) && plan.commands.all?(String)
|
|
134
|
-
raise ArgumentError, "description plan contains invalid fields"
|
|
135
|
-
end
|
|
136
|
-
commands = Script.new(plan.commands).map { |command| command.text.dup.freeze }.freeze
|
|
137
|
-
evidence = plan.evidence.to_h do |interface, fields|
|
|
138
|
-
unless interface.is_a?(String) && fields.is_a?(Array) && fields.size == 3 && fields.all?(String)
|
|
139
|
-
raise ArgumentError, "description plan contains invalid evidence"
|
|
140
|
-
end
|
|
141
|
-
[interface.dup.freeze, fields.map { |field| field.dup.freeze }.freeze]
|
|
142
|
-
end.freeze
|
|
143
|
-
changes = plan.changes.map { |change| copy_change(change) }.freeze
|
|
144
|
-
raise ArgumentError, "empty description plan cannot contain commands" if changes.empty? && !commands.empty?
|
|
128
|
+
# 计划自身拥有不可变审批树;这里仅处理批次的地址和输入预检。
|
|
129
|
+
def normalize_plan(plan)
|
|
130
|
+
raise ArgumentError, "plans must contain Topology::Plan values" unless plan.instance_of?(Topology::Plan)
|
|
131
|
+
host = normalize_host(plan.host)
|
|
132
|
+
raise ArgumentError, "description plan contains an unsupported vendor" unless Net::Connector.vendors.include?(plan.vendor)
|
|
145
133
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def copy_change(change)
|
|
151
|
-
unless change.is_a?(Topology::Change) && change.old_description.is_a?(String) &&
|
|
152
|
-
change.neighbor.is_a?(Topology::Neighbor) && change.neighbor.to_h.values.all? { |value| value.is_a?(String) || value.is_a?(Symbol) }
|
|
153
|
-
raise ArgumentError, "description plan contains an invalid change"
|
|
134
|
+
Script.new(plan.commands)
|
|
135
|
+
plan.changes.each do |change|
|
|
136
|
+
Topology::InterfaceDescription.validate_interface!(change.interface)
|
|
137
|
+
Topology::InterfaceDescription.validate!(change.new_description)
|
|
154
138
|
end
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Topology::Change.new(interface: change.interface.dup.freeze, old_description: change.old_description.dup.freeze,
|
|
159
|
-
new_description: change.new_description.dup.freeze, neighbor: neighbor).freeze
|
|
139
|
+
raise ArgumentError, "empty description plan cannot contain commands" if plan.changes.empty? && !plan.commands.empty?
|
|
140
|
+
|
|
141
|
+
host == plan.host ? plan : plan.with(host: host)
|
|
160
142
|
end
|
|
161
143
|
|
|
162
144
|
# 策略只固定一次;清单与连接使用同一快照,凭据仍在每台任务开始时动态读取。
|
|
@@ -46,8 +46,9 @@ module Net
|
|
|
46
46
|
|
|
47
47
|
return false if policy == :verified && outcomes.any? { |outcome| outcome.success? && !server_verified?(outcome) }
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
statuses = counts
|
|
50
|
+
SUCCESS_STATUSES.any? { |status| statuses.key?(status) } &&
|
|
51
|
+
statuses.keys.all? { |status| SUCCESS_STATUSES.include?(status) || IGNORABLE_STATUSES.include?(status) }
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def with(**attributes)
|
|
@@ -61,14 +62,14 @@ module Net
|
|
|
61
62
|
policy: policy, duration_ms: duration_ms, report_diagnostic: diagnostic, log_directory: log_directory)
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
#
|
|
65
|
+
# 统计不创建逐设备明细;文本与进度输出复用此入口,复用批次拥有的状态计数。
|
|
65
66
|
def statistics
|
|
66
67
|
batch_data = batch_summary
|
|
68
|
+
attempted = ATTEMPTED_STATUSES.sum { |status| batch_data[:counts].fetch(status, 0) }
|
|
67
69
|
batch_data.merge(
|
|
68
70
|
schema_version: 2, policy: policy, policy_success: policy_success?, duration_ms: duration_ms,
|
|
69
|
-
coverage: { complete: !outcomes.empty? &&
|
|
70
|
-
attempted: outcomes.
|
|
71
|
-
skipped: outcomes.count { |outcome| !ATTEMPTED_STATUSES.include?(outcome.status) } },
|
|
71
|
+
coverage: { complete: !outcomes.empty? && attempted == outcomes.size,
|
|
72
|
+
attempted: attempted, skipped: outcomes.size - attempted },
|
|
72
73
|
callback_errors: callback_errors.map { |entry| { host: entry[:host], error_type: Error::Metadata.type(entry[:error_type]) } },
|
|
73
74
|
report_location: report_location, report_error: Error::Metadata.type(report_error),
|
|
74
75
|
verification: mode == :tftp ? { verified: outcomes.count { |outcome| server_verified?(outcome) },
|
|
@@ -86,14 +87,16 @@ module Net
|
|
|
86
87
|
|
|
87
88
|
# 生成可写入报告及供命令行展示的结构化摘要。
|
|
88
89
|
def batch_summary
|
|
90
|
+
statuses = counts
|
|
91
|
+
succeeded = SUCCESS_STATUSES.sum { |status| statuses.fetch(status, 0) }
|
|
92
|
+
partial = statuses.fetch(:saved_with_error, 0) + statuses.fetch(:reported_with_error, 0)
|
|
93
|
+
failed = statuses.fetch(:failed, 0)
|
|
89
94
|
{
|
|
90
95
|
mode: mode, status: status, started_at: started_at.iso8601, finished_at: finished_at.iso8601,
|
|
91
96
|
total: outcomes.size,
|
|
92
|
-
succeeded:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
skipped: outcomes.count(&:skipped?),
|
|
96
|
-
counts: counts,
|
|
97
|
+
succeeded: succeeded, partial: partial, failed: failed,
|
|
98
|
+
skipped: outcomes.size - succeeded - partial - failed,
|
|
99
|
+
counts: statuses,
|
|
97
100
|
callback_errors: callback_errors
|
|
98
101
|
}
|
|
99
102
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Value Required INTERFACE ((?:[^"\\\s]|\\.)+)
|
|
2
|
+
Value DESCRIPTION (.*)
|
|
3
|
+
|
|
4
|
+
Start
|
|
5
|
+
^config system interface\s*$$ -> Interfaces
|
|
6
|
+
|
|
7
|
+
Interfaces
|
|
8
|
+
^ edit "${INTERFACE}"\s*$$ -> Interface
|
|
9
|
+
^ edit ${INTERFACE}\s*$$ -> Interface
|
|
10
|
+
^end\s*$$ -> Start
|
|
11
|
+
|
|
12
|
+
Interface
|
|
13
|
+
^ set description\s*$$ -> Error "interface description has no value"
|
|
14
|
+
^ set description ${DESCRIPTION}$$
|
|
15
|
+
^ config .+$$ -> Nested
|
|
16
|
+
^ next\s*$$ -> Record Interfaces
|
|
17
|
+
|
|
18
|
+
Nested
|
|
19
|
+
^ end\s*$$ -> Interface
|
|
20
|
+
|
|
21
|
+
EOF
|
|
@@ -4,5 +4,11 @@ Value Required CHASSIS_ID (\S+)
|
|
|
4
4
|
Value Required NEIGHBOR_INTERFACE (Port\s+\d+|\S+)
|
|
5
5
|
|
|
6
6
|
Start
|
|
7
|
-
^\s*
|
|
7
|
+
^\s*System Name\s+Local Interface\b.*$$ -> NameFirst
|
|
8
|
+
^\s*(?:Local Interface|LocalIf)\s+.*$$ -> LocalFirst
|
|
9
|
+
|
|
10
|
+
NameFirst
|
|
11
|
+
^\s*${NEIGHBOR_NAME}\s+${LOCAL_INTERFACE}\s+${CHASSIS_ID}\s+${NEIGHBOR_INTERFACE}\s*$$ -> Record
|
|
12
|
+
|
|
13
|
+
LocalFirst
|
|
8
14
|
^\s*${LOCAL_INTERFACE}\s+${CHASSIS_ID}\s+${NEIGHBOR_INTERFACE}\s+${NEIGHBOR_NAME}\s*$$ -> Record
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Value Required INTERFACE (\S+(?:[ \t]+\d[\w/.:_-]*)?)
|
|
2
|
+
Value DESCRIPTION (.*)
|
|
3
|
+
|
|
4
|
+
Start
|
|
5
|
+
^interface ${INTERFACE}$$ -> Interface
|
|
6
|
+
|
|
7
|
+
Interface
|
|
8
|
+
^\s+description\s*$$ -> Error "interface description has no value"
|
|
9
|
+
^\s+description ${DESCRIPTION}$$
|
|
10
|
+
^#$$ -> Record Start
|
|
11
|
+
^interface .+ -> Continue.Record
|
|
12
|
+
^interface ${INTERFACE}$$
|
|
13
|
+
^return$$ -> Record End
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Value Required LOCAL_INTERFACE ([A-Za-z][A-Za-z0-9-]*[ \t]*\d[\w/.:-]*)
|
|
2
|
+
Value Required NEIGHBOR_NAME (\S+)
|
|
3
|
+
Value Required NEIGHBOR_INTERFACE ([A-Za-z][A-Za-z0-9-]*[ \t]*\d[\w/.:-]*|\S+)
|
|
4
|
+
|
|
5
|
+
Start
|
|
6
|
+
^\s*Local Intf\s+Neighbor Dev\s+Neighbor Intf\s+Exptime.*$$ -> Neighbors
|
|
7
|
+
|
|
8
|
+
Neighbors
|
|
9
|
+
^\s*${LOCAL_INTERFACE}\s+${NEIGHBOR_NAME}\s+${NEIGHBOR_INTERFACE}\s+\d+\s*$$ -> Record
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
Template, Vendor, Command
|
|
2
|
-
cisco_ios/show_ip_interface_brief.textfsm,
|
|
3
|
-
cisco/show_cdp_neighbors_detail.textfsm,
|
|
4
|
-
cisco_nxos/show_cdp_neighbors_detail.textfsm,
|
|
5
|
-
h3c/
|
|
6
|
-
h3c/
|
|
7
|
-
hillstone/show_lldp_neighbor_information.textfsm,
|
|
8
|
-
palo_alto/show_lldp_neighbors_all.textfsm,
|
|
9
|
-
fortinet/diagnose_lldp_rx_neighbor.textfsm,
|
|
10
|
-
fortinet/diagnose_lldp_rx_neighbor.textfsm,
|
|
11
|
-
juniper/show_lldp_neighbors_detail.textfsm,
|
|
12
|
-
cisco_nxos/show_ip_interface_brief.textfsm,
|
|
13
|
-
cisco_nxos/show_running_config.textfsm,
|
|
2
|
+
cisco_ios/show_ip_interface_brief.textfsm, \Acisco_ios\z, \Ashow ip interface brief\z
|
|
3
|
+
cisco/show_cdp_neighbors_detail.textfsm, \Acisco_ios\z, \Ashow cdp neighbors detail\z
|
|
4
|
+
cisco_nxos/show_cdp_neighbors_detail.textfsm, \Acisco_nxos\z, \Ashow cdp neighbors detail\z
|
|
5
|
+
h3c/display_lldp_neighbor_information_list.textfsm, \Ah3c\z, \Adisplay lldp neighbor-information list\z
|
|
6
|
+
h3c/display_lldp_neighbor_information_list.textfsm, \Ah3c_wireless\z, \Adisplay lldp neighbor-information list\z
|
|
7
|
+
hillstone/show_lldp_neighbor_information.textfsm, \Ahillstone\z, \Ashow lldp neighbor-information\z
|
|
8
|
+
palo_alto/show_lldp_neighbors_all.textfsm, \Apalo_alto\z, \Ashow lldp neighbors all\z
|
|
9
|
+
fortinet/diagnose_lldp_rx_neighbor.textfsm, \Afortinet\z, \Adiagnose lldp rx neighbor\z
|
|
10
|
+
fortinet/diagnose_lldp_rx_neighbor.textfsm, \Afortinet\z, \Adiagnose lldprx neighbor\z
|
|
11
|
+
juniper/show_lldp_neighbors_detail.textfsm, \Ajuniper\z, \Ashow lldp neighbors detail(?: \| no-more)?\z
|
|
12
|
+
cisco_nxos/show_ip_interface_brief.textfsm, \Acisco_nxos\z, \Ashow ip interface brief(?: vrf [A-Za-z0-9_.-]+)?\z
|
|
13
|
+
cisco_nxos/show_running_config.textfsm, \Acisco_nxos\z, \Ashow running-config\z
|
|
14
|
+
cisco/show_running_config.textfsm, \Acisco_ios\z, \Ashow running-config\z
|
|
15
|
+
h3c/display_current_configuration.textfsm, \Ah3c\z, \A(?:display current-configuration|dis cur)\z
|
|
16
|
+
h3c/display_current_configuration.textfsm, \Ah3c_wireless\z, \A(?:display current-configuration|dis cur)\z
|
|
17
|
+
huawei/display_current_configuration.textfsm, \Ahuawei\z, \A(?:display current-configuration|dis cur)\z
|
|
18
|
+
huawei/display_lldp_neighbor_brief.textfsm, \Ahuawei\z, \Adisplay lldp neighbor brief\z
|
|
19
|
+
hillstone/show_configuration_running.textfsm, \Ahillstone\z, \Ashow configuration running\z
|
|
20
|
+
radware/cfg_dump.textfsm, \Aradware\z, \A/cfg/dump\z
|
|
21
|
+
palo_alto/show.textfsm, \Apalo_alto\z, \Ashow\z
|
|
22
|
+
fortinet/show_full_configuration.textfsm, \Afortinet\z, \Ashow full-configuration\z
|
|
23
|
+
juniper/show_configuration_display_set.textfsm, \Ajuniper\z, \Ashow configuration \| display set(?: \| no-more)?\z
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
Value Required INTERFACE (\S+)
|
|
2
|
+
Value UNIT (\d+)
|
|
2
3
|
Value DESCRIPTION (.*)
|
|
3
4
|
|
|
4
5
|
Start
|
|
6
|
+
^set interfaces \S+ (?:unit \d+ )?description\s*$$ -> Error "interface description has no value"
|
|
7
|
+
^set interfaces ${INTERFACE} unit ${UNIT} description ${DESCRIPTION}$$ -> Record
|
|
8
|
+
^set interfaces ${INTERFACE} unit ${UNIT}\s+.*$$ -> Continue.Record
|
|
5
9
|
^set interfaces ${INTERFACE} description ${DESCRIPTION}$$ -> Record
|
|
6
10
|
^set interfaces ${INTERFACE}\s+.*$$ -> Record
|
|
@@ -2,5 +2,6 @@ Value Required INTERFACE (\S+)
|
|
|
2
2
|
Value DESCRIPTION (.*)
|
|
3
3
|
|
|
4
4
|
Start
|
|
5
|
+
^set network interface ethernet \S+ comment\s*$$ -> Error "interface comment has no value"
|
|
5
6
|
^set network interface ethernet ${INTERFACE} comment ${DESCRIPTION}$$ -> Record
|
|
6
7
|
^set network interface ethernet ${INTERFACE}\s+.*$$ -> Record
|
|
@@ -2,12 +2,14 @@ Value Required INTERFACE (\d+)
|
|
|
2
2
|
Value DESCRIPTION (.*)
|
|
3
3
|
|
|
4
4
|
Start
|
|
5
|
-
^/(?:c|cfg)/port
|
|
5
|
+
^/(?:c|cfg)/port \d+/name\s*$$ -> Error "port name has no value"
|
|
6
|
+
^/(?:c|cfg)/port ${INTERFACE}/name ${DESCRIPTION}$$ -> Record
|
|
6
7
|
^/(?:c|cfg)/port ${INTERFACE}\s*$$ -> Port
|
|
7
8
|
|
|
8
9
|
Port
|
|
9
|
-
|
|
10
|
+
^/(?:c|cfg)/port \d+/name\s*$$ -> Error "port name has no value"
|
|
11
|
+
^\s+name\s*$$ -> Error "port name has no value"
|
|
10
12
|
^\s+name ${DESCRIPTION}$$
|
|
11
13
|
^/.* -> Continue.Record
|
|
12
|
-
^/(?:c|cfg)/port ${INTERFACE}/name
|
|
14
|
+
^/(?:c|cfg)/port ${INTERFACE}/name ${DESCRIPTION}$$ -> Record Start
|
|
13
15
|
^/(?:c|cfg)/port ${INTERFACE}\s*$$
|
|
@@ -9,7 +9,7 @@ module Net
|
|
|
9
9
|
class Fortinet < Net::Connector::Base
|
|
10
10
|
# LLDP 命令自 7.6.3 改名;先核对只读版本信息,不通过失败重试猜测方言。
|
|
11
11
|
class Topology < Net::Connector::Topology::Strategy
|
|
12
|
-
def self.supports?(capability) = capability
|
|
12
|
+
def self.supports?(capability) = %i[neighbors interface_descriptions].include?(capability)
|
|
13
13
|
|
|
14
14
|
def neighbor_command
|
|
15
15
|
status = @device.execute_command("get system status").value!
|
|
@@ -22,6 +22,29 @@ module Net
|
|
|
22
22
|
(release <=> [7, 6, 3]) >= 0 ? "diagnose lldp rx neighbor" : "diagnose lldprx neighbor"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def description_template = "fortinet/show_full_configuration.textfsm"
|
|
26
|
+
|
|
27
|
+
# 接口名称大小写与设备配置一致,不套用 Cisco 接口简称。
|
|
28
|
+
def interface_key(name) = decode_description(name)
|
|
29
|
+
|
|
30
|
+
def configuration_interface(name) = name
|
|
31
|
+
|
|
32
|
+
def decode_description(value) = Net::Connector::Topology::QuotedText.decode(value, host: @device.host, split_unquoted: true)
|
|
33
|
+
|
|
34
|
+
# 模板只接受当前视图的顶层接口表,嵌套 VDOM 或残缺配置不能冒充完整结果。
|
|
35
|
+
def validate_descriptions!(config, rows)
|
|
36
|
+
tables = config.scan(/^config system interface[ \t]*$/).size
|
|
37
|
+
nested = config.match?(/^[ \t]+config system interface[ \t]*$/)
|
|
38
|
+
table = config[/^config system interface[ \t]*\n(.*?)^end[ \t]*$/m, 1]
|
|
39
|
+
expected = table&.scan(/^ edit[ \t]+([^\r\n]+)/)&.flatten
|
|
40
|
+
names = expected&.map { |name| decode_description(name) }
|
|
41
|
+
return if !nested && tables == 1 && names == rows.map { |row| interface_key(row.fetch("INTERFACE")) } &&
|
|
42
|
+
table.scan(/^ next[ \t]*$/).size == names.size
|
|
43
|
+
|
|
44
|
+
raise ParsingError.new("FortiOS interface table is incomplete or outside the current view",
|
|
45
|
+
code: :unrecognized_output, host: @device.host, phase: :parse)
|
|
46
|
+
end
|
|
47
|
+
|
|
25
48
|
def neighbor_template(_output) = "fortinet/diagnose_lldp_rx_neighbor.textfsm"
|
|
26
49
|
|
|
27
50
|
def expected_neighbor_count(output, _template) = output.scan(/^\s*\d+\s+port\b/).size
|
|
@@ -30,18 +30,17 @@ module Net
|
|
|
30
30
|
# 只认可本厂商明确的保存完成行,设备提示符返回本身不能证明持久化。
|
|
31
31
|
def persistence_pattern = /\ASaved the current configuration to mainboard device successfully\.\z/i
|
|
32
32
|
|
|
33
|
-
# H3C
|
|
33
|
+
# H3C 列顺序由同一模板按表头分支;策略先拒绝未知表头。
|
|
34
34
|
def neighbor_template(output)
|
|
35
|
-
return "h3c/
|
|
36
|
-
return "h3c/display_lldp_neighbor_information_list_local_first.textfsm" if output.match?(/^\s*(?:Local Interface|LocalIf)\s+/im)
|
|
35
|
+
return "h3c/display_lldp_neighbor_information_list.textfsm" if output.match?(TABLE_HEADER)
|
|
37
36
|
|
|
38
37
|
raise ParsingError.new("H3C LLDP table header was not recognized", code: :unrecognized_output,
|
|
39
38
|
host: @device.host, phase: :discover)
|
|
40
39
|
end
|
|
41
40
|
|
|
42
41
|
# 按表头后的有效数据行计数,未知行会让完整性校验失败。
|
|
43
|
-
def expected_neighbor_count(output,
|
|
44
|
-
local_column =
|
|
42
|
+
def expected_neighbor_count(output, _template)
|
|
43
|
+
local_column = output.match?(/^[ \t]*System Name[ \t]+Local Interface\b/i) ? 1 : 0
|
|
45
44
|
lines = neighbor_lines(output)
|
|
46
45
|
return unless lines.all? do |line|
|
|
47
46
|
interface = line.split.fetch(local_column, nil)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../device/base"
|
|
4
|
+
require_relative "../../device/topology/strategy"
|
|
5
|
+
require_relative "../../device/topology/interface_blocks"
|
|
6
|
+
|
|
7
|
+
module Net
|
|
8
|
+
module Connector
|
|
9
|
+
module Vendor
|
|
10
|
+
class Huawei < Net::Connector::Base
|
|
11
|
+
# VRP 简表支持统一的 LLDP 读取;不假设不同 VRP 的提交与保存阶段相同。
|
|
12
|
+
class Topology < Net::Connector::Topology::Strategy
|
|
13
|
+
include Net::Connector::Topology::InterfaceBlocks
|
|
14
|
+
|
|
15
|
+
HEADER = /^[ \t]*Local Intf[ \t]+Neighbor Dev[ \t]+Neighbor Intf[ \t]+Exptime/i
|
|
16
|
+
private_constant :HEADER
|
|
17
|
+
|
|
18
|
+
def self.supports?(capability) = %i[neighbors interface_descriptions].include?(capability)
|
|
19
|
+
|
|
20
|
+
def neighbor_command = "display lldp neighbor brief"
|
|
21
|
+
|
|
22
|
+
def neighbor_template(_output) = "huawei/display_lldp_neighbor_brief.textfsm"
|
|
23
|
+
|
|
24
|
+
def description_template = "huawei/display_current_configuration.textfsm"
|
|
25
|
+
|
|
26
|
+
def expected_neighbor_count(output, _template)
|
|
27
|
+
return unless output.match?(HEADER)
|
|
28
|
+
|
|
29
|
+
neighbor_lines(output).size
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def empty_neighbor_output?(output) = output.match?(HEADER) && neighbor_lines(output).empty?
|
|
33
|
+
|
|
34
|
+
# VRP 的端口类型和编号允许用空格分开;身份比较去掉该空格。
|
|
35
|
+
def interface_key(name) = super(name.delete(" \t"))
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def neighbor_lines(output)
|
|
40
|
+
output.lines("\n").drop_while { |line| !line.match?(HEADER) }.drop(1).map(&:strip).reject do |line|
|
|
41
|
+
line.empty? || line.match?(/\A[-=]+\z/) || line.match?(/\A(?:<[^>]+>|\[[^\]]+\])\z/)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../device/base"
|
|
4
4
|
require_relative "huawei/tftp"
|
|
5
|
+
require_relative "huawei/topology"
|
|
5
6
|
|
|
6
7
|
module Net
|
|
7
8
|
module Connector
|
|
@@ -13,6 +14,7 @@ module Net
|
|
|
13
14
|
profile do
|
|
14
15
|
running_config_strategy Net::Connector::RunningConfig::Rendered
|
|
15
16
|
tftp_strategy Tftp
|
|
17
|
+
topology_strategy Topology
|
|
16
18
|
commands do
|
|
17
19
|
running_config "dis cur"
|
|
18
20
|
save_config "save force"
|
|
@@ -17,6 +17,12 @@ module Net
|
|
|
17
17
|
|
|
18
18
|
def neighbor_template(_output) = "juniper/show_lldp_neighbors_detail.textfsm"
|
|
19
19
|
|
|
20
|
+
# 逻辑单元的描述独立于物理端口,不能覆盖端口的 LLDP 身份。
|
|
21
|
+
def description_key(row)
|
|
22
|
+
unit = row.fetch("UNIT", "")
|
|
23
|
+
unit.empty? ? interface_key(row.fetch("INTERFACE")) : "#{interface_key(row.fetch("INTERFACE"))}.#{unit}"
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
def description_template = "juniper/show_configuration_display_set.textfsm"
|
|
21
27
|
|
|
22
28
|
def expected_neighbor_count(output, _template) = output.scan(/^\s*Local Interface\s*:/i).size
|
|
@@ -27,12 +33,7 @@ module Net
|
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
# 不闭合的描述必须报错,不能把截断的首行当作完整描述。
|
|
30
|
-
def decode_description(value)
|
|
31
|
-
Shellwords.split(value).join(" ")
|
|
32
|
-
rescue ArgumentError
|
|
33
|
-
raise ParsingError.new("Junos interface description is incomplete", code: :unrecognized_output,
|
|
34
|
-
host: @device.host, phase: :parse), cause: nil
|
|
35
|
-
end
|
|
36
|
+
def decode_description(value) = Net::Connector::Topology::QuotedText.decode(value, host: @device.host, split_unquoted: true)
|
|
36
37
|
end
|
|
37
38
|
end
|
|
38
39
|
end
|
|
@@ -40,13 +40,7 @@ module Net
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# 模板按行读取;未闭合的引号表示证据不完整,不能把首行当成旧描述。
|
|
43
|
-
def decode_description(value)
|
|
44
|
-
value = value.strip
|
|
45
|
-
value.start_with?('"') ? Shellwords.split(value).join(" ") : value
|
|
46
|
-
rescue ArgumentError
|
|
47
|
-
raise ParsingError.new("PAN-OS interface comment is not a complete single-line value",
|
|
48
|
-
code: :unrecognized_output, host: @device.host, phase: :parse)
|
|
49
|
-
end
|
|
43
|
+
def decode_description(value) = Net::Connector::Topology::QuotedText.decode(value, host: @device.host)
|
|
50
44
|
|
|
51
45
|
# 用 PAN-OS set 语法生成接口备注变更。
|
|
52
46
|
def change_commands(change)
|
|
@@ -12,6 +12,8 @@ module Net
|
|
|
12
12
|
# Alteon 只支持读取端口描述,不提供邻居发现或自动改写。
|
|
13
13
|
def self.supports?(capability) = capability == :interface_descriptions
|
|
14
14
|
|
|
15
|
+
def decode_description(value) = Net::Connector::Topology::QuotedText.decode(value, host: @device.host)
|
|
16
|
+
|
|
15
17
|
# 从设备配置转储解析端口名称。
|
|
16
18
|
def description_template = "radware/cfg_dump.textfsm"
|
|
17
19
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-connector
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- net-connector contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-18 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: expect-pty
|
|
@@ -362,9 +362,14 @@ files:
|
|
|
362
362
|
- lib/net/connector/device/tftp/strategy.rb
|
|
363
363
|
- lib/net/connector/device/tftp/target.rb
|
|
364
364
|
- lib/net/connector/device/topology.rb
|
|
365
|
+
- lib/net/connector/device/topology/change.rb
|
|
365
366
|
- lib/net/connector/device/topology/immediate_strategy.rb
|
|
367
|
+
- lib/net/connector/device/topology/interface_blocks.rb
|
|
366
368
|
- lib/net/connector/device/topology/interface_description.rb
|
|
367
369
|
- lib/net/connector/device/topology/interface_name.rb
|
|
370
|
+
- lib/net/connector/device/topology/neighbor.rb
|
|
371
|
+
- lib/net/connector/device/topology/plan.rb
|
|
372
|
+
- lib/net/connector/device/topology/quoted_text.rb
|
|
368
373
|
- lib/net/connector/device/topology/stages.rb
|
|
369
374
|
- lib/net/connector/device/topology/strategy.rb
|
|
370
375
|
- lib/net/connector/engine/authentication.rb
|
|
@@ -439,11 +444,13 @@ files:
|
|
|
439
444
|
- lib/net/connector/templates/cisco_nxos/show_ip_interface_brief.textfsm
|
|
440
445
|
- lib/net/connector/templates/cisco_nxos/show_running_config.textfsm
|
|
441
446
|
- lib/net/connector/templates/fortinet/diagnose_lldp_rx_neighbor.textfsm
|
|
447
|
+
- lib/net/connector/templates/fortinet/show_full_configuration.textfsm
|
|
442
448
|
- lib/net/connector/templates/h3c/display_current_configuration.textfsm
|
|
443
|
-
- lib/net/connector/templates/h3c/
|
|
444
|
-
- lib/net/connector/templates/h3c/display_lldp_neighbor_information_list_name_first.textfsm
|
|
449
|
+
- lib/net/connector/templates/h3c/display_lldp_neighbor_information_list.textfsm
|
|
445
450
|
- lib/net/connector/templates/hillstone/show_configuration_running.textfsm
|
|
446
451
|
- lib/net/connector/templates/hillstone/show_lldp_neighbor_information.textfsm
|
|
452
|
+
- lib/net/connector/templates/huawei/display_current_configuration.textfsm
|
|
453
|
+
- lib/net/connector/templates/huawei/display_lldp_neighbor_brief.textfsm
|
|
447
454
|
- lib/net/connector/templates/index
|
|
448
455
|
- lib/net/connector/templates/juniper/show_configuration_display_set.textfsm
|
|
449
456
|
- lib/net/connector/templates/juniper/show_lldp_neighbors_detail.textfsm
|
|
@@ -473,6 +480,7 @@ files:
|
|
|
473
480
|
- lib/net/connector/vendor/hillstone/topology.rb
|
|
474
481
|
- lib/net/connector/vendor/huawei.rb
|
|
475
482
|
- lib/net/connector/vendor/huawei/tftp.rb
|
|
483
|
+
- lib/net/connector/vendor/huawei/topology.rb
|
|
476
484
|
- lib/net/connector/vendor/juniper.rb
|
|
477
485
|
- lib/net/connector/vendor/juniper/remote_backup.rb
|
|
478
486
|
- lib/net/connector/vendor/juniper/topology.rb
|
data/lib/net/connector/templates/h3c/display_lldp_neighbor_information_list_name_first.textfsm
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
Value Required LOCAL_INTERFACE (\S+)
|
|
2
|
-
Value NEIGHBOR_NAME (\S+)
|
|
3
|
-
Value Required CHASSIS_ID (\S+)
|
|
4
|
-
Value Required NEIGHBOR_INTERFACE (Port\s+\d+|\S+)
|
|
5
|
-
|
|
6
|
-
Start
|
|
7
|
-
^\s*display lldp neighbor-information list\s*$$
|
|
8
|
-
^\s*${NEIGHBOR_NAME}\s+${LOCAL_INTERFACE}\s+${CHASSIS_ID}\s+${NEIGHBOR_INTERFACE}\s*$$ -> Record
|