vxlan 0.4.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b045bbd7eb98d8771aac733587cf171bdf5c8c0da0f7ce3a4226317454da139
4
- data.tar.gz: 06d4bddb07fa64f80ca0b2cb283c02321ad0bf54f56c94ec9c1538ebe052b447
3
+ metadata.gz: 203d6c19eed76f37e033ba015e36a39953de0ba63dc5fd4130873a83f1e097c3
4
+ data.tar.gz: bb21ff1519721b7ee99cdedc5af2a3ac34c8542563c06ce9d3cd1328c31c4d6f
5
5
  SHA512:
6
- metadata.gz: 6b5636dc50b48a4f0eed0c5c6b8babb2d650eae98f8a6ee78b1969b940327ba44e8f5b5bb16e12dd7b706cc0044756568aefae2fb9523e3034e72adbf132c2bc
7
- data.tar.gz: 73d115544a200fab0ae647981720af491b05dca15f17b3f7a8ed021559221b101012842b2c7856dca0fa28edc7d9a4d2bb1d06ba439f78595b45aec3c5965c60
6
+ metadata.gz: aa2702cbc3fc196d77c7cb4ac24aca422f4e03eaf6ac4967d07b946e762ff774b9359d24deae4430ca9e7a8644992909e00b185c48d14b575d30bbeb037f381c
7
+ data.tar.gz: fea09bbf0ea945b4c33913f759aad4e4181bd4da4148593ade4e177a18939938ef7c0b327bbce9da0b3ac18e95d9bee9ebced5afd2014b4c35978bd9b80fb7c0
data/CHANGELOG.md CHANGED
@@ -14,6 +14,11 @@ Changes to the public contract are recorded here. Versions follow Semantic Versi
14
14
 
15
15
  ## Unreleased
16
16
 
17
+ ## 0.4.1 (2026-10-22)
18
+
19
+ - Delegate command dispatch, common options, initialization, terminal interaction, offline IO and collection entrypoint orchestration to ctx_cli 0.1.0.
20
+ - Keep domain policies, device capabilities and ctx_runner completion semantics in their existing layers.
21
+
17
22
  - Delegate batch completion and cancellation/failure precedence to CtxRunner::Batch, keeping domain review and CLI presentation explicit.
18
23
 
19
24
  ## 0.3.1 (2026-10-21)
data/bin/README.md CHANGED
@@ -17,7 +17,7 @@ bundle exec ruby examples/offline.rb
17
17
 
18
18
  `bin/` 的公开内容是 `vxlan` 与本说明;现场 `vxlan.yml`、`.env`、`vxlans/` 由用户按需创建,不进入版本库或安装包。配置和凭据模板统一位于 `lib/vxlan/cli/templates/`,使用 `--init` 生成,不维护重复模板。
19
19
 
20
- 离线解析和帮助不加载 net-connector。采集配置与联网入口可选依赖 `net-connector ~> 0.16.0`(已包含 `.env` 加载依赖 dotenv)。本仓库 Gemfile 的 `examples` 组已声明连接器,不进入 vxlan.gemspec 的运行依赖。只安装 vxlan gem 的宿主需要自行声明相应可选依赖。
20
+ 离线解析和帮助不加载 net-connector。采集配置与联网入口可选依赖 `net-connector ~> 0.17.1`(已包含 `.env` 加载依赖 dotenv)。本仓库 Gemfile 的 `examples` 组已声明连接器,不进入 vxlan.gemspec 的运行依赖。只安装 vxlan gem 的宿主需要自行声明相应可选依赖。
21
21
 
22
22
  ## 正式命令
23
23
 
@@ -25,7 +25,7 @@ bundle exec ruby examples/offline.rb
25
25
 
