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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/docs/architecture.md +6 -0
  4. data/docs/usage.md +4 -0
  5. data/docs/usage.zh-CN.md +4 -0
  6. data/lib/net/connector/collect/progress.rb +21 -21
  7. data/lib/net/connector/device/base.rb +12 -8
  8. data/lib/net/connector/device/save_config.rb +2 -2
  9. data/lib/net/connector/engine/execution.rb +37 -10
  10. data/lib/net/connector/engine/log/messages.rb +16 -16
  11. data/lib/net/connector/engine/session.rb +5 -4
  12. data/lib/net/connector/engine/transports.rb +60 -41
  13. data/lib/net/connector/inventory/filters.rb +2 -2
  14. data/lib/net/connector/netdisco/cli/backup.rb +6 -6
  15. data/lib/net/connector/netdisco/cli/credentials.rb +2 -2
  16. data/lib/net/connector/netdisco/cli/help.rb +16 -16
  17. data/lib/net/connector/netdisco/cli/options.rb +44 -44
  18. data/lib/net/connector/netdisco/cli/setup.rb +18 -18
  19. data/lib/net/connector/netdisco/cli/topology.rb +21 -21
  20. data/lib/net/connector/netdisco/cli.rb +24 -24
  21. data/lib/net/connector/netdisco/config/defaults.rb +1 -1
  22. data/lib/net/connector/netdisco/config/initializer.rb +13 -13
  23. data/lib/net/connector/netdisco/progress.rb +17 -17
  24. data/lib/net/connector/netdisco/report/text.rb +15 -15
  25. data/lib/net/connector/netdisco/ssh/archive/client.rb +4 -4
  26. data/lib/net/connector/netdisco/ssh/authentication.rb +8 -10
  27. data/lib/net/connector/netdisco/topology/files.rb +8 -8
  28. data/lib/net/connector/netdisco/topology/presentation.rb +15 -15
  29. data/lib/net/connector/progress.rb +36 -36
  30. data/lib/net/connector/resources.rb +1 -1
  31. data/lib/net/connector/version.rb +1 -1
  32. 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 ? "抽样(每厂商最多 #{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}(#{device.vendor})")
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 "备份已保存(#{outcome.backup.bytes} 字节)"
28
+ when :backed_up then "Backup saved (#{outcome.backup.bytes} bytes)"
29
29
  when :reported_uploaded
