vxlan 0.4.0 → 0.4.2

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.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ctx_cli/collection"
3
4
  require "vxlan"
4
5
  require "ctx_runner/batch"
5
6
  require_relative "options"
@@ -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.16.0;请在宿主 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,71 @@ 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
+ begin
72
+ Net::Connector::Resources.prepare!(parallel: [ devices.size, options.values.fetch("parallel") ].min)
73
+ rescue ArgumentError
74
+ raise CtxCli::Collection::Error, "本机文件描述符额度不足;请提高终端 ulimit -n 或降低 --parallel。已停止,未连接设备。", cause: nil
75
+ end
76
+ end
77
+ credentials
78
+ end
79
+
80
+ def report(options)
81
+ Report.new(options.values.fetch("directory"), scripts: options.flags[:scripts] == true)
82
+ rescue ArgumentError
83
+ raise CtxCli::Collection::Error, "报告目录无效;请使用真实私有目录、权限0700,并检查目录路径。", cause: nil
84
+ end
85
+ def after_collect(report, options) = report.review_collected_fabric(options.fabric_policy)
86
+
108
87
  # 缺失用户名时不尝试连接;密码允许省略以支持明确的 SSH key/agent。
109
- def credentials!
110
- username = secret("NC_CISCO_NXOS_USERNAME") || secret("NC_DEVICE_USERNAME")
111
- 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"))
88
+ def credentials!(options)
89
+ username = secret(options, "NC_CISCO_NXOS_USERNAME") || secret(options, "NC_DEVICE_USERNAME")
90
+ raise CtxCli::Collection::Error, "请设置 NC_DEVICE_USERNAME 或 NC_CISCO_NXOS_USERNAME;密码可通过环境或 SSH key/agent 提供" unless username
91
+ options.values.fetch("ssh").transform_keys(&:to_sym).merge(username: username,
92
+ password: secret(options, "NC_CISCO_NXOS_PASSWORD") || secret(options, "NC_DEVICE_PASSWORD"), enable_password: secret(options, "NC_ENABLE_PASSWORD"))
114
93
  end
115
94
 
116
95
  # 空白环境值视为缺失,不修改或回显凭据。
117
- def secret(name)
118
- value = @options.environment[name]
96
+ def secret(options, name)
97
+ value = options.environment[name]
119
98
  value unless value.nil? || value.strip.empty?
120
99
  end
121
100
 
122
101
  # 每个任务使用独立 Collector 和会话;公共执行器负责有界并发和线程回收。
123
- def execute(devices, &block)
124
- parallel = @options.values.fetch("parallel")
102
+ def execute(devices, options, &block)
103
+ parallel = options.values.fetch("parallel")
125
104
  return @executor.call(devices, parallel, &block) if @executor
126
105
  Net::Connector::Parallel::Pool.new(parallel: parallel).map(devices, &block)
127
106
  end
128
107
 
129
108
  # 单台失败保留其他设备和已获得维度;报告持久化失败交由批次终止处理。
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
138
- end
139
-
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)
109
+ def collect(device, options, context:, progress:)
110
+ credentials = context.merge(on_event: progress.method(:event))
111
+ connector = @connector_factory ? @connector_factory.call(device, credentials) : Connector.new(device, credentials)
112
+ snapshot = Vxlan::Collector.new.collect(connector: connector, target: device, fabric_key: options.values.fetch("fabric"),
113
+ model: device["model"], dimensions: options.values.fetch("dimensions").map(&:to_sym))
114
+ snapshot
158
115
  end
159
116
 
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
117
+ private :load_network, :credentials!, :secret
183
118
  end
184
119
  end
185
120
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ctx_cli/collection"
4
+ require "ctx_cli/input"
3
5
  require "optparse"
6
+ require "ctx_cli/options"
7
+ require "ctx_cli/terminal"
8
+ require "ctx_cli/initializer"
4
9
  require "net/connector/inventory/filters"
5
10
  require "yaml"
6
11
  require "ipaddr"
7
- require "fileutils"
8
- require "tempfile"
9
12
 
10
13
  module Vxlan
11
14
  module CLI
@@ -47,8 +50,8 @@ module Vxlan
47
50
 
48
51
  # 自动化拒绝强制问答;帮助分支无需检查终端或消费输入。
49
52
  def validate_interaction!(input:, error:)
50
- if flags[:interactive] && !(input.tty? && error.tty?)
51
- raise ArgumentError, "--interactive 需要交互终端;自动化请使用 --init --non-interactive"
53
+ if flags[:interactive] && !CtxCli::Terminal.tty?(input, error)
54
+ raise CtxCli::Collection::Error, "--interactive 需要交互终端;自动化请使用 --init --non-interactive"
52
55
  end
53
56
  end
54
57
 
@@ -63,7 +66,7 @@ module Vxlan
63
66
  @environment = @files.environment
