net-connector 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/docs/architecture.md +2 -0
- data/docs/usage.md +2 -0
- data/docs/usage.zh-CN.md +2 -0
- data/lib/net/connector/device/running_config/strategy.rb +1 -1
- data/lib/net/connector/device/running_config.rb +1 -1
- data/lib/net/connector/device/tftp/strategy.rb +2 -2
- data/lib/net/connector/device/topology/immediate_strategy.rb +1 -1
- data/lib/net/connector/engine/command.rb +2 -2
- data/lib/net/connector/engine/dialogue.rb +22 -4
- data/lib/net/connector/engine/known_hosts.rb +4 -4
- data/lib/net/connector/engine/log/stream.rb +1 -1
- data/lib/net/connector/engine/log.rb +2 -2
- data/lib/net/connector/engine/response_reader.rb +7 -7
- data/lib/net/connector/engine/result.rb +16 -1
- data/lib/net/connector/engine/terminal_renderer.rb +1 -1
- data/lib/net/connector/engine/transport.rb +11 -4
- data/lib/net/connector/netdisco/backup_run.rb +1 -1
- data/lib/net/connector/netdisco/cli/options.rb +1 -1
- data/lib/net/connector/netdisco/connection.rb +1 -1
- data/lib/net/connector/netdisco/progress.rb +1 -1
- data/lib/net/connector/vendor/h3c/topology.rb +1 -1
- data/lib/net/connector/vendor/hillstone/topology.rb +1 -1
- data/lib/net/connector/vendor/palo_alto/running_config.rb +1 -1
- data/lib/net/connector/vendor/palo_alto/topology.rb +1 -1
- data/lib/net/connector/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ff14cb75482326d5580fefe4341ab39d727c1bc0d35daa90ebf34f534dbf1d1
|
|
4
|
+
data.tar.gz: eb6da5eca7eed4764f9dfa12e67708625d32cb597f206a2af1711047bcd1582c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57c9e407fab11396ce830e35a54eddf0f5266864e76840407c7df691fa0ddf4e3f894c0e708a62f79c4e8ee2ba2236c04d3c503206e64d4d0892a1d17b74eb16
|
|
7
|
+
data.tar.gz: bfeb19f27232509e79a96b1156342365ece7ecec96a66bd56207da48dcaf3db334940aab8d8966fb0028403aa50762b7d989da3618b892b952aa49bc43293825
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.11.0 - 2026-10-17
|
|
4
|
+
|
|
5
|
+
- 固定协议行分隔符,不再依赖宿主的 `$/`:并发 SSH 主机密钥冲突仍会拒绝,脚本、PAN-OS 配置、TFTP/保存证据及日志按设备行解析;标准输入凭据和隐藏密码只消费当前一行。
|
|
6
|
+
- 传输事件在构造时保存不可变的二进制快照,自定义传输复用缓冲区不再覆盖已接收分片或超时证据;PTY 将字节规范化集中到事件边界,避免重复转换。自定义传输应为每次读取创建新事件,不能再修改已构造事件的字段。
|
|
7
|
+
- 结果汇总在拼接前统一为原始字节,混合 UTF-8、二进制及其他编码的已完成步骤不再触发编码冲突;单条结果省去中间数组与拼接,返回值仍是独立、可修改的二进制字符串。
|
|
8
|
+
- 命令回执在构造时校验命令、输出、提示及有限非负耗时,避免非法对象进入已完成步骤后才在汇总或诊断时失败。
|
|
9
|
+
- 自定义对话提前校验错误匹配与交互列表,复制并冻结自身规则,不再冻结调用方数组;无效规则在会话开始前报告。
|
|
10
|
+
- 字节、诊断、SSH 信任文件及终端进度拼接使用显式空分隔符,避免宿主设置 Ruby 的 `$,` 后将额外字符插入响应、错误证据或文件。
|
|
11
|
+
|
|
3
12
|
## 0.10.3 - 2026-10-16
|
|
4
13
|
|
|
5
14
|
- 保护会话锁取得、登记和释放的异步取消边界,完整操作、单次执行和关闭共用持锁作用域;业务等待仍可取消,非局部退出仍释放锁。
|
data/docs/architecture.md
CHANGED
|
@@ -139,6 +139,8 @@ Diagnostic 只保存固定词表中的码、类型、阶段和受控产物状态
|
|
|
139
139
|
|
|
140
140
|
## 迁移与尚未启用的能力
|
|
141
141
|
|
|
142
|
+
自定义字节传输每次 `read` 返回新的 `Net::Connector::Event`。事件及其 `before`、`match` 字节串不可变;构造时复制可变输入并统一为二进制,调用方可继续复用源缓冲区。已有事件不能再通过 Struct 的字段赋值器修改,错误对象的身份保留。设备协议、脚本、SSH 信任文件和标准输入凭据使用显式换行符,不继承宿主的 `$/` 或 `$,` 设置。
|
|
143
|
+
|
|
142
144
|
| 变化 | 调用方需要保留的约定 |
|
|
143
145
|
| --- | --- |
|
|
144
146
|
| 配置采集默认输出敏感 | 所有日志模式及错误正文隐藏配置;Result.config/steps 和备份仍保留原数据。自定义敏感业务需声明 output_sensitive,不以注册登录密码代替正文保护 |
|
data/docs/usage.md
CHANGED
|
@@ -122,6 +122,8 @@ warn "#{result.error.code}: #{result.steps.size} completed commands" if result.f
|
|
|
122
122
|
|
|
123
123
|
`execute_command` runs one command; `execute_script` accepts a `Script` or command array. `Script.load(path)` reads a file. Commands are validated before I/O; completed steps survive later failures. `save_config` explicitly sends save commands; ordinary scripts do not save automatically.
|
|
124
124
|
|
|
125
|
+
`result.output` concatenates completed steps as raw bytes without transcoding. Each call returns a mutable binary string; changing it does not change the frozen step outputs. Use `result.config` for the cleaned configuration, or `result.value!` to raise on failure before reading the successful value.
|
|
126
|
+
|
|
125
127
|
Set `max_script_output_bytes:` to bound cumulative raw responses, including additional queries. Default is `nil`. Exceeding it returns `ScriptOutputLimitExceeded` (`:script_output_limit_exceeded`) and stops subsequent commands; the current command may already have executed. `max_output_bytes` separately limits each response. Neither setting is a process-memory limit.
|
|
126
128
|
|
|
127
129
|
Per-command prompts use `interactions: [Net::Connector::Interaction.new(/Token:\z/, ->(_) { "value\n" }, sensitive: true)]`. Mark commands containing secrets `sensitive: true`. For safe command text with sensitive output, use `output_sensitive: true`; `running_config` enables this automatically.
|
data/docs/usage.zh-CN.md
CHANGED
|
@@ -169,6 +169,8 @@ end
|
|
|
169
169
|
|
|
170
170
|
`execute_command` 执行一条命令;`execute_script` 接收 `Script` 或命令数组;`Script.load(path)` 读取脚本文件。所有命令在设备 I/O 前校验,后续步骤失败时仍保留已完成结果,库不会自动重放命令。`save_config` 显式执行厂商保存命令,普通脚本不会自动保存。
|
|
171
171
|
|
|
172
|
+
`result.output` 按顺序拼接已完成步骤的原始字节,不执行字符编码转换。每次调用返回可修改的二进制字符串,修改它不会改变已冻结的步骤输出。清理后的配置使用 `result.config`;需要失败时先抛出错误,再读取成功值时使用 `result.value!`。
|
|
173
|
+
|
|
172
174
|
创建连接器时可设置 `max_script_output_bytes: 8 * 1024 * 1024`,限制每个脚本及其追加查询累计收到的原始响应字节;默认 `nil` 保持原有完整输出行为。达到上限后不再发送下一命令,当前响应超过上限时保留刚完成的步骤并返回 `ScriptOutputLimitExceeded`(`code: :script_output_limit_exceeded`)。命令可能已经执行,不会自动重试。原有 `max_output_bytes` 继续限制单次读取响应;累计预算不是进程内存上限,详细计数规则见架构文档。
|
|
173
175
|
|
|
174
176
|
厂商档案处理分页和常见确认提示。特定命令可给 `execute_command` 传入 `interactions: [Net::Connector::Interaction.new(/Token:\z/, ->(_) { "value\n" }, sensitive: true)]`。敏感命令及交互会暂停回显日志并在错误中脱敏;若普通命令文本包含秘密,必须显式标记 `sensitive: true`。
|
|
@@ -18,7 +18,7 @@ module Net
|
|
|
18
18
|
|
|
19
19
|
# 普通采集命令保持视图不变;需要切换视图的采集策略显式调整。
|
|
20
20
|
def prompt_text(_command)
|
|
21
|
-
TerminalRenderer.render(@device.current_prompt.to_s).lines.last.to_s.strip
|
|
21
|
+
TerminalRenderer.render(@device.current_prompt.to_s).lines("\n").last.to_s.strip
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# 公共策略不增加逐命令响应检查。
|
|
@@ -104,7 +104,7 @@ module Net
|
|
|
104
104
|
def config_body?(step)
|
|
105
105
|
body = TerminalRenderer.render(step.output.delete_suffix(step.prompt.to_s)).strip
|
|
106
106
|
# 只需确认首个正文行;不为大配置创建全部行对象或重新拼接整份响应。
|
|
107
|
-
body.each_line.with_index.any? do |line, index|
|
|
107
|
+
body.each_line("\n").with_index.any? do |line, index|
|
|
108
108
|
text = line.strip
|
|
109
109
|
!text.empty? && !(index.zero? && text == step.command.text)
|
|
110
110
|
end
|
|
@@ -59,10 +59,10 @@ module Net
|
|
|
59
59
|
# 移除精确命令、静态响应和提示符,保留回车重绘前后的设备消息。
|
|
60
60
|
def evidence_lines(step, output)
|
|
61
61
|
prompt = TerminalRenderer.render(step.prompt.to_s).strip
|
|
62
|
-
responses = step.command.interactions.filter_map { |reply| reply.static_response&.chomp }
|
|
62
|
+
responses = step.command.interactions.filter_map { |reply| reply.static_response&.chomp("\n") }
|
|
63
63
|
echoes = [step.command.text, "#{prompt}#{step.command.text}", *responses]
|
|
64
64
|
# 原文中的每次回车分开核对,避免重绘回显误报,也保留被覆盖的诊断。
|
|
65
|
-
output.each_line.flat_map { |line| line.split(/\r(?!\n)/) }.map do |line|
|
|
65
|
+
output.each_line("\n").flat_map { |line| line.split(/\r(?!\n)/) }.map do |line|
|
|
66
66
|
# 样式不改变字符历史;光标移动和擦除不能拿来删掉原始失败消息。
|
|
67
67
|
line.gsub(/\e\[[0-9;:]*m/, "").strip
|
|
68
68
|
end.reject { |line| line.empty? || line == prompt || echoes.include?(line) }
|
|
@@ -29,7 +29,7 @@ module Net
|
|
|
29
29
|
failure = /\b(?:error|failed|failure|denied|cannot|unable|unsuccessful|aborted)\b/i
|
|
30
30
|
next false if history.match?(failure) || TerminalRenderer.render(history).match?(failure) || rendered.match?(failure)
|
|
31
31
|
|
|
32
|
-
rendered.lines.any? { |line| line.strip.match?(persistence_pattern) }
|
|
32
|
+
rendered.lines("\n").any? { |line| line.strip.match?(persistence_pattern) }
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -70,8 +70,8 @@ module Net
|
|
|
70
70
|
def self.parse(text, name: nil)
|
|
71
71
|
raise ArgumentError, "script must be a String" unless text.is_a?(String)
|
|
72
72
|
|
|
73
|
-
commands = text.each_line.with_index(1).filter_map do |line, line_number|
|
|
74
|
-
line = line.chomp
|
|
73
|
+
commands = text.each_line("\n").with_index(1).filter_map do |line, line_number|
|
|
74
|
+
line = line.chomp("\n")
|
|
75
75
|
next if line.strip.empty? || line.lstrip.start_with?("#")
|
|
76
76
|
|
|
77
77
|
Command.new(line, source: name, line: line_number)
|
|
@@ -70,10 +70,10 @@ module Net
|
|
|
70
70
|
[login_prompt, command_prompt, password_prompt, username_prompt, enable_prompt].each do |pattern|
|
|
71
71
|
raise ArgumentError, "prompt must be a nonempty Regexp" unless pattern.is_a?(Regexp) && !pattern.match?("")
|
|
72
72
|
end
|
|
73
|
-
@authentication_errors = authentication_errors
|
|
74
|
-
@command_errors = command_errors
|
|
75
|
-
@login_interactions = login_interactions
|
|
76
|
-
@command_interactions = command_interactions
|
|
73
|
+
@authentication_errors = patterns(authentication_errors, :authentication_errors)
|
|
74
|
+
@command_errors = patterns(command_errors, :command_errors)
|
|
75
|
+
@login_interactions = interactions(login_interactions, :login_interactions)
|
|
76
|
+
@command_interactions = interactions(command_interactions, :command_interactions)
|
|
77
77
|
freeze
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -88,6 +88,24 @@ module Net
|
|
|
88
88
|
|
|
89
89
|
private
|
|
90
90
|
|
|
91
|
+
# 自定义对话遵循与 Profile 相同的非空匹配规则,不冻结调用方数组。
|
|
92
|
+
def patterns(values, name)
|
|
93
|
+
unless values.is_a?(Array) && values.all? { |value| value.is_a?(Regexp) && !value.match?("") }
|
|
94
|
+
raise ArgumentError, "#{name} must be an Array of nonempty Regexp objects"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
values.map { |value| value.frozen? ? value : value.dup.freeze }.freeze
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 交互对象自身不可变,只复制所属列表。
|
|
101
|
+
def interactions(values, name)
|
|
102
|
+
unless values.is_a?(Array) && values.all?(Interaction)
|
|
103
|
+
raise ArgumentError, "#{name} must be an Array of Interaction objects"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
values.dup.freeze
|
|
107
|
+
end
|
|
108
|
+
|
|
91
109
|
# 只截取命中错误模式的当前行,避免把整段设备配置写进诊断信息。
|
|
92
110
|
def diagnostic_line(output)
|
|
93
111
|
match = command_errors.lazy.filter_map { |pattern| pattern.match(output) }.first
|
|
@@ -31,7 +31,7 @@ module Net
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def commit
|
|
34
|
-
additions = File.binread(path).lines - @original.lines
|
|
34
|
+
additions = File.binread(path).lines("\n") - @original.lines("\n")
|
|
35
35
|
return if additions.empty?
|
|
36
36
|
|
|
37
37
|
with_lock do
|
|
@@ -42,10 +42,10 @@ module Net
|
|
|
42
42
|
reject_changed_key!(file.path) unless @replace
|
|
43
43
|
remove_host(file.path) if @replace
|
|
44
44
|
contents = File.binread(file.path)
|
|
45
|
-
entries = contents.lines
|
|
45
|
+
entries = contents.lines("\n")
|
|
46
46
|
additions.each { |line| entries << line unless entries.include?(line) }
|
|
47
47
|
File.open(file.path, "wb", 0o600) do |replacement|
|
|
48
|
-
replacement.write(entries.map { |line| line.end_with?("\n") ? line : line + "\n" }.join)
|
|
48
|
+
replacement.write(entries.map { |line| line.end_with?("\n") ? line : line + "\n" }.join(""))
|
|
49
49
|
replacement.flush
|
|
50
50
|
replacement.fsync
|
|
51
51
|
end
|
|
@@ -130,7 +130,7 @@ module Net
|
|
|
130
130
|
output, status = Open3.capture2e("ssh-keygen", "-f", path, "-F", @host)
|
|
131
131
|
raise IOError, "reading device host key failed" unless [0, 1].include?(status.exitstatus)
|
|
132
132
|
|
|
133
|
-
output.lines.reject { |line| line.start_with?("#") }.map { |line| line.split[1, 2] }.uniq
|
|
133
|
+
output.lines("\n").reject { |line| line.start_with?("#") }.map { |line| line.split[1, 2] }.uniq
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
def remove_host(path)
|
|
@@ -138,8 +138,8 @@ module Net
|
|
|
138
138
|
def log_response_output(bytes)
|
|
139
139
|
return unless debug? && !@writer && !@redactor.output_sensitive?
|
|
140
140
|
|
|
141
|
-
@redactor.call(TerminalRenderer.render(@redactor.call(bytes))).each_line do |line|
|
|
142
|
-
write_event("device_output", level: :debug, output: line.chomp)
|
|
141
|
+
@redactor.call(TerminalRenderer.render(@redactor.call(bytes))).each_line("\n") do |line|
|
|
142
|
+
write_event("device_output", level: :debug, output: line.chomp("\n"))
|
|
143
143
|
end
|
|
144
144
|
rescue Error
|
|
145
145
|
raise
|
|
@@ -42,7 +42,7 @@ module Net
|
|
|
42
42
|
end
|
|
43
43
|
next if Expect.monotonic < deadline
|
|
44
44
|
|
|
45
|
-
raise build_read_error(nil, phase, command, raw.join), cause: nil
|
|
45
|
+
raise build_read_error(nil, phase, command, raw.join("")), cause: nil
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -63,22 +63,22 @@ module Net
|
|
|
63
63
|
def validate_event!(event, bytes, raw, phase, command)
|
|
64
64
|
if bytes > @session.configuration.max_output_bytes
|
|
65
65
|
raise @session.build_error(OutputLimitExceeded, "device output exceeded max_output_bytes",
|
|
66
|
-
phase: phase, command: command, output: (raw + [event.match]).join), cause: nil
|
|
66
|
+
phase: phase, command: command, output: (raw + [event.match]).join("")), cause: nil
|
|
67
67
|
end
|
|
68
|
-
raise build_read_error(event, phase, command, raw.join), cause: nil unless event.matched?
|
|
68
|
+
raise build_read_error(event, phase, command, raw.join("")), cause: nil unless event.matched?
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# 完整提示符必须消费字节;交互标记可从业务输出排除,但原始响应始终保留。
|
|
72
72
|
def finish_response(prompt, raw, output, phase, command)
|
|
73
73
|
if prompt.empty?
|
|
74
74
|
raise @session.build_error(PromptError, "prompt pattern did not consume any output",
|
|
75
|
-
phase: phase, command: command, output: raw.join), cause: nil
|
|
75
|
+
phase: phase, command: command, output: raw.join("")), cause: nil
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
raw << prompt
|
|
79
79
|
output << prompt
|
|
80
|
-
raw_text = raw.join
|
|
81
|
-
Response.new(raw: raw_text, output: (output == raw) ? raw_text : output.join, prompt: prompt)
|
|
80
|
+
raw_text = raw.join("")
|
|
81
|
+
Response.new(raw: raw_text, output: (output == raw) ? raw_text : output.join(""), prompt: prompt)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# 重复提示和缺失应答必须在写入前失败;所有应答共用读取操作的截止时间。
|
|
@@ -91,7 +91,7 @@ module Net
|
|
|
91
91
|
value = interaction_response(reply, prompt, phase, command)
|
|
92
92
|
raise build_interaction_error("response unavailable", phase, command), cause: nil unless value.is_a?(String)
|
|
93
93
|
|
|
94
|
-
@session.redactor.remember(value.chomp) if reply.sensitive?
|
|
94
|
+
@session.redactor.remember(value.chomp("\n")) if reply.sensitive?
|
|
95
95
|
@session.write(value, deadline: deadline, phase: phase, command: command)
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "errors"
|
|
4
|
+
require_relative "command"
|
|
4
5
|
|
|
5
6
|
module Net
|
|
6
7
|
module Connector
|
|
@@ -10,6 +11,13 @@ module Net
|
|
|
10
11
|
|
|
11
12
|
# 保存命令输出、提示符和耗时,并冻结结果。
|
|
12
13
|
def initialize(command:, output:, prompt:, duration:)
|
|
14
|
+
raise ArgumentError, "command must be a Command" unless command.is_a?(Command)
|
|
15
|
+
raise ArgumentError, "output must be a String" unless output.is_a?(String)
|
|
16
|
+
raise ArgumentError, "prompt must be a String or nil" unless prompt.nil? || prompt.is_a?(String)
|
|
17
|
+
unless duration.is_a?(Numeric) && duration.real? && duration.finite? && duration >= 0
|
|
18
|
+
raise ArgumentError, "duration must be a finite nonnegative real number"
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
@command = command
|
|
14
22
|
@output = output.frozen? ? output : output.dup.freeze
|
|
15
23
|
@prompt = prompt&.dup&.freeze
|
|
@@ -51,7 +59,14 @@ module Net
|
|
|
51
59
|
def each(&) = steps.each(&)
|
|
52
60
|
|
|
53
61
|
# 拼接所有已完成步骤的输出字节。
|
|
54
|
-
def output
|
|
62
|
+
def output
|
|
63
|
+
return steps.first.output.b if steps.size == 1
|
|
64
|
+
|
|
65
|
+
# 拼接前统一编码;仅在拼接后调用 b 无法避免不同编码之间的冲突。
|
|
66
|
+
steps.map do |step|
|
|
67
|
+
step.output.encoding == Encoding::BINARY ? step.output : step.output.b
|
|
68
|
+
end.join("").force_encoding(Encoding::BINARY)
|
|
69
|
+
end
|
|
55
70
|
|
|
56
71
|
# 成功时返回配置或输出,失败时重新抛出领域错误。
|
|
57
72
|
def value!
|
|
@@ -177,7 +177,7 @@ module Net
|
|
|
177
177
|
return text.b if text.valid_encoding?
|
|
178
178
|
raise Encoding::InvalidByteSequenceError, "terminal editing produced invalid UTF-8" if @strict_utf8
|
|
179
179
|
|
|
180
|
-
text.scrub { |invalid| invalid.bytes.map { |byte| format("\\x%02X", byte) }.join }.b
|
|
180
|
+
text.scrub { |invalid| invalid.bytes.map { |byte| format("\\x%02X", byte) }.join("") }.b
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
end
|
|
@@ -8,9 +8,16 @@ module Net
|
|
|
8
8
|
module Connector
|
|
9
9
|
# 传输事件使用从零开始的模式索引;最后一个额外索引表示流式数据。
|
|
10
10
|
Event = Struct.new(:index, :before, :match, :error) do
|
|
11
|
-
#
|
|
11
|
+
# 固定每次读取的字节快照,传输复用缓冲区时不能改写先前分片。
|
|
12
12
|
def initialize(index: nil, before: "".b, match: "".b, error: nil)
|
|
13
|
-
|
|
13
|
+
unless before.is_a?(String) && match.is_a?(String)
|
|
14
|
+
raise ArgumentError, "before and match must be Strings"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
before = before.b.freeze unless before.frozen? && before.encoding == Encoding::BINARY
|
|
18
|
+
match = match.b.freeze unless match.frozen? && match.encoding == Encoding::BINARY
|
|
19
|
+
super(index, before, match, error)
|
|
20
|
+
freeze
|
|
14
21
|
end
|
|
15
22
|
|
|
16
23
|
# 判断读取是否命中了模式而不是传输错误。
|
|
@@ -79,8 +86,8 @@ module Net
|
|
|
79
86
|
def read(patterns, timeout:)
|
|
80
87
|
reader = @channel.respond_to?(:expect_result) ? :expect_result : :expect
|
|
81
88
|
result = @channel.public_send(reader, *patterns, STREAM, timeout: timeout)
|
|
82
|
-
Event.new(index: result.number && (result.number - 1), before: result.before.to_s
|
|
83
|
-
match: result.match.to_s
|
|
89
|
+
Event.new(index: result.number && (result.number - 1), before: result.before.to_s,
|
|
90
|
+
match: result.match.to_s, error: result.error)
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
# 临时设置写入超时,完成写入后恢复原设置。
|
|
@@ -44,7 +44,7 @@ module Net
|
|
|
44
44
|
preview = plan_summary(plan)
|
|
45
45
|
@output.puts JSON.generate(plan: preview) if options[:json]
|
|
46
46
|
@output.flush
|
|
47
|
-
return 1 if options[:stdin_credentials] && @input.gets&.strip != "RUN"
|
|
47
|
+
return 1 if options[:stdin_credentials] && @input.gets("\n")&.strip != "RUN"
|
|
48
48
|
|
|
49
49
|
Storage::PrivateFile.write(File.join(batch_directory, "plan.json"), JSON.pretty_generate(preview))
|
|
50
50
|
report = execute(fleet, settings, plan, batch_directory, progress, success_policy, server_root)
|
|
@@ -138,7 +138,7 @@ module Net
|
|
|
138
138
|
require "io/console"
|
|
139
139
|
output.print "#{label}:"
|
|
140
140
|
output.flush
|
|
141
|
-
password = input.noecho(
|
|
141
|
+
password = input.noecho { |terminal| terminal.gets("\n") }&.chomp("\n")
|
|
142
142
|
output.puts
|
|
143
143
|
raise ArgumentError, "密码不能为空" if password.nil? || password.empty?
|
|
144
144
|
|
|
@@ -61,7 +61,7 @@ module Net
|
|
|
61
61
|
|
|
62
62
|
# 过滤表头、分隔线和设备提示符,保留待解析的邻居行。
|
|
63
63
|
def neighbor_lines(output)
|
|
64
|
-
output.lines.drop_while { |line| !line.match?(TABLE_HEADER) }.map(&:strip).reject do |line|
|
|
64
|
+
output.lines("\n").drop_while { |line| !line.match?(TABLE_HEADER) }.map(&:strip).reject do |line|
|
|
65
65
|
line.empty? || line.match?(TABLE_HEADER) || line.match?(/\A[-=+ ]+\z/) ||
|
|
66
66
|
line.match?(/\A(?:<[^>]+>|\[[^\]]+\])\z/)
|
|
67
67
|
end
|
|
@@ -52,7 +52,7 @@ module Net
|
|
|
52
52
|
|
|
53
53
|
# 去掉命令、表头、总数和提示符,只保留邻居数据行。
|
|
54
54
|
def neighbor_lines(output)
|
|
55
|
-
output.lines.map(&:strip).reject do |line|
|
|
55
|
+
output.lines("\n").map(&:strip).reject do |line|
|
|
56
56
|
line.empty? || line == neighbor_command || line.match?(/\ASystem Name\s+Local Interface\b/i) ||
|
|
57
57
|
line.match?(/\ATotal lldp neighbor number:\s*\d+\.?\z/i) ||
|
|
58
58
|
line.match?(/\A[-=+ ]+\z/) || line.match?(/\A\S+(?:#|\([MBF]\))\z/)
|
|
@@ -67,7 +67,7 @@ module Net
|
|
|
67
67
|
# 邻居字段区只有空行和提示符时,才视为明确的空块。
|
|
68
68
|
def empty_neighbor_block?(block)
|
|
69
69
|
tail = block.split(/^\s*Neighbor information:\s*$/i, 2)
|
|
70
|
-
tail.size == 2 && tail.last.lines.all? { |line| line.strip.empty? || line.match?(/^\S+[>#]\s*$/) }
|
|
70
|
+
tail.size == 2 && tail.last.lines("\n").all? { |line| line.strip.empty? || line.match?(/^\S+[>#]\s*$/) }
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
end
|