30
- outcome.backup&.verification == :server_verified ? "配置已归档(#{outcome.backup.server_bytes} 字节)" :
31
- "设备报告上传完成;服务器文件尚未核验"
32
- when :saved_with_error then "备份有产物但未完全成功(#{error_code || outcome.status})"
33
- when :reported_with_error then "设备报告上传但收尾异常(#{error_code || outcome.status})"
34
- else "未完成(#{error_code || outcome.status})"
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, "其中 #{unverified} 台仅设备报告上传完成,服务器文件尚未核验") if unverified.positive?
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, "回调异常 #{report.callback_errors.size},报告错误 #{summary[:report_error] || "无"}")
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} | 本次 #{@total} 台;并发 #{parallel}")
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? ? "" : ";#{Topology::Presentation.issues(issues)}"
93
- emit(device.host, "#{label};耗时 #{duration_ms} ms#{error_code ? "(#{error_code})" : ""}#{reasons}")
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 ? "完成" : (failed ? "未完成" : "存在待处理项")
105
- write(nil, "拓扑批次结束:#{counts.map { |status, count| "#{Topology::Presentation.status(status)} #{count} 台" }.join(",")};结果 #{result}")
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 ? "完成 #{@completed} 待核实 #{@operation_pending} 失败 #{@failed}" : super
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 = ["网络设备备份报告", "=" * 72,
18
- "开始时间:#{report.started_at.getlocal("+08:00").strftime("%Y-%m-%d %H:%M:%S %:z")}",
19
- "结束时间:#{report.finished_at.getlocal("+08:00").strftime("%Y-%m-%d %H:%M:%S %:z")}",
20
- "清单:#{plan.inventory.size} 台;本次:#{plan.ready.size} 台;并发:#{parallel}",
21
- "成功:#{summary[:succeeded]};部分成功:#{summary[:partial]};失败:#{summary[:failed]};跳过:#{summary[:skipped]}",
22
- "耗时:#{(report.duration_ms.to_f / 1000).round(1)} 秒;批次状态:#{summary[:status]}",
23
- "成功策略:#{report.policy};策略通过:#{report.policy_success?}",
24
- "回调异常:#{report.callback_errors.size};报告错误:#{summary[:report_error] || "无"}",
25
- "状态计数:#{report.counts.map { |key, value| "#{key}=#{value}" }.join(",")}",
26
- "未完成分类:#{groups.empty? ? "无" : groups.map { |key, items| "#{key}=#{items.size}" }.join(",")}",
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 << "无" if failed.empty?
29
+ lines << "none" if failed.empty?
30
30
  if report.mode == :tftp
31
31
  verification = summary.fetch(:verification)
32
- lines << "TFTP 服务器文件:已核验 #{verification[:verified]},未核验 #{verification[:unverified]}。"
32
+ lines << "TFTP server files: verified #{verification[:verified]}, unverified #{verification[:unverified]}."
33
33
  end
34
- lines += ["=" * 72, "备份目录:#{directory}", "完整明细:#{File.join(directory, "summary.json")}"]
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) || "无错误码"} | #{item.duration_ms} ms"
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
- "已读取 NC_TFTP_SSH_PASSWORD,但 SSH 认证或主连接建立失败;请检查账号、密码、主机密钥及网络。密码含 # 或 $ 时请在 .env 中用单引号包裹"
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
- "请在项目根目录 .env 设置 NC_TFTP_SSH_PASSWORD,或配置可用的 SSH 密钥/agent"
30
+ "Set NC_TFTP_SSH_PASSWORD in the project root .env, or configure a working SSH key/agent"
31
31
  end
32
- super("SSH 归档认证不可用:#{detail}。已停止,未连接设备。")
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 归档目录预检失败:请检查服务器 python3、TFTP 根目录及写权限;archive、批次目录和 .net-connector-locks 须由 SSH 账号拥有且权限为 0700。已停止,未连接设备。")
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
- session = Expect::Session.new(buffer_limit: 65_536, write_timeout: 5)
18
- session.spawn(*arguments, raw: true)
19
- deadline = Expect.monotonic + 15
20
- challenge = session.expect(PASSWORD, REJECTED, timeout: [deadline - Expect.monotonic, 0].max)
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
- session.write(password + "\n")
24
- completion = session.expect(PASSWORD, REJECTED, timeout: [deadline - Expect.monotonic, 0].max)
25
- completion.error == :eof
26
- ensure
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 = ["设备:#{text(outcome[:name])}(#{text(outcome[:host])})", "厂商:#{text(outcome[:vendor])}",
39
- "状态:#{Presentation.status(outcome[:status], changes: outcome.fetch(:changes, 0))};候选变更:#{outcome.fetch(:changes, 0)}",
40
- outcome[:script_file] ? "命令文件:#{outcome[:script_file]}" : "命令文件:未生成(没有已核实的配置变更)"]
41
- lines << "命令校验 SHA-256:#{outcome[:script_sha256]}" if outcome[:script_sha256]
42
- lines << "诊断:#{text(outcome.dig(:diagnostic, :error_code))}" if outcome[:diagnostic]
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])} 现有=#{row[:current].inspect} 目标=#{row[:expected].inspect}"
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| "未核实:#{text(issue[:interface])} #{text(Presentation.reason(issue[:code]))}" })
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: "已生成计划", partial_planned: "已生成部分计划", unverified: "含待核实接口",
10
- discovered: "发现完成", applied: "应用完成", partial_applied: "部分应用完成", manual_required: "需人工处理",
11
- unsupported_operation: "不支持,已跳过", failed: "执行失败", partial: "执行部分失败",
12
- missing_credentials: "缺少凭据", interrupted: "已中断", not_started: "未开始", offline_review: "离线重审" }.freeze
13
- REASONS = { unknown_neighbor_name: "邻居未提供名称", unknown_neighbor_interface: "邻居未提供端口",
14
- ambiguous_neighbor: "同一接口存在多个邻居", interface_missing: "配置中未找到本地接口",
15
- unsafe_description: "目标描述不符合安全约束" }.freeze
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 "无需修改" if value.to_sym == :planned && changes == 0
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} 个接口" }.join(";")
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 "未完成" if summary[:orchestration_error_type] || summary[:interrupted] || summary[:progress_error]
47
- return "完成" if summary[:success]
48
- return "执行存在失败" if summary.fetch(:statistics).fetch(:failed).positive?
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 "没有可处理设备" if summary[:devices].empty?
50
+ return "no eligible devices" if summary[:devices].empty?
51
51
 
