net-connector 0.4.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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE +21 -0
  4. data/README.md +277 -0
  5. data/docs/RELEASING.md +81 -0
  6. data/docs/VERIFICATION.md +73 -0
  7. data/docs/architecture.md +268 -0
  8. data/exe/net-connector-backup +6 -0
  9. data/lib/net/connector/device/base.rb +368 -0
  10. data/lib/net/connector/device/interface_description.rb +41 -0
  11. data/lib/net/connector/device/interface_name.rb +56 -0
  12. data/lib/net/connector/device/profile.rb +446 -0
  13. data/lib/net/connector/device/running_config/strategy.rb +29 -0
  14. data/lib/net/connector/device/running_config.rb +69 -0
  15. data/lib/net/connector/engine/authentication.rb +77 -0
  16. data/lib/net/connector/engine/base.rb +4 -0
  17. data/lib/net/connector/engine/command.rb +101 -0
  18. data/lib/net/connector/engine/configuration.rb +131 -0
  19. data/lib/net/connector/engine/core.rb +6 -0
  20. data/lib/net/connector/engine/dialogue.rb +223 -0
  21. data/lib/net/connector/engine/errors.rb +183 -0
  22. data/lib/net/connector/engine/execution.rb +64 -0
  23. data/lib/net/connector/engine/log.rb +227 -0
  24. data/lib/net/connector/engine/log_messages.rb +47 -0
  25. data/lib/net/connector/engine/profile.rb +4 -0
  26. data/lib/net/connector/engine/recovery.rb +57 -0
  27. data/lib/net/connector/engine/result.rb +67 -0
  28. data/lib/net/connector/engine/session.rb +333 -0
  29. data/lib/net/connector/engine/terminal_renderer.rb +158 -0
  30. data/lib/net/connector/engine/transport.rb +184 -0
  31. data/lib/net/connector/engine.rb +4 -0
  32. data/lib/net/connector/netdisco/batch.rb +71 -0
  33. data/lib/net/connector/netdisco/cli.rb +142 -0
  34. data/lib/net/connector/netdisco/client.rb +148 -0
  35. data/lib/net/connector/netdisco/config_file.rb +87 -0
  36. data/lib/net/connector/netdisco/device.rb +75 -0
  37. data/lib/net/connector/netdisco/fleet.rb +258 -0
  38. data/lib/net/connector/netdisco/planner.rb +77 -0
  39. data/lib/net/connector/netdisco/result_store.rb +43 -0
  40. data/lib/net/connector/netdisco/rules.rb +131 -0
  41. data/lib/net/connector/netdisco/settings.rb +183 -0
  42. data/lib/net/connector/netdisco/worker.rb +75 -0
  43. data/lib/net/connector/netdisco.rb +14 -0
  44. data/lib/net/connector/operations/local_backup.rb +49 -0
  45. data/lib/net/connector/operations/parse_output.rb +60 -0
  46. data/lib/net/connector/operations/private_file.rb +25 -0
  47. data/lib/net/connector/operations/running_config/cisco.rb +14 -0
  48. data/lib/net/connector/operations/running_config/hillstone.rb +12 -0
  49. data/lib/net/connector/operations/running_config/palo_alto.rb +12 -0
  50. data/lib/net/connector/operations/running_config/strategy.rb +3 -0
  51. data/lib/net/connector/operations/running_config.rb +16 -0
  52. data/lib/net/connector/operations/saved_config.rb +72 -0
  53. data/lib/net/connector/operations/tftp/cisco_ios.rb +11 -0
  54. data/lib/net/connector/operations/tftp/cisco_nxos.rb +11 -0
  55. data/lib/net/connector/operations/tftp/h3c.rb +11 -0
  56. data/lib/net/connector/operations/tftp/hillstone.rb +11 -0
  57. data/lib/net/connector/operations/tftp/huawei.rb +11 -0
  58. data/lib/net/connector/operations/tftp/palo_alto.rb +11 -0
  59. data/lib/net/connector/operations/tftp/radware.rb +11 -0
  60. data/lib/net/connector/operations/tftp/strategy.rb +77 -0
  61. data/lib/net/connector/operations/tftp_backup.rb +120 -0
  62. data/lib/net/connector/operations/topology/cisco.rb +11 -0
  63. data/lib/net/connector/operations/topology/h3c.rb +11 -0
  64. data/lib/net/connector/operations/topology/hillstone.rb +11 -0
  65. data/lib/net/connector/operations/topology/palo_alto.rb +11 -0
  66. data/lib/net/connector/operations/topology/radware.rb +11 -0
  67. data/lib/net/connector/operations/topology/strategy.rb +53 -0
  68. data/lib/net/connector/operations/topology.rb +192 -0
  69. data/lib/net/connector/operations.rb +20 -0
  70. data/lib/net/connector/templates/cisco_cdp_neighbors_detail.textfsm +7 -0
  71. data/lib/net/connector/templates/cisco_ios_running_config_interfaces.textfsm +12 -0
  72. data/lib/net/connector/templates/cisco_ios_show_ip_interface_brief.textfsm +7 -0
  73. data/lib/net/connector/templates/h3c_interface_descriptions.textfsm +11 -0
  74. data/lib/net/connector/templates/h3c_lldp_local_first.textfsm +8 -0
  75. data/lib/net/connector/templates/h3c_lldp_name_first.textfsm +8 -0
  76. data/lib/net/connector/templates/hillstone_interface_descriptions.textfsm +11 -0
  77. data/lib/net/connector/templates/hillstone_lldp_neighbors.textfsm +7 -0
  78. data/lib/net/connector/templates/index +8 -0
  79. data/lib/net/connector/templates/palo_alto_interface_descriptions.textfsm +6 -0
  80. data/lib/net/connector/templates/palo_alto_lldp_neighbors.textfsm +11 -0
  81. data/lib/net/connector/templates/radware_port_names.textfsm +13 -0
  82. data/lib/net/connector/vendor/cisco_ios/running_config.rb +19 -0
  83. data/lib/net/connector/vendor/cisco_ios/tftp_backup.rb +32 -0
  84. data/lib/net/connector/vendor/cisco_ios/topology.rb +33 -0
  85. data/lib/net/connector/vendor/cisco_ios.rb +48 -0
  86. data/lib/net/connector/vendor/cisco_nxos/running_config.rb +22 -0
  87. data/lib/net/connector/vendor/cisco_nxos/tftp_backup.rb +34 -0
  88. data/lib/net/connector/vendor/cisco_nxos.rb +55 -0
  89. data/lib/net/connector/vendor/h3c/tftp_backup.rb +43 -0
  90. data/lib/net/connector/vendor/h3c/topology.rb +56 -0
  91. data/lib/net/connector/vendor/h3c.rb +77 -0
  92. data/lib/net/connector/vendor/h3c_wireless.rb +14 -0
  93. data/lib/net/connector/vendor/hillstone/running_config.rb +13 -0
  94. data/lib/net/connector/vendor/hillstone/tftp_backup.rb +40 -0
  95. data/lib/net/connector/vendor/hillstone/topology.rb +48 -0
  96. data/lib/net/connector/vendor/hillstone.rb +43 -0
  97. data/lib/net/connector/vendor/huawei/tftp_backup.rb +34 -0
  98. data/lib/net/connector/vendor/huawei.rb +64 -0
  99. data/lib/net/connector/vendor/palo_alto/running_config.rb +70 -0
  100. data/lib/net/connector/vendor/palo_alto/tftp_backup.rb +36 -0
  101. data/lib/net/connector/vendor/palo_alto/topology.rb +63 -0
  102. data/lib/net/connector/vendor/palo_alto.rb +43 -0
  103. data/lib/net/connector/vendor/radware/tftp_backup.rb +40 -0
  104. data/lib/net/connector/vendor/radware/topology.rb +14 -0
  105. data/lib/net/connector/vendor/radware.rb +62 -0
  106. data/lib/net/connector/version.rb +7 -0
  107. data/lib/net/connector.rb +48 -0
  108. data/lib/net-connector.rb +3 -0
  109. metadata +517 -0
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../device/base"
4
+ require_relative "huawei/tftp_backup"
5
+
6
+ module Net
7
+ module Connector
8
+ module Huawei
9
+ # Huawei 命令行连接器;提权延迟到命令批次开始时执行。
10
+ class Connector < Net::Connector::Base
11
+ vendor :huawei
12
+ # 声明 Huawei 的命令、提示、诊断、提权和保存确认规则。
13
+ profile do
14
+ running_config_strategy Net::Connector::RunningConfig::Rendered
15
+ tftp_strategy Net::Connector::Huawei::TftpBackup
16
+ commands do
17
+ running_config "dis cur"
18
+ save_config "save force"
19
+ end
20
+
21
+ prompts do
22
+ login(/(?:\A|(?<=[\r\n]))\s*\S+[>\]]\s*\z/)
23
+ command(/(?:\A|(?<=[\r\n]))[^\r\n]+[>\]]\s*\z/)
24
+ username(/(?:login|Username):\s*\z/i)
25
+ end
26
+
27
+ errors do
28
+ command(
29
+ /^\s*\^/,
30
+ /^[ \t]*error:/i,
31
+ /^[ \t]*Permission\s+denied\./i,
32
+ /
33
+ ^[ \t]*%[ \t]+(?:(?:Unrecognized|Ambiguous|Incomplete)\s+command|Too\s+many\s+parameters)
34
+ \s+found\s+at\s+'\^'\s+position\.
35
+ /ix
36
+ )
37
+ end
38
+
39
+ privilege do
40
+ command "su"
41
+ prompt(/privilege\s+(?:level\s+is|is\s+).+>\s*\z/im)
42
+ end
43
+
44
+ command_timeout 10
45
+
46
+ interactions do
47
+ confirm %r{Are\s+you\s+sure\?\s*\[Y/N\]}i, response: "y\n"
48
+ confirm(/press\s+the\s+enter\s+key\)/i, response: "\n")
49
+ confirm %r{Continue\?\s*\[Y/N\]}i, response: "y\n"
50
+ confirm(/Please\s+input\s+the\s+file\s+name\(\*\.cfg\)\[[^\]]+\]\s*\z/i, response: "\n")
51
+ confirm %r{overwrite\?\s*\[Y/N\]}i, response: "y\n"
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ # 登录后若提示符以 ] 结尾,则确认设备已经处于特权视图。
58
+ def after_login(session, response)
59
+ session.mark_privileged! if response.prompt.include?("]")
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require_relative "../../device/running_config/strategy"
5
+
6
+ module Net
7
+ module Connector
8
+ module PaloAlto
9
+ class RunningConfig < Net::Connector::RunningConfig::Strategy
10
+ def result_step(result)
11
+ result.steps.find { |step| step.command.text == "show" }
12
+ end
13
+
14
+ # 采集切换视图时只改变结束符,仍绑定已认证的设备身份。
15
+ def prompt_text(command)
16
+ prompt = super
17
+ case command.text
18
+ when "configure" then prompt.sub(/[>#]\z/, "#")
19
+ when "exit" then prompt.sub(/[>#]\z/, ">")
20
+ else prompt
21
+ end
22
+ end
23
+
24
+ def check_response(command, response, _execution)
25
+ return unless command.text == "show config diff"
26
+ return if body(response.output, command.text).empty?
27
+
28
+ raise DeviceError.new("PAN-OS candidate differs from running configuration",
29
+ code: :uncommitted_configuration, host: @device.host, phase: :collect)
30
+ end
31
+
32
+ def clean(text)
33
+ normalized = body(text, "show").sub(/(?:\A|\n)[ \t]*\[edit\][ \t]*\z/, "").strip
34
+ unless set_commands?(normalized)
35
+ raise DeviceError.new("PAN-OS configuration must be set format",
36
+ code: :unsupported_configuration_format, host: @device.host, phase: :collect)
37
+ end
38
+ normalized
39
+ end
40
+
41
+ private
42
+
43
+ # 证书等引号内换行必须闭合,不能将截断文本当成配置。
44
+ def set_commands?(text)
45
+ return false if text.empty?
46
+
47
+ pending = +""
48
+ text.each_line do |line|
49
+ next if pending.empty? && line.strip.empty?
50
+ return false if pending.empty? && !line.start_with?("set ")
51
+
52
+ pending << line
53
+ begin
54
+ Shellwords.split(pending)
55
+ pending.clear
56
+ rescue ArgumentError
57
+ # 未闭合引号继续读取;末尾仍未闭合则拒绝整个采集结果。
58
+ end
59
+ end
60
+ pending.empty?
61
+ end
62
+
63
+ def body(text, command)
64
+ text.gsub("\r\n", "\n").sub(/\A[ \t]*#{Regexp.escape(command)}[ \t]*\n/, "")
65
+ .sub(@device.profile.command_prompt, "").strip
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../operations/tftp/strategy"
4
+
5
+ module Net
6
+ module Connector
7
+ module PaloAlto
8
+ class TftpBackup < Operations::Tftp::Strategy
9
+ # 返回 PAN-OS 固定使用的运行配置文件名。
10
+ def default_path(_source_file) = "running-config.xml"
11
+
12
+ # 构造当前厂商的 TFTP 导出交互脚本。
13
+ def script(target, source_file:, vrf: nil)
14
+ raise ArgumentError, "PAN-OS does not use source_file" if source_file
15
+ raise ArgumentError, "PAN-OS TFTP backup does not use vrf" if vrf
16
+ unless target.path == "running-config.xml"
17
+ raise ArgumentError, "PAN-OS TFTP export uses the fixed filename running-config.xml"
18
+ end
19
+
20
+ Script.new([Command.new("tftp export configuration to #{target.host} from running-config.xml",
21
+ timeout: 180)])
22
+ end
23
+
24
+ # 检查设备回显是否确认 TFTP 传输完成。
25
+ def complete?(result)
26
+ completion_lines(result).any? do |line|
27
+ line.match?(/\ASent\s+[1-9]\d*\s+bytes(?:\s+in\s+[\d.]+\s+(?:secs?|seconds?))?[.!]?\z/i)
28
+ end
29
+ end
30
+
31
+ # 返回 PAN-OS 实际上传的固定文件名。
32
+ def remote_path(_target, _result) = "running-config.xml"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../operations/topology/strategy"
4
+
5
+ module Net
6
+ module Connector
7
+ module PaloAlto
8
+ class Topology < Operations::Topology::Strategy
9
+ def self.supports?(capability)
10
+ %i[neighbors interface_descriptions interface_description_changes].include?(capability)
11
+ end
12
+
13
+ def neighbor_command = "show lldp neighbors all"
14
+ def description_template = "palo_alto_interface_descriptions.textfsm"
15
+ def enter_configuration = "configure"
16
+ def finish_commands = ["commit", "exit"]
17
+
18
+ def expected_neighbor_count(output, _template)
19
+ neighbor_blocks(output).count { |block| !empty_neighbor_block?(block) }
20
+ end
21
+
22
+ def empty_neighbor_output?(output)
23
+ return true if output.match?(/No LLDP neighbors/i)
24
+
25
+ blocks = neighbor_blocks(output)
26
+ !blocks.empty? && blocks.all? { |block| empty_neighbor_block?(block) }
27
+ end
28
+
29
+ private
30
+
31
+ # PAN-OS 会列出启用 LLDP、但没有邻居的本机接口;只有明确为空的
32
+ # Neighbor information 块可跳过,缺字段的非空块继续交给数量校验拒绝。
33
+ def neighbor_blocks(output)
34
+ output.split(/^\s*Local information:\s*$/i).select { |block| block.match?(/^\s*Local interface:\s*\S+/i) }
35
+ end
36
+
37
+ def empty_neighbor_block?(block)
38
+ tail = block.split(/^\s*Neighbor information:\s*$/i, 2)
39
+ tail.size == 2 && tail.last.lines.all? { |line| line.strip.empty? || line.match?(/^\S+[>#]\s*$/) }
40
+ end
41
+
42
+ public
43
+
44
+ # 模板按行读取;未闭合的引号表示证据不完整,不能把首行当成旧描述。
45
+ def decode_description(value)
46
+ value = value.strip
47
+ value.start_with?('"') ? Shellwords.split(value).join(" ") : value
48
+ rescue ArgumentError
49
+ raise ParsingError.new("PAN-OS interface comment is not a complete single-line value",
50
+ code: :unrecognized_output, host: @device.host, phase: :parse)
51
+ end
52
+
53
+ def change_commands(change)
54
+ [%Q(set network interface ethernet #{change.interface} comment "#{change.new_description}")]
55
+ end
56
+
57
+ def script_command(command)
58
+ command == "commit" ? Command.new(command, timeout: 300) : command
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../device/base"
4
+ require_relative "palo_alto/running_config"
5
+ require_relative "palo_alto/tftp_backup"
6
+ require_relative "palo_alto/topology"
7
+
8
+ module Net
9
+ module Connector
10
+ module PaloAlto
11
+ # Palo Alto Networks PAN-OS 运维命令行和运行配置采集连接器。
12
+ class Connector < Net::Connector::Base
13
+ vendor :palo_alto
14
+ # 声明 PAN-OS 的命令、提示、超时、诊断和登录确认规则。
15
+ profile do
16
+ running_config_strategy Net::Connector::PaloAlto::RunningConfig
17
+ tftp_strategy Net::Connector::PaloAlto::TftpBackup
18
+ topology_strategy Net::Connector::PaloAlto::Topology
19
+ commands do
20
+ running_config "set cli pager off", "set cli config-output-format set", "show config diff",
21
+ "configure", "show", "exit", "show config diff"
22
+ end
23
+
24
+ prompts do
25
+ login(/(?:\A|(?<=[\r\n]))(?:[^\s@]+)@[^\s>#]+[>#]\s*\z/)
26
+ command(/(?:\A|(?<=[\r\n]))(?:[^\s@]+)@[^\s>#]+[>#]\s*\z/)
27
+ end
28
+
29
+ errors do
30
+ command(/^[ \t]*(?:Invalid syntax|Unknown command|Server error|Command failed)(?:[.:]|\s)/i)
31
+ end
32
+
33
+ command_timeout 120
34
+
35
+ interactions do
36
+ login %r{(?:acknowledge|accept)[\s\S]*(?:yes/no|\[y/n\]|enter\s+yes)[^\r\n]*\z}i,
37
+ response: "yes\n"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../operations/tftp/strategy"
4
+
5
+ module Net
6
+ module Connector
7
+ module Radware
8
+ class TftpBackup < Operations::Tftp::Strategy
9
+ # 生成 Radware 压缩配置的远端文件名。
10
+ def default_path(_source_file) = TftpTarget.filename(@device.host, extension: "tgz")
11
+
12
+ # 构造当前厂商的 TFTP 导出交互脚本。
13
+ def script(target, source_file:, vrf: nil)
14
+ raise ArgumentError, "Radware Alteon does not use source_file" if source_file
15
+ raise ArgumentError, "Radware TFTP backup does not use vrf" if vrf
16
+
17
+ Script.new([Command.new("/cfg/ptcfg #{target.host} -tftp", timeout: 180, interactions: [
18
+ interaction(/Enter\s+(?:hostname|IP address)[^\r\n]*:\s*\z/i, "#{target.host}\n"),
19
+ interaction(/Enter\s+name\s+of\s+(?:\.tgz\s+)?file[^\r\n]*:\s*\z/i, "#{target.path}\n"),
20
+ interaction(/Enter\s+username[^\r\n]*:\s*\z/i, "\n"),
21
+ interaction(/Include\s+private\s+keys\?\s*\[y\/n\]:\s*\z/i, "n\n"),
22
+ interaction(/Enter\s+"?mansync"?\s+to\s+get\s+real\/group\/virt\s+internal\s+index\s+config:\s*\z/i,
23
+ "mansync\n")
24
+ ])])
25
+ end
26
+
27
+ # 检查设备回显是否确认 TFTP 传输完成。
28
+ def complete?(result)
29
+ completion_lines(result).any? do |line|
30
+ line.match?(/\A
31
+ (?:Current\s+)?(?:configuration|config)\s+
32
+ (?:successfully\s+(?:uploaded|tftp'd)|(?:uploaded|tftp'd)(?:\s+successfully)?|
33
+ complete(?:d)?(?:\s+successfully)?)[.!]?
34
+ \z/ix)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../operations/topology/strategy"
4
+
5
+ module Net
6
+ module Connector
7
+ module Radware
8
+ class Topology < Operations::Topology::Strategy
9
+ def self.supports?(capability) = capability == :interface_descriptions
10
+ def description_template = "radware_port_names.textfsm"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../device/base"
4
+ require_relative "radware/tftp_backup"
5
+ require_relative "radware/topology"
6
+
7
+ module Net
8
+ module Connector
9
+ module Radware
10
+ # Radware Alteon 和 DefensePro 命令行配置采集连接器。
11
+ class Connector < Net::Connector::Base
12
+ vendor :radware
13
+ # 声明菜单式 CLI 的命令、提示、超时、诊断和确认规则。
14
+ profile do
15
+ running_config_strategy Net::Connector::RunningConfig::Rendered
16
+ tftp_strategy Net::Connector::Radware::TftpBackup
17
+ topology_strategy Net::Connector::Radware::Topology
18
+ commands do
19
+ running_config "/cfg/dump"
20
+ save_config "/cfg/save"
21
+ end
22
+
23
+ prompts do
24
+ login(/(?:\A|(?<=[\r\n]))(?:>>[^\r\n]*|[^\r\n]+[>#])\s*\z/)
25
+ command(/(?:\A|(?<=[\r\n]))(?:>>[^\r\n]*|[^\r\n]+#)\s*\z/)
26
+ end
27
+
28
+ errors do
29
+ command(/^[ \t]*(?:Unknown|Invalid|Error)\s+command\b/i)
30
+ end
31
+
32
+ command_timeout 30
33
+
34
+ interactions do
35
+ confirm %r{Confirm\s+Sync\s+to\s+Peer\s+\[y/n\]:\s*\z}i, response: "y\n"
36
+ confirm %r{Synchronize\s+configuration\s+changes\?\s+\[y/n\]:\s*\z}i, response: "y\n"
37
+ confirm %r{Confirm\s+saving\s+to\s+FLASH\s+\[y/n\]:\s*\z}i, response: "y\n"
38
+ confirm %r{Confirm\s+operation\s+without\s+applying\s+changes\s+
39
+ \(leave\s+them\s+pending\)\s+\[y/n\]:\s*\z}ix, response: "y\n"
40
+ confirm %r{Confirm\s+operation\s+without\s+saving\s+changes\s+
41
+ \(leave\s+them\s+pending\)\s+\[y/n\]:?\s*\z}ix, response: "y\n"
42
+ confirm(/Confirm\s+seeing\s+above\s+note\s+\[y\]:\s*\z/i, response: "y\n")
43
+ confirm(/Please\s+enter\s+y\s+to\s+perform\s+the\s+action,\s+n\s+to\s+skip\s+it:\s*\z/i,
44
+ response: "y\n"
45
+ )
46
+ confirm %r{Display\s+private\s+keys\?\s+\[y/n\]:\s*\z}i, response: "n\n"
47
+ end
48
+ end
49
+
50
+ # 连接有效期间返回登录横幅中的 HA 状态。
51
+ def ha_state = connected? ? @ha_state : nil
52
+
53
+ protected
54
+
55
+ # 从登录横幅提取 HA 状态并冻结,供连接期间查询。
56
+ def after_login(_session, response)
57
+ @ha_state = response.raw[/^\s*HA\s+State:\s*(\S+)/i, 1]&.freeze
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Connector
5
+ VERSION = "0.4.0"
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "connector/version"
4
+ require_relative "connector/device/base"
5
+
6
+ module Net
7
+ module Connector
8
+ VENDORS = {
9
+ h3c: "H3c",
10
+ h3c_wireless: "H3cWireless",
11
+ cisco_ios: "CiscoIos",
12
+ cisco_nxos: "CiscoNxos",
13
+ radware: "Radware",
14
+ palo_alto: "PaloAlto",
15
+ huawei: "Huawei",
16
+ hillstone: "Hillstone"
17
+ }.freeze
18
+
19
+ ALIASES = {
20
+ "cisco_n9k" => :cisco_nxos,
21
+ "paloalto" => :palo_alto
22
+ }.freeze
23
+
24
+ # 返回当前支持的厂商标识集合。
25
+ def self.vendors = VENDORS.keys
26
+
27
+ # 根据厂商标识查找连接器类。
28
+ def self.vendor_class(vendor)
29
+ key = vendor.to_s.downcase.tr(" -", "__")
30
+ key = ALIASES.fetch(key, key.to_sym)
31
+ name = VENDORS.fetch(key) { raise ArgumentError, "unsupported vendor: #{vendor.inspect}" }
32
+ require_relative "connector/vendor/#{key}"
33
+ const_get(name, false).const_get(:Connector, false)
34
+ end
35
+
36
+ # 根据厂商标识和连接参数创建设备对象。
37
+ def self.build(vendor, **settings)
38
+ vendor_class(vendor).new(**settings)
39
+ end
40
+
41
+ # 在代码块内连接设备并确保会话关闭。
42
+ def self.open(vendor, **settings)
43
+ raise ArgumentError, "open requires a block" unless block_given?
44
+
45
+ vendor_class(vendor).open(**settings) { |device| yield device }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "net/connector"