net-connector 0.17.2 → 0.18.1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/docs/architecture.md +14 -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/error/metadata.rb +2 -1
  10. data/lib/net/connector/engine/execution.rb +37 -10
  11. data/lib/net/connector/engine/known_hosts.rb +19 -6
  12. data/lib/net/connector/engine/log/messages.rb +16 -16
  13. data/lib/net/connector/engine/session.rb +5 -4
  14. data/lib/net/connector/engine/transports.rb +60 -41
  15. data/lib/net/connector/inventory/filters.rb +2 -2
  16. data/lib/net/connector/netdisco/cli/backup.rb +6 -6
  17. data/lib/net/connector/netdisco/cli/credentials.rb +2 -2
  18. data/lib/net/connector/netdisco/cli/help.rb +16 -16
  19. data/lib/net/connector/netdisco/cli/options.rb +44 -44
  20. data/lib/net/connector/netdisco/cli/setup.rb +18 -18
  21. data/lib/net/connector/netdisco/cli/topology.rb +21 -21
  22. data/lib/net/connector/netdisco/cli.rb +24 -24
  23. data/lib/net/connector/netdisco/config/defaults.rb +1 -1
  24. data/lib/net/connector/netdisco/config/initializer.rb +13 -13
  25. data/lib/net/connector/netdisco/progress.rb +17 -17
  26. data/lib/net/connector/netdisco/report/text.rb +15 -15
  27. data/lib/net/connector/netdisco/ssh/archive/client.rb +109 -79
  28. data/lib/net/connector/netdisco/ssh/archive/protocol.rb +107 -0
  29. data/lib/net/connector/netdisco/ssh/archive/worker.py +35 -19
  30. data/lib/net/connector/netdisco/ssh/authentication.rb +8 -10
  31. data/lib/net/connector/netdisco/topology/files.rb +8 -8
  32. data/lib/net/connector/netdisco/topology/presentation.rb +15 -15
  33. data/lib/net/connector/progress.rb +36 -36
  34. data/lib/net/connector/resources.rb +1 -1
  35. data/lib/net/connector/storage/atomic_file.rb +60 -3
  36. data/lib/net/connector/version.rb +1 -1
  37. metadata +11 -10
@@ -58,13 +58,13 @@ module Net
58
58
  finish_interrupted(fleet, progress, options, batch_directory, plan, settings)
59
59
  130
60
60
  rescue EOFError
61
- @error.puts "配置输入未完成,已停止;请检查先前提示的文件。"
61
+ @error.puts "Configuration setup is incomplete; stopped. Check the files listed above."
62
62
  2
63
63
  rescue ArgumentError, Inventory::Error => exception
64
- @error.puts "备份参数或清单错误:#{exception.message}"
64
+ @error.puts "Backup argument or inventory error: #{exception.message}"
65
65
  2
66
66
  rescue StandardError => exception
67
- @error.puts "备份未完成(#{exception.class})"
67
+ @error.puts "Backup incomplete (#{exception.class})"
68
68
  2
69
69
  end
70
70
 
@@ -111,10 +111,10 @@ module Net
111
111
  # 清单离线回退和本地保存故障单独提示,不覆盖设备操作结果。
112
112
  def inventory_warning(fleet)
113
113
  if fleet.inventory_status && fleet.inventory_status[:source] == :stale_cache
114
- @error.puts "清单刷新失败,使用历史缓存(#{fleet.inventory_status[:refresh_error_code]})"
114
+ @error.puts "Inventory refresh failed; using the previous cache (#{fleet.inventory_status[:refresh_error_code]})"
115
115
  end
116
116
  return unless fleet.inventory_status && fleet.inventory_status[:write_error_type]
117
- @error.puts "清单缓存写入失败(#{fleet.inventory_status[:write_error_type]})"
117
+ @error.puts "Inventory cache write failed (#{fleet.inventory_status[:write_error_type]})"
118
118
 
119
119
  end
120
120
 
@@ -188,7 +188,7 @@ module Net
188
188
  progress.finish(report)
189
189
  progress.location(report.report_location) if report.report_location
190
190
  summary_path = File.join(batch_directory, "summary.txt")
191
- progress.location(summary_path, label: "文本报告") if File.file?(summary_path)
191
+ progress.location(summary_path, label: "Text report") if File.file?(summary_path)
192
192
  updated = record_progress_error(report, progress)
193
193
  unless updated.equal?(report)
194
194
  # 末尾显示也可能首次失败,只补写诊断,不重复设备任务或事件。
@@ -18,10 +18,10 @@ module Net
18
18
  begin
19
19
  values = JSON.parse(input.gets("\n") || "")
20
20
  rescue JSON::ParserError
21
- raise ArgumentError, "凭据 JSON 无效", cause: nil
21
+ raise ArgumentError, "Invalid credentials JSON", cause: nil
22
22
  end
23
23
  keys = %w[netdisco_username netdisco_password device_username device_password]