52
- { plan: "已生成计划,存在待修正或待核实项", discover: "发现结束,仍有待核实接口",
53
- apply: "应用结束,仍有待处理项" }.fetch(summary[:mode], "存在待处理项")
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, "正在读取 Netdisco 设备清单#{config ? "(配置:#{config})" : ""}")
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} | 清单 #{total} 台;本次 #{@total} 台;跳过 #{total - @total} 台;并发 #{parallel}")
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, "开始#{label}") : refresh
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};#{hint}" if hint
62
+ message = "#{message}; #{hint}" if hint
63
63
  if @verbose || !success
64
- emit(host, "#{message};耗时 #{duration_ms} ms")
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, "未完成分类:#{failures.sort_by { |code, count| [-count, code.to_s] }.map { |code, count| "#{code}=#{count}" }.join(",")}")
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, "批次结束:成功 #{succeeded},部分成功 #{partial},失败 #{failed},跳过 #{skipped};耗时 #{(monotonic - @started_at).round(1)} 秒;结果 #{success ? "完成" : "未完成"}")
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] = "采集" if @stages.key?(host) && event.name == "login_complete" && event.fields[:status] == "ok"
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 "正在连接并登录(#{fields[:protocol]})"
100
+ when "connect" then "Connecting and logging in (#{fields[:protocol]})"
101
101
  when "login_complete"
102
- fields[:status] == "ok" ? "登录成功" : "登录失败(#{fields[:code]})"
103
- when "connect_failed" then "连接失败(#{fields[:code]})"
104
- when "command_start" then "执行命令:#{fields[:text]};等待响应"
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" ? "命令响应完成(#{fields[:duration_ms]} ms)" :
107
- "命令失败(#{fields[:code]})"
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" ? "脚本处理完成" : "脚本处理失败(#{fields[:code]})"
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}:#{path}")
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 "设备拒绝认证,请核对账号、密码或 AAA 策略"
164
- when :authentication_error then "认证交互未完成,请核对认证方式或设备 AAA 日志"
165
- when :host_key_untrusted then "主机密钥尚未信任,请确认指纹并登记"
166
- when :host_key_changed then "已登记主机密钥发生变化,请核实设备身份"
167
- when :login_timeout then "未在期限内获得登录提示,请检查 SSH 握手与登录交互"
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} | 登录 #{@stages.values.count("登录")} 采集 #{@stages.values.count("采集")} 排队 #{[@total - @completed - @active, 0].max}"
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 = "成功 #{@succeeded} 未完成 #{@failed}"
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
- "约 #{duration((@total - @completed) / rate)}"
201
+ "about #{duration((@total - @completed) / rate)}"
202
202
  else
203
- "估算中"
203
+ "estimating"
204
204
  end
205
- line = "耗时 #{duration(elapsed)} | #{format("%.1f", rate)} 台/秒 | 剩余 #{eta}"
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 += " | 最久 #{host} #{@stages[host]} #{(monotonic - started).floor}s"
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} 执行中 #{@active}] [#{host || "批次"}] #{message}"
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, "本机文件描述符额度不足:#{parallel} 并发至少预留 #{required} 个文件描述符;请提高终端 ulimit -n 或降低 --parallel。已停止,未连接设备。", cause: nil
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
@@ -3,6 +3,6 @@
3
3
  module Net
4
4
  module Connector
5
5
  # gemspec、CLI 与发布工具共用此版本,运行时读取不触发设备依赖加载。
6
- VERSION = "0.17.2"
6
+ VERSION = "0.18.0"
7
7
  end
8
8
  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.17.2
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-22 00:00:00.000000000 Z
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.0
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.0
25
+ version: 0.2.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: expect-pty
28
28
  requirement: !ruby/object:Gem::Requirement