netstack 0.1.1 → 0.2.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 +22 -3
- data/{LICENSE.txt → LICENSE} +7 -6
- data/NOTICE +25 -0
- data/README.md +58 -20
- data/docs/acceptance.md +92 -0
- data/docs/backups.md +100 -0
- data/docs/configuration.md +198 -0
- data/docs/connector.md +187 -0
- data/docs/ddi.md +142 -0
- data/docs/deployment.md +47 -0
- data/docs/foundations.md +45 -0
- data/docs/implementation.md +68 -0
- data/docs/inventory.md +42 -0
- data/docs/monitoring.md +227 -0
- data/docs/motor_integration.md +96 -0
- data/docs/performance.md +39 -0
- data/docs/release.md +20 -0
- data/docs/vxlan.md +91 -0
- data/docs/wireless.md +81 -0
- data/lib/netstack/backups/artifact.rb +23 -0
- data/lib/netstack/backups/diff.rb +15 -0
- data/lib/netstack/backups/repository.rb +77 -0
- data/lib/netstack/backups/request.rb +78 -0
- data/lib/netstack/circuits/business_calendar.rb +51 -0
- data/lib/netstack/circuits/capacity_review.rb +191 -0
- data/lib/netstack/circuits/endpoint.rb +25 -0
- data/lib/netstack/circuits/monitoring_plan.rb +212 -0
- data/lib/netstack/configuration/artifact.rb +85 -0
- data/lib/netstack/configuration/artifact_resolver.rb +30 -0
- data/lib/netstack/configuration/bootstrap/payload.rb +67 -0
- data/lib/netstack/configuration/bootstrap/validator.rb +224 -0
- data/lib/netstack/configuration/catalog.rb +60 -0
- data/lib/netstack/configuration/generator.rb +87 -0
- data/lib/netstack/configuration/input_contract.rb +82 -0
- data/lib/netstack/configuration/payload.rb +168 -0
- data/lib/netstack/configuration/reader.rb +124 -0
- data/lib/netstack/configuration/support.rb +30 -0
- data/lib/netstack/configuration/system_configuration.rb +56 -0
- data/lib/netstack/configuration/template_manager.rb +174 -0
- data/lib/netstack/configuration/template_store.rb +40 -0
- data/lib/netstack/configuration/validator.rb +566 -0
- data/lib/netstack/connector/authentication.rb +77 -0
- data/lib/netstack/connector/authentication_error.rb +11 -0
- data/lib/netstack/connector/backup.rb +19 -0
- data/lib/netstack/connector/backup_busy.rb +11 -0
- data/lib/netstack/connector/backup_persistence_error.rb +32 -0
- data/lib/netstack/connector/command.rb +63 -0
- data/lib/netstack/connector/command_result.rb +24 -0
- data/lib/netstack/connector/command_timeout.rb +11 -0
- data/lib/netstack/connector/configuration.rb +138 -0
- data/lib/netstack/connector/connection_closed.rb +11 -0
- data/lib/netstack/connector/connection_error.rb +11 -0
- data/lib/netstack/connector/device.rb +284 -0
- data/lib/netstack/connector/device_error.rb +11 -0
- data/lib/netstack/connector/dialogue.rb +55 -0
- data/lib/netstack/connector/error.rb +44 -0
- data/lib/netstack/connector/error_metadata.rb +58 -0
- data/lib/netstack/connector/event.rb +24 -0
- data/lib/netstack/connector/execution.rb +110 -0
- data/lib/netstack/connector/interaction.rb +55 -0
- data/lib/netstack/connector/internal_error.rb +11 -0
- data/lib/netstack/connector/known_hosts.rb +128 -0
- data/lib/netstack/connector/local_backup.rb +63 -0
- data/lib/netstack/connector/log/event.rb +65 -0
- data/lib/netstack/connector/log/formatter.rb +18 -0
- data/lib/netstack/connector/log/messages.rb +54 -0
- data/lib/netstack/connector/log/redacting_writer.rb +33 -0
- data/lib/netstack/connector/log/transcript.rb +35 -0
- data/lib/netstack/connector/log.rb +262 -0
- data/lib/netstack/connector/log_error.rb +11 -0
- data/lib/netstack/connector/login_timeout.rb +11 -0
- data/lib/netstack/connector/output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/parsing_error.rb +11 -0
- data/lib/netstack/connector/profile/builder.rb +238 -0
- data/lib/netstack/connector/profile.rb +225 -0
- data/lib/netstack/connector/prompt_error.rb +11 -0
- data/lib/netstack/connector/recovery.rb +59 -0
- data/lib/netstack/connector/redactor.rb +100 -0
- data/lib/netstack/connector/response.rb +25 -0
- data/lib/netstack/connector/response_reader.rb +140 -0
- data/lib/netstack/connector/result.rb +50 -0
- data/lib/netstack/connector/running_config/rendered.rb +16 -0
- data/lib/netstack/connector/running_config/strategy.rb +28 -0
- data/lib/netstack/connector/running_config.rb +113 -0
- data/lib/netstack/connector/save_config.rb +23 -0
- data/lib/netstack/connector/script.rb +59 -0
- data/lib/netstack/connector/script_error.rb +11 -0
- data/lib/netstack/connector/script_output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/session.rb +414 -0
- data/lib/netstack/connector/session_busy.rb +11 -0
- data/lib/netstack/connector/storage/backup_lock.rb +117 -0
- data/lib/netstack/connector/storage/private_file.rb +111 -0
- data/lib/netstack/connector/storage/safe_file.rb +64 -0
- data/lib/netstack/connector/storage/saved_config.rb +76 -0
- data/lib/netstack/connector/terminal_renderer.rb +186 -0
- data/lib/netstack/connector/terminal_text.rb +32 -0
- data/lib/netstack/connector/textfsm.rb +71 -0
- data/lib/netstack/connector/tftp/file_upload.rb +53 -0
- data/lib/netstack/connector/tftp/strategy.rb +67 -0
- data/lib/netstack/connector/tftp.rb +116 -0
- data/lib/netstack/connector/tftp_completion_error.rb +36 -0
- data/lib/netstack/connector/tftp_receipt.rb +54 -0
- data/lib/netstack/connector/tftp_target.rb +65 -0
- data/lib/netstack/connector/topology/checkpoint_error.rb +16 -0
- data/lib/netstack/connector/topology/deployment_adapter.rb +163 -0
- data/lib/netstack/connector/topology/execution_result.rb +34 -0
- data/lib/netstack/connector/topology/immediate_strategy.rb +39 -0
- data/lib/netstack/connector/topology/interface_description.rb +46 -0
- data/lib/netstack/connector/topology/interface_name.rb +60 -0
- data/lib/netstack/connector/topology/plan.rb +105 -0
- data/lib/netstack/connector/topology/strategy.rb +76 -0
- data/lib/netstack/connector/topology.rb +255 -0
- data/lib/netstack/connector/transport_error.rb +11 -0
- data/lib/netstack/connector/transports.rb +201 -0
- data/lib/netstack/connector/underlying_error.rb +25 -0
- data/lib/netstack/connector/unsupported_operation.rb +11 -0
- data/lib/netstack/connector/vendors/cisco_ios/running_config.rb +22 -0
- data/lib/netstack/connector/vendors/cisco_ios/tftp_backup.rb +41 -0
- data/lib/netstack/connector/vendors/cisco_ios/topology.rb +54 -0
- data/lib/netstack/connector/vendors/cisco_ios.rb +44 -0
- data/lib/netstack/connector/vendors/cisco_nxos/running_config.rb +25 -0
- data/lib/netstack/connector/vendors/cisco_nxos/tftp_backup.rb +43 -0
- data/lib/netstack/connector/vendors/cisco_nxos.rb +51 -0
- data/lib/netstack/connector/vendors/h3c/tftp_backup.rb +34 -0
- data/lib/netstack/connector/vendors/h3c/topology.rb +72 -0
- data/lib/netstack/connector/vendors/h3c.rb +75 -0
- data/lib/netstack/connector/vendors/h3c_wireless.rb +13 -0
- data/lib/netstack/connector/vendors/hillstone/running_config.rb +16 -0
- data/lib/netstack/connector/vendors/hillstone/tftp_backup.rb +55 -0
- data/lib/netstack/connector/vendors/hillstone/topology.rb +64 -0
- data/lib/netstack/connector/vendors/hillstone.rb +39 -0
- data/lib/netstack/connector/vendors/huawei/tftp_backup.rb +26 -0
- data/lib/netstack/connector/vendors/huawei.rb +62 -0
- data/lib/netstack/connector/vendors/palo_alto/running_config.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/palo_alto/topology.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto.rb +39 -0
- data/lib/netstack/connector/vendors/radware/running_config.rb +21 -0
- data/lib/netstack/connector/vendors/radware/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/radware/topology.rb +19 -0
- data/lib/netstack/connector/vendors/radware.rb +59 -0
- data/lib/netstack/connector/write_timeout.rb +11 -0
- data/lib/netstack/connector.rb +32 -0
- data/lib/netstack/ddi/applier.rb +107 -0
- data/lib/netstack/ddi/deployment.rb +137 -0
- data/lib/netstack/ddi/inputs.rb +61 -0
- data/lib/netstack/ddi/manager.rb +197 -0
- data/lib/netstack/ddi/plan.rb +99 -0
- data/lib/netstack/ddi/scope.rb +127 -0
- data/lib/netstack/ddi/settings.rb +44 -0
- data/lib/netstack/ddi/state.rb +166 -0
- data/lib/netstack/deployment/checkpoint_error.rb +14 -0
- data/lib/netstack/deployment/cli_adapter.rb +64 -0
- data/lib/netstack/deployment/executor.rb +131 -0
- data/lib/netstack/deployment/plan.rb +82 -0
- data/lib/netstack/deployment/receipt.rb +78 -0
- data/lib/netstack/deployment/recovery.rb +19 -0
- data/lib/netstack/deployment/step.rb +16 -0
- data/lib/netstack/dimension.rb +30 -0
- data/lib/netstack/error.rb +14 -0
- data/lib/netstack/input_error.rb +9 -0
- data/lib/netstack/integrations/bluecat/client.rb +84 -0
- data/lib/netstack/integrations/http/client.rb +146 -0
- data/lib/netstack/integrations/http/error.rb +10 -0
- data/lib/netstack/integrations/http/response.rb +28 -0
- data/lib/netstack/integrations/http_delivery/adapter.rb +51 -0
- data/lib/netstack/integrations/http_delivery/client.rb +127 -0
- data/lib/netstack/integrations/http_delivery/result.rb +27 -0
- data/lib/netstack/integrations/oxidized/client.rb +89 -0
- data/lib/netstack/integrations/zabbix/client.rb +211 -0
- data/lib/netstack/integrations/zabbix/error.rb +10 -0
- data/lib/netstack/inventory/address.rb +30 -0
- data/lib/netstack/inventory/changes.rb +13 -0
- data/lib/netstack/inventory/cli_parser.rb +75 -0
- data/lib/netstack/inventory/collector.rb +99 -0
- data/lib/netstack/inventory/device.rb +14 -0
- data/lib/netstack/inventory/interface.rb +25 -0
- data/lib/netstack/inventory/neighbor.rb +15 -0
- data/lib/netstack/inventory/reconciler.rb +74 -0
- data/lib/netstack/inventory/snapshot.rb +69 -0
- data/lib/netstack/inventory/transceiver.rb +23 -0
- data/lib/netstack/ipv4.rb +204 -0
- data/lib/netstack/ipv6.rb +88 -0
- data/lib/netstack/monitoring/applier.rb +179 -0
- data/lib/netstack/monitoring/device_plan.rb +23 -0
- data/lib/netstack/monitoring/host_plan.rb +152 -0
- data/lib/netstack/monitoring/inputs.rb +73 -0
- data/lib/netstack/monitoring/measurements.rb +89 -0
- data/lib/netstack/monitoring/mutation.rb +37 -0
- data/lib/netstack/monitoring/plan.rb +69 -0
- data/lib/netstack/monitoring/probe_plan.rb +19 -0
- data/lib/netstack/monitoring/state_comparison.rb +37 -0
- data/lib/netstack/range_set.rb +181 -0
- data/lib/netstack/routing/lookup.rb +46 -0
- data/lib/netstack/routing/lookup_result.rb +14 -0
- data/lib/netstack/routing/route.rb +32 -0
- data/lib/netstack/routing/snapshot.rb +23 -0
- data/lib/netstack/target.rb +15 -0
- data/lib/netstack/unsupported_capability.rb +9 -0
- data/lib/netstack/values.rb +70 -0
- data/lib/netstack/version.rb +1 -1
- data/lib/netstack/vxlan/auditor.rb +187 -0
- data/lib/netstack/vxlan/cisco_nxos/config_parser.rb +327 -0
- data/lib/netstack/vxlan/cisco_nxos/deployment_adapter.rb +63 -0
- data/lib/netstack/vxlan/cisco_nxos/runtime_parser.rb +182 -0
- data/lib/netstack/vxlan/cisco_nxos/segment_renderer.rb +42 -0
- data/lib/netstack/vxlan/collector.rb +77 -0
- data/lib/netstack/vxlan/fabric.rb +29 -0
- data/lib/netstack/vxlan/finding.rb +20 -0
- data/lib/netstack/vxlan/node.rb +51 -0
- data/lib/netstack/vxlan/route_policy.rb +57 -0
- data/lib/netstack/vxlan/runtime_result.rb +24 -0
- data/lib/netstack/vxlan/segment.rb +35 -0
- data/lib/netstack/vxlan/segment_plan.rb +25 -0
- data/lib/netstack/vxlan/segment_planner.rb +156 -0
- data/lib/netstack/vxlan/snapshot.rb +34 -0
- data/lib/netstack/vxlan/validation.rb +58 -0
- data/lib/netstack/vxlan/verifier.rb +52 -0
- data/lib/netstack/vxlan/vpc_domain.rb +20 -0
- data/lib/netstack/wireless/access_point.rb +12 -0
- data/lib/netstack/wireless/ap_group.rb +12 -0
- data/lib/netstack/wireless/assessment.rb +13 -0
- data/lib/netstack/wireless/assessor.rb +83 -0
- data/lib/netstack/wireless/authentication_connection.rb +12 -0
- data/lib/netstack/wireless/bss.rb +12 -0
- data/lib/netstack/wireless/client.rb +12 -0
- data/lib/netstack/wireless/collector.rb +191 -0
- data/lib/netstack/wireless/configuration.rb +13 -0
- data/lib/netstack/wireless/finding.rb +12 -0
- data/lib/netstack/wireless/h3c/config_parser.rb +229 -0
- data/lib/netstack/wireless/h3c/lldp_parser.rb +69 -0
- data/lib/netstack/wireless/h3c/runtime_parser.rb +240 -0
- data/lib/netstack/wireless/huawei/runtime_parser.rb +83 -0
- data/lib/netstack/wireless/invalid_snapshot.rb +11 -0
- data/lib/netstack/wireless/neighbor.rb +12 -0
- data/lib/netstack/wireless/normalizer.rb +90 -0
- data/lib/netstack/wireless/parse_result.rb +13 -0
- data/lib/netstack/wireless/radio.rb +12 -0
- data/lib/netstack/wireless/reconciler.rb +115 -0
- data/lib/netstack/wireless/reconciliation.rb +11 -0
- data/lib/netstack/wireless/security_policy.rb +12 -0
- data/lib/netstack/wireless/service_template.rb +12 -0
- data/lib/netstack/wireless/service_template_bindings.rb +56 -0
- data/lib/netstack/wireless/snapshot.rb +150 -0
- data/lib/netstack/wireless/state.rb +23 -0
- data/lib/netstack/wireless/view.rb +154 -0
- data/lib/netstack.rb +10 -3
- data/resources/configuration/cisco-bootstrap.erb +118 -0
- data/resources/configuration/cisco-distributed-gateway.erb +40 -0
- data/resources/configuration/cisco-n9k-vpc.erb +62 -0
- data/resources/configuration/cisco-server-port.erb +43 -0
- data/resources/configuration/h3c-bootstrap.erb +108 -0
- data/resources/configuration/h3c-irf.erb +57 -0
- data/resources/configuration/h3c-server-port.erb +36 -0
- data/resources/configuration/hillstone-ipsec-vpn.erb +68 -0
- data/resources/configuration/hillstone-policy.erb +31 -0
- data/resources/configuration/paloalto-ipsec-vpn.erb +39 -0
- data/resources/configuration/radware-load-balancer.erb +26 -0
- data/resources/textfsm/cisco_cdp_neighbors_detail.textfsm +7 -0
- data/resources/textfsm/cisco_ios_running_config_interfaces.textfsm +12 -0
- data/resources/textfsm/cisco_ios_show_ip_interface_brief.textfsm +7 -0
- data/resources/textfsm/h3c_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/h3c_lldp_local_first.textfsm +8 -0
- data/resources/textfsm/h3c_lldp_name_first.textfsm +8 -0
- data/resources/textfsm/hillstone_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/hillstone_lldp_neighbors.textfsm +7 -0
- data/resources/textfsm/index +8 -0
- data/resources/textfsm/palo_alto_interface_descriptions.textfsm +6 -0
- data/resources/textfsm/palo_alto_lldp_neighbors.textfsm +11 -0
- data/resources/textfsm/radware_port_names.textfsm +13 -0
- metadata +588 -27
- data/.rubocop.yml +0 -13
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -40
- data/Rakefile +0 -8
- data/bin/console +0 -15
- data/bin/setup +0 -8
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
require "open3"
|
|
6
|
+
require "expect/pty"
|
|
7
|
+
|
|
8
|
+
module Netstack
|
|
9
|
+
module Connector
|
|
10
|
+
# 可替换的字节传输层;厂商语法属于设备边界,不属于此层。
|
|
11
|
+
module Transports
|
|
12
|
+
# Expect 适配器拥有伪终端和进程,在限制未匹配尾部时不丢弃数据。
|
|
13
|
+
class Pty
|
|
14
|
+
# 即使读到完整行也保留 32 KiB,因为提示符可能跨越多次读取和多行。
|
|
15
|
+
STREAM = /\A[\s\S]{32768}(?=[\s\S]{32768})/n
|
|
16
|
+
attr_reader :configuration
|
|
17
|
+
|
|
18
|
+
# 保存配置和可替换的信道工厂,便于测试和设备差异注入。
|
|
19
|
+
def initialize(configuration, channel_factory: nil, terminal_size: nil)
|
|
20
|
+
@configuration = configuration
|
|
21
|
+
@channel_factory = channel_factory
|
|
22
|
+
@terminal_size = terminal_size
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 打开伪终端、设置终端大小并启动设备进程。
|
|
26
|
+
def open
|
|
27
|
+
configuration.validate_endpoint!
|
|
28
|
+
@channel = if @channel_factory
|
|
29
|
+
@channel_factory.call
|
|
30
|
+
else
|
|
31
|
+
Expect.new(raw_pty: true, reset_timeout_on_read: false, buffer_limit: nil,
|
|
32
|
+
preserve_buffer: false, log_stdout: false, log_listeners: false,
|
|
33
|
+
debug_level: 0, write_timeout: configuration.write_timeout)
|
|
34
|
+
end
|
|
35
|
+
# Profile 使用宽、高;PTY 使用行数、列数。
|
|
36
|
+
@channel.slave.winsize = @terminal_size.reverse if @terminal_size
|
|
37
|
+
@channel.log_output = method(:write_log_output)
|
|
38
|
+
@channel.spawn(*argv)
|
|
39
|
+
self
|
|
40
|
+
rescue Exception # rubocop:disable Lint/RescueException -- A half-open PTY must be released on interrupts.
|
|
41
|
+
begin
|
|
42
|
+
close
|
|
43
|
+
rescue
|
|
44
|
+
# 尝试释放半打开信道后,仍然抛出原始失败。
|
|
45
|
+
end
|
|
46
|
+
raise
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 从信道读取下一事件,并把字符串字段转换为字节字符串。
|
|
50
|
+
def read(patterns, timeout:)
|
|
51
|
+
# 0.5.3 起 expect 直接返回 Result;旧版 expect 只返回模式序号。
|
|
52
|
+
result = if @channel.respond_to?(:expect_result)
|
|
53
|
+
@channel.expect_result(*patterns, STREAM, timeout: timeout)
|
|
54
|
+
else
|
|
55
|
+
@channel.expect(*patterns, STREAM, timeout: timeout)
|
|
56
|
+
end
|
|
57
|
+
Event.new(index: result.number && (result.number - 1), before: result.before.to_s.b,
|
|
58
|
+
match: result.match.to_s.b, error: result.error)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 临时设置写入超时,完成写入后恢复原设置。
|
|
62
|
+
def write(bytes, timeout: configuration.write_timeout)
|
|
63
|
+
previous = @channel.write_timeout
|
|
64
|
+
@channel.write_timeout = timeout
|
|
65
|
+
@channel.write(bytes)
|
|
66
|
+
ensure
|
|
67
|
+
@channel.write_timeout = previous if @channel
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 判断信道尚未建立或已经关闭。
|
|
71
|
+
def closed? = !@channel || @channel.closed?
|
|
72
|
+
|
|
73
|
+
# 设置日志输出目标;nil 表示暂时停止记录。
|
|
74
|
+
def log_output=(target)
|
|
75
|
+
@log_target = target
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 清除信道和日志目标,并强制关闭底层进程。
|
|
79
|
+
def close
|
|
80
|
+
channel = @channel
|
|
81
|
+
@channel = nil
|
|
82
|
+
@log_target = nil
|
|
83
|
+
channel&.hard_close
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# 把人工交互结果转换为设备 EOF、超时或输入状态。
|
|
87
|
+
def interact(**)
|
|
88
|
+
stopped = @channel.interact(**)
|
|
89
|
+
return :device_eof if stopped.equal?(@channel)
|
|
90
|
+
|
|
91
|
+
stopped.nil? ? :timeout : :input
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 返回传输主机和关闭状态摘要。
|
|
95
|
+
def inspect = "#<#{self.class} host=#{configuration.host.inspect} closed=#{closed?}>"
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# 检查缓冲上限并把设备输出写入当前日志目标。
|
|
100
|
+
def write_log_output(bytes)
|
|
101
|
+
if @channel.buffer.bytesize > configuration.max_output_bytes
|
|
102
|
+
raise OutputLimitExceeded.new("transport buffer exceeded max_output_bytes",
|
|
103
|
+
phase: :read, output: @channel.buffer.dup), cause: nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@log_target&.write(bytes)
|
|
107
|
+
@log_target.flush if @log_target.respond_to?(:flush)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 生成 OpenSSH 参数,并处理显式请求的旧版协商和主机密钥操作。
|
|
112
|
+
class Ssh < Pty
|
|
113
|
+
attr_reader :legacy_arguments
|
|
114
|
+
|
|
115
|
+
# 保存旧版 SSH 参数并复用伪终端初始化。
|
|
116
|
+
def initialize(configuration, legacy_arguments: [], **)
|
|
117
|
+
super(configuration, **)
|
|
118
|
+
@legacy_arguments = legacy_arguments.dup.freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def open
|
|
122
|
+
configuration.validate_endpoint!
|
|
123
|
+
unless configuration.host_key_policy == :strict
|
|
124
|
+
@known_hosts = KnownHosts.new(configuration, replace: @replace_host_key)
|
|
125
|
+
end
|
|
126
|
+
super
|
|
127
|
+
rescue Exception # rubocop:disable Lint/RescueException -- Release a staged trust file on failed startup.
|
|
128
|
+
close
|
|
129
|
+
raise
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def authenticated
|
|
133
|
+
@known_hosts&.commit
|
|
134
|
+
@replace_host_key = false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def close
|
|
138
|
+
super
|
|
139
|
+
ensure
|
|
140
|
+
@known_hosts&.close
|
|
141
|
+
@known_hosts = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# 返回 SSH 协议标识。
|
|
145
|
+
def protocol = :ssh
|
|
146
|
+
|
|
147
|
+
# 根据配置生成经过端点校验的 SSH 参数数组。
|
|
148
|
+
def argv
|
|
149
|
+
configuration.validate_endpoint!
|
|
150
|
+
checking = (configuration.host_key_policy == :strict) ? "yes" : "accept-new"
|
|
151
|
+
arguments = ["ssh", *legacy_arguments, "-tt", "-o", "StrictHostKeyChecking=#{checking}",
|
|
152
|
+
"-o", "NumberOfPasswordPrompts=1", "-o", "UpdateHostKeys=no",
|
|
153
|
+
"-o", "ConnectTimeout=#{[configuration.login_timeout.ceil, 1].max}"]
|
|
154
|
+
known_hosts = @known_hosts&.path || configuration.known_hosts
|
|
155
|
+
arguments += ["-o", "UserKnownHostsFile=#{known_hosts}"] if known_hosts
|
|
156
|
+
arguments += ["-p", configuration.port.to_s] if configuration.port
|
|
157
|
+
arguments + ["-l", configuration.username, configuration.host]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# 创建带旧版协商参数的同类传输对象。
|
|
161
|
+
def with_legacy(arguments)
|
|
162
|
+
self.class.new(configuration, legacy_arguments: arguments, channel_factory: @channel_factory,
|
|
163
|
+
terminal_size: @terminal_size)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 创建不继承 SSH 专用端口的 Telnet 传输对象。
|
|
167
|
+
def as_telnet
|
|
168
|
+
Telnet.new(configuration, channel_factory: @channel_factory, terminal_size: @terminal_size)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# 恢复只标记下一次会话副本;共享信任文件在认证成功后才更新。
|
|
172
|
+
def replace_host_key
|
|
173
|
+
raise IOError, "host key replacement is not enabled" unless configuration.host_key_policy == :replace
|
|
174
|
+
|
|
175
|
+
@replace_host_key = true
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Telnet 只能显式启用;SSH 专用端口不能带入回退连接。
|
|
181
|
+
class Telnet < Pty
|
|
182
|
+
# 返回 Telnet 协议标识。
|
|
183
|
+
def protocol = :telnet
|
|
184
|
+
|
|
185
|
+
# 根据配置生成经过端点校验的 Telnet 参数数组。
|
|
186
|
+
def argv
|
|
187
|
+
configuration.validate_endpoint!
|
|
188
|
+
port = (configuration.protocol == :telnet) ? configuration.port : nil
|
|
189
|
+
["telnet", "-l", configuration.username, configuration.host, *(port ? [port.to_s] : [])]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
TYPES = { ssh: Ssh, telnet: Telnet }.freeze
|
|
194
|
+
|
|
195
|
+
# 按协议选择具体传输实现。
|
|
196
|
+
def self.build(configuration, **)
|
|
197
|
+
TYPES.fetch(configuration.protocol).new(configuration, **)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
require "expect/redactor"
|
|
6
|
+
|
|
7
|
+
module Netstack
|
|
8
|
+
module Connector
|
|
9
|
+
# 安全保存底层异常,不保留包含凭据的原始消息。
|
|
10
|
+
class UnderlyingError
|
|
11
|
+
attr_reader :type, :message, :backtrace
|
|
12
|
+
|
|
13
|
+
# 保存脱敏后的异常类型、消息和调用栈。
|
|
14
|
+
def initialize(error, redactor, sensitive: false)
|
|
15
|
+
@type = error.is_a?(UnderlyingError) ? error.type : error.class.name
|
|
16
|
+
@message = (sensitive ? "[REDACTED]" : redactor.call(error.message)).freeze
|
|
17
|
+
@backtrace = (sensitive ? [] : Array(error.backtrace).map { |line| redactor.call(line).freeze }).freeze
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 返回底层异常类型摘要。
|
|
22
|
+
def inspect = "#<#{self.class} type=#{type}>"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class CiscoIos
|
|
9
|
+
class RunningConfig < Netstack::Connector::RunningConfig::Rendered
|
|
10
|
+
# 去掉终端长度设置、保存命令回显和易变化的配置时间注释。
|
|
11
|
+
def clean(text)
|
|
12
|
+
config = super
|
|
13
|
+
config.gsub!(/^[^\n]*#\s*terminal\s+length\s+0\s*\n/i, "")
|
|
14
|
+
config.sub!(/^[^\n]*#\s*copy\s+run(?:ning-config)?\s+(?:start|startup-config)\s*\n.*\z/im, "")
|
|
15
|
+
config.gsub!(/^!.*(?:Last\s+configuration\s+change|NVRAM\s+config).*\n/i, "")
|
|
16
|
+
config
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class CiscoIos
|
|
9
|
+
class TftpBackup < Tftp::Strategy
|
|
10
|
+
# 在连接前拒绝当前厂商不支持的参数组合。
|
|
11
|
+
def validate_options!(_target, source_file:, vrf: nil)
|
|
12
|
+
raise ArgumentError, "Cisco IOS does not use source_file" unless source_file.nil?
|
|
13
|
+
raise ArgumentError, "Cisco IOS TFTP backup does not use vrf" unless vrf.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def receipt_metadata(_target, **) = { configuration_kind: :running, format: :cfg }
|
|
17
|
+
|
|
18
|
+
# 构造当前厂商的 TFTP 导出交互脚本。
|
|
19
|
+
def script(target, source_file:, vrf: nil)
|
|
20
|
+
validate_options!(target, source_file: source_file, vrf: vrf)
|
|
21
|
+
|
|
22
|
+
Script.new([Command.new("copy running-config tftp:", timeout: 180,
|
|
23
|
+
interactions: [
|
|
24
|
+
interaction(/Address\s+or\s+name\s+of\s+remote\s+host\s*\[[^\]]*\]\?\s*\z/i,
|
|
25
|
+
"#{target.host}\n"),
|
|
26
|
+
interaction(/Destination\s+filename\s*\[[^\]]*\]\?\s*\z/i,
|
|
27
|
+
"#{target.path}\n")
|
|
28
|
+
])])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 检查设备回显是否确认 TFTP 传输完成。
|
|
32
|
+
def device_reported_complete?(result)
|
|
33
|
+
completion_lines(result).any? do |line|
|
|
34
|
+
copied_bytes?(line) || line.match?(/\A\[OK(?:\s*-\s*[1-9]\d*\s+bytes)?\]\z/i)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class CiscoIos
|
|
9
|
+
class Topology < Netstack::Connector::Topology::ImmediateStrategy
|
|
10
|
+
# IOS 提供 CDP 邻居、配置描述读取和描述变更。
|
|
11
|
+
def self.supports?(capability)
|
|
12
|
+
%i[neighbors interface_descriptions interface_description_changes].include?(capability)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# 使用详细 CDP 输出获取对端设备与端口。
|
|
16
|
+
def neighbor_command = "show cdp neighbors detail"
|
|
17
|
+
|
|
18
|
+
# 从运行配置解析接口描述。
|
|
19
|
+
def description_template = "cisco_ios_running_config_interfaces.textfsm"
|
|
20
|
+
|
|
21
|
+
# 标记发现协议,供邻居记录使用。
|
|
22
|
+
def protocol = :cdp
|
|
23
|
+
|
|
24
|
+
# 进入全局配置视图。
|
|
25
|
+
def enter_configuration_command = "configure terminal"
|
|
26
|
+
|
|
27
|
+
# 先回到执行视图读回,随后才执行档案中的保存命令。
|
|
28
|
+
def leave_configuration_commands = ["end"]
|
|
29
|
+
|
|
30
|
+
def persistence_pattern = /\A(?:\[OK\]|Copy complete\.)\z/i
|
|
31
|
+
|
|
32
|
+
# 每个 Device ID 块都须解析;设备提供总数时还须与块数相符。
|
|
33
|
+
def expected_neighbor_count(output, _template)
|
|
34
|
+
count = output.scan(/^\s*Device ID:/i).size
|
|
35
|
+
totals = output.scan(/^\s*Total cdp entries displayed\s*:\s*(\d+)\b/i).flatten
|
|
36
|
+
return unless totals.all? { |total| total.to_i == count }
|
|
37
|
+
|
|
38
|
+
count
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 只有设备明确报告零 CDP 邻居时才认可空表。
|
|
42
|
+
def empty_neighbor_output?(output)
|
|
43
|
+
output.match?(/Total cdp entries displayed\s*:\s*0\b|no cdp neighbors/i)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 复用公共接口描述命令,保留本机接口原名。
|
|
47
|
+
def change_commands(change)
|
|
48
|
+
Netstack::Connector::Topology::InterfaceDescription.commands(interface: change.interface, description: change.new_description)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class CiscoIos < Netstack::Connector::Device
|
|
9
|
+
vendor :cisco_ios
|
|
10
|
+
# 声明 IOS 的命令、提示、分页、诊断和保存确认规则。
|
|
11
|
+
profile do
|
|
12
|
+
running_config_strategy Netstack::Connector::Vendors::CiscoIos::RunningConfig
|
|
13
|
+
tftp_strategy Netstack::Connector::Vendors::CiscoIos::TftpBackup
|
|
14
|
+
topology_strategy Netstack::Connector::Vendors::CiscoIos::Topology
|
|
15
|
+
commands do
|
|
16
|
+
running_config "terminal length 0", "show running-config"
|
|
17
|
+
save_config "copy running-config startup-config"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
prompts do
|
|
21
|
+
login(/(?:\A|(?<=[\r\n]))\S+[>#]\s*\z/)
|
|
22
|
+
command(/(?:\A|(?<=[\r\n]))\S+#\s*\z/)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
pager do
|
|
26
|
+
pattern(/(?:\A|(?<=[\r\n]))[^\S\r\n]*--More--[^\r\n]*(?=\r|\n|\z)/i)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
errors do
|
|
30
|
+
command(/^[ \t]*%[ \t]+(?:Invalid|Incomplete|Ambiguous)\s+(?:input|command)\b/i, /^[ \t]*\^/)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
command_timeout 60
|
|
34
|
+
|
|
35
|
+
interactions do
|
|
36
|
+
confirm(/Destination\s+filename\s+\[[^\]]+\]\?\s*\z/i, response: "\n")
|
|
37
|
+
confirm(%r{Do\s+you\s+want\s+to\s+overwrite\?\s*\[yes/no\]\s*\z}i, response: "yes\n")
|
|
38
|
+
confirm(/Proceed\s+with\s+copy\?\s*\[confirm\]\s*\z/i, response: "\n")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class CiscoNxos
|
|
9
|
+
class RunningConfig < CiscoIos::RunningConfig
|
|
10
|
+
# 先清除 NX-OS 分页与进度回显,再复用 IOS 的配置清理规则。
|
|
11
|
+
def clean(text)
|
|
12
|
+
without_pager = text.to_s.gsub(
|
|
13
|
+
/[ \t]*(?:\x1b\[[0-9;]*[A-Za-z])*--More--
|
|
14
|
+
(?:\x1b\[[0-9;]*[A-Za-z])*(?:[\r\x00\x08 ]|\x1b\[[0-9;]*[A-Za-z])*/ix, ""
|
|
15
|
+
)
|
|
16
|
+
config = super(without_pager)
|
|
17
|
+
config.sub!(/\A.*?^(?=[^\n]*#\s*show\s+running-config\s*$)/im, "")
|
|
18
|
+
config.gsub(/^[ \t]*(?:\[[# ]*\]|\])[ \t]*\d+%[ \t]*\n?/i, "")
|
|
19
|
+
.gsub(/^[ \t]*Copy\s+complete\b[^\n]*\n?/i, "")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class CiscoNxos
|
|
9
|
+
class TftpBackup < Tftp::Strategy
|
|
10
|
+
# 在连接前拒绝当前厂商不支持的参数组合。
|
|
11
|
+
def validate_options!(_target, source_file:, **)
|
|
12
|
+
raise ArgumentError, "Cisco NX-OS does not use source_file" unless source_file.nil?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def receipt_metadata(_target, **) = { configuration_kind: :running, format: :cfg }
|
|
16
|
+
|
|
17
|
+
# 构造当前厂商的 TFTP 导出交互脚本。
|
|
18
|
+
def script(target, source_file:, vrf: nil)
|
|
19
|
+
validate_options!(target, source_file: source_file, vrf: vrf)
|
|
20
|
+
|
|
21
|
+
vrf ||= "management"
|
|
22
|
+
command = "copy running-config tftp://#{target.host}/#{target.path} vrf #{vrf}"
|
|
23
|
+
Script.new([Command.new(command, timeout: 180,
|
|
24
|
+
interactions: [
|
|
25
|
+
interaction(/Enter\s+vrf\s*\([^\r\n]*\):\s*\z/i,
|
|
26
|
+
"#{vrf}\n")
|
|
27
|
+
])])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 检查设备回显是否确认 TFTP 传输完成。
|
|
31
|
+
def device_reported_complete?(result)
|
|
32
|
+
completion_lines(result).any? do |line|
|
|
33
|
+
line.match?(/\A
|
|
34
|
+
(?:transfer|copy)(?:\s+operation)?\s+
|
|
35
|
+
(?:success(?:ful(?:ly)?)?|succeeded|complete(?:d)?(?:\s+successfully)?)[.!]?
|
|
36
|
+
\z/ix) || copied_bytes?(line)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class CiscoNxos < Netstack::Connector::Device
|
|
9
|
+
vendor :cisco_nxos
|
|
10
|
+
# 声明 NX-OS 的命令、提示、分页、诊断和保存确认规则。
|
|
11
|
+
profile do
|
|
12
|
+
running_config_strategy Netstack::Connector::Vendors::CiscoNxos::RunningConfig
|
|
13
|
+
tftp_strategy Netstack::Connector::Vendors::CiscoNxos::TftpBackup
|
|
14
|
+
topology_strategy Netstack::Connector::Vendors::CiscoIos::Topology
|
|
15
|
+
commands do
|
|
16
|
+
running_config "terminal length 0", "show running-config"
|
|
17
|
+
save_config "copy run start"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
prompts do
|
|
21
|
+
login(/\S+[>#]\s*\z/)
|
|
22
|
+
command(/\S+#\s*\z/)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
pager do
|
|
26
|
+
pattern(/(?:\A|(?<=[\r\n]))[ \t]*(?:\x1b\[[0-9;]*[A-Za-z])*--More--
|
|
27
|
+
(?:[ \t\x00\x08]|\x1b\[[0-9;]*[A-Za-z])*(?=\r|\n|\z)/ix)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
errors do
|
|
31
|
+
command(
|
|
32
|
+
/^[ \t]*%?[ \t]*(?:(?:Invalid|Ambiguous|Incomplete)\s+command(?:\s+\([^)]+\))?|
|
|
33
|
+
Invalid\s+parameter\s+detected)\s+at\s+'\^'\s+marker\./ix,
|
|
34
|
+
/^[ \t]*\^/,
|
|
35
|
+
/^[ \t]*%[ \t]+(?:Invalid|Ambiguous|Incomplete)\s+command\b/i,
|
|
36
|
+
/^[ \t]*%?[ \t]*Invalid\s+parameter\s+detected\b/i,
|
|
37
|
+
/^[ \t]*syntax\s+error\b/i
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
command_timeout 60
|
|
42
|
+
|
|
43
|
+
interactions do
|
|
44
|
+
confirm(/Destination\s+filename\s+\[[^\]]+\]\?\s*\z/i, response: "\n")
|
|
45
|
+
confirm(%r{Do\s+you\s+want\s+to\s+overwrite\?\s*\[yes/no\]\s*\z}i, response: "yes\n")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class H3c
|
|
9
|
+
class TftpBackup < Tftp::FileUpload
|
|
10
|
+
# 自动探测选择的是启动配置;显式文件仍不能推断为运行或启动配置。
|
|
11
|
+
def receipt_metadata(target, source_file:, explicit_source:)
|
|
12
|
+
super.merge(configuration_kind: explicit_source ? :saved_file : :startup)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# 选取并校验 H3C 启动配置文件路径。
|
|
16
|
+
def resolve_source_file(value)
|
|
17
|
+
return TftpTarget.validate_source_file!(value) if value
|
|
18
|
+
|
|
19
|
+
output = @device.execute_command("display startup").value!
|
|
20
|
+
rendered = TerminalRenderer.render(output)
|
|
21
|
+
path = rendered[/^\s*Next main startup saved-configuration file:\s*(\S+)/i, 1]
|
|
22
|
+
path ||= rendered[/^\s*Current startup saved-configuration file:\s*(\S+)/i, 1]
|
|
23
|
+
if path.nil? || path.casecmp?("NULL")
|
|
24
|
+
raise DeviceError.new("device has no saved startup configuration",
|
|
25
|
+
code: :startup_config_missing, host: @device.host, phase: :tftp_backup)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
TftpTarget.validate_source_file!(path.sub(/\(\*\)\z/, ""))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adapted from net-connector; copyright (c) 2026 net-connector contributors (MIT).
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Connector
|
|
7
|
+
module Vendors
|
|
8
|
+
class H3c
|
|
9
|
+
class Topology < Netstack::Connector::Topology::ImmediateStrategy
|
|
10
|
+
TABLE_HEADER = /^\s*(?:System Name\s+Local Interface\b|(?:Local Interface|LocalIf)\s+)/i
|
|
11
|
+
# H3C 提供 LLDP 邻居、配置描述读取和描述变更。
|
|
12
|
+
def self.supports?(capability)
|
|
13
|
+
%i[neighbors interface_descriptions interface_description_changes].include?(capability)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 读取设备的 LLDP 邻居列表。
|
|
17
|
+
def neighbor_command = "display lldp neighbor-information list"
|
|
18
|
+
|
|
19
|
+
# 从运行配置解析当前接口描述。
|
|
20
|
+
def description_template = "h3c_interface_descriptions.textfsm"
|
|
21
|
+
|
|
22
|
+
# 进入系统视图以修改接口描述。
|
|
23
|
+
def enter_configuration_command = "system-view"
|
|
24
|
+
|
|
25
|
+
# 返回用户视图读回;保存交给后续独立阶段。
|
|
26
|
+
def leave_configuration_commands = ["return"]
|
|
27
|
+
|
|
28
|
+
def persistence_pattern = /\ASaved the current configuration to mainboard device successfully\.\z/i
|
|
29
|
+
|
|
30
|
+
# H3C 不同版本的列顺序不同,按表头选对应模板。
|
|
31
|
+
def neighbor_template(output)
|
|
32
|
+
return "h3c_lldp_name_first.textfsm" if output.match?(/^\s*System Name\s+Local Interface\b/im)
|
|
33
|
+
return "h3c_lldp_local_first.textfsm" if output.match?(/^\s*(?:Local Interface|LocalIf)\s+/im)
|
|
34
|
+
|
|
35
|
+
raise ParsingError.new("H3C LLDP table header was not recognized", code: :unrecognized_output,
|
|
36
|
+
host: @device.host, phase: :discover)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 按表头后的有效数据行计数,未知行会让完整性校验失败。
|
|
40
|
+
def expected_neighbor_count(output, template)
|
|
41
|
+
local_column = template == "h3c_lldp_name_first.textfsm" ? 1 : 0
|
|
42
|
+
lines = neighbor_lines(output)
|
|
43
|
+
return unless lines.all? do |line|
|
|
44
|
+
interface = line.split.fetch(local_column, nil)
|
|
45
|
+
interface&.match?(/\A[A-Za-z][A-Za-z-]*\d+(?:\/\d+)+\z/)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
lines.size
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 表头之后的未知行不能作为零邻居证据。
|
|
52
|
+
def empty_neighbor_output?(output) = neighbor_lines(output).empty?
|
|
53
|
+
|
|
54
|
+
# 在接口视图设置描述后用 quit 返回系统视图。
|
|
55
|
+
def change_commands(change)
|
|
56
|
+
Netstack::Connector::Topology::InterfaceDescription.commands(interface: change.interface, description: change.new_description, leave: "quit")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# 过滤表头、分隔线和设备提示符,保留待解析的邻居行。
|
|
62
|
+
def neighbor_lines(output)
|
|
63
|
+
output.lines.drop_while { |line| !line.match?(TABLE_HEADER) }.map(&:strip).reject do |line|
|
|
64
|
+
line.empty? || line.match?(TABLE_HEADER) || line.match?(/\A[-=+ ]+\z/) ||
|
|
65
|
+
line.match?(/\A(?:<[^>]+>|\[[^\]]+\])\z/)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|