net-connector 0.11.0 → 0.12.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 +14 -0
- data/README.md +1 -1
- data/README.zh-CN.md +1 -1
- data/docs/VERIFICATION.md +3 -3
- data/docs/architecture.md +66 -56
- data/docs/netdisco.md +9 -5
- data/docs/netdisco.zh-CN.md +14 -10
- data/docs/templates.md +53 -0
- data/docs/usage.md +6 -1
- data/docs/usage.zh-CN.md +7 -2
- data/docs/vendors.md +50 -0
- data/lib/net/connector/concurrency/observer.rb +71 -0
- data/lib/net/connector/concurrency/pool.rb +116 -0
- data/lib/net/connector/device/base.rb +6 -2
- data/lib/net/connector/device/local_backup.rb +14 -10
- data/lib/net/connector/device/profile/builder.rb +6 -0
- data/lib/net/connector/device/profile.rb +4 -2
- data/lib/net/connector/device/remote_backup/receipt.rb +36 -0
- data/lib/net/connector/device/remote_backup/target.rb +33 -0
- data/lib/net/connector/device/remote_backup.rb +51 -0
- data/lib/net/connector/device/running_config/strategy.rb +3 -3
- data/lib/net/connector/device/running_config.rb +2 -2
- data/lib/net/connector/device/scope.rb +45 -0
- data/lib/net/connector/device/tftp/file_upload.rb +7 -9
- data/lib/net/connector/device/tftp/strategy.rb +20 -6
- data/lib/net/connector/device/topology/immediate_strategy.rb +3 -3
- data/lib/net/connector/device/topology/stages.rb +1 -1
- data/lib/net/connector/device/topology.rb +1 -1
- data/lib/net/connector/engine/command/result.rb +34 -0
- data/lib/net/connector/engine/command.rb +1 -50
- data/lib/net/connector/engine/core.rb +1 -0
- data/lib/net/connector/engine/dialogue.rb +3 -67
- data/lib/net/connector/engine/error/metadata.rb +57 -0
- data/lib/net/connector/engine/errors.rb +3 -3
- data/lib/net/connector/engine/event.rb +27 -0
- data/lib/net/connector/engine/execution.rb +2 -2
- data/lib/net/connector/engine/interaction.rb +57 -0
- data/lib/net/connector/engine/log/event.rb +2 -2
- data/lib/net/connector/engine/log/{stream.rb → redacting_writer.rb} +1 -24
- data/lib/net/connector/engine/log/transcript.rb +32 -0
- data/lib/net/connector/engine/log.rb +9 -8
- data/lib/net/connector/engine/response.rb +21 -0
- data/lib/net/connector/engine/response_reader.rb +8 -2
- data/lib/net/connector/engine/result.rb +3 -27
- data/lib/net/connector/engine/script.rb +57 -0
- data/lib/net/connector/engine/session.rb +23 -4
- data/lib/net/connector/engine/terminal/renderer.rb +186 -0
- data/lib/net/connector/engine/terminal/text.rb +35 -0
- data/lib/net/connector/engine/{transport.rb → transports.rb} +1 -21
- data/lib/net/connector/netdisco/batch.rb +5 -3
- data/lib/net/connector/netdisco/cli/backup.rb +173 -0
- data/lib/net/connector/netdisco/cli/credentials.rb +39 -0
- data/lib/net/connector/netdisco/cli.rb +3 -3
- data/lib/net/connector/netdisco/device/access.rb +31 -0
- data/lib/net/connector/netdisco/device.rb +3 -3
- data/lib/net/connector/netdisco/diagnostic.rb +7 -7
- data/lib/net/connector/netdisco/fleet.rb +32 -41
- data/lib/net/connector/netdisco/inventory/budget.rb +53 -0
- data/lib/net/connector/netdisco/inventory/cache.rb +226 -0
- data/lib/net/connector/netdisco/inventory/http.rb +255 -0
- data/lib/net/connector/netdisco/inventory/postgres.rb +236 -0
- data/lib/net/connector/netdisco/inventory.rb +47 -0
- data/lib/net/connector/netdisco/operations/batch.rb +78 -0
- data/lib/net/connector/netdisco/operations/outcome.rb +90 -0
- data/lib/net/connector/netdisco/operations.rb +13 -20
- data/lib/net/connector/netdisco/progress.rb +4 -4
- data/lib/net/connector/netdisco/report/files.rb +3 -3
- data/lib/net/connector/netdisco/report/store.rb +47 -0
- data/lib/net/connector/netdisco/report/text.rb +2 -2
- data/lib/net/connector/netdisco/report.rb +2 -2
- data/lib/net/connector/netdisco/rules.rb +5 -0
- data/lib/net/connector/netdisco/settings.rb +32 -32
- data/lib/net/connector/netdisco/tftp/archive.rb +214 -0
- data/lib/net/connector/netdisco/tftp/verifier.rb +93 -0
- data/lib/net/connector/netdisco/worker.rb +3 -3
- data/lib/net/connector/netdisco.rb +1 -1
- data/lib/net/connector/storage/{private_file.rb → atomic_file.rb} +1 -1
- data/lib/net/connector/storage/{safe_file.rb → file_reader.rb} +1 -1
- data/lib/net/connector/storage/saved_config.rb +5 -5
- data/lib/net/connector/storage.rb +2 -2
- data/lib/net/connector/templates/cisco_nxos/show_cdp_neighbors_detail.textfsm +11 -0
- data/lib/net/connector/templates/cisco_nxos/show_ip_interface_brief.textfsm +10 -0
- data/lib/net/connector/templates/cisco_nxos/show_running_config.textfsm +12 -0
- data/lib/net/connector/templates/fortinet/diagnose_lldp_rx_neighbor.textfsm +7 -0
- data/lib/net/connector/templates/index +12 -7
- data/lib/net/connector/templates/juniper/show_configuration_display_set.textfsm +6 -0
- data/lib/net/connector/templates/juniper/show_lldp_neighbors_detail.textfsm +19 -0
- data/lib/net/connector/textfsm.rb +2 -2
- data/lib/net/connector/vendor/cisco/running_config.rb +25 -0
- data/lib/net/connector/vendor/cisco/topology.rb +53 -0
- data/lib/net/connector/vendor/cisco.rb +25 -0
- data/lib/net/connector/vendor/cisco_ios/tftp.rb +40 -0
- data/lib/net/connector/vendor/cisco_ios.rb +5 -14
- data/lib/net/connector/vendor/cisco_nxos/tftp.rb +46 -0
- data/lib/net/connector/vendor/cisco_nxos/topology.rb +22 -0
- data/lib/net/connector/vendor/cisco_nxos.rb +7 -17
- data/lib/net/connector/vendor/fortinet/tftp.rb +30 -0
- data/lib/net/connector/vendor/fortinet/topology.rb +35 -0
- data/lib/net/connector/vendor/fortinet.rb +40 -0
- data/lib/net/connector/vendor/h3c/tftp.rb +36 -0
- data/lib/net/connector/vendor/h3c/topology.rb +50 -47
- data/lib/net/connector/vendor/h3c.rb +5 -5
- data/lib/net/connector/vendor/h3c_wireless.rb +2 -2
- data/lib/net/connector/vendor/hillstone/running_config.rb +8 -5
- data/lib/net/connector/vendor/hillstone/tftp.rb +54 -0
- data/lib/net/connector/vendor/hillstone/topology.rb +44 -41
- data/lib/net/connector/vendor/hillstone.rb +6 -6
- data/lib/net/connector/vendor/huawei/tftp.rb +28 -0
- data/lib/net/connector/vendor/huawei.rb +4 -4
- data/lib/net/connector/vendor/juniper/remote_backup.rb +41 -0
- data/lib/net/connector/vendor/juniper/topology.rb +40 -0
- data/lib/net/connector/vendor/juniper.rb +31 -0
- data/lib/net/connector/vendor/palo_alto/running_config.rb +70 -67
- data/lib/net/connector/vendor/palo_alto/tftp.rb +48 -0
- data/lib/net/connector/vendor/palo_alto/topology.rb +54 -51
- data/lib/net/connector/vendor/palo_alto.rb +6 -6
- data/lib/net/connector/vendor/radware/running_config.rb +11 -8
- data/lib/net/connector/vendor/radware/tftp.rb +50 -0
- data/lib/net/connector/vendor/radware/topology.rb +10 -7
- data/lib/net/connector/vendor/radware.rb +6 -6
- data/lib/net/connector/vendor/topsec.rb +27 -0
- data/lib/net/connector/vendor.rb +24 -0
- data/lib/net/connector/version.rb +1 -1
- data/lib/net/connector.rb +3 -19
- metadata +72 -43
- data/lib/net/connector/background_task.rb +0 -69
- data/lib/net/connector/device_session.rb +0 -43
- data/lib/net/connector/engine/error_metadata.rb +0 -52
- data/lib/net/connector/engine/terminal_renderer.rb +0 -184
- data/lib/net/connector/engine/terminal_text.rb +0 -33
- data/lib/net/connector/execution_pool.rb +0 -114
- data/lib/net/connector/netdisco/backup_run.rb +0 -171
- data/lib/net/connector/netdisco/client.rb +0 -281
- data/lib/net/connector/netdisco/connection.rb +0 -37
- data/lib/net/connector/netdisco/database_client.rb +0 -233
- data/lib/net/connector/netdisco/inventory_budget.rb +0 -49
- data/lib/net/connector/netdisco/inventory_cache.rb +0 -224
- data/lib/net/connector/netdisco/operation_batch.rb +0 -154
- data/lib/net/connector/netdisco/result_store.rb +0 -44
- data/lib/net/connector/netdisco/tftp_archive.rb +0 -212
- data/lib/net/connector/netdisco/tftp_verification.rb +0 -91
- data/lib/net/connector/vendor/cisco_ios/running_config.rb +0 -22
- data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +0 -41
- data/lib/net/connector/vendor/cisco_ios/topology.rb +0 -50
- data/lib/net/connector/vendor/cisco_nxos/running_config.rb +0 -13
- data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +0 -43
- data/lib/net/connector/vendor/h3c/tftp_backup.rb +0 -33
- data/lib/net/connector/vendor/hillstone/tftp_backup.rb +0 -55
- data/lib/net/connector/vendor/huawei/tftp_backup.rb +0 -25
- data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +0 -49
- data/lib/net/connector/vendor/radware/tftp_backup.rb +0 -49
- /data/lib/net/connector/templates/{cisco_cdp_neighbors_detail.textfsm → cisco/show_cdp_neighbors_detail.textfsm} +0 -0
- /data/lib/net/connector/templates/{cisco_ios_running_config_interfaces.textfsm → cisco/show_running_config.textfsm} +0 -0
- /data/lib/net/connector/templates/{cisco_ios_show_ip_interface_brief.textfsm → cisco_ios/show_ip_interface_brief.textfsm} +0 -0
- /data/lib/net/connector/templates/{h3c_interface_descriptions.textfsm → h3c/display_current_configuration.textfsm} +0 -0
- /data/lib/net/connector/templates/{h3c_lldp_local_first.textfsm → h3c/display_lldp_neighbor_information_list_local_first.textfsm} +0 -0
- /data/lib/net/connector/templates/{h3c_lldp_name_first.textfsm → h3c/display_lldp_neighbor_information_list_name_first.textfsm} +0 -0
- /data/lib/net/connector/templates/{hillstone_interface_descriptions.textfsm → hillstone/show_configuration_running.textfsm} +0 -0
- /data/lib/net/connector/templates/{hillstone_lldp_neighbors.textfsm → hillstone/show_lldp_neighbor_information.textfsm} +0 -0
- /data/lib/net/connector/templates/{palo_alto_interface_descriptions.textfsm → palo_alto/show.textfsm} +0 -0
- /data/lib/net/connector/templates/{palo_alto_lldp_neighbors.textfsm → palo_alto/show_lldp_neighbors_all.textfsm} +0 -0
- /data/lib/net/connector/templates/{radware_port_names.textfsm → radware/cfg_dump.textfsm} +0 -0
data/docs/templates.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# TextFSM 模板组织
|
|
2
|
+
|
|
3
|
+
模板只描述设备输出的记录结构,不负责连接、命令执行、完整性判断或配置变更。厂商策略负责选择模板并核对记录数量,公共 TextFSM 适配器负责严格 UTF-8 与终端文本边界。
|
|
4
|
+
|
|
5
|
+
## 目录与命名
|
|
6
|
+
|
|
7
|
+
`lib/net/connector/templates/` 统一采用 `<平台>/<完整命令名>[_格式变体].textfsm`。命令使用小写,空格、连字符和路径分隔符转为下划线;不使用缩写命令、字段用途名或额外的命令子目录。管道的输出格式选项保留为命令词,纯分页控制 `no-more` 不进入文件名。
|
|
8
|
+
|
|
9
|
+
例如,`show running-config` 对应 `show_running_config.textfsm`,`display current-configuration` 对应 `display_current_configuration.textfsm`,`/cfg/dump` 对应 `cfg_dump.textfsm`,`show configuration | display set` 对应 `show_configuration_display_set.textfsm`。PAN-OS 配置视图的命令是 `show`,其 set 输出对应 `palo_alto/show.textfsm`。H3C 同一命令的列序变体使用 `_name_first` 和 `_local_first` 后缀。
|
|
10
|
+
|
|
11
|
+
| 路径 | 输入与用途 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `cisco/show_cdp_neighbors_detail.textfsm` | IOS 的详细 CDP 邻居输出 |
|
|
14
|
+
| `cisco/show_running_config.textfsm` | IOS 运行配置中的接口和描述 |
|
|
15
|
+
| `cisco_ios/show_ip_interface_brief.textfsm` | IOS 的接口、IP 地址与状态摘要 |
|
|
16
|
+
| `cisco_nxos/show_cdp_neighbors_detail.textfsm` | NX-OS CDP 详细邻居块 |
|
|
17
|
+
| `cisco_nxos/show_running_config.textfsm` | NX-OS 运行配置中的接口和描述 |
|
|
18
|
+
| `cisco_nxos/show_ip_interface_brief.textfsm` | NX-OS 多 VRF 接口、IP 及 protocol/link/admin 状态 |
|
|
19
|
+
| `h3c/display_current_configuration.textfsm` | Comware 运行配置中的接口和描述 |
|
|
20
|
+
| `h3c/display_lldp_neighbor_information_list_name_first.textfsm` | 邻居名称位于首列的 LLDP 列表 |
|
|
21
|
+
| `h3c/display_lldp_neighbor_information_list_local_first.textfsm` | 本地接口位于首列的 LLDP 列表 |
|
|
22
|
+
| `hillstone/show_configuration_running.textfsm` | StoneOS 运行配置中的接口和描述 |
|
|
23
|
+
| `hillstone/show_lldp_neighbor_information.textfsm` | StoneOS 的 LLDP 邻居表 |
|
|
24
|
+
| `palo_alto/show.textfsm` | PAN-OS set 格式中的接口和备注 |
|
|
25
|
+
| `palo_alto/show_lldp_neighbors_all.textfsm` | PAN-OS 按接口分块的 LLDP 邻居输出 |
|
|
26
|
+
| `radware/cfg_dump.textfsm` | Alteon 配置转储中的端口名称 |
|
|
27
|
+
| `fortinet/diagnose_lldp_rx_neighbor.textfsm` | FortiOS 改名前后 LLDP 邻居查询的共同输出 |
|
|
28
|
+
| `juniper/show_lldp_neighbors_detail.textfsm` | Junos LLDP 详细邻居块 |
|
|
29
|
+
| `juniper/show_configuration_display_set.textfsm` | Junos 已提交 set 配置中的物理接口描述 |
|
|
30
|
+
|
|
31
|
+
H3C Wireless 沿用 H3C 模板。H3C 邻居策略检查表头后选择列顺序变体;未知表头直接拒绝,不尝试多个模板后采用碰巧解析出的部分结果。
|
|
32
|
+
|
|
33
|
+
## 索引与显式选择
|
|
34
|
+
|
|
35
|
+
根目录的 `index` 保持 TextFSM 的 `Template, Vendor, Command` 格式。Template 使用相对资源根目录的路径,Vendor 是连接器平台标识,Command 是命令匹配规则。一个共享模板可以被多个平台索引项引用。
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
parser = Net::Connector::TextFSM.new
|
|
39
|
+
rows = parser.call(output, vendor: :cisco_ios, command: "show ip interface brief")
|
|
40
|
+
interfaces = parser.call(config, template: "cisco/show_running_config.textfsm")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
命令索引处理固定输出格式。依赖输出表头或视图上下文的模板由对应厂商策略显式选择。调用方也可以提供独立的 `template_dir:`,其索引和路径遵循相同规则。
|
|
44
|
+
|
|
45
|
+
## 修改与验证
|
|
46
|
+
|
|
47
|
+
修改模板时保留设备命令和字段语义,使用对应设备输出夹具核对记录。空结果不能代替未知或部分输出;完整性约束仍由拓扑等领域策略维护。`net-connector.gemspec` 递归收录模板,包检查验证每个资源的字节和路径,安装冒烟测试从已安装 gem 执行解析,避免源码目录掩盖资源遗漏。
|
|
48
|
+
|
|
49
|
+
## NX-OS 输出边界
|
|
50
|
+
|
|
51
|
+
NX-OS 模板独立放在 `cisco_nxos/`,CDP 按邻居块清理状态并与设备块数量核对,残缺块不能借用下一条记录的字段。接口描述在空行或下一个顶层配置段结束,防止 route-map 等后续段落的描述覆盖接口描述。IP 接口摘要保留 `VRF`、`INTERFACE`、`IP_ADDRESS`、`PROTOCOL`、`LINK_STATUS`、`ADMIN_STATUS` 六个字段;`show ip interface brief` 与带 `vrf` 参数的命令共用此模板。
|
|
52
|
+
|
|
53
|
+
输出字段参考 [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/)。测试使用脱敏合成输出,不代表真实设备及所有固件均已验证。
|
data/docs/usage.md
CHANGED
|
@@ -16,9 +16,14 @@ Use these vendor keys with `Net::Connector.open` or `Net::Connector.build`:
|
|
|
16
16
|
| `:hillstone` | Hillstone StoneOS | `show configuration running` | `save all` |
|
|
17
17
|
| `:radware` | Radware Alteon | `/cfg/dump` | `/cfg/save` |
|
|
18
18
|
| `:palo_alto` | Palo Alto PAN-OS | Set-format export from configuration view | Unsupported |
|
|
19
|
+
| `:topsec` | Topsec TopOS firewall | `show-running nostop` | Unsupported |
|
|
20
|
+
| `:fortinet` | FortiGate / FortiOS | `show full-configuration` | `execute cfg save` |
|
|
21
|
+
| `:juniper` | Juniper Junos OS | `show configuration \| display set \| no-more` | No implicit candidate commit |
|
|
19
22
|
|
|
20
23
|
`device.supports?(:tftp_backup)` checks implemented capabilities without connecting. It does not establish firmware compatibility or account permissions. See the [capability matrix](architecture.md#厂商能力) (Chinese).
|
|
21
24
|
|
|
25
|
+
See [platform details and native Junos FTP/SCP backups](vendors.md). Junos supports `remote_backup(host:, path:, username:, protocol: :scp, password: nil)`; use `supports?(:remote_backup)` to check this separately from TFTP.
|
|
26
|
+
|
|
22
27
|
## Sessions and local backups
|
|
23
28
|
|
|
24
29
|
```ruby
|
|
@@ -62,7 +67,7 @@ Explicit failure raises `:transfer_failed`; missing completion evidence raises `
|
|
|
62
67
|
|
|
63
68
|
```ruby
|
|
64
69
|
interfaces = device.parse_command("show ip interface brief") # Cisco IOS
|
|
65
|
-
rows = device.parse_config(template: "
|
|
70
|
+
rows = device.parse_config(template: "cisco/show_running_config.textfsm")
|
|
66
71
|
|
|
67
72
|
saved = Net::Connector::Storage::SavedConfig.new(directory: "/var/backups/network")
|
|
68
73
|
rows = saved.parse(host: "192.0.2.10", template: "/path/to/template.textfsm")
|
data/docs/usage.zh-CN.md
CHANGED
|
@@ -24,8 +24,13 @@
|
|
|
24
24
|
| `:palo_alto` | Palo Alto PAN-OS | 候选视图中的 set 格式导出 | 不支持 |
|
|
25
25
|
| `:huawei` | 华为 | `dis cur` | `save force` |
|
|
26
26
|
| `:hillstone` | 山石 StoneOS | `show configuration running` | `save all` |
|
|
27
|
+
| `:topsec` | 天融信 TopOS 防火墙 | `show-running nostop` | 未开放 |
|
|
28
|
+
| `:fortinet` | FortiGate / FortiOS | `show full-configuration` | `execute cfg save` |
|
|
29
|
+
| `:juniper` | Juniper Junos OS | `show configuration \| display set \| no-more` | 不隐式提交候选库 |
|
|
27
30
|
|
|
28
|
-
厂商标识仅使用上表的规范名称。厂商连接器位于 `Net::Connector` 下,例如 `Net::Connector::H3cWireless
|
|
31
|
+
厂商标识仅使用上表的规范名称。厂商连接器位于 `Net::Connector` 下,例如 `Net::Connector::Vendor::H3cWireless`。
|
|
32
|
+
|
|
33
|
+
Junos 的 FTP/SCP 原生导出使用 `remote_backup(host:, path:, username:, protocol: :scp, password: nil)`,通过 `supports?(:remote_backup)` 查询。详细能力、视图及命令依据见[新增平台说明](vendors.md)。
|
|
29
34
|
|
|
30
35
|
## 登录与本地备份
|
|
31
36
|
|
|
@@ -94,7 +99,7 @@ end
|
|
|
94
99
|
`parse_config` 采集运行配置,要求显式指定模板。内置 Cisco IOS 模板只提取接口名称和描述,并非完整配置模型:
|
|
95
100
|
|
|
96
101
|
```ruby
|
|
97
|
-
interfaces = device.parse_config(template: "
|
|
102
|
+
interfaces = device.parse_config(template: "cisco/show_running_config.textfsm")
|
|
98
103
|
```
|
|
99
104
|
|
|
100
105
|
两种方法都返回由模板字段名组成的哈希数组;匹配不到记录时返回 `[]`。模板缺失或无效会抛出 `ParsingError`,设备命令失败则保留原始连接器错误。可用 `template:` 指定外部模板,或用 `template_dir:` 指定含 `index` 的模板目录。每次解析使用独立解析器,批量任务之间不共享状态。已有本地备份也可离线解析:
|
data/docs/vendors.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Topsec、Fortinet 与 Juniper
|
|
2
|
+
|
|
3
|
+
三个平台使用 `Vendor` 注册表、Profile 和现有会话执行器,均支持 SSH/Telnet 命令执行、运行配置采集与本地私有备份。能力声明区分设备系统,不能仅凭品牌把 FortiMail、ScreenOS 等产品映射到 FortiOS 或 Junos。
|
|
4
|
+
|
|
5
|
+
| 平台键 | 系统与登录视图 | 采集命令 | 保存 | 原生远端备份 | 拓扑读取 |
|
|
6
|
+
| --- | --- | --- | --- | --- | --- |
|
|
7
|
+
| `topsec` | TopOS 防火墙,`TopsecOS#` 或 `#` 管理视图 | `show-running nostop` | 未开放 | 使用本地采集备份 | 未开放 |
|
|
8
|
+
| `fortinet` | FortiGate/FortiOS,当前账号授权的根或 VDOM 视图 | `show full-configuration` | `execute cfg save` | TFTP `execute backup full-config tftp` | LLDP 邻居 |
|
|
9
|
+
| `juniper` | Junos OS,`user@host>` 操作视图 | `show configuration \| display set \| no-more` | 已提交配置已持久化;不隐式提交候选库 | FTP/SCP `show configuration \| save` | LLDP 邻居、物理接口描述 |
|
|
10
|
+
|
|
11
|
+
这些能力通过合成会话、解析样例和安装检查验证,尚无真实设备验收记录。Topsec 只实现有检索依据的 TopOS 配置读取方言;其他系列需依据其 CLI 手册接入,不能套用 Cisco 的命令。
|
|
12
|
+
|
|
13
|
+
## Junos 原生 FTP/SCP 导出
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
Net::Connector.open(:juniper, host: "192.0.2.1", username: "audit",
|
|
17
|
+
password: ENV.fetch("NC_DEVICE_PASSWORD")) do |device|
|
|
18
|
+
receipt = device.remote_backup(host: "backup.example", path: "site/srx.cfg",
|
|
19
|
+
username: "archive", protocol: :scp,
|
|
20
|
+
password: ENV["NC_ARCHIVE_PASSWORD"])
|
|
21
|
+
puts "#{receipt.protocol}: #{receipt.server}/#{receipt.path}"
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`protocol:` 接受 `:scp`(默认)或 `:ftp`。`host:` 是 IPv4 地址或 DNS 名称,`path:` 是无空格、无目录穿越的相对路径;账号单独传入,不接受含密码的 URL。`password:` 可省略以使用设备端已有认证配置,提供时仅在密码挑战中发送一次并脱敏。SCP 服务器主机密钥应事先在设备上建立信任,连接器不自动回答接受主机密钥的询问。FTP 为明文协议。
|
|
26
|
+
|
|
27
|
+
导出保持在操作视图,`show configuration` 读取当前已提交配置。只有目标 URL 完全一致且写出行数大于零的 `Wrote ... lines of output to '...'` 完成行才生成 `RemoteBackup::Receipt`。回执标记 `verification: :device_reported`,不声称已读取远端文件或计算摘要。无完成证据时抛出 `transfer_unconfirmed`,设备错误保持原异常;上传确认后的收尾失败抛出 `RemoteBackup::CompletionError`,`error.receipt` 保留上传事实,不自动重传。
|
|
28
|
+
|
|
29
|
+
`device.supports?(:remote_backup)` 可离线检查原生 FTP/SCP 导出能力。目前由 Junos 实现;Fleet/CLI 的本地备份与 TFTP 批次接口保持各自用途。Junos `file copy` 文档列出 FTP、HTTP、HTTPS、SCP,不提供可直接套用的 TFTP 上传接口;`save` 配置模式导出候选内容,`commit` 激活候选库,均不能冒充只读的已提交配置备份。自动接口描述提交暂未开放。
|
|
30
|
+
|
|
31
|
+
## FortiOS 视图、分页与版本
|
|
32
|
+
|
|
33
|
+
采集使用当前账号授权的视图;VDOM 管理账号得到所属 VDOM 的配置,不能把它当成整个设备的全局归档。分页用空格应答,不通过 `config system console` 修改设备的持久设置。`execute cfg save` 为显式保存请求;常见自动保存模式下无需另存。
|
|
34
|
+
|
|
35
|
+
TFTP 导出包括默认值,不接受 `source_file:` 或 `vrf:`。成功必须包含完整的 `Send config file to tftp server OK.` 行;仅出现提示符、命令回显、通用进度或其他传输成功词不算完成。`TftpReceipt` 与已有厂商一致,默认仅为设备报告,服务器文件核验仍由现有 TFTP 核验流程负责。
|
|
36
|
+
|
|
37
|
+
LLDP 查询先读取 `get system status`,明确解析版本后选择命令:7.6.2 及以前使用 `diagnose lldprx neighbor`,7.6.3 起使用 `diagnose lldp rx neighbor`。无法识别版本时直接报解析错误,不猜测命令。接口行数量与解析记录必须一致,空响应不会被当作零邻居。
|
|
38
|
+
|
|
39
|
+
Junos LLDP 使用 `show lldp neighbors detail | no-more`,逐个本地接口块核对记录。残缺记录及只有标题的输出会报错;接口描述从已提交的 set 配置中读取,保留 `ge-`、`xe-`、`ae` 等原接口身份。
|
|
40
|
+
|
|
41
|
+
## 命令依据
|
|
42
|
+
|
|
43
|
+
- [FireMon TopSec 配置获取接口](https://docs.firemon.com/feature/Content/ADMINISTRATION/DEVICE/Offline_Import_or_Batch_Configs/Additional_Devices.htm):TopSec 的运行配置使用 `show-running nostop`,启动配置使用 `show nostop`,两者不混用。
|
|
44
|
+
- [Fortinet 配置 TFTP 备份](https://community.fortinet.com/fortigate-3/technical-tip-backup-configuration-files-via-tftp-99896):FortiGate `execute backup full-config tftp` 参数。
|
|
45
|
+
- [FortiOS execute cfg](https://docs.fortinet.com/document/fortigate/7.6.5/cli-reference/169686240/execute-cfg):显式保存命令。
|
|
46
|
+
- [Fortinet LLDP 邻居查询](https://community.fortinet.com/fortigate-3/technical-tip-use-lldp-on-fortigate-to-verify-connected-switches-210084):7.6.3 命令改名及邻居输出字段。
|
|
47
|
+
- [Juniper Junos 升级前备份指南](https://www.juniper.net/documentation/us/en/software/junos/srx-upgrade/srx-upgrade.pdf):操作视图 `show configuration | save` 导出已提交配置。
|
|
48
|
+
- [Juniper save 配置模式语义](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/statement/save-cli-config.html):配置模式的层级导出与远端 SCP。
|
|
49
|
+
- [Juniper file copy 协议](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/command/file-copy.html):支持的远端文件传输 URL。
|
|
50
|
+
- [Juniper LLDP 邻居字段](https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/command/show-lldp-neighbors.html):接口、机箱、端口和系统名字段,`detail` 选项要求 Junos 19.1R2 或以上。
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
module Concurrency
|
|
6
|
+
# 后台观察线程属于调用作用域;停止、等待和异常交付不能盖掉前台原故障。
|
|
7
|
+
module Observer
|
|
8
|
+
# task 自行等待停止信号,stop 只负责唤醒;正常前台也必须观察后台失败。
|
|
9
|
+
def self.run(task:, stop:)
|
|
10
|
+
unless task.respond_to?(:call) && stop.respond_to?(:call) && block_given?
|
|
11
|
+
raise ArgumentError, "background task requires task, stop and a foreground block"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
thread = value = failure = nil
|
|
15
|
+
completion = Queue.new
|
|
16
|
+
begin
|
|
17
|
+
# 创建与登记之间不交付异步中断;子线程恢复即时交付,捕获后同步传回所有者。
|
|
18
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
19
|
+
thread = Thread.new { observe(task, completion) }
|
|
20
|
+
end
|
|
21
|
+
value = yield
|
|
22
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 收尾后向所有者重新抛出原故障,包括中断。
|
|
23
|
+
failure = error
|
|
24
|
+
ensure
|
|
25
|
+
begin
|
|
26
|
+
# 二次中断不能跳过 join;恢复交付时也保留已经发生的前台故障。
|
|
27
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
28
|
+
cleanup_error = finish(thread, stop) if thread
|
|
29
|
+
failure ||= cleanup_error
|
|
30
|
+
end
|
|
31
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 保留前台故障,观察延迟到达的取消。
|
|
32
|
+
failure ||= error
|
|
33
|
+
end
|
|
34
|
+
# break 和非局部 return 也会展开作用域;异常交付必须在 ensure 内,不能绕过。
|
|
35
|
+
failure ||= completion.pop unless completion.empty?
|
|
36
|
+
raise failure, cause: failure.cause if failure
|
|
37
|
+
end
|
|
38
|
+
value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.observe(task, completion)
|
|
42
|
+
failure = nil
|
|
43
|
+
Thread.handle_interrupt(Exception => :immediate) { task.call }
|
|
44
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 不让 global abort_on_exception 将子线程故障传播到无关主线程。
|
|
45
|
+
failure = error
|
|
46
|
+
ensure
|
|
47
|
+
completion << failure
|
|
48
|
+
end
|
|
49
|
+
private_class_method :observe
|
|
50
|
+
|
|
51
|
+
def self.finish(thread, stop)
|
|
52
|
+
failure = nil
|
|
53
|
+
begin
|
|
54
|
+
stop.call
|
|
55
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 唤醒失败时仍必须终止并回收自有线程。
|
|
56
|
+
failure = error
|
|
57
|
+
thread.kill
|
|
58
|
+
ensure
|
|
59
|
+
begin
|
|
60
|
+
thread.join
|
|
61
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 等待失败也不能覆盖先前故障。
|
|
62
|
+
failure ||= error
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
failure
|
|
66
|
+
end
|
|
67
|
+
private_class_method :finish
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
module Concurrency
|
|
6
|
+
# 仅管理有限并发和线程所有权;业务层负责将普通任务故障转换为领域结果。
|
|
7
|
+
class Pool
|
|
8
|
+
DEFAULT_CONCURRENCY = 4
|
|
9
|
+
MAX_CONCURRENCY = 50
|
|
10
|
+
|
|
11
|
+
def self.validate_concurrency!(concurrency)
|
|
12
|
+
unless concurrency.is_a?(Integer) && (1..MAX_CONCURRENCY).cover?(concurrency)
|
|
13
|
+
raise ArgumentError, "concurrency must be an Integer in 1..#{MAX_CONCURRENCY}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
concurrency
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 环境只提供默认并发数;显式构造参数优先,非法值不创建线程。
|
|
20
|
+
def self.environment_concurrency(env: ENV)
|
|
21
|
+
value = env.fetch("NC_CONCURRENCY", DEFAULT_CONCURRENCY.to_s)
|
|
22
|
+
unless value.is_a?(String) && value.match?(/\A[0-9]+\z/)
|
|
23
|
+
raise ArgumentError, "NC_CONCURRENCY must be a positive integer"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
validate_concurrency!(Integer(value, 10))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(concurrency: self.class.environment_concurrency)
|
|
30
|
+
@concurrency = self.class.validate_concurrency!(concurrency)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 保序返回块的值;任何未处理异常均在调用线程抛出,停止并回收本次所有线程。
|
|
34
|
+
# 只复制任务数组,任务内容仍由业务层在下发前校验和固定,不自动重放块。
|
|
35
|
+
def map(items)
|
|
36
|
+
raise ArgumentError, "items must be an Array" unless items.is_a?(Array)
|
|
37
|
+
raise ArgumentError, "map requires a block" unless block_given?
|
|
38
|
+
|
|
39
|
+
items = items.dup
|
|
40
|
+
queue = Queue.new
|
|
41
|
+
items.each_with_index { |item, index| queue << [index, item] }
|
|
42
|
+
count = [items.size, @concurrency].min
|
|
43
|
+
count.times { queue << nil }
|
|
44
|
+
gate = Queue.new
|
|
45
|
+
finished = Queue.new
|
|
46
|
+
results = Array.new(items.size)
|
|
47
|
+
threads = []
|
|
48
|
+
completed = false
|
|
49
|
+
primary_failure = nil
|
|
50
|
+
begin
|
|
51
|
+
count.times do
|
|
52
|
+
# 仅延迟线程创建到登记的异步中断,避免尚未登记的工作线程失去所有者。
|
|
53
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
54
|
+
threads << Thread.new do
|
|
55
|
+
failure = nil
|
|
56
|
+
exhausted = false
|
|
57
|
+
begin
|
|
58
|
+
# 全部线程成功建立后才允许设备操作,创建失败不能留下已发送的变更。
|
|
59
|
+
Thread.handle_interrupt(Exception => :immediate) do
|
|
60
|
+
gate.pop
|
|
61
|
+
while (task = queue.pop)
|
|
62
|
+
index, item = task
|
|
63
|
+
results[index] = yield item
|
|
64
|
+
end
|
|
65
|
+
exhausted = true
|
|
66
|
+
end
|
|
67
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 将中断同步交给调用线程,避免全局 abort_on_exception 波及其他线程。
|
|
68
|
+
failure = error
|
|
69
|
+
ensure
|
|
70
|
+
# Thread.exit/kill 不经过 rescue;不能把尚未完成的任务伪装成合法 nil 结果。
|
|
71
|
+
failure ||= RuntimeError.new("execution worker stopped before completing its tasks") unless exhausted
|
|
72
|
+
finished << [Thread.current, failure]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
count.times { gate << true }
|
|
78
|
+
count.times do
|
|
79
|
+
thread, failure = finished.pop
|
|
80
|
+
thread.join
|
|
81
|
+
raise failure, cause: failure.cause if failure
|
|
82
|
+
end
|
|
83
|
+
completed = true
|
|
84
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 记录取消或原任务故障,收尾中再次取消不能覆盖它。
|
|
85
|
+
primary_failure = error
|
|
86
|
+
raise
|
|
87
|
+
ensure
|
|
88
|
+
stop_threads(threads, primary_failure) unless completed
|
|
89
|
+
end
|
|
90
|
+
results
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# kill 运行任务自身的 ensure;次级关闭故障不能盖掉已观察到的原始异常。
|
|
96
|
+
def stop_threads(threads, primary_failure)
|
|
97
|
+
# 仅回收期间延迟异步取消;交付二次中断之前必须 kill 并 join 全部拥有线程。
|
|
98
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
99
|
+
threads.each(&:kill)
|
|
100
|
+
threads.each do |thread|
|
|
101
|
+
begin
|
|
102
|
+
thread.join
|
|
103
|
+
rescue Exception # rubocop:disable Lint/RescueException -- 等待全部拥有线程收尾,保留调用方原始故障或中断。
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue Exception => error # rubocop:disable Lint/RescueException -- 恢复交付时保留原故障,二次取消不改变批次失败原因。
|
|
109
|
+
# 二次取消可能已以原故障为 cause;不能反向关联形成异常原因环。
|
|
110
|
+
error = primary_failure || error
|
|
111
|
+
raise error, cause: error.cause
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "scope"
|
|
5
5
|
require_relative "../engine/core"
|
|
6
6
|
require_relative "profile"
|
|
7
7
|
require_relative "running_config"
|
|
8
8
|
require_relative "save_config"
|
|
9
9
|
require_relative "local_backup"
|
|
10
|
+
require_relative "remote_backup"
|
|
10
11
|
require_relative "tftp"
|
|
11
12
|
require_relative "topology"
|
|
12
13
|
require_relative "../textfsm"
|
|
@@ -19,6 +20,7 @@ module Net
|
|
|
19
20
|
include RunningConfig::Capability
|
|
20
21
|
include SaveConfig
|
|
21
22
|
include LocalBackup::Capability
|
|
23
|
+
include RemoteBackup::Capability
|
|
22
24
|
include Tftp::Capability
|
|
23
25
|
include Topology::Capability
|
|
24
26
|
include TextFSM::Capability
|
|
@@ -63,7 +65,7 @@ module Net
|
|
|
63
65
|
def self.open(**)
|
|
64
66
|
raise ArgumentError, "open requires a block" unless block_given?
|
|
65
67
|
|
|
66
|
-
value, failure =
|
|
68
|
+
value, failure = Net::Connector::Device::Scope.call(factory: -> { new(**) }) do |device|
|
|
67
69
|
device.connect
|
|
68
70
|
yield device
|
|
69
71
|
end
|
|
@@ -100,6 +102,8 @@ module Net
|
|
|
100
102
|
!config_commands.empty?
|
|
101
103
|
when :save_config
|
|
102
104
|
!save_commands.empty?
|
|
105
|
+
when :remote_backup
|
|
106
|
+
!profile.remote_backup_strategy.nil?
|
|
103
107
|
when :tftp_backup
|
|
104
108
|
!profile.tftp_strategy.nil?
|
|
105
109
|
when :neighbors, :interface_descriptions, :interface_description_changes
|
|
@@ -7,11 +7,15 @@ require_relative "../storage"
|
|
|
7
7
|
module Net
|
|
8
8
|
module Connector
|
|
9
9
|
Backup = Data.define(:path, :bytes, :sha256, :collected_at, :change, :previous_sha256) do
|
|
10
|
-
#
|
|
10
|
+
# 完成回执拥有自己的元数据;调用方和结果回调不能原地改写已保存文件的证据。
|
|
11
11
|
def initialize(path:, bytes:, sha256:, collected_at:, change: nil, previous_sha256: nil)
|
|
12
|
-
super
|
|
12
|
+
super(path: path&.dup&.freeze, bytes: bytes, sha256: sha256&.dup&.freeze,
|
|
13
|
+
collected_at: collected_at&.dup&.freeze, change: change, previous_sha256: previous_sha256&.dup&.freeze)
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
# Ruby 3.2 的原生 Data#with 不调用初始化;更新也须重新复制并冻结元数据。
|
|
17
|
+
def with(**attributes) = attributes.empty? ? self : self.class.new(**to_h.merge(attributes))
|
|
18
|
+
|
|
15
19
|
# 判断本次采集是否产生新的配置内容。
|
|
16
20
|
def changed? = [:created, :changed].include?(change)
|
|
17
21
|
end
|
|
@@ -21,14 +25,14 @@ module Net
|
|
|
21
25
|
attr_reader :backup, :receipt, :underlying_type
|
|
22
26
|
|
|
23
27
|
def initialize(backup:, write_error:, host: nil)
|
|
24
|
-
unless backup.instance_of?(Backup) && Storage::
|
|
28
|
+
unless backup.instance_of?(Backup) && Storage::AtomicFile.receipt_error?(write_error) &&
|
|
25
29
|
write_error.receipt.committed? && write_error.receipt.path == backup.path
|
|
26
30
|
raise ArgumentError, "backup persistence error requires a matching committed backup receipt"
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
@backup, @receipt = backup, write_error.receipt
|
|
30
34
|
@underlying_type = write_error.underlying_type
|
|
31
|
-
code = if write_error.instance_of?(Storage::
|
|
35
|
+
code = if write_error.instance_of?(Storage::AtomicFile::DirectorySyncUnsupported)
|
|
32
36
|
:backup_durability_unsupported
|
|
33
37
|
elsif receipt.durable?
|
|
34
38
|
:backup_finalize_failed
|
|
@@ -47,7 +51,7 @@ module Net
|
|
|
47
51
|
raise ArgumentError, "backup must be a completed Backup" unless backup.instance_of?(Backup)
|
|
48
52
|
|
|
49
53
|
@backup = backup
|
|
50
|
-
@underlying_type =
|
|
54
|
+
@underlying_type = Error::Metadata.type(underlying.class.name) || "StandardError"
|
|
51
55
|
super("backup completed; path lock finalization failed", code: :backup_finalize_failed, host: host, phase: :backup)
|
|
52
56
|
end
|
|
53
57
|
end
|
|
@@ -83,7 +87,7 @@ module Net
|
|
|
83
87
|
def collect_and_write(destination)
|
|
84
88
|
contents = @device.running_config.value!
|
|
85
89
|
digest = Digest::SHA256.hexdigest(contents)
|
|
86
|
-
previous = Storage::
|
|
90
|
+
previous = Storage::FileReader.fingerprint(destination, missing: true, replace_symlink: true)
|
|
87
91
|
previous_digest = previous&.sha256
|
|
88
92
|
change = if previous_digest.nil?
|
|
89
93
|
:created
|
|
@@ -97,7 +101,7 @@ module Net
|
|
|
97
101
|
sha256: digest, collected_at: Time.now.utc, change: change,
|
|
98
102
|
previous_sha256: previous_digest)
|
|
99
103
|
# 内容未变也须核对权限与目录项身份;只在两者仍有效时保留原文件和 mtime。
|
|
100
|
-
if change != :unchanged || previous.mode != 0o600 || !Storage::
|
|
104
|
+
if change != :unchanged || previous.mode != 0o600 || !Storage::FileReader.same_entry?(destination, previous)
|
|
101
105
|
write_backup(backup, contents)
|
|
102
106
|
end
|
|
103
107
|
backup
|
|
@@ -105,9 +109,9 @@ module Net
|
|
|
105
109
|
|
|
106
110
|
# 替换后失败仍交付已提交的备份回执,不能让调用方误判为没有产物。
|
|
107
111
|
def write_backup(backup, contents)
|
|
108
|
-
Storage::
|
|
109
|
-
rescue Storage::
|
|
110
|
-
raise unless Storage::
|
|
112
|
+
Storage::AtomicFile.write(backup.path, contents)
|
|
113
|
+
rescue Storage::AtomicFile::WriteError => error
|
|
114
|
+
raise unless Storage::AtomicFile.receipt_error?(error) && error.receipt.committed?
|
|
111
115
|
|
|
112
116
|
host = @device.host if @device.respond_to?(:host)
|
|
113
117
|
raise BackupPersistenceError.new(backup: backup, write_error: error, host: host), cause: nil
|
|
@@ -85,6 +85,12 @@ module Net
|
|
|
85
85
|
self
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# 指定设备原生 FTP/SCP 导出策略;nil 明确关闭继承能力。
|
|
89
|
+
def remote_backup_strategy(klass)
|
|
90
|
+
@values[:remote_backup_strategy] = klass
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
88
94
|
# 指定运行配置采集策略,覆盖继承的厂商规则。
|
|
89
95
|
def running_config_strategy(klass)
|
|
90
96
|
@values[:running_config_strategy] = klass
|
|
@@ -21,7 +21,7 @@ module Net
|
|
|
21
21
|
:privilege_prompt, :pager_pattern, :pager_response,
|
|
22
22
|
:authentication_error_patterns, :command_error_patterns,
|
|
23
23
|
:login_interactions, :confirmation_interactions, :legacy_ssh_arguments,
|
|
24
|
-
:privilege_command, :command_timeout, :terminal_size, :tftp_strategy, :topology_strategy, :running_config_strategy
|
|
24
|
+
:privilege_command, :command_timeout, :terminal_size, :tftp_strategy, :topology_strategy, :running_config_strategy, :remote_backup_strategy
|
|
25
25
|
|
|
26
26
|
# 使用默认的公共规则创建空设备档案;登录和命令提示符仍由厂商声明。
|
|
27
27
|
def self.default
|
|
@@ -46,7 +46,7 @@ module Net
|
|
|
46
46
|
authentication_error_patterns: DEFAULT_AUTHENTICATION_ERROR_PATTERNS,
|
|
47
47
|
command_error_patterns: [], login_interactions: [], confirmation_interactions: [],
|
|
48
48
|
legacy_ssh_arguments: DEFAULT_LEGACY_SSH_ARGUMENTS, privilege_command: nil,
|
|
49
|
-
command_timeout: 25, terminal_size: nil, tftp_strategy: nil, topology_strategy: nil, running_config_strategy: nil)
|
|
49
|
+
command_timeout: 25, terminal_size: nil, tftp_strategy: nil, topology_strategy: nil, running_config_strategy: nil, remote_backup_strategy: nil)
|
|
50
50
|
@config_commands = command_list(config_commands, :config_commands, allow_nil: true)
|
|
51
51
|
@save_commands = command_list(save_commands, :save_commands)
|
|
52
52
|
@login_prompt = pattern(login_prompt, :login_prompt, allow_nil: true)
|
|
@@ -64,6 +64,7 @@ module Net
|
|
|
64
64
|
@privilege_command = optional_text(privilege_command, :privilege_command)
|
|
65
65
|
@command_timeout = duration(command_timeout, :command_timeout)
|
|
66
66
|
@terminal_size = validate_terminal_size!(terminal_size)
|
|
67
|
+
@remote_backup_strategy = strategy_class(remote_backup_strategy, :remote_backup_strategy, %i[script completed?])
|
|
67
68
|
@running_config_strategy = strategy_class(running_config_strategy, :running_config_strategy,
|
|
68
69
|
%i[clean result_step prompt_text validate_response!])
|
|
69
70
|
@tftp_strategy = strategy_class(tftp_strategy, :tftp_strategy,
|
|
@@ -105,6 +106,7 @@ module Net
|
|
|
105
106
|
terminal_size: terminal_size,
|
|
106
107
|
tftp_strategy: tftp_strategy,
|
|
107
108
|
topology_strategy: topology_strategy,
|
|
109
|
+
remote_backup_strategy: remote_backup_strategy,
|
|
108
110
|
running_config_strategy: running_config_strategy
|
|
109
111
|
}
|
|
110
112
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "target"
|
|
4
|
+
require_relative "../../engine/errors"
|
|
5
|
+
|
|
6
|
+
module Net
|
|
7
|
+
module Connector
|
|
8
|
+
class RemoteBackup
|
|
9
|
+
# 不保存认证秘密;此回执只说明设备报告写出,未读取远端文件。
|
|
10
|
+
Receipt = Data.define(:server, :path, :protocol, :username, :completed_at, :configuration_kind, :verification) do
|
|
11
|
+
def initialize(server:, path:, protocol:, username:, completed_at:, configuration_kind: :running, verification: :device_reported)
|
|
12
|
+
Target.new(host: server, path: path, protocol: protocol, username: username)
|
|
13
|
+
unless completed_at.is_a?(Time) && configuration_kind == :running && verification == :device_reported
|
|
14
|
+
raise ArgumentError, "invalid remote backup receipt"
|
|
15
|
+
end
|
|
16
|
+
super(server: server.dup.freeze, path: path.dup.freeze, protocol: protocol, username: username.dup.freeze,
|
|
17
|
+
completed_at: completed_at.dup.freeze, configuration_kind: configuration_kind, verification: verification)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 完成后出现收尾错误时保留上传事实,避免调用方盲目重传。
|
|
22
|
+
class CompletionError < Error
|
|
23
|
+
attr_reader :receipt, :underlying_type
|
|
24
|
+
|
|
25
|
+
def initialize(receipt:, host:, underlying:)
|
|
26
|
+
raise ArgumentError, "remote backup receipt required" unless receipt.instance_of?(Receipt)
|
|
27
|
+
|
|
28
|
+
@receipt = receipt
|
|
29
|
+
@underlying_type = Error::Metadata.type(underlying.class.name)
|
|
30
|
+
super("device reported remote backup completion but finalization failed",
|
|
31
|
+
code: :transfer_finalize_failed, host: host, phase: :remote_backup)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module Connector
|
|
5
|
+
class RemoteBackup
|
|
6
|
+
# 目标必须可直接作为一个 CLI 单词使用,不接受 URL 内密码或命令片段。
|
|
7
|
+
Target = Data.define(:host, :path, :username, :protocol) do
|
|
8
|
+
def initialize(host:, path:, username:, protocol:)
|
|
9
|
+
unless %i[ftp scp].include?(protocol)
|
|
10
|
+
raise ArgumentError, "remote backup protocol must be ftp or scp"
|
|
11
|
+
end
|
|
12
|
+
unless username.is_a?(String) && username.match?(/\A[A-Za-z0-9_][A-Za-z0-9_.-]*\z/)
|
|
13
|
+
raise ArgumentError, "remote backup username must be a safe account name"
|
|
14
|
+
end
|
|
15
|
+
unless host.is_a?(String) && host.bytesize <= 253 && host.match?(/\A[A-Za-z0-9][A-Za-z0-9.-]*\z/) && !host.include?("..")
|
|
16
|
+
raise ArgumentError, "remote backup host must be an IPv4 address or DNS name"
|
|
17
|
+
end
|
|
18
|
+
unless path.is_a?(String) && path.bytesize <= 220 &&
|
|
19
|
+
path.match?(/\A[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*\z/) &&
|
|
20
|
+
path.split("/").none? { |part| %w[. ..].include?(part) }
|
|
21
|
+
raise ArgumentError, "remote backup path must be a relative filename without traversal"
|
|
22
|
+
end
|
|
23
|
+
super(host: host.dup.freeze, path: path.dup.freeze, username: username.dup.freeze, protocol: protocol)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def url
|
|
27
|
+
"#{protocol}://#{username}@#{host}/#{path}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
private_constant :Target
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../engine/script"
|
|
4
|
+
require_relative "remote_backup/receipt"
|
|
5
|
+
|
|
6
|
+
module Net
|
|
7
|
+
module Connector
|
|
8
|
+
# 设备原生 FTP/SCP 导出;与采集到本机的 LocalBackup 分别保留完成证据。
|
|
9
|
+
class RemoteBackup
|
|
10
|
+
module Capability
|
|
11
|
+
def remote_backup(host:, path:, username:, protocol: :scp, password: nil)
|
|
12
|
+
RemoteBackup.new(self).call(host: host, path: path, username: username, protocol: protocol, password: password)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(device) = @device = device
|
|
17
|
+
|
|
18
|
+
def call(host:, path:, username:, protocol:, password: nil)
|
|
19
|
+
receipt = nil
|
|
20
|
+
implementation = @device.profile.remote_backup_strategy
|
|
21
|
+
unless implementation
|
|
22
|
+
raise UnsupportedOperation.new("native FTP/SCP backup is not supported for this vendor",
|
|
23
|
+
host: @device.host, phase: :remote_backup)
|
|
24
|
+
end
|
|
25
|
+
target = Target.new(host: host, path: path, username: username, protocol: protocol)
|
|
26
|
+
if !password.nil? && (!password.is_a?(String) || password.match?(/[\r\n\x00]/))
|
|
27
|
+
raise ArgumentError, "remote backup password must be a single-line String"
|
|
28
|
+
end
|
|
29
|
+
password = password&.dup&.freeze
|
|
30
|
+
strategy = implementation.new(@device)
|
|
31
|
+
@device.with_operation(:remote_backup) do
|
|
32
|
+
result = @device.execute_operation(strategy.script(target, password: password), name: :remote_backup, privilege: false)
|
|
33
|
+
unless strategy.completed?(target, result)
|
|
34
|
+
result.value!
|
|
35
|
+
raise DeviceError.new("device did not confirm remote backup completion", code: :transfer_unconfirmed,
|
|
36
|
+
host: @device.host, phase: :remote_backup)
|
|
37
|
+
end
|
|
38
|
+
receipt = Receipt.new(server: target.host, path: target.path, protocol: target.protocol, username: target.username,
|
|
39
|
+
completed_at: Time.now.utc)
|
|
40
|
+
result.value! if result.failure?
|
|
41
|
+
receipt
|
|
42
|
+
end
|
|
43
|
+
rescue StandardError => error
|
|
44
|
+
raise unless receipt
|
|
45
|
+
raise if error.instance_of?(CompletionError)
|
|
46
|
+
|
|
47
|
+
raise CompletionError.new(receipt: receipt, host: @device.host, underlying: error), cause: nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|