26
26
  ```sh
27
27
  gem install vxlan
28
- gem install net-connector -v '~> 0.16.0'
28
+ gem install net-connector -v '~> 0.17.1'
29
29
  vxlan --help
30
30
  vxlan collect --init --non-interactive
31
31
  vxlan collect --inventory-only
@@ -213,3 +213,7 @@ vxlan collect --hosts 192.0.2.0/24 --name edge --exclude-name spare --model 'S68
213
213
  IP 支持单地址、CIDR、范围;名称及型号支持 contains(默认)、prefix、glob,不区分大小写。`--vendor`/`--exclude-vendor` 使用连接器平台标识(如 cisco_nxos、h3c、h3c_wireless、huawei),不是原始厂商标签。同字段条件为 OR,跨字段为 AND,排除优先。缺失型号不命中包含规则。`--model`/`--exclude-model`、`--name-match`/`--model-match` 可组合;筛选只选择已有清单,不扫描地址、不扩大平台支持范围。
214
214
 
215
215
  YAML 顶层对应 hosts/exclude_hosts、names/exclude_names、vendors/exclude_vendors、models/exclude_models、name_match/model_match。Netdisco inventory 段使用 include_hosts/include_names/include_vendors/include_models 及对应 exclude 字段。
216
+
217
+ ## ctx_cli 公共入口
218
+
219
+ 分发、公共选项、配置初始化、离线输入输出、终端确认和采集入口由 ctx_cli 组合提供;领域规则与 ctx_runner 业务状态保持各自职责。`CTX_CLI_PATH=../ctx_cli bundle exec rake` 用于源码联调;普通入口先安装 ctx_cli 候选包。
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
+ require "ctx_cli/terminal"
4
5
  require "json"
5
6
  require "digest"
6
7
  require "ctx_runner/execution"
@@ -59,7 +60,8 @@ module Vxlan
59
60
  end
60
61
  rest = parser.parse(argv.dup)
61
62
  return @out.puts(parser) || 0 if options[:help]
62
- raise ArgumentError, "必须使用真实交互终端" unless @input.tty? && @out.tty? && @error.tty?
63
+ terminal = CtxCli::Terminal.new(input: @input, out: @out)
64
+ terminal.require_tty!(@error)
63
65
  raise ArgumentError, "需要单个计划" unless rest.empty? && options[:plan] && options[:port].between?(1, 65_535)
64
66
  body = File.read(options[:plan])
65
67
  hash = Digest::SHA256.hexdigest(body)
@@ -71,9 +73,8 @@ module Vxlan
71
73
  @out.puts Array(plan["prerequisites"]).map { |value| "条件:#{value}" }
72
74
  @out.puts "计划 SHA256:#{hash}\n拟执行命令:\n#{change.commands.join("\n")}\n条件回退(不自动执行):\n#{change.rollback.join("\n")}"
73
75
  token = "APPLY #{host} #{hash[0, 12]} #{options[:item]}"
74
- @out.print "确认业务策略及维护窗口后输入 #{token};其他输入取消:"
75
- @out.flush
76
- return @out.puts("已取消,未连接设备。") || 1 unless @input.gets&.strip == token
76
+ confirmed = terminal.confirm(token: token, prompt: "确认业务策略及维护窗口后输入 #{token};其他输入取消:")
77
+ return @out.puts("已取消,未连接设备。") || 1 unless confirmed
77
78
  settings = credentials(options)
78
79
  @store = Report.new(options[:directory])
79
80
  @execution = CtxRunner::Execution.new(plan: plan, change: change, journal: @store, plan_sha256: hash, item: options[:item])
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "vxlan"
4
4
  require "ctx_runner/batch"
5
+ require "ctx_cli/collection"
5
6
  require_relative "options"
6
7
  require_relative "report"
7
8
  require_relative "connector"
@@ -17,79 +18,31 @@ module Vxlan
17
18
  @defaults = options
18
19
  end
19
20
 
20
- # 先预检全部输入和凭据,再创建报告和连接;失败分类不打印原始异常正文。
21
+ # 公共入口编排顺序与失败收尾;以下方法是显式领域适配协议。
21
22
  def run(argv)
22
- # 每次运行独立持有批次状态;预检失败不能改写上一次报告。
23
- @options, @store, @progress, @devices, @batch = nil, nil, nil, [], nil
24
- @options = Options.new(argv, env: @env, out: @out, **@defaults)
25
- return 0 if @options.informational?
26
- @options.validate_interaction!(input: @input, error: @error)
27
- @options.load_environment
28
- if @options.flags[:init]
29
- @options.initialize_files
30
- return 0
23
+ progress = ->(options) do
24
+ require "net/connector/collect/progress"
25
+ Net::Connector::Collect::Progress.new(io: @error, enabled: !options.flags[:quiet], verbose: options.flags[:verbose])
31
26
  end
32
- return 0 if @options.missing?(input: @input, error: @error)
33
- @options.load
34
- require "net/connector/collect/progress"
35
- @progress = Net::Connector::Collect::Progress.new(io: @error, enabled: !@options.flags[:quiet], verbose: @options.flags[:verbose])
36
- @progress.notice("配置:#{@options.config}")
37
- return output(@options.public_values.merge("sources" => @options.sources)) if @options.flags[:show_config]
38
- @devices = devices = inventory
39
- return output(devices) if @options.flags[:inventory_only]
40
- if devices.empty?
41
- @progress.notice("没有匹配设备,未建立连接")
42
- @out.puts JSON.generate(status: :no_devices, total: 0) if @options.flags[:json]
43
- return 1
44
- end
45
- credentials = credentials!
46
- load_network unless @connector_factory && @executor
47
- unless @connector_factory
48
- devices.each { |device| Net::Connector::Configuration.new(host: device.fetch("host"), **credentials).validate_endpoint! }
49
- end
50
- unless @executor
51
- require "net/connector/resources"
52
- Net::Connector::Resources.prepare!(parallel: [ devices.size, @options.values.fetch("parallel") ].min)
53
- end
54
- @store = Report.new(@options.values.fetch("directory"), scripts: @options.flags[:scripts] == true)
55
- @batch = CtxRunner::Batch.new(report: @store, progress: @progress, requested: devices)
56
- outcome = @batch.run(present: ->(summary) { @out.puts JSON.pretty_generate(summary) if @options.flags[:json] }) do
57
- @progress.plan(devices: devices, total: @inventory_total, parallel: @options.values.fetch("parallel"), label: "VXLAN 只读采集")
58
- @progress.with_updates do
59
- execute(devices) { |device| collect(device, credentials) }
60
- end
61
- @store.review_collected_fabric(@options.fabric_policy)
62
- end
63
- completion(outcome)
64
- rescue Interrupt
65
- diagnostic "采集已取消,已保存的设备报告保留;未保存结果见报告状态"
66
- 130
67
- rescue SystemCallError, IOError => error
68
- diagnostic "VXLAN 参数或报告错误:#{report_diagnostic(error)}"
69
- 2
70
- rescue OptionParser::ParseError, ArgumentError, LoadError => error
71
- diagnostic "VXLAN 参数、依赖或报告错误:#{error.message};使用 --help 查看参数"
72
- 2
73
- rescue StandardError => error
74
- diagnostic "VXLAN 清单或执行失败:#{CtxDriver::Failure.code(error, :collection_failed)}(#{error.class.name})"
75
- 2
27
+ CtxCli::Collection.new(adapter: self, options: ->(arguments) { Options.new(arguments, env: @env, out: @out, **@defaults) },
28
+ progress: progress, batch: CtxRunner::Batch, label: "VXLAN", report_error: Report::Error,
29
+ failure_code: ->(error) { CtxDriver::Failure.code(error, :collection_failed) },
30
+ out: @out, error: @error, input: @input).run(argv)
76
31
  end
77
32
 
78
- private
79
-
80
33
  # 只在需要库存或网络执行时加载可选依赖,帮助和静态预览保持独立。
81
34
  def load_network
82
35
  require "net/connector"
83
36
  rescue LoadError
84
- raise LoadError, "联网采集需要 net-connector >= 0.17.0;请在宿主 Gemfile 添加或 gem install net-connector", cause: nil
37
+ raise LoadError, "联网采集需要 net-connector >= 0.17.1;请在宿主 Gemfile 添加或 gem install net-connector", cause: nil
85
38
  end
86
39
 
87
40
  # YAML 静态设备或显式 Netdisco 来源二选一;所有重复地址在连接前拒绝。
88
- def inventory
89
- values = @options.values
90
- rows = if @options.dynamic_inventory?
91
- @progress.reading_inventory
92
- settings = @options.netdisco_settings
41
+ def inventory(options, progress:)
42
+ values = options.values
43
+ rows = if options.dynamic_inventory?
44
+ progress.reading_inventory
45
+ settings = options.netdisco_settings
93
46
  Net::Connector::Netdisco::Fleet.new(settings: settings).devices.filter_map do |device|
94
47
  next unless device.ready? && device.vendor == :cisco_nxos
95
48
  { "host" => device.host, "name" => device.name || device.host, "model" => device.model }
@@ -97,89 +50,63 @@ module Vxlan
97
50
  else
98
51
  values.fetch("devices")
99
52
  end
100
- targets = @options.normalize_devices(rows)
101
- @inventory_total = targets.size
53
+ targets = options.normalize_devices(rows)
54
+ total = targets.size
102
55
  selection = Net::Connector::Inventory::Filters.selection(values)
103
- targets.select do |device|
56
+ selected = targets.select do |device|
104
57
  selection.selected?(device.fetch("host"), "cisco_nxos", name: device["name"], model: device["model"])
105
58
  end
59
+ [ selected, total ]
106
60
  end
107
61
 
62
+ # 所有设备先完成凭据、端点与资源预检,再允许创建报告和连接。
63
+ def prepare(devices, options)
64
+ credentials = credentials!(options)
65
+ load_network unless @connector_factory && @executor
66
+ unless @connector_factory
67
+ devices.each { |device| Net::Connector::Configuration.new(host: device.fetch("host"), **credentials).validate_endpoint! }
68
+ end
69
+ unless @executor
70
+ require "net/connector/resources"
71
+ Net::Connector::Resources.prepare!(parallel: [ devices.size, options.values.fetch("parallel") ].min)
72
+ end
73
+ credentials
74
+ end
75
+
76
+ def report(options) = Report.new(options.values.fetch("directory"), scripts: options.flags[:scripts] == true)
77
+ def after_collect(report, options) = report.review_collected_fabric(options.fabric_policy)
78
+
108
79
  # 缺失用户名时不尝试连接;密码允许省略以支持明确的 SSH key/agent。
109
- def credentials!
110
- username = secret("NC_CISCO_NXOS_USERNAME") || secret("NC_DEVICE_USERNAME")
80
+ def credentials!(options)
81
+ username = secret(options, "NC_CISCO_NXOS_USERNAME") || secret(options, "NC_DEVICE_USERNAME")
111
82
  raise ArgumentError, "请设置 NC_DEVICE_USERNAME 或 NC_CISCO_NXOS_USERNAME;密码可通过环境或 SSH key/agent 提供" unless username
112
- @options.values.fetch("ssh").transform_keys(&:to_sym).merge(username: username,
113
- password: secret("NC_CISCO_NXOS_PASSWORD") || secret("NC_DEVICE_PASSWORD"), enable_password: secret("NC_ENABLE_PASSWORD"))
83
+ options.values.fetch("ssh").transform_keys(&:to_sym).merge(username: username,
84
+ password: secret(options, "NC_CISCO_NXOS_PASSWORD") || secret(options, "NC_DEVICE_PASSWORD"), enable_password: secret(options, "NC_ENABLE_PASSWORD"))
114
85
  end
115
86
 
116
87
  # 空白环境值视为缺失,不修改或回显凭据。
117
- def secret(name)
118
- value = @options.environment[name]
88
+ def secret(options, name)
89
+ value = options.environment[name]
119
90
  value unless value.nil? || value.strip.empty?
120
91
  end
121
92
 
122
93
  # 每个任务使用独立 Collector 和会话;公共执行器负责有界并发和线程回收。
123
- def execute(devices, &block)
124
- parallel = @options.values.fetch("parallel")
94
+ def execute(devices, options, &block)
95
+ parallel = options.values.fetch("parallel")
125
96
  return @executor.call(devices, parallel, &block) if @executor
126
97
  Net::Connector::Parallel::Pool.new(parallel: parallel).map(devices, &block)
127
98
  end
128
99
 
129
100
  # 单台失败保留其他设备和已获得维度;报告持久化失败交由批次终止处理。
130
- def collect(device, credentials)
131
- credentials = credentials.merge(on_event: @progress.method(:event))
132
- @batch.collect(device) do
133
- connector = @connector_factory ? @connector_factory.call(device, credentials) : Connector.new(device, credentials)
134
- snapshot = Vxlan::Collector.new.collect(connector: connector, target: device, fabric_key: @options.values.fetch("fabric"),
135
- model: device["model"], dimensions: @options.values.fetch("dimensions").map(&:to_sym))
136
- snapshot
137
- end
101
+ def collect(device, options, context:, progress:)
102
+ credentials = context.merge(on_event: progress.method(:event))
103
+ connector = @connector_factory ? @connector_factory.call(device, credentials) : Connector.new(device, credentials)
104
+ snapshot = Vxlan::Collector.new.collect(connector: connector, target: device, fabric_key: options.values.fetch("fabric"),
105
+ model: device["model"], dimensions: options.values.fetch("dimensions").map(&:to_sym))
106
+ snapshot
138
107
  end
139
108
 
140
- # Runner 决定批次结论;CLI 仅呈现安全诊断并映射退出码。
141
- def completion(outcome)
142
- if outcome.finalization_error
143
- diagnostic "VXLAN 摘要报告错误:#{outcome.finalization_error}"
144
- end
145
- if outcome.recovery_failed
146
- diagnostic "摘要恢复未确认;请依据退出码及报告目录检查,不能将旧摘要视为本次成功。"
147
- end
148
- if outcome.status == :interrupted
149
- diagnostic "采集已取消,已保存的设备报告保留;未保存结果见报告状态"
150
- elsif (failure = outcome.failure)
151
- if failure[:kind] == :report
152
- diagnostic "VXLAN 参数或报告错误:#{failure[:detail]}"
153
- else
154
- diagnostic "VXLAN 清单或执行失败:#{failure[:error_type]};已采集结果见批次报告"
155
- end
156
- end
157
- { complete: 0, incomplete: 1, failed: 2, interrupted: 130 }.fetch(outcome.status)
158
- end
159
-
160
- # 错误提示失败不能覆盖取消或执行退出码;已失效的进度流不再重试。
161
- def diagnostic(message)
162
- return if @progress&.output_error
163
- @error.puts(message)
164
- rescue StandardError
165
- nil
166
- end
167
-
168
- # 自有发布错误消息不含正文;外部文件异常只显示类型,不打印 message/cause。
169
- def report_diagnostic(error)
170
- error.instance_of?(Report::Error) ? error.message : error.class.name
171
- end
172
-
173
- # 预览默认显示逐设备身份;公开配置由显式开关展示。
174
- def output(value)
175
- if @options.flags[:json] || value.is_a?(Hash)
176
- @out.puts JSON.pretty_generate(value)
177
- else
178
- value.each { |device| @out.puts "#{device.fetch('host')}\t#{device.fetch('name')}\t#{device['model']}" }
179
- @out.puts "设备:#{value.size} 台;未建立设备连接"
180
- end
181
- value.is_a?(Array) && value.empty? ? 1 : 0
182
- end
109
+ private :load_network, :credentials!, :secret
183
110
  end
184
111
  end
185
112
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
+ require "ctx_cli/options"
5
+ require "ctx_cli/terminal"
6
+ require "ctx_cli/initialization"
4
7
  require "net/connector/inventory/filters"
5
8
  require "yaml"
6
9
  require "ipaddr"
7
- require "fileutils"
8
- require "tempfile"
9
10
 
10
11
  module Vxlan
11
12
  module CLI
@@ -47,7 +48,7 @@ module Vxlan
47
48
 
48
49
  # 自动化拒绝强制问答;帮助分支无需检查终端或消费输入。
49
50
  def validate_interaction!(input:, error:)
50
- if flags[:interactive] && !(input.tty? && error.tty?)
51
+ if flags[:interactive] && !CtxCli::Terminal.tty?(input, error)
51
52
  raise ArgumentError, "--interactive 需要交互终端;自动化请使用 --init --non-interactive"
52
53
  end
53
54
  end
@@ -63,7 +64,7 @@ module Vxlan
63
64
  @environment = @files.environment
64
65
  @config, @env_file = @files.config, @files.env_file
65
66
  rescue LoadError
66
- raise LoadError, "采集配置需要 net-connector >= 0.17.0;读取 .env 还需要 dotenv", cause: nil
67
+ raise LoadError, "采集配置需要 net-connector >= 0.17.1;读取 .env 还需要 dotenv", cause: nil
67
68
  end
68
69
 
69
70
  # 公共与专用文件先合并,进程值和 CLI 最后覆盖;数组不追加设备。
@@ -150,22 +151,11 @@ module Vxlan
150
151
  end
151
152
  overrides_parallel = @overrides.key?("parallel") || parallel_source&.fetch("VXLAN_PARALLEL", nil)
152
153
  document = overrides_parallel ? values : values.reject { |key, _| key == "parallel" }
153
- template = template.lines.select { |line| line.start_with?("#") }.join +
154
+ template = template.lines("\n").select { |line| line.start_with?("#") }.join("") +
154
155
  Net::Connector::Config.dump(document, name: "vxlan", path: config)
155
156
  { config => template, env_file => File.read(File.join(__dir__, "templates/env.example")) }.each do |path, body|
156
- FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
157
- begin
158
- Tempfile.create([ ".vxlan-init-", ".tmp" ], File.dirname(path)) do |file|
159
- file.chmod(0o600)
160
- file.write(body)
161
- file.flush
162
- file.fsync
163
- File.link(file.path, path)
164
- end
165
- @out.puts "已创建:#{path}"
166
- rescue Errno::EEXIST
167
- @out.puts "已保留:#{path}"
168
- end
157
+ status = CtxCli::Initialization.new(prefix: ".vxlan-init-").create(path: path, contents: body)
158
+ @out.puts "#{status == :created ? '已创建' : '已保留'}:#{path}"
169
159
  end
170
160
  @out.puts "请检查配置及凭据后再次运行;本次未读取清单、未连接设备。"
171
161
  end
@@ -175,10 +165,10 @@ module Vxlan
175
165
  @files.load
176
166
  @config, @env_file = @files.config, @files.env_file
177
167
  return false unless @files.missing?
178
- if input.tty? && error.tty? && flags[:interactive] != false
179
- error.print "配置不存在:#{config};是否创建配置与环境模板?[y/N] "
180
- error.flush
181
- if input.gets&.strip&.match?(/\Ay(?:es)?\z/i)
168
+ terminal = CtxCli::Terminal.new(input: input, out: error)
169
+ if terminal.tty? && flags[:interactive] != false
170
+ answer = terminal.question("配置不存在:#{config};是否创建配置与环境模板?[y/N] ")
171
+ if answer&.match?(/\Ay(?:es)?\z/i)
182
172
  initialize_files
183
173
  return true
184
174
  end
@@ -232,10 +222,14 @@ module Vxlan
232
222
  def parser
233
223
  OptionParser.new do |p|
234
224
  p.banner = "用法:#{@command} [选项](只读 VXLAN 采集)"
235
- p.on("--config FILE", "最高优先级 YAML,叠加系统/用户/脚本/工作目录配置") { |v| @explicit_config = File.expand_path(v) }
236
- p.on("--env-file FILE", "最高优先级环境文件;进程环境仍优先") { |v| @explicit_env_file = File.expand_path(v) }
237
- p.on("-p", "--parallel N", Integer, "设备并发 1~500;继承 NC_PARALLEL,未配置时 4") { |v| @overrides["parallel"] = v }
238
- p.on("--directory DIR", "按设备保存报告的目录") { |v| @overrides["directory"] = File.expand_path(v) }
225
+ CtxCli::Options.configuration(p, parallel_help: "设备并发 1~500;继承 NC_PARALLEL,未配置时 4") do |key, value|
226
+ case key
227
+ when :config then @explicit_config = File.expand_path(value, Dir.pwd)
228
+ when :env_file then @explicit_env_file = File.expand_path(value, Dir.pwd)
229
+ when :directory then @overrides["directory"] = File.expand_path(value, Dir.pwd)
230
+ when :parallel then @overrides["parallel"] = value
231
+ end
232
+ end
239
233
  p.on("--fabric-policy FILE", "显式 Fabric/Leaf/Border/Spine 清单 YAML") { |v| @overrides["fabric_policy"] = File.expand_path(v) }
240
234
  p.on("--fabric NAME", "采集所属 Fabric 身份") { |v| @overrides["fabric"] = v }
241
235
  p.on("--dimensions LIST", "逗号分隔维度;--help 查看支持项") { |v| @overrides["dimensions"] = list(v) }
@@ -246,29 +240,19 @@ module Vxlan
246
240
  @overrides[key] = value
247
241
  end
248
242
  end
249
- p.on("--init", "创建缺失配置和环境模板后退出;已有文件不覆盖") { flags[:init] = true }
250
- p.on("--interactive", "缺少配置时询问是否创建;要求真实终端") { flags[:interactive] = true }
251
- p.on("--non-interactive", "禁用问答;创建文件须同时指定 --init") { flags[:interactive] = false }
243
+ CtxCli::Options.initialization(p, flags)
252
244
  p.on("--plan", "--inventory-only", "预览筛选清单,不连接设备") { flags[:inventory_only] = true }
253
245
  p.on("--show-config", "查看公开有效配置") { flags[:show_config] = true }
254
- p.on("--json", "stdout 输出 JSON;默认显示可读摘要") { flags[:json] = true }
255
- p.on("--quiet", "--no-progress", "关闭 stderr 进度,可配合 --json") { flags[:quiet] = true }
246
+ CtxCli::Options.output(p, flags)
256
247
  p.on("--[no-]scripts", "可选导出配置建议,默认仅诊断") { |value| flags[:scripts] = value }
257
- p.on("--verbose", "显示逐台结果及安全命令事件") { flags[:verbose] = true }
258
- p.on("-h", "--help", "查看选项帮助") { flags[:help] = true }
259
- p.on("--help-all", "查看全部选项、路径与环境约定") { flags[:help_all] = true }
260
- p.on("--examples", "查看常用命令范例,不执行任务") { flags[:examples] = true }
248
+ CtxCli::Options.information(p, flags)
261
249
  p.separator "维度:#{Vxlan::Collector::COMMANDS.keys.join(', ')}"
262
250
  p.separator "优先级:CLI > 进程环境 > YAML > .env > 内置默认值。退出码:0 完整/预览;1 未完成/无目标;2 参数/持久化错误;130 取消。"
263
251
  end
264
252
  end
265
253
 
266
254
  # 列表中的空项属于输入错误,不能被静默删掉。
267
- def list(value)
268
- values = value.split(",", -1).map(&:strip)
269
- raise ArgumentError, "列表不能有空项" if values.any?(&:empty?)
270
- values
271
- end
255
+ def list(value) = CtxCli::Options.list(value)
272
256
 
273
257
  # 先验证整批策略,避免在前面设备连接后才发现后面的非法配置。
274
258
  def validate!
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "optparse"
3
+ require "ctx_cli/command"
4
+ require "ctx_cli/options"
5
+ require "ctx_cli/input"
6
+ require "ctx_cli/output"
4
7
  require "json"
5
8
  require "time"
6
9
  require "vxlan"
@@ -12,45 +15,40 @@ module Vxlan
12
15
  # 注入标准流,便于在宿主测试中验证无需网络的行为。
13
16
  def initialize(out: $stdout, error: $stderr, input: $stdin, command: "vxlan parse")
14
17
  @out, @error, @input, @command = out, error, input, command
18
+ @output = CtxCli::Output.new(out: out, error: error)
15
19
  end
16
20
 
17
21
  # 参数错误与部分证据分开返回;默认只显示元数据,JSON 显式导出事实。
18
22
  def run(argv)
19
- options = { dimension: :configuration, complete: false }
20
- parser = OptionParser.new do |p|
21
- p.banner = "用法:#{@command} [选项](离线文件或 stdin)"
23
+ command = CtxCli::Command.new(banner: "用法:#{@command} [选项](离线文件或 stdin)", defaults: { dimension: :configuration, complete: false }, out: @out) do |p, options|
22
24
  p.on("--file FILE", "读取文件,默认 stdin") { |v| options[:file] = v }
23
25
  p.on("--dimension NAME", "configuration 或运行维度") { |v| options[:dimension] = v.to_sym }
24
26
  p.on("--model NAME", "设备型号") { |v| options[:model] = v }
25
27
  p.on("--complete", "声明输入已完整采集,仍保留解析缺失诊断") { options[:complete] = true }
26
28
  p.on("--json", "输出事实 JSON,可能包含配置敏感信息") { options[:json] = true }
27
- p.on("-h", "--help", "查看帮助") { options[:help] = true }
28
- p.on("--help-all", "查看全部选项") { options[:help] = true }
29
- p.on("--examples", "查看示例") { options[:examples] = true }
29
+ CtxCli::Options.information(p, options)
30
30
  p.separator "维度:#{Vxlan::Collector::COMMANDS.keys.join(', ')}"
31
31
  end
32
- rest = parser.parse(argv.dup)
33
- raise ArgumentError, "不接受位置参数" unless rest.empty?
34
- return @out.puts(parser) || 0 if options[:help]
35
- return @out.puts("#{@command} --file running.cfg --complete\ncat nve.txt | #{@command} --dimension nve_peers --complete --json") || 0 if options[:examples]
32
+ options = command.parse(argv)
33
+ return 0 if command.informational?(examples: "#{@command} --file running.cfg --complete\ncat nve.txt | #{@command} --dimension nve_peers --complete --json")
36
34
  raise ArgumentError, "未知维度" unless Vxlan::Collector::COMMANDS.key?(options[:dimension])
37
35
 
38
- body = options[:file] ? File.read(options[:file]) : @input.read
36
+ body = CtxCli::Input.read(path: options[:file], input: @input)
39
37
  observation = if options[:dimension] == :configuration
40
38
  Vxlan::Config.new(model: options[:model]).parse(body, complete: options[:complete])
41
39
  else
42
40
  Vxlan::RuntimeParser.new(model: options[:model]).parse(options[:dimension], body, complete: options[:complete])
43
41
  end
44
42
  if options[:json]
45
- @out.puts JSON.pretty_generate(observation.to_h)
43
+ @output.json(observation.to_h)
46
44
  else
47
45
  @out.puts "维度:#{observation.name};证据:#{observation.status};记录:#{observation.records.size}"
48
46
  @out.puts "诊断:#{observation.issues.map { |issue| issue[:code] }.uniq.join(', ')}" unless observation.issues.empty?
49
47
  @out.puts "证据完整性不代表 Fabric 健康。"
50
48
  end
51
49
  observation.complete? ? 0 : 1
52
- rescue OptionParser::ParseError, ArgumentError, SystemCallError => error
53
- @error.puts "离线解析参数或文件错误(#{error.class.name});使用 --help 查看参数"
50
+ rescue OptionParser::ParseError, ArgumentError, SystemCallError, IOError => error
51
+ @output.diagnostic("离线解析参数或文件错误;使用 --help 查看参数", exception: error)
54
52
  2
55
53
  end
56
54
  end
data/lib/vxlan/cli.rb CHANGED
@@ -1,46 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "vxlan/version"
4
+ require "ctx_cli/application"
4
5
 
5
6
  module Vxlan
6
7
  # 命令入口按需加载采集或解析实现;核心库无需加载 CLI。
7
8
  module CLI
8
9
  # 由调用方拥有退出行为,库入口只返回退出码。
9
10
  def self.run(argv, env: ENV, out: $stdout, error: $stderr, input: $stdin, **collect_options)
10
- arguments = argv.dup
11
- command = arguments.shift
12
- case command
13
- when nil, "--help", "--help-all", "-h"
14
- return usage(out) if arguments.empty?
15
- when "--version", "-v"
16
- return out.puts(Vxlan::VERSION) || 0 if arguments.empty?
17
- when "collect"
18
- begin
19
- require_relative "cli/collect"
20
- rescue LoadError
21
- error.puts "collect 需要含共享清单筛选的当前 net-connector;请安装项目依赖。"
22
- return 2
11
+ commands = {
12
+ "collect" => ->(arguments) do
13
+ begin
14
+ require_relative "cli/collect"
15
+ rescue LoadError
16
+ error.puts "collect 需要含共享清单筛选的当前 net-connector;请安装项目依赖。"
17
+ next 2
18
+ end
19
+ Collect.new(env: env, out: out, error: error, input: input, **collect_options).run(arguments)
20
+ end,
21
+ "apply" => ->(arguments) do
22
+ require_relative "cli/apply"
23
+ Apply.new(env: env, out: out, error: error, input: input, connector_factory: collect_options[:connector_factory], discovery: collect_options.fetch(:discovery, {})).run(arguments)
24
+ end,
25
+ "review" => ->(arguments) do
26
+ require_relative "cli/review"
27
+ Review.new(out: out, error: error).run(arguments)
28
+ end,
29
+ "parse" => ->(arguments) do
30
+ require_relative "cli/parse"
31
+ Parse.new(out: out, error: error, input: input, command: collect_options.fetch(:command, "vxlan parse")).run(arguments)
23
32
  end
24
- return Collect.new(env: env, out: out, error: error, input: input, **collect_options).run(arguments)
25
- when "apply"
26
- require_relative "cli/apply"
27
- return Apply.new(env: env, out: out, error: error, input: input, connector_factory: collect_options[:connector_factory], discovery: collect_options.fetch(:discovery, {})).run(arguments)
28
- when "review"
29
- require_relative "cli/review"
30
- return Review.new(out: out, error: error).run(arguments)
31
- when "parse"
32
- require_relative "cli/parse"
33
- return Parse.new(out: out, error: error, input: input, command: collect_options.fetch(:command, "vxlan parse")).run(arguments)
34
- end
35
- error.puts "未知命令或多余参数;使用 vxlan --help 查看用法"
36
- 2
33
+ }
34
+ CtxCli::Application.new(name: "vxlan", version: Vxlan::VERSION, usage: USAGE,
35
+ commands: commands, out: out, error: error).run(argv)
37
36
  end
38
37
 
39
38
  # 全局帮助不读取配置、环境文件或设备清单。
40
- def self.usage(out)
41
- out.puts "用法:vxlan collect [选项] 只读采集与报告\n vxlan parse [选项] 离线文件或 stdin 解析\n vxlan review --batch DIR 离线修复建议\n vxlan apply --plan FILE --item ID 交互确认后下发\n vxlan --version\n使用 vxlan collect --help 、vxlan parse --help 或 vxlan review --help 查看参数;--help-all 查看完整约定,--examples 查看范例。"
42
- 0
43
- end
44
- private_class_method :usage
39
+ USAGE = "用法:vxlan collect [选项] 只读采集与报告\n vxlan parse [选项] 离线文件或 stdin 解析\n vxlan review --batch DIR 离线修复建议\n vxlan apply --plan FILE --item ID 交互确认后下发\n vxlan --version\n使用 vxlan collect --help 、vxlan parse --help 或 vxlan review --help 查看参数;--help-all 查看完整约定,--examples 查看范例。".freeze
40
+ private_constant :USAGE
45
41
  end
46
42
  end
data/lib/vxlan/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Vxlan
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.4.0"
5
+ VERSION = "0.4.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vxlan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ctx_cli
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.1.0
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: ctx_runner
14
28
  requirement: !ruby/object:Gem::Requirement