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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -4
- data/README.md +3 -1
- data/bin/README.md +4 -0
- data/docs/superpowers/plans/2026-10-22-ctx-cli.md +138 -0
- data/docs/superpowers/specs/2026-10-22-ctx-cli-design.md +123 -0
- data/lib/vxlan/change_plan.rb +1 -8
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +2 -2
- data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +2 -2
- data/lib/vxlan/cisco_nxos/syntax/policies.rb +1 -1
- data/lib/vxlan/cisco_nxos/syntax/routing.rb +1 -1
- data/lib/vxlan/cli/apply.rb +10 -7
- data/lib/vxlan/cli/collect.rb +60 -125
- data/lib/vxlan/cli/options.rb +28 -42
- data/lib/vxlan/cli/parse.rb +13 -15
- data/lib/vxlan/cli/report.rb +3 -3
- data/lib/vxlan/cli/review/fabric.rb +4 -2
- data/lib/vxlan/cli/review/report.rb +3 -2
- data/lib/vxlan/cli/review.rb +30 -61
- data/lib/vxlan/cli.rb +27 -31
- data/lib/vxlan/collection_plan.rb +1 -1
- data/lib/vxlan/fabric/review.rb +1 -1
- data/lib/vxlan/version.rb +1 -1
- metadata +19 -3
data/lib/vxlan/cli/collect.rb
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
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 =
|
|
90
|
-
rows = if
|
|
91
|
-
|
|
92
|
-
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 =
|
|
101
|
-
|
|
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
|
|
112
|
-
|
|
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 =
|
|
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 =
|
|
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,
|
|
131
|
-
credentials =
|
|
132
|
-
@
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
data/lib/vxlan/cli/options.rb
CHANGED
|
@@ -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] && !
|
|
51
|
-
raise
|
|
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.
|
|
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(
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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!
|
data/lib/vxlan/cli/parse.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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 =
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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/report.rb
CHANGED
|
@@ -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 =
|
|
40
|
+
document = CtxRunner::Document.call(body)
|
|
40
41
|
advice = report_advice(Recommendations.new(document).call)
|
|
41
|
-
document
|
|
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] ? "规则与依赖版本 #{
|
|
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 =
|
|
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"],
|
|
43
|
-
config_sha256 ||=
|
|
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] }
|