64
67
  @config, @env_file = @files.config, @files.env_file
65
68
  rescue LoadError
66
- raise LoadError, "采集配置需要 net-connector >= 0.17.0;读取 .env 还需要 dotenv", cause: nil
69
+ raise LoadError, "采集配置需要 net-connector >= 0.17.1;读取 .env 还需要 dotenv", cause: nil
67
70
  end
68
71
 
69
72
  # 公共与专用文件先合并,进程值和 CLI 最后覆盖;数组不追加设备。
@@ -90,7 +93,7 @@ module Vxlan
90
93
  @fabric_policy = nil
91
94
  if values["fabric_policy"]
92
95
  raise ArgumentError, "fabric_policy 须为路径" unless values["fabric_policy"].is_a?(String)
93
- policy = YAML.safe_load(File.read(values["fabric_policy"]), aliases: false)
96
+ policy = YAML.safe_load(CtxCli::Input.read(path: values["fabric_policy"]), aliases: false)
94
97
  Vxlan::Fabric::Scope.new(policy)
95
98
  @fabric_policy = CtxParser::Immutable.copy(policy)
96
99
  end
@@ -150,22 +153,11 @@ module Vxlan
150
153
  end
151
154
  overrides_parallel = @overrides.key?("parallel") || parallel_source&.fetch("VXLAN_PARALLEL", nil)
152
155
  document = overrides_parallel ? values : values.reject { |key, _| key == "parallel" }
153
- template = template.lines.select { |line| line.start_with?("#") }.join +
156
+ template = template.lines("\n").select { |line| line.start_with?("#") }.join("") +
154
157
  Net::Connector::Config.dump(document, name: "vxlan", path: config)
155
158
  { 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
159
+ status = CtxCli::Initializer.new(prefix: ".vxlan-init-").create(path: path, contents: body)
160
+ @out.puts "#{status == :created ? '已创建' : '已保留'}:#{path}"
169
161
  end
170
162
  @out.puts "请检查配置及凭据后再次运行;本次未读取清单、未连接设备。"
171
163
  end
@@ -175,10 +167,10 @@ module Vxlan
175
167
  @files.load
176
168
  @config, @env_file = @files.config, @files.env_file
177
169
  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)
170
+ terminal = CtxCli::Terminal.new(input: input, out: error)
171
+ if terminal.tty? && flags[:interactive] != false
172
+ answer = terminal.question("配置不存在:#{config};是否创建配置与环境模板?[y/N] ")
173
+ if answer&.match?(/\Ay(?:es)?\z/i)
182
174
  initialize_files
183
175
  return true
184
176
  end
@@ -232,10 +224,14 @@ module Vxlan
232
224
  def parser
233
225
  OptionParser.new do |p|
234
226
  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) }
227
+ CtxCli::Options.configuration(p, parallel_help: "设备并发 1~500;继承 NC_PARALLEL,未配置时 4") do |key, value|
228
+ case key
229
+ when :config then @explicit_config = File.expand_path(value, Dir.pwd)
230
+ when :env_file then @explicit_env_file = File.expand_path(value, Dir.pwd)
231
+ when :directory then @overrides["directory"] = File.expand_path(value, Dir.pwd)
232
+ when :parallel then @overrides["parallel"] = value
233
+ end
234
+ end
239
235
  p.on("--fabric-policy FILE", "显式 Fabric/Leaf/Border/Spine 清单 YAML") { |v| @overrides["fabric_policy"] = File.expand_path(v) }
240
236
  p.on("--fabric NAME", "采集所属 Fabric 身份") { |v| @overrides["fabric"] = v }
241
237
  p.on("--dimensions LIST", "逗号分隔维度;--help 查看支持项") { |v| @overrides["dimensions"] = list(v) }
@@ -246,29 +242,19 @@ module Vxlan
246
242
  @overrides[key] = value
247
243
  end
248
244
  end
249
- p.on("--init", "创建缺失配置和环境模板后退出;已有文件不覆盖") { flags[:init] = true }
250
- p.on("--interactive", "缺少配置时询问是否创建;要求真实终端") { flags[:interactive] = true }
251
- p.on("--non-interactive", "禁用问答;创建文件须同时指定 --init") { flags[:interactive] = false }
245
+ CtxCli::Options.initialization(p, flags)
252
246
  p.on("--plan", "--inventory-only", "预览筛选清单,不连接设备") { flags[:inventory_only] = true }
253
247
  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 }
248
+ CtxCli::Options.output(p, flags)
256
249
  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 }
250
+ CtxCli::Options.information(p, flags)
261
251
  p.separator "维度:#{Vxlan::Collector::COMMANDS.keys.join(', ')}"
262
252
  p.separator "优先级:CLI > 进程环境 > YAML > .env > 内置默认值。退出码:0 完整/预览;1 未完成/无目标;2 参数/持久化错误;130 取消。"
263
253
  end
264
254
  end
