net-connector 0.17.2 → 0.18.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 +7 -0
- data/docs/architecture.md +6 -0
- data/docs/usage.md +4 -0
- data/docs/usage.zh-CN.md +4 -0
- data/lib/net/connector/collect/progress.rb +21 -21
- data/lib/net/connector/device/base.rb +12 -8
- data/lib/net/connector/device/save_config.rb +2 -2
- data/lib/net/connector/engine/execution.rb +37 -10
- data/lib/net/connector/engine/log/messages.rb +16 -16
- data/lib/net/connector/engine/session.rb +5 -4
- data/lib/net/connector/engine/transports.rb +60 -41
- data/lib/net/connector/inventory/filters.rb +2 -2
- data/lib/net/connector/netdisco/cli/backup.rb +6 -6
- data/lib/net/connector/netdisco/cli/credentials.rb +2 -2
- data/lib/net/connector/netdisco/cli/help.rb +16 -16
- data/lib/net/connector/netdisco/cli/options.rb +44 -44
- data/lib/net/connector/netdisco/cli/setup.rb +18 -18
- data/lib/net/connector/netdisco/cli/topology.rb +21 -21
- data/lib/net/connector/netdisco/cli.rb +24 -24
- data/lib/net/connector/netdisco/config/defaults.rb +1 -1
- data/lib/net/connector/netdisco/config/initializer.rb +13 -13
- data/lib/net/connector/netdisco/progress.rb +17 -17
- data/lib/net/connector/netdisco/report/text.rb +15 -15
- data/lib/net/connector/netdisco/ssh/archive/client.rb +4 -4
- data/lib/net/connector/netdisco/ssh/authentication.rb +8 -10
- data/lib/net/connector/netdisco/topology/files.rb +8 -8
- data/lib/net/connector/netdisco/topology/presentation.rb +15 -15
- data/lib/net/connector/progress.rb +36 -36
- data/lib/net/connector/resources.rb +1 -1
- data/lib/net/connector/version.rb +1 -1
- metadata +6 -6
|
@@ -12,26 +12,26 @@ module Net
|
|
|
12
12
|
# 备份入口保留清单对象接口,显示由公共批次组件处理。
|
|
13
13
|
def plan(plan, parallel:, limit_per_vendor: nil)
|
|
14
14
|
@operation_mode = false
|
|
15
|
-
mode = limit_per_vendor ? "
|
|
15
|
+
mode = limit_per_vendor ? "sample (at most #{limit_per_vendor} devices per vendor)" : "all devices"
|
|
16
16
|
super(total: plan.inventory.size, selected: plan.ready.size, parallel: parallel, label: mode)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# 设备对象只在领域入口转换为显示身份。
|
|
20
|
-
def start(device, label: "
|
|
21
|
-
super(host: device.host, label: "#{label}
|
|
20
|
+
def start(device, label: "Backup")
|
|
21
|
+
super(host: device.host, label: "#{label}(#{device.vendor})")
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# 分别展示已保存、设备报告上传和服务器核验状态。
|
|
25
25
|
def result(outcome)
|
|
26
26
|
error_code = diagnostic_code(outcome)
|
|
27
27
|
message = case outcome.status
|
|
28
|
-
when :backed_up then "
|
|
28
|
+
when :backed_up then "Backup saved (#{outcome.backup.bytes} bytes)"
|
|
29
29
|
when :reported_uploaded
|
|
30
|
-
outcome.backup&.verification == :server_verified ? "
|
|
31
|
-
"
|
|
32
|
-
when :saved_with_error then "
|
|
33
|
-
when :reported_with_error then "
|
|
34
|
-
else "
|
|
30
|
+
outcome.backup&.verification == :server_verified ? "Configuration archived (#{outcome.backup.server_bytes} bytes)" :
|
|
31
|
+
"Device reported a completed upload; server file has not been verified"
|
|
32
|
+
when :saved_with_error then "Backup artifact exists but the operation did not fully succeed (#{error_code || outcome.status})"
|
|
33
|
+
when :reported_with_error then "Device reported an upload but finalization failed (#{error_code || outcome.status})"
|
|
34
|
+
else "Incomplete (#{error_code || outcome.status})"
|
|
35
35
|
end
|
|
36
36
|
super(host: outcome.device.host, success: outcome.success?, duration_ms: outcome.duration_ms, message: message, error_code: error_code)
|
|
37
37
|
end
|
|
@@ -45,10 +45,10 @@ module Net
|
|
|
45
45
|
.group_by { |item| diagnostic_code(item) || item.status }.transform_values(&:size)
|
|
46
46
|
super(success: report.policy_success?, **summary.slice(:succeeded, :partial, :failed, :skipped), failures: failures)
|
|
47
47
|
unverified = summary[:verification]&.fetch(:unverified, 0) || 0
|
|
48
|
-
write(nil, "
|
|
48
|
+
write(nil, "Of these, #{unverified} devices only reported a completed upload; server files have not been verified") if unverified.positive?
|
|
49
49
|
return unless !report.callback_errors.empty? || report.report_error
|
|
50
50
|
|
|
51
|
-
write(nil, "
|
|
51
|
+
write(nil, "Callback errors #{report.callback_errors.size}, report error #{summary[:report_error] || "none"}")
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# 清空上一阶段计数,初始化本次拓扑操作的任务规模与计时。
|
|
@@ -61,7 +61,7 @@ module Net
|
|
|
61
61
|
@stages.clear
|
|
62
62
|
@task_started.clear
|
|
63
63
|
@run_started = monotonic
|
|
64
|
-
emit(nil, "#{label} |
|
|
64
|
+
emit(nil, "#{label} | Selected #{@total} devices; concurrency #{parallel}")
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -89,8 +89,8 @@ module Net
|
|
|
89
89
|
@failures[error_code || status] += 1 unless succeeded || skipped
|
|
90
90
|
if @verbose || failed
|
|
91
91
|
label = Topology::Presentation.status(status, changes: changes)
|
|
92
|
-
reasons = issues.empty? ? "" : "
|
|
93
|
-
emit(device.host, "#{label}
|
|
92
|
+
reasons = issues.empty? ? "" : "; #{Topology::Presentation.issues(issues)}"
|
|
93
|
+
emit(device.host, "#{label}; elapsed #{duration_ms} ms#{error_code ? "(#{error_code})" : ""}#{reasons}")
|
|
94
94
|
elsif !@tty && ((@completed % 25).zero? || @completed == @total)
|
|
95
95
|
emit(nil, status_line)
|
|
96
96
|
end
|
|
@@ -101,15 +101,15 @@ module Net
|
|
|
101
101
|
# 输出拓扑批次的最终状态计数和完成判断。
|
|
102
102
|
def finish_operations(success:, counts:)
|
|
103
103
|
failed = counts.any? { |status, count| Topology::Presentation::FAILURES.include?(status) && count.positive? }
|
|
104
|
-
result = success ? "
|
|
105
|
-
write(nil, "
|
|
104
|
+
result = success ? "complete" : (failed ? "incomplete" : "pending items remain")
|
|
105
|
+
write(nil, "Topology batch finished: #{counts.map { |status, count| "#{Topology::Presentation.status(status)} #{count} devices" }.join(", ")}; result #{result}")
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
private
|
|
109
109
|
|
|
110
110
|
# 拓扑进度保留待核实项计数,备份使用通用成功与失败计数。
|
|
111
111
|
def progress_counts
|
|
112
|
-
@operation_mode ? "
|
|
112
|
+
@operation_mode ? "complete #{@completed} unverified #{@operation_pending} failed #{@failed}" : super
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
# 与持久化摘要采用同一诊断白名单,保留领域结果自身的原始错误字段。
|
|
@@ -14,24 +14,24 @@ module Net
|
|
|
14
14
|
summary = report.statistics
|
|
15
15
|
failed = report.outcomes.reject { |item| item.success? || %i[filtered sample_limit].include?(item.status) }
|
|
16
16
|
groups = failed.group_by { |item| diagnostic_code(item) || item.status }
|
|
17
|
-
lines = ["
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"-" * 72, "
|
|
17
|
+
lines = ["Network device backup report", "=" * 72,
|
|
18
|
+
"Started: #{report.started_at.getlocal("+08:00").strftime("%Y-%m-%d %H:%M:%S %:z")}",
|
|
19
|
+
"Finished: #{report.finished_at.getlocal("+08:00").strftime("%Y-%m-%d %H:%M:%S %:z")}",
|
|
20
|
+
"Inventory: #{plan.inventory.size} devices; selected: #{plan.ready.size} devices; concurrency: #{parallel}",
|
|
21
|
+
"Succeeded: #{summary[:succeeded]}; partial: #{summary[:partial]}; failed: #{summary[:failed]}; skipped: #{summary[:skipped]}",
|
|
22
|
+
"Elapsed: #{(report.duration_ms.to_f / 1000).round(1)} seconds; batch status: #{summary[:status]}",
|
|
23
|
+
"Success policy: #{report.policy}; policy passed: #{report.policy_success?}",
|
|
24
|
+
"Callback errors: #{report.callback_errors.size}; report error: #{summary[:report_error] || "none"}",
|
|
25
|
+
"Status counts: #{report.counts.map { |key, value| "#{key}=#{value}" }.join(", ")}",
|
|
26
|
+
"Incomplete categories: #{groups.empty? ? "none" : groups.map { |key, items| "#{key}=#{items.size}" }.join(", ")}",
|
|
27
|
+
"-" * 72, "Incomplete devices: "]
|
|
28
28
|
lines.concat(failure_lines(failed))
|
|
29
|
-
lines << "
|
|
29
|
+
lines << "none" if failed.empty?
|
|
30
30
|
if report.mode == :tftp
|
|
31
31
|
verification = summary.fetch(:verification)
|
|
32
|
-
lines << "TFTP
|
|
32
|
+
lines << "TFTP server files: verified #{verification[:verified]}, unverified #{verification[:unverified]}."
|
|
33
33
|
end
|
|
34
|
-
lines += ["=" * 72, "
|
|
34
|
+
lines += ["=" * 72, "Backup directory: #{directory}", "Full details: #{File.join(directory, "summary.json")}"]
|
|
35
35
|
Net::Connector::Storage::AtomicFile.write(File.join(directory, "summary.txt"), lines.join("\n") + "\n")
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -39,7 +39,7 @@ module Net
|
|
|
39
39
|
def self.failure_lines(failed)
|
|
40
40
|
failed.map do |item|
|
|
41
41
|
label = item.device.name.to_s.gsub(/[[:cntrl:]]/, " ")
|
|
42
|
-
"#{item.device.host} | #{label} | #{item.status} | #{diagnostic_code(item) || "
|
|
42
|
+
"#{item.device.host} | #{label} | #{item.status} | #{diagnostic_code(item) || "no error code"} | #{item.duration_ms} ms"
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
private_class_method :failure_lines
|
|
@@ -25,11 +25,11 @@ module Net
|
|
|
25
25
|
# 构造不含凭据值的认证不可用提示,在设备任务开始前拒绝执行。
|
|
26
26
|
def initialize(password_configured: false)
|
|
27
27
|
detail = if password_configured
|
|
28
|
-
"
|
|
28
|
+
"NC_TFTP_SSH_PASSWORD was read, but SSH authentication or master connection setup failed; check username, password, host key and network. Wrap passwords containing # or $ in single quotes in .env"
|
|
29
29
|
else
|
|
30
|
-
"
|
|
30
|
+
"Set NC_TFTP_SSH_PASSWORD in the project root .env, or configure a working SSH key/agent"
|
|
31
31
|
end
|
|
32
|
-
super("SSH
|
|
32
|
+
super("SSH archive authentication unavailable: #{detail}. Stopped without connecting to devices.")
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -37,7 +37,7 @@ module Net
|
|
|
37
37
|
class PreparationUnavailable < ArgumentError
|
|
38
38
|
# 提示检查远端运行时及私有目录,不携带 SSH 输出或设备配置。
|
|
39
39
|
def initialize
|
|
40
|
-
super("SSH
|
|
40
|
+
super("SSH archive directory preflight failed: check server python3, the TFTP root and write permissions; archive, batch directories and .net-connector-locks must be owned by the SSH user with mode 0700. Stopped without connecting to devices.")
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -14,17 +14,15 @@ module Net
|
|
|
14
14
|
|
|
15
15
|
# 在同一认证期限内等待一次密码提示及连接结束;主连接可用性由调用方另行核对。
|
|
16
16
|
def self.respond(arguments, password:)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return false unless challenge.number == 1
|
|
17
|
+
Expect.spawn(*arguments, raw: true, buffer_limit: 65_536, write_timeout: 5) do |session|
|
|
18
|
+
deadline = Expect.monotonic + 15
|
|
19
|
+
challenge = session.expect(PASSWORD, REJECTED, timeout: [deadline - Expect.monotonic, 0].max)
|
|
20
|
+
next false unless challenge.number == 1
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
session&.close
|
|
22
|
+
session.write(password + "\n")
|
|
23
|
+
completion = session.expect(PASSWORD, REJECTED, timeout: [deadline - Expect.monotonic, 0].max)
|
|
24
|
+
completion.error == :eof
|
|
25
|
+
end
|
|
28
26
|
end
|
|
29
27
|
end
|
|
30
28
|
end
|
|
@@ -35,16 +35,16 @@ module Net
|
|
|
35
35
|
path = outcome[:device_report]
|
|
36
36
|
return unless path
|
|
37
37
|
|
|
38
|
-
lines = ["
|
|
39
|
-
"
|
|
40
|
-
outcome[:script_file] ? "
|
|
41
|
-
lines << "
|
|
42
|
-
lines << "
|
|
43
|
-
lines << "
|
|
38
|
+
lines = ["Device: #{text(outcome[:name])} (#{text(outcome[:host])})", "Vendor: #{text(outcome[:vendor])}",
|
|
39
|
+
"Status: #{Presentation.status(outcome[:status], changes: outcome.fetch(:changes, 0))}; candidate changes: #{outcome.fetch(:changes, 0)}",
|
|
40
|
+
outcome[:script_file] ? "Command file: #{outcome[:script_file]}" : "Command file: not generated (no verified configuration changes)"]
|
|
41
|
+
lines << "Command SHA-256: #{outcome[:script_sha256]}" if outcome[:script_sha256]
|
|
42
|
+
lines << "Diagnostics: #{text(outcome.dig(:diagnostic, :error_code))}" if outcome[:diagnostic]
|
|
43
|
+
lines << "Configuration commands come from this plan; generating a file does not mean commands were sent. Check status and device receipts for execution results."
|
|
44
44
|
lines.concat(outcome.fetch(:neighbors, []).map do |row|
|
|
45
|
-
"#{text(row[:interface])} #{text(row[:status])}
|
|
45
|
+
"#{text(row[:interface])} #{text(row[:status])} current=#{row[:current].inspect} target=#{row[:expected].inspect}"
|
|
46
46
|
end)
|
|
47
|
-
lines.concat(outcome.fetch(:issues, []).map { |issue| "
|
|
47
|
+
lines.concat(outcome.fetch(:issues, []).map { |issue| "Unverified: #{text(issue[:interface])} #{text(Presentation.reason(issue[:code]))}" })
|
|
48
48
|
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
49
49
|
Storage::AtomicFile.write(path, lines.join("\n") + "\n")
|
|
50
50
|
end
|
|
@@ -6,18 +6,18 @@ module Net
|
|
|
6
6
|
module Topology
|
|
7
7
|
# JSON 状态码保持稳定;终端、汇总和设备说明共用中文含义及分类统计。
|
|
8
8
|
module Presentation
|
|
9
|
-
STATUSES = { planned: "
|
|
10
|
-
discovered: "
|
|
11
|
-
unsupported_operation: "
|
|
12
|
-
missing_credentials: "
|
|
13
|
-
REASONS = { unknown_neighbor_name: "
|
|
14
|
-
ambiguous_neighbor: "
|
|
15
|
-
unsafe_description: "
|
|
9
|
+
STATUSES = { planned: "plan generated", partial_planned: "partial plan generated", unverified: "contains unverified interfaces",
|
|
10
|
+
discovered: "discovery complete", applied: "apply complete", partial_applied: "partial apply complete", manual_required: "manual action required",
|
|
11
|
+
unsupported_operation: "unsupported; skipped", failed: "execution failed", partial: "execution partially failed",
|
|
12
|
+
missing_credentials: "missing credentials", interrupted: "interrupted", not_started: "not started", offline_review: "offline review" }.freeze
|
|
13
|
+
REASONS = { unknown_neighbor_name: "neighbor name missing", unknown_neighbor_interface: "neighbor port missing",
|
|
14
|
+
ambiguous_neighbor: "multiple neighbors on one interface", interface_missing: "local interface not found in configuration",
|
|
15
|
+
unsafe_description: "target description violates safety constraints" }.freeze
|
|
16
16
|
FAILURES = %i[failed partial missing_credentials interrupted not_started].freeze
|
|
17
17
|
|
|
18
18
|
# 已规划但没有变更时明确显示无需修改,避免读者将 planned 理解为已经执行。
|
|
19
19
|
def self.status(value, changes: nil)
|
|
20
|
-
return "
|
|
20
|
+
return "no changes needed" if value.to_sym == :planned && changes == 0
|
|
21
21
|
|
|
22
22
|
STATUSES.fetch(value.to_sym, value.to_s)
|
|
23
23
|
end
|
|
@@ -27,7 +27,7 @@ module Net
|
|
|
27
27
|
|
|
28
28
|
# 汇总逐接口的问题数量,不把身份不足与设备连接或命令失败混为一谈。
|
|
29
29
|
def self.issues(rows)
|
|
30
|
-
rows.map { |row| row[:code] }.tally.map { |code, count| "#{reason(code)} #{count}
|
|
30
|
+
rows.map { |row| row[:code] }.tally.map { |code, count| "#{reason(code)} #{count} interfaces" }.join("; ")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# 设备与接口分别统计;含待核实项的设备也可能拥有可执行的局部计划。
|
|
@@ -43,14 +43,14 @@ module Net
|
|
|
43
43
|
|
|
44
44
|
# 只读计划中有差异或未知证据表示待处理,不能把生成计划描述为配置下发完成。
|
|
45
45
|
def self.result(summary)
|
|
46
|
-
return "
|
|
47
|
-
return "
|
|
48
|
-
return "
|
|
46
|
+
return "incomplete" if summary[:orchestration_error_type] || summary[:interrupted] || summary[:progress_error]
|
|
47
|
+
return "complete" if summary[:success]
|
|
48
|
+
return "execution contains failures" if summary.fetch(:statistics).fetch(:failed).positive?
|
|
49
49
|
|
|
50
|
-
return "
|
|
50
|
+
return "no eligible devices" if summary[:devices].empty?
|
|
51
51
|
|
|
52
|
-
{ plan: "
|
|
53
|
-
apply: "
|
|
52
|
+
{ plan: "plan generated; corrections or verification still required", discover: "discovery finished; unverified interfaces remain",
|
|
53
|
+
apply: "apply finished; pending items remain" }.fetch(summary[:mode], "pending items remain")
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -27,26 +27,26 @@ module Net
|
|
|
27
27
|
|
|
28
28
|
# 输出当前开始读取清单的进度提示。
|
|
29
29
|
def reading_inventory(config: nil)
|
|
30
|
-
write(nil, "
|
|
30
|
+
write(nil, "Reading Netdisco device inventory#{config ? "(configuration: #{config})" : ""}")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# 以实际选中设备初始化进度,跳过的条目不计入执行任务总数。
|
|
34
|
-
def plan(total:, selected:, parallel:, label: "
|
|
34
|
+
def plan(total:, selected:, parallel:, label: "all devices")
|
|
35
35
|
@mutex.synchronize do
|
|
36
36
|
@total = selected
|
|
37
37
|
@completed = @active = 0
|
|
38
38
|
@run_started = monotonic
|
|
39
|
-
emit(nil, "#{label} |
|
|
39
|
+
emit(nil, "#{label} | Inventory #{total} devices; selected #{@total}; skipped #{total - @total} devices; concurrency #{parallel}")
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# 任务开始只表示进入登录阶段,不代表设备已连接。
|
|
44
|
-
def start(host:, label: "
|
|
44
|
+
def start(host:, label: "collecting")
|
|
45
45
|
@mutex.synchronize do
|
|
46
46
|
@active += 1
|
|
47
|
-
@stages[host] = "
|
|
47
|
+
@stages[host] = "logging in"
|
|
48
48
|
@task_started[host] = monotonic
|
|
49
|
-
@verbose ? emit(host, "
|
|
49
|
+
@verbose ? emit(host, "starting#{label}") : refresh
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -59,9 +59,9 @@ module Net
|
|
|
59
59
|
@task_started.delete(host)
|
|
60
60
|
success ? @succeeded += 1 : @failed += 1
|
|
61
61
|
hint = failure_hint(error_code)
|
|
62
|
-
message = "#{message}
|
|
62
|
+
message = "#{message}; #{hint}" if hint
|
|
63
63
|
if @verbose || !success
|
|
64
|
-
emit(host, "#{message}
|
|
64
|
+
emit(host, "#{message}; elapsed #{duration_ms} ms")
|
|
65
65
|
elsif !@tty && ((@completed % 25).zero? || @completed == @total)
|
|
66
66
|
emit(nil, status_line)
|
|
67
67
|
end
|
|
@@ -72,9 +72,9 @@ module Net
|
|
|
72
72
|
# 完成百分比只表示任务结束,最终结果始终由已保存的业务报告决定。
|
|
73
73
|
def finish(success:, succeeded:, partial:, failed:, skipped:, failures: {})
|
|
74
74
|
unless failures.empty?
|
|
75
|
-
write(nil, "
|
|
75
|
+
write(nil, "Incomplete categories: #{failures.sort_by { |code, count| [-count, code.to_s] }.map { |code, count| "#{code}=#{count}" }.join(", ")}")
|
|
76
76
|
end
|
|
77
|
-
write(nil, "
|
|
77
|
+
write(nil, "Batch finished: succeeded #{succeeded}, partial #{partial}, failed #{failed}, skipped #{skipped}; elapsed #{(monotonic - @started_at).round(1)} seconds; result #{success ? "complete" : "incomplete"}")
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# 普通批次提示也复用锁、脱敏和输出故障处理。
|
|
@@ -89,7 +89,7 @@ module Net
|
|
|
89
89
|
unless @verbose
|
|
90
90
|
@mutex.synchronize do
|
|
91
91
|
host = event.fields[:host]
|
|
92
|
-
@stages[host] = "
|
|
92
|
+
@stages[host] = "collecting" if @stages.key?(host) && event.name == "login_complete" && event.fields[:status] == "ok"
|
|
93
93
|
refresh
|
|
94
94
|
end
|
|
95
95
|
return
|
|
@@ -97,16 +97,16 @@ module Net
|
|
|
97
97
|
|
|
98
98
|
fields = event.fields
|
|
99
99
|
message = case event.name
|
|
100
|
-
when "connect" then "
|
|
100
|
+
when "connect" then "Connecting and logging in (#{fields[:protocol]})"
|
|
101
101
|
when "login_complete"
|
|
102
|
-
fields[:status] == "ok" ? "
|
|
103
|
-
when "connect_failed" then "
|
|
104
|
-
when "command_start" then "
|
|
102
|
+
fields[:status] == "ok" ? "Login succeeded" : "Login failed (#{fields[:code]})"
|
|
103
|
+
when "connect_failed" then "Connection failed (#{fields[:code]})"
|
|
104
|
+
when "command_start" then "Executing command: #{fields[:text]}; waiting for response"
|
|
105
105
|
when "command_complete"
|
|
106
|
-
fields[:status] == "response_received" ? "
|
|
107
|
-
"
|
|
106
|
+
fields[:status] == "response_received" ? "Command response complete (#{fields[:duration_ms]} ms)" :
|
|
107
|
+
"Command failed (#{fields[:code]})"
|
|
108
108
|
when "operation_complete"
|
|
109
|
-
fields[:status] == "completed" ? "
|
|
109
|
+
fields[:status] == "completed" ? "Script complete" : "Script failed (#{fields[:code]})"
|
|
110
110
|
end
|
|
111
111
|
write(fields[:host], message) if message
|
|
112
112
|
end
|
|
@@ -151,8 +151,8 @@ module Net
|
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
# 显示已保存报告的位置与可读标签。
|
|
154
|
-
def location(path, label: "
|
|
155
|
-
write(nil, "#{label}
|
|
154
|
+
def location(path, label: "report")
|
|
155
|
+
write(nil, "#{label}: #{path}")
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
private
|
|
@@ -160,15 +160,15 @@ module Net
|
|
|
160
160
|
# 根据已知错误码返回简短排查建议,不展示任意异常正文。
|
|
161
161
|
def failure_hint(code)
|
|
162
162
|
case code
|
|
163
|
-
when :authentication_failed then "
|
|
164
|
-
when :authentication_error then "
|
|
165
|
-
when :host_key_untrusted then "
|
|
166
|
-
when :host_key_changed then "
|
|
167
|
-
when :login_timeout then "
|
|
168
|
-
when :connection_timeout then "SSH
|
|
169
|
-
when :device_error then "
|
|
170
|
-
when :missing_completeness_evidence then "
|
|
171
|
-
when :report_not_saved then "
|
|
163
|
+
when :authentication_failed then "Device rejected authentication; check username, password or AAA policy"
|
|
164
|
+
when :authentication_error then "Authentication dialogue incomplete; check authentication method or device AAA logs"
|
|
165
|
+
when :host_key_untrusted then "Host key is not yet trusted; verify and register its fingerprint"
|
|
166
|
+
when :host_key_changed then "Registered host key changed; verify device identity"
|
|
167
|
+
when :login_timeout then "Login prompt not received before the deadline; check SSH handshake and login dialogue"
|
|
168
|
+
when :connection_timeout then "SSH connection or handshake timed out; check service and network"
|
|
169
|
+
when :device_error then "Device rejected the command; check vendor mapping and account permissions"
|
|
170
|
+
when :missing_completeness_evidence then "Completeness evidence missing; see the device report for incomplete dimensions"
|
|
171
|
+
when :report_not_saved then "Device report persistence unconfirmed"
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
@@ -176,11 +176,11 @@ module Net
|
|
|
176
176
|
def status_line
|
|
177
177
|
percent = @total.positive? ? @completed * 100 / @total : 0
|
|
178
178
|
counts = progress_counts
|
|
179
|
-
"#{@completed}/#{@total} #{percent}% | #{counts} |
|
|
179
|
+
"#{@completed}/#{@total} #{percent}% | #{counts} | Logging in #{@stages.values.count("logging in")} collecting #{@stages.values.count("collecting")} queued #{[@total - @completed - @active, 0].max}"
|
|
180
180
|
end
|
|
181
181
|
|
|
182
182
|
# 默认统计设备任务,领域适配器可提供自己的计数文案。
|
|
183
|
-
def progress_counts = "
|
|
183
|
+
def progress_counts = "succeeded #{@succeeded} incomplete #{@failed}"
|
|
184
184
|
|
|
185
185
|
# 读取单调时钟秒数,避免系统校时影响期限与耗时计算。
|
|
186
186
|
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -198,14 +198,14 @@ module Net
|
|
|
198
198
|
eta = if @completed == @total && @total.positive?
|
|
199
199
|
"00:00:00"
|
|
200
200
|
elsif @completed >= 5 && rate.positive?
|
|
201
|
-
"
|
|
201
|
+
"about #{duration((@total - @completed) / rate)}"
|
|
202
202
|
else
|
|
203
|
-
"
|
|
203
|
+
"estimating"
|
|
204
204
|
end
|
|
205
|
-
line = "
|
|
205
|
+
line = "elapsed #{duration(elapsed)} | #{format("%.1f", rate)} devices/s | remaining #{eta}"
|
|
206
206
|
host, started = @task_started.min_by { |_key, value| value }
|
|
207
207
|
if started && monotonic - started >= 10
|
|
208
|
-
line += " |
|
|
208
|
+
line += " | longest #{host} #{@stages[host]} #{(monotonic - started).floor}s"
|
|
209
209
|
end
|
|
210
210
|
line
|
|
211
211
|
end
|
|
@@ -260,7 +260,7 @@ module Net
|
|
|
260
260
|
|
|
261
261
|
percent = @total.positive? ? " #{(@completed * 100.0 / @total).floor}%" : ""
|
|
262
262
|
clear_refresh
|
|
263
|
-
line = "[#{Time.now.strftime("%H:%M:%S")}] [#{@completed}/#{@total}#{percent}
|
|
263
|
+
line = "[#{Time.now.strftime("%H:%M:%S")}] [#{@completed}/#{@total}#{percent} running #{@active}] [#{host || "batch"}] #{message}"
|
|
264
264
|
line = "[#{Time.now.strftime("%H:%M:%S")}] #{host ? "#{host} " : ""}#{message}" unless @verbose
|
|
265
265
|
write_output(line.gsub(/[[:cntrl:]]/, " ") + "\n")
|
|
266
266
|
end
|
|
@@ -32,7 +32,7 @@ module Net
|
|
|
32
32
|
Process.setrlimit(:NOFILE, required, hard)
|
|
33
33
|
end
|
|
34
34
|
rescue SystemCallError, ArgumentError, NotImplementedError
|
|
35
|
-
raise ArgumentError, "
|
|
35
|
+
raise ArgumentError, "Insufficient local file descriptor limit: #{parallel} concurrent devices require at least #{required} file descriptors; raise the terminal ulimit -n or lower --parallel. Stopped without connecting to devices.", cause: nil
|
|
36
36
|
end
|
|
37
37
|
private_class_method :reserve!
|
|
38
38
|
end
|
metadata
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-connector
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- net-connector contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ctx_cli
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.2.
|
|
18
|
+
version: 0.2.1
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.2.
|
|
25
|
+
version: 0.2.1
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: expect-pty
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|