24
- raise ArgumentError, "需要四个非空凭据字符串" unless values.is_a?(Hash) && keys.all? { |key| values[key].is_a?(String) && !values[key].strip.empty? }
24
+ raise ArgumentError, "Four nonempty credential strings are required" unless values.is_a?(Hash) && keys.all? { |key| values[key].is_a?(String) && !values[key].strip.empty? }
25
25
 
26
26
  inventory = settings.inventory(credentials: {
27
27
  username: values.fetch("netdisco_username"), password: values.fetch("netdisco_password")
@@ -24,25 +24,25 @@ module Net
24
24
  def self.examples(program:, mode:)
25
25
  command = "ruby #{File.basename(program)}"
26
26
  cases = if mode == :topology
27
- [["预览过滤后的清单", "--inventory-only --name edge --cache-only"],
28
- ["发现邻居", "--discover --hosts 192.0.2.10-20"],
29
- ["生成只读计划", "--plan --host 192.0.2.10"],
30
- ["明确应用并读回保存", "--apply --host 192.0.2.10"]]
27
+ [["Preview filtered inventory", "--inventory-only --name edge --cache-only"],
28
+ ["Discover neighbors", "--discover --hosts 192.0.2.10-20"],
29
+ ["Generate a read-only plan", "--plan --host 192.0.2.10"],
30
+ ["Explicitly apply, read back and save", "--apply --host 192.0.2.10"]]
31
31
  elsif mode == :tftp
32
- [["预览待上传设备", "--plan --hosts 192.0.2.10-20"],
33
- ["单台上传,服务器沿用配置", "--host 192.0.2.10"],
34
- ["连续 IP 范围并发上传", "--hosts 192.0.2.10-20 --parallel 8"],
35
- ["上传并核验服务器文件", "--host 192.0.2.10 --tftp-root /srv/tftp --success-policy verified"]]
32
+ [["Preview devices selected for upload", "--plan --hosts 192.0.2.10-20"],
33
+ ["Upload from one device using the configured server", "--host 192.0.2.10"],
34
+ ["Upload concurrently from an IP range", "--hosts 192.0.2.10-20 --parallel 8"],
35
+ ["Upload and verify server files", "--host 192.0.2.10 --tftp-root /srv/tftp --success-policy verified"]]
36
36
  else
37
- [["沿用配置执行备份", ""], ["预览连续 IP 范围", "--plan --hosts 192.0.2.10-20"],
38
- ["单台设备配置备份", "--host 192.0.2.10"],
39
- ["按名称筛选并排除设备", "--name szx10 --exclude-name csw,asw,wlc --parallel 8"],
40
- ["使用数据库清单预览", "--config examples/inventory.yml --plan"]]
37
+ [["Back up using the configuration", ""], ["Preview an IP range", "--plan --hosts 192.0.2.10-20"],
38
+ ["Back up one device configuration", "--host 192.0.2.10"],
39
+ ["Filter by name and exclude devices", "--name szx10 --exclude-name csw,asw,wlc --parallel 8"],
40
+ ["Preview database inventory", "--config examples/inventory.yml --plan"]]
41
41
  end
42
- cases.unshift(["首次生成缺失配置后退出", "--init --non-interactive"])
43
- ["常用示例(IP 请替换为实际清单地址):",
44
- *cases.map { |label, arguments| " #{label}:\n #{command} #{arguments}" },
45
- "包含条件取交集,排除优先;同类参数可逗号分隔或重复传参。"].join("\n")
42
+ cases.unshift(["Create missing configuration and exit", "--init --non-interactive"])
43
+ ["Common examples (replace IPs with actual inventory addresses):",
44
+ *cases.map { |label, arguments| " #{label}:\n #{command} #{arguments}" },
45
+ "Intersect different inclusion types; exclusions take precedence. Repeat options or separate values with commas."].join("\n")
46
46
  end
47
47
  end
48
48
  end
@@ -25,15 +25,15 @@ module Net
25
25
  raise OptionParser::InvalidArgument unless remaining.empty?
26
26
  credentials = values[:environment].keys.any? { |key| key.match?(/USERNAME|PASSWORD/) }
27
27
  if values[:stdin_credentials] && (credentials || values[:ask_password] || values[:ask_netdisco_password])
28
- raise ArgumentError, "--stdin-credentials 不能与命令行凭据或密码提示同时使用"
28
+ raise ArgumentError, "--stdin-credentials cannot be combined with command-line credentials or password prompts"
29
29
  end
30
- read_password(values, :ask_password, "NC_DEVICE_PASSWORD", "设备密码", input: input, output: error)
31
- read_password(values, :ask_netdisco_password, "ND_PASSWORD", "Netdisco 密码", input: input, output: error)
30
+ read_password(values, :ask_password, "NC_DEVICE_PASSWORD", "Device password", input: input, output: error)
31
+ read_password(values, :ask_netdisco_password, "ND_PASSWORD", "Netdisco password", input: input, output: error)
32
32
 
33
33
  values
34
34
  rescue OptionParser::ParseError
35
- limits = mode == :topology ? "并发数须为 1 至 500,设备上限须为正整数" : "并发数须为 1 至 500,抽样须为 1 至 5"
36
- raise ArgumentError, "参数无效:#{limits};使用 --help 查看参数", cause: nil
35
+ limits = mode == :topology ? "Concurrency must be 1 to 500; device limit must be a positive integer" : "Concurrency must be 1 to 500; sample size must be 1 to 5"
36
+ raise ArgumentError, "Invalid arguments: #{limits}; use --help for options", cause: nil
37
37
  end
38
38
 
39
39
  # 组装当前模式的选项、帮助和扩展参数,解析器自身不执行设备任务。
@@ -41,80 +41,80 @@ module Net
41
41
  raise ArgumentError, "unknown CLI mode" unless %i[backup tftp topology].include?(mode)
42
42
 
43
43
  OptionParser.new do |opts|
44
- default = mode == :topology ? "默认只读规划" : "默认沿用配置"
45
- opts.banner = "用法:ruby #{File.basename(program)} [选项](#{default})\n命令行参数优先;未指定的项沿用配置及默认值。无筛选时选择全部就绪设备。"
44
+ default = mode == :topology ? "read-only planning by default" : "use the configuration by default"
45
+ opts.banner = "Usage: ruby #{File.basename(program)} [options] (#{default})\nCommand-line options take precedence; unspecified values use configuration and defaults. Without filters, all ready devices are selected."
46
46
  add_settings(opts, values, mode: mode)
47
47
  add_execution(opts, values, mode: mode)
48
48
  add_terminal_options(opts, values)
49
49
  if mode == :topology
50
50
  add_topology(opts, values)
51
51
  else
52
- opts.on("--plan", "--inventory-only", "预览过滤后的清单,不连接设备") { values[:inventory_only] = true }
52
+ opts.on("--plan", "--inventory-only", "Preview filtered inventory without connecting to devices") { values[:inventory_only] = true }
53
53
  end
54
54
  additional&.call(opts, values)
55
- opts.on("--examples", "查看常用命令范例,不执行任务") do
55
+ opts.on("--examples", "Show common command examples without executing tasks") do
56
56
  output.puts Help.examples(program: program, mode: mode)
57
57
  values[:help] = true
58
58
  end
59
- opts.on("-h", "--help", "显示常用选项") do
59
+ opts.on("-h", "--help", "Show common options") do
60
60
  output.puts Help.common(opts, mode: mode)
61
61
  values[:help] = true
62
62
  end
63
- opts.on("--help-all", "显示完整选项(凭据、SSH、自动化等)") { output.puts opts; values[:help] = true }
63
+ opts.on("--help-all", "Show all options, including credentials, SSH and automation") { output.puts opts; values[:help] = true }
64
64
  end
65
65
  end
66
66
 
67
67
  # 登记进度、JSON、密码输入及配置问答等终端选项。
68
68
  def self.add_terminal_options(opts, values)
69
- opts.on("--ask-password", "隐藏输入设备密码") { values[:ask_password] = true }
70
- opts.on("--ask-netdisco-password", "隐藏输入 Netdisco 密码") { values[:ask_netdisco_password] = true }
69
+ opts.on("--ask-password", "Prompt for the device password without echoing input") { values[:ask_password] = true }
70
+ opts.on("--ask-netdisco-password", "Prompt for the Netdisco password without echoing input") { values[:ask_netdisco_password] = true }
71
71
  CtxCli::Options.output(opts, values, quiet: :no_progress)
72
- opts.on("--stdin-credentials", "从标准输入读取凭据并等待 RUN") { values[:stdin_credentials] = true }
72
+ opts.on("--stdin-credentials", "Read credentials from stdin and wait for RUN") { values[:stdin_credentials] = true }
73
73
  CtxCli::Options.initialization(opts, values)
74
74
  end
75
75
 
76
76
  # 登记互斥的拓扑执行模式和格式化参数,应用模式必须显式选择。
77
77
  def self.add_topology(opts, values)
78
- { discover: "发现邻居并检查描述", plan: "生成配置计划", apply: "修正、读回并保存" }.each do |mode, description|
78
+ { discover: "Discover neighbors and check descriptions", plan: "Generate a configuration plan", apply: "Apply corrections, read back and save" }.each do |mode, description|
79
79
  opts.on("--#{mode}", description) do
80
- raise ArgumentError, "--discover、--plan、--apply 不能同时使用" if values[:topology_mode]
80
+ raise ArgumentError, "--discover, --plan and --apply are mutually exclusive" if values[:topology_mode]
81
81
 
82
82
  values[:topology_mode] = mode
83
83
  values[:apply] = true if mode == :apply
84
84
  end
85
85
  end
86
- opts.on("--inventory-only", "只读取并过滤清单,不连接设备") { values[:inventory_only] = true }
87
- opts.on("--limit N", Integer, "最多选择 N 台可发现设备") do |value|
86
+ opts.on("--inventory-only", "Read and filter inventory without connecting to devices") { values[:inventory_only] = true }
87
+ opts.on("--limit N", Integer, "Select at most N discoverable devices") do |value|
88
88
  raise OptionParser::InvalidArgument unless value.positive?
89
89
 
90
90
  values[:topology_limit] = value
91
91
  end
92
- opts.on("--description-format TEXT", "模板仅支持 %{neighbor}、%{port}") { |value| values[:environment]["NC_TOPOLOGY_DESCRIPTION_FORMAT"] = value }
93
- opts.on("--full-port-names", "不缩写邻居端口") { values[:environment]["NC_TOPOLOGY_ABBREVIATE"] = "false" }
94
- opts.on("--lowercase-ports", "邻居端口转换成小写") { values[:environment]["NC_TOPOLOGY_LOWERCASE"] = "true" }
92
+ opts.on("--description-format TEXT", "Template supports only %{neighbor} and %{port}") { |value| values[:environment]["NC_TOPOLOGY_DESCRIPTION_FORMAT"] = value }
93
+ opts.on("--full-port-names", "Keep full neighbor port names") { values[:environment]["NC_TOPOLOGY_ABBREVIATE"] = "false" }
94
+ opts.on("--lowercase-ports", "Convert neighbor port names to lowercase") { values[:environment]["NC_TOPOLOGY_LOWERCASE"] = "true" }
95
95
  end
96
96
 
97
97
  # 正式 CLI 与批次示例共用并发、抽样、核验和成功策略选项。
98
98
  def self.add_execution(opts, values, mode: nil)
99
- opts.on("--refresh-inventory", "强制刷新清单;远端失败时回退已验证历史") { values[:force_refresh] = true }
100
- opts.on("--cache-only", "仅使用已验证的清单缓存,不查询远端") { values[:environment]["ND_CACHE_MODE"] = "only" }
101
- opts.on("--cache-mode MODE", %w[prefer only refresh], "清单缓存 prefer / only / refresh") do |value|
99
+ opts.on("--refresh-inventory", "Force inventory refresh; fall back to verified history on remote failure") { values[:force_refresh] = true }
100
+ opts.on("--cache-only", "Use only verified cached inventory without querying the remote source") { values[:environment]["ND_CACHE_MODE"] = "only" }
101
+ opts.on("--cache-mode MODE", %w[prefer only refresh], "Inventory cache: prefer / only / refresh") do |value|
102
102
  values[:environment]["ND_CACHE_MODE"] = value
103
103
  end
104
- opts.on("-p", "--parallel N", Integer, "并发设备数,1 至 500") do |count|
104
+ opts.on("-p", "--parallel N", Integer, "Concurrent devices, 1 to 500") do |count|
105
105
  raise OptionParser::InvalidArgument unless (1..Settings::MAX_PARALLEL).cover?(count)
106
106
 
107
107
  values[:environment]["NC_PARALLEL"] = count.to_s
108
108
  end
109
109
  return if mode == :topology
110
110
 
111
- opts.on("--sample N", "--limit-per-vendor N", Integer, "每厂商抽样 1 至 5 台") do |count|
111
+ opts.on("--sample N", "--limit-per-vendor N", Integer, "Sample 1 to 5 devices per vendor") do |count|
112
112
  Planner.validate_limit!(count)
113
113
  values[:limit] = count
114
114
  end
115
- opts.on("--tftp-root DIR", "本机 TFTP 服务器文件目录") { |path| values[:tftp_root] = path }
116
- opts.on("--all", "选择全部就绪设备") { values[:all] = true }
117
- opts.on("--success-policy POLICY", %w[strict selected verified], "成功判定:strict / selected / verified(TFTP 文件核验)") do |policy|
115
+ opts.on("--tftp-root DIR", "Local directory containing TFTP server files") { |path| values[:tftp_root] = path }
116
+ opts.on("--all", "Select all ready devices") { values[:all] = true }
117
+ opts.on("--success-policy POLICY", %w[strict selected verified], "Success policy: strict / selected / verified (TFTP file verification)") do |policy|
118
118
  values[:success_policy] = policy.to_sym
119
119
  end
120
120
  end
@@ -136,7 +136,7 @@ module Net
136
136
 
137
137
  # CLI > 原进程环境 > YAML > .env;公开环境字段转为内部策略,凭据仍独立读取。
138
138
  def self.settings(options, env: ENV, mode: :backup, configuration: nil, defaults: {})
139
- raise ArgumentError, "--all 不能与抽样同时使用" if options[:all] && options[:limit]
139
+ raise ArgumentError, "--all cannot be combined with sampling" if options[:all] && options[:limit]
140
140
  if options[:host] && options.fetch(:environment).key?("NC_INCLUDE_HOSTS")
141
141
  raise ArgumentError, "--host cannot be combined with --hosts"
142
142
  end
@@ -180,14 +180,14 @@ module Net
180
180
  def self.add_settings(opts, values, mode: nil)
181
181
  add_filters(opts, values)
182
182
  {
183
- "--username USER" => ["NC_DEVICE_USERNAME", "设备登录用户名"],
184
- "--password PASSWORD" => ["NC_DEVICE_PASSWORD", "设备登录密码(可改用 --ask-password)"],
185
- "--enable-password PASSWORD" => ["NC_ENABLE_PASSWORD", "设备提权密码"],
186
- "--netdisco-url URL" => ["ND_URL", "Netdisco 地址"],
187
- "--netdisco-username USER" => ["ND_USERNAME", "Netdisco 用户名"],
188
- "--netdisco-password PASSWORD" => ["ND_PASSWORD", "Netdisco 密码"],
189
- "--directory DIR" => ["NC_BACKUP_DIRECTORY", "#{mode == :topology ? "拓扑报告" : "备份"}目录,相对启动目录"],
190
- "--config FILE" => ["NC_CONFIG", "YAML 配置,相对启动目录"]
183
+ "--username USER" => ["NC_DEVICE_USERNAME", "Device login username"],
184
+ "--password PASSWORD" => ["NC_DEVICE_PASSWORD", "Device login password (or use --ask-password)"],
185
+ "--enable-password PASSWORD" => ["NC_ENABLE_PASSWORD", "Device privilege password"],
186
+ "--netdisco-url URL" => ["ND_URL", "Netdisco URL"],
187
+ "--netdisco-username USER" => ["ND_USERNAME", "Netdisco username"],
188
+ "--netdisco-password PASSWORD" => ["ND_PASSWORD", "Netdisco password"],
189
+ "--directory DIR" => ["NC_BACKUP_DIRECTORY", "#{mode == :topology ? "Topology report" : "Backup"} directory, relative to the startup directory"],
190
+ "--config FILE" => ["NC_CONFIG", "YAML configuration, relative to the startup directory"]
191
191
  }.each do |flag, (key, description)|
192
192
  opts.on(flag, description) do |value|
193
193
  raise OptionParser::InvalidArgument if value.empty?
@@ -195,12 +195,12 @@ module Net
195
195
  values[:environment][key] = value
196
196
  end
197
197
  end
198
- opts.on("--env-file FILE", "最后叠加的环境文件,相对启动目录") { |path| values[:env_file] = path }
199
- opts.on("--show-config", "显示生效的非敏感设置与配置来源") { values[:show_config] = true }
200
- opts.on("--host-key-policy POLICY", %w[strict accept_new replace], "主机密钥策略:strict / accept_new / replace(变化时自动替换)") do |policy|
198
+ opts.on("--env-file FILE", "Environment file loaded last, relative to the startup directory") { |path| values[:env_file] = path }
199
+ opts.on("--show-config", "Show effective nonsensitive settings and configuration sources") { values[:show_config] = true }
200
+ opts.on("--host-key-policy POLICY", %w[strict accept_new replace], "Host key policy: strict / accept_new / replace (automatically replace changed keys)") do |policy|
201
201
  values[:environment]["NC_HOST_KEY_POLICY"] = policy
202
202
  end
203
- opts.on("--known-hosts FILE", "持久保存主机密钥的文件") do |path|
203
+ opts.on("--known-hosts FILE", "File for persistent host keys") do |path|
204
204
  values[:environment]["NC_KNOWN_HOSTS"] = path
205
205
  end
206
206
  end
@@ -217,7 +217,7 @@ module Net
217
217
  values[:environment][environment_key] = value
218
218
  end
219
219
  end
220
- opts.on("--host IP", "选择单台设备,可重复") do |value|
220
+ opts.on("--host IP", "Select one device; may be repeated") do |value|
221
221
  raise OptionParser::InvalidArgument if value.empty? || value.match?(/[\/,]/)
222
222
 
223
223
  begin
@@ -254,7 +254,7 @@ module Net
254
254
  def self.read_password(values, flag, key, label, input:, output:)
255
255
  return unless values[flag]
256
256
 
257
- raise ArgumentError, "密码提示与显式密码不能同时使用" if values[:environment].key?(key)
257
+ raise ArgumentError, "Password prompts cannot be combined with explicit passwords" if values[:environment].key?(key)
258
258
  values[:environment][key] = CtxCli::Terminal.new(input: input, out: output).password(label: label)
259
259
  end
260
260
  private_class_method :read_password, :parser_for
@@ -12,8 +12,8 @@ module Net
12
12
  # 首次运行的文件检查和创建问答;完成初始化后必须退出,不触发设备任务。
13
13
  class Setup
14
14
  ENVIRONMENT_TEMPLATE = <<~TEXT.freeze
15
- # 进程环境优先于此文件;命令行参数 > 原进程环境 > YAML > .env > 内置默认值。
16
- # 下列凭据按实际认证方式取消注释并填写;不自动保存当前进程中的密码。
15
+ # Process environment takes precedence over this file; CLI > original process environment > YAML > .env > built-in defaults.
16
+ # Uncomment and fill in credentials for the authentication method in use; current process passwords are not saved automatically.
17
17
  # ND_URL=
18
18
  # ND_USERNAME=
19
19
  # ND_PASSWORD=
@@ -21,16 +21,16 @@ module Net
21
21
  # NC_DEVICE_USERNAME=
22
22
  # NC_DEVICE_PASSWORD=
23
23
  # NC_ENABLE_PASSWORD=
24
- # PostgreSQL 清单使用 ND_DB_HOST / ND_DB_NAME / ND_DB_USER / ND_DB_PASS。
24
+ # PostgreSQL inventory uses ND_DB_HOST / ND_DB_NAME / ND_DB_USER / ND_DB_PASS.
25
25
  # ND_DB_HOST=
26
26
  # ND_DB_NAME=
27
27
  # ND_DB_USER=
28
28
  # ND_DB_PASS=
29
- # TFTP SSH 归档使用现有密钥/agent,或设置以下密码。
30
- # 密码含 # 或 $ 时请用单引号包裹,例如 KEY='your-password'。
29
+ # TFTP SSH archiving uses existing keys/agent, or the password below.
30
+ # Wrap passwords containing # or $ in single quotes, for example KEY='your-password'.
31
31
  # NC_TFTP_SSH_PASSWORD=
32
- # 公共参数优先写入各入口 YAML;可在运行时使用 -p / --config / --directory。
33
- # 未设置 NC_CONFIG 时按系统、用户、入口目录及启动目录逐层发现配置。
32
+ # Prefer each entrypoint's YAML for common options; use -p / --config / --directory at runtime.
33
+ # Without NC_CONFIG, configuration is discovered in system, user, entrypoint and startup directories, in that order.
34
34
  TEXT
35
35
 
36
36
  # 保存调用方 IO 与路径;不读取标准输入、不加载凭据、不创建文件。
@@ -51,7 +51,7 @@ module Net
51
51
  missing_config = path && missing?(path)
52
52
  missing_env = @env_path && missing?(@env_path)
53
53
  return !!@options[:init] unless missing_config || missing_env
54
- raise ArgumentError, "--interactive 需要交互终端;自动化请使用 --init --non-interactive" if @options[:interactive] && !terminal?
54
+ raise ArgumentError, "--interactive requires an interactive terminal; use --init --non-interactive for automation" if @options[:interactive] && !terminal?
55
55
 
56
56
  create_config = missing_config && approved?(path, required: true)
57
57
  create_env = missing_env && approved?(@env_path, required: false)
@@ -59,12 +59,12 @@ module Net
59
59
  if create_env
60
60
  FileUtils.mkdir_p(File.dirname(File.expand_path(@env_path)), mode: 0o700)
61
61
  @initializer.create_environment(path: @env_path, contents: ENVIRONMENT_TEMPLATE)
62
- @output.puts "已初始化环境模板:#{@env_path}(未写入密码)"
62
+ @output.puts "Environment template initialized: #{@env_path}(passwords were not written)"
63
63
  end
64
64
  stopped = create_config || create_env || @options[:init]
65
- @output.puts "请检查配置及凭据后再次运行;本次未读取清单、未连接设备。" if stopped
66
- @output.puts "HTTP 清单需设置 netdisco.url 或 ND_URL,并提供 ND_API_KEY 或 ND_USERNAME / ND_PASSWORD;设备凭据支持通用及厂商配置。" if create_config && document.dig("netdisco", "source") == "http"
67
- @output.puts "TFTP 请填写 tftp.server 或 tftp.servers,并按实际环境配置文件核验目录;未猜测服务器、VRF 或源接口。" if create_config && @mode == :tftp
65
+ @output.puts "Check configuration and credentials, then run again; inventory was not read and no devices were connected." if stopped
66
+ @output.puts "HTTP inventory requires netdisco.url or ND_URL and ND_API_KEY or ND_USERNAME / ND_PASSWORD; device credentials support common and per-vendor settings." if create_config && document.dig("netdisco", "source") == "http"
67
+ @output.puts "For TFTP, set tftp.server or tftp.servers and the file verification directory for your environment; server, VRF and source interface are not inferred." if create_config && @mode == :tftp
68
68
  !!stopped
69
69
  end
70
70
 
@@ -83,14 +83,14 @@ module Net
83
83
  end
84
84
  FileUtils.mkdir_p(File.dirname(File.expand_path(path)), mode: 0o700)
85
85
  @initializer.create(path: path, document: document, interactive: questionnaire?, mode: @mode, inherited_parallel: inherited_parallel)
86
- @output.puts "已初始化配置:#{path}"
86
+ @output.puts "Configuration initialized: #{path}"
87
87
  document
88
88
  end
89
89
 
90
90
  # 缺失与无效文件分开处理,拒绝符号链接、目录及不可读取文件。
91
91
  def missing?(path)
92
92
  stat = File.lstat(path)
93
- raise ArgumentError, "配置路径必须是可读普通文件:#{path}" unless stat.file? && File.readable?(path)
93
+ raise ArgumentError, "Configuration path must be a readable regular file: #{path}" unless stat.file? && File.readable?(path)
94
94
 
95
95
  false
96
96
  rescue Errno::ENOENT
@@ -114,12 +114,12 @@ module Net
114
114
  def approved?(path, required:)
115
115
  return true if @options[:init]
116
116
  unless terminal? && @options[:interactive] != false
117
- raise ArgumentError, "缺少配置:#{path};请先执行 --init --non-interactive 创建,或使用 --config 指定已有文件" if required
117
+ raise ArgumentError, "Missing configuration: #{path}; first run --init --non-interactive to create it, or use --config to select an existing file" if required
118
118
 
119
119
  return false
120
120
  end
121
121
  loop do
122
- @output.print "未找到 #{path},是否创建?[y/N]:"
122
+ @output.print "Not found: #{path}; create it? [y/N]: "
123
123
  @output.flush
124
124
  line = @input.gets("\n")
125
125
  raise EOFError unless line
@@ -127,11 +127,11 @@ module Net
127
127
  answer = line.strip.downcase
128
128
  return true if %w[y yes].include?(answer)
129
129
  if answer.empty? || %w[n no].include?(answer)
130
- raise ArgumentError, "未创建必需配置:#{path};本次停止" if required
130
+ raise ArgumentError, "Required configuration was not created: #{path}; stopped" if required
131
131
 
132
132
  return false
133
133
  end
134
- @output.puts "请输入 y 或 n。"
134
+ @output.puts "Enter y or n."
135
135
  end
136
136
  end
137
137
 
@@ -43,7 +43,7 @@ module Net
43
43
  return 0
44
44
  end
45
45
  policy = settings.topology_options
46
- raise ArgumentError, "应用配置必须显式传入 --apply;--plan 仅生成计划,--discover 只读发现" if policy.mode == :apply && !options[:apply]
46
+ raise ArgumentError, "Applying configuration requires explicit --apply; --plan only generates a plan and --discover only reads device state" if policy.mode == :apply && !options[:apply]
47
47
 
48
48
  inventory, credentials = CLI::Credentials.resolve(settings, input: @input, stdin_credentials: options[:stdin_credentials])
49
49
  @progress = Progress.new(io: @error, enabled: !options[:no_progress], verbose: options[:verbose])
@@ -58,13 +58,13 @@ module Net
58
58
  rescue Interrupt
59
59
  130
60
60
  rescue EOFError
61
- @error.puts "配置输入未完成,已停止;请检查先前提示的文件。"
61
+ @error.puts "Configuration setup is incomplete; stopped. Check the files listed above."
62
62
  2
63
63
  rescue ArgumentError, Inventory::Error => exception
64
- @error.puts "拓扑参数或清单错误:#{exception.message}"
64
+ @error.puts "Topology argument or inventory error: #{exception.message}"
65
65
  2
66
66
  rescue StandardError => exception
67
- @error.puts "拓扑未完成(#{Error::Metadata.type(exception.class.name) || "StandardError"})"
67
+ @error.puts "Topology incomplete (#{Error::Metadata.type(exception.class.name) || "StandardError"})"
68
68
  2
69
69
  end
70
70
 
@@ -108,8 +108,8 @@ module Net
108
108
  status = fleet.inventory_status
109
109
  return unless status
110
110
 
111
- @error.puts "清单刷新失败,使用历史缓存(#{status[:refresh_error_code]})" if status[:source] == :stale_cache
112
- @error.puts "清单缓存写入失败(#{status[:write_error_type]})" if status[:write_error_type]
111
+ @error.puts "Inventory refresh failed; using the previous cache (#{status[:refresh_error_code]})" if status[:source] == :stale_cache
112
+ @error.puts "Inventory cache write failed (#{status[:write_error_type]})" if status[:write_error_type]
113
113
  end
114
114
 
115
115
  # 仅预览就绪清单,不建立设备连接;无选中设备时返回未完成。
@@ -125,7 +125,7 @@ module Net
125
125
  # 先发现并保存审批计划,仅在显式应用模式执行变更,失败后保存已有任务回执。
126
126
  # 发现结果先落盘并生成校验摘要,再进入应用;取消只汇总回执,不重新下发配置。
127
127
  def execute(devices, settings, credentials, policy, options)
128
- raise ArgumentError, "请在 topology.directory 或 --directory 设置拓扑报告目录" unless policy.directory
128
+ raise ArgumentError, "Set the topology report directory with topology.directory or --directory" unless policy.directory
129
129
 
130
130
  directory = Storage::BatchDirectory.create(policy.directory)
131
131
  selected = devices.select(&:ready?)
@@ -137,12 +137,12 @@ module Net
137
137
  @events_mutex = Mutex.new
138
138
  @task_results = {}
139
139
  credentials = observed_credentials(credentials)
140
- @progress.operation_plan(selected, parallel: policy.parallel, label: "拓扑发现/规划")
140
+ @progress.operation_plan(selected, parallel: policy.parallel, label: "Topology discovery/planning")
141
141
  outcomes = @progress.with_updates { collect(selected, settings, credentials, policy, directory) }
142
142
  save_plan(directory, outcomes)
143
143
  if policy.mode == :apply
144
144
  applying = outcomes.filter_map { |outcome| @devices_by_host[outcome[:host]] if %i[planned partial_planned].include?(outcome[:status]) }
145
- @progress.operation_plan(applying, parallel: policy.parallel, label: "拓扑应用")
145
+ @progress.operation_plan(applying, parallel: policy.parallel, label: "Topology apply")
146
146
  outcomes = @progress.with_updates { apply_plans(outcomes, settings, credentials, policy, directory) }
147
147
  end
148
148
  summary = build_summary(devices, outcomes, policy, directory)
@@ -266,7 +266,7 @@ module Net
266
266
  begin
267
267
  # 阻塞 I/O 仍可取消;完成值或同步失败先固定,再交付返回间隙中的异步中断。
268
268
  Thread.handle_interrupt(Exception => :on_blocking) do
269
- @progress.start(device, label: "拓扑操作")
269
+ @progress.start(device, label: "Topology operation")
270
270
  result = yield
271
271
  rescue StandardError => failure
272
272
  result = { host: device.host, vendor: device.vendor, status: :failed, diagnostic: Diagnostic.from(failure)&.to_h }
@@ -375,7 +375,7 @@ module Net
375
375
  summary[:devices].each { |outcome| Netdisco::Topology::Files.write_report(outcome) }
376
376
  lines = result_lines(summary) + summary[:devices].flat_map do |item|
377
377
  ["#{item[:host]} #{item[:name].to_s.gsub(/[[:cntrl:]]/, " ")} #{Netdisco::Topology::Presentation.status(item[:status], changes: item.fetch(:changes, 0))} changes=#{item.fetch(:changes, 0)}",
378
- " 设备报告:#{item[:device_report]}", " 命令文件:#{item[:script_file] || "未生成"}"] +
378
+ " Device report: #{item[:device_report]}", " Command file: #{item[:script_file] || "not generated"}"] +
379
379
  item.fetch(:neighbors, []).map do |row|
380
380
  " #{row[:interface]} #{row[:status]} current=#{row[:current].inspect} expected=#{row[:expected].inspect}"
381
381
  end + item.fetch(:issues, []).map { |issue| " #{issue[:interface]} #{Netdisco::Topology::Presentation.reason(issue[:code])}" } +
@@ -398,10 +398,10 @@ module Net
398
398
  # 默认只打印设备、接口两层摘要;逐设备及命令详情已经写入独立报告。
399
399
  def result_lines(data)
400
400
  stats = data.fetch(:statistics)
401
- ["结果:#{Netdisco::Topology::Presentation.result(data)};模式:#{data[:mode]}",
402
- "设备:采集完成 #{stats[:collected]} 台;无需修改 #{stats[:unchanged]} 台;有候选变更 #{stats[:change_devices]} 台;失败 #{stats[:failed]} 台",
403
- "接口:候选变更 #{stats[:changes]} 个;待核实 #{stats[:review_interfaces]} 个(涉及 #{stats[:review_devices]} 台设备)",
404
- "待核实原因:#{Netdisco::Topology::Presentation.issues(data[:devices].flat_map { |item| item.fetch(:issues, []) })}"]
401
+ ["Result: #{Netdisco::Topology::Presentation.result(data)}; mode: #{data[:mode]}",
402
+ "Devices: collected #{stats[:collected]}; unchanged #{stats[:unchanged]}; with candidate changes #{stats[:change_devices]}; failed #{stats[:failed]} devices",
403
+ "Interfaces: candidate changes #{stats[:changes]}; unverified #{stats[:review_interfaces]} (across #{stats[:review_devices]} devices)",
404
+ "Unverified reasons: #{Netdisco::Topology::Presentation.issues(data[:devices].flat_map { |item| item.fetch(:issues, []) })}"]
405
405
  end
406
406
 
407
407
  # 按 JSON 或安全文本格式输出设备结果及报告位置。
@@ -410,7 +410,7 @@ module Net
410
410
  @output.puts JSON.generate(data)
411
411
  else
412
412
  if data.key?(:total)
413
- @output.puts "清单 #{data[:total]} 台;本次 #{data[:ready]} 台;跳过 #{data[:total] - data[:ready]} 台"
413
+ @output.puts "Inventory #{data[:total]} devices; selected #{data[:ready]}; skipped #{data[:total] - data[:ready]} devices"
414
414
  end
415
415
  if data.key?(:total)
416
416
  data.fetch(:devices).each do |item|
@@ -421,11 +421,11 @@ module Net
421
421
  if data[:directory]
422
422
  @output.puts result_lines(data)
423
423
  scripts = data[:devices].count { |item| item[:script_file] }
424
- @output.puts "命令文件:#{File.join(data[:directory], "scripts")}(#{scripts} 台有变更)" if scripts.positive?
425
- @output.puts "设备报告:#{File.join(data[:directory], "devices")}"
426
- @output.puts "计划:#{File.join(data[:directory], "plan.json")}"
427
- @output.puts "报告:#{File.join(data[:directory], "summary.json")}"
428
- @output.puts "文本报告:#{File.join(data[:directory], "summary.txt")}"
424
+ @output.puts "Command file: #{File.join(data[:directory], "scripts")}(#{scripts} devices with changes)" if scripts.positive?
425
+ @output.puts "Device report: #{File.join(data[:directory], "devices")}"
426
+ @output.puts "Plan: #{File.join(data[:directory], "plan.json")}"
427
+ @output.puts "Report: #{File.join(data[:directory], "summary.json")}"
428
+ @output.puts "Text report: #{File.join(data[:directory], "summary.txt")}"
429
429
  end
430
430
  end
431
431
  end