265
255
 
266
256
  # 列表中的空项属于输入错误,不能被静默删掉。
267
- def list(value)
268
- values = value.split(",", -1).map(&:strip)
269
- raise ArgumentError, "列表不能有空项" if values.any?(&:empty?)
270
- values
271
- end
257
+ def list(value) = CtxCli::Options.list(value)
272
258
 
273
259
  # 先验证整批策略,避免在前面设备连接后才发现后面的非法配置。
274
260
  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
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "time"
5
5
  require "ctx_runner/report"
6
+ require "ctx_runner/document"
6
7
  require_relative "review/report"
7
8
  require_relative "review/fabric"
8
9
 
@@ -36,9 +37,9 @@ module Vxlan
36
37
  name = self.class.device_basename(device)
37
38
  # 保存状态属于批次宿主;不能让正在发布的文件预先声明自身已保存。
38
39
  body = result.reject { |key, _| key == :report_saved }.merge(snapshot: snapshot && snapshot.to_h)
39
- document = JSON.parse(JSON.generate(serializable(body)))
40
+ document = CtxRunner::Document.call(body)
40
41
  advice = report_advice(Recommendations.new(document).call)
41
- document["recommendations"] = advice
42
+ document = document.merge("recommendations" => advice)
42
43
  json = JSON.pretty_generate(document) + "\n"
43
44
  prepare_directory(File.join(directory, "evidence"))
44
45
  publish("evidence/#{name}.json", json)
@@ -71,7 +72,6 @@ module Vxlan
71
72
  # 保持领域呈现接口,事务与恢复状态仅在公共报告中实现。
72
73
  def prepare_directory(path) = @report.prepare_directory(path)
73
74
  def publish(relative, body) = @report.publish(relative, body)
74
- def serializable(value) = @report.serializable(value)
75
75
  def checkpoint(relative, value) = @report.checkpoint(relative, value)
76
76
  def finalize(relative, summary, checkpoint:) = @report.finalize(relative, summary, checkpoint: checkpoint)
77
77
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require "ctx_cli/input"
5
+ require "ctx_runner/fingerprint"
4
6
  require_relative "../../fabric/review"
5
7
 
6
8
  module Vxlan
@@ -51,7 +53,7 @@ module Vxlan
51
53
  value = row.key?(:value) ? row[:value] : row.reject { |key, _| %i[host name vlan entries semantics].include?(key) }
52
54
  value = value.reject { |key, _| key == :entries } if value.is_a?(Hash)
53
55
  if row[:semantics]
54
- policy = row[:policy] ? "规则与依赖版本 #{ChangePlan.fingerprint(row[:policy])[0, 12]}" : "依赖对象未验证"
56
+ policy = row[:policy] ? "规则与依赖版本 #{CtxRunner::Fingerprint.call(row[:policy])[0, 12]}" : "依赖对象未验证"
55
57
  value = "#{row[:entries].size} 条规则,#{policy}"
56
58
  end
57
59
  [ row[:vlan] || row[:interface] || "公共", display_fabric_value(value) ]
@@ -91,7 +93,7 @@ module Vxlan
91
93
  # 实时收尾读取已经保存的事实;审查摘要交给采集摘要一起发布,避免相互覆盖。
92
94
  def review_collected_fabric(policy)
93
95
  sources = Dir[File.join(directory, "evidence", "*.json")].sort.map do |path|
94
- body = File.read(path)
96
+ body = CtxCli::Input.read(path: path)
95
97
  [ File.expand_path(path), body, JSON.parse(body) ]
96
98
  end
97
99
  build_review(sources, policy: policy, standalone: false) unless sources.empty?
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
+ require "ctx_runner/fingerprint"
4
5
  require_relative "../../recommendations"
5
6
  require_relative "../../change_plan"
6
7
 
@@ -39,8 +40,8 @@ module Vxlan
39
40
  plan = ChangePlan.candidate(item[:change], config: config)
40
41
  next unless plan
41
42
  change = plan.to_h
42
- id = [ basename, change["kind"], "vlan#{change['vlan']}", change["direction"], ChangePlan.fingerprint(change)[0, 12] ].compact.join("--")
43
- config_sha256 ||= ChangePlan.fingerprint(config)
43
+ id = [ basename, change["kind"], "vlan#{change['vlan']}", change["direction"], CtxRunner::Fingerprint.call(change)[0, 12] ].compact.join("--")
44
+ config_sha256 ||= CtxRunner::Fingerprint.call(config)
44
45
  entry[:changes][id] = { schema_version: 1, platform: "cisco_nxos", device: document.fetch("device"), expected_config_sha256: config_sha256,
45
46
  source: source, source_sha256: source_sha256, change: change, commands: plan.commands, rollback: plan.rollback,
46
47
  scope: item[:scope], title: item[:title], prerequisites: item[:prerequisites], impact: item[:impact], verify: item[:verify] }