vxlan 0.3.1 → 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 +4 -4
- data/CHANGELOG.md +17 -0
- data/bin/README.md +18 -2
- data/lib/vxlan/change_plan.rb +5 -11
- data/lib/vxlan/cli/apply.rb +18 -44
- data/lib/vxlan/cli/collect.rb +54 -152
- data/lib/vxlan/cli/options.rb +34 -49
- data/lib/vxlan/cli/parse.rb +13 -15
- data/lib/vxlan/cli/report.rb +34 -178
- data/lib/vxlan/cli/review/report.rb +2 -10
- data/lib/vxlan/cli.rb +28 -27
- data/lib/vxlan/collection_plan.rb +35 -0
- data/lib/vxlan/collector.rb +21 -62
- data/lib/vxlan/observation.rb +3 -24
- data/lib/vxlan/version.rb +1 -1
- metadata +30 -1
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
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "time"
|
|
5
|
-
require "
|
|
6
|
-
require "fileutils"
|
|
5
|
+
require "ctx_runner/report"
|
|
7
6
|
require_relative "review/report"
|
|
8
7
|
require_relative "review/fabric"
|
|
9
8
|
|
|
@@ -14,46 +13,21 @@ module Vxlan
|
|
|
14
13
|
include Review::Report
|
|
15
14
|
include Review::Fabric
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
# 私有证据发布由公共业务对象管理;本类只组织领域报告。
|
|
17
|
+
class Error < CtxRunner::Report::Error; end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
def initialize(root, scripts: false)
|
|
20
|
+
@scripts = scripts
|
|
21
|
+
@report = CtxRunner::Report.new(root, error_class: Error)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
def directory = @report.directory
|
|
25
|
+
def recovery_failed? = @report.recovery_failed?
|
|
26
|
+
def write_execution(state) = @report.write_execution(state)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
super("报告写入失败:#{relative};阶段 #{phase};#{committed ? '已替换,收尾未确认' : '尚未替换'}")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# 区分原子替换已完成与目录同步尚未确认。
|
|
35
|
-
def committed? = @committed
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# 批次目录排他创建,重复时间戳不会覆盖上一批次。
|
|
39
|
-
def initialize(root, scripts: false)
|
|
40
|
-
@scripts = scripts
|
|
41
|
-
root = File.expand_path(root)
|
|
42
|
-
prepare_directory(root)
|
|
43
|
-
stat = File.lstat(root)
|
|
44
|
-
unless stat.directory? && (stat.mode & 0o077).zero?
|
|
45
|
-
raise ArgumentError, "报告根目录须为真实私有目录,权限0700;请设置独立目录并检查 chmod 0700"
|
|
46
|
-
end
|
|
47
|
-
stem = Time.now.utc.strftime("%Y-%m-%d_%H-%M-%S")
|
|
48
|
-
sequence = 0
|
|
49
|
-
begin
|
|
50
|
-
@directory = File.join(root, sequence.zero? ? stem : "#{stem}_#{sequence}")
|
|
51
|
-
Dir.mkdir(directory, 0o700)
|
|
52
|
-
rescue Errno::EEXIST
|
|
53
|
-
sequence += 1
|
|
54
|
-
retry
|
|
55
|
-
end
|
|
56
|
-
sync_directory(root)
|
|
28
|
+
# 采集和离线审查共用稳定名称,保留 IPv6 地址并避免设备名成为路径。
|
|
29
|
+
def self.device_basename(device)
|
|
30
|
+
"#{device.fetch('name').gsub(/[^A-Za-z0-9_.-]/, '_')[0, 100]}-#{device.fetch('host').gsub(/[^A-Za-z0-9_.-]/, '_')}"
|
|
57
31
|
end
|
|
58
32
|
|
|
59
33
|
# 每台完成后立即原子保存,即使批次随后取消仍可读取已完成证据。
|
|
@@ -73,151 +47,33 @@ module Vxlan
|
|
|
73
47
|
|
|
74
48
|
# 摘要不嵌入配置事实,只汇总状态与诊断,供终端和自动化共用。
|
|
75
49
|
def finish(results, interrupted: false, requested: [], report_error: nil, execution_error: nil, output_error: nil)
|
|
76
|
-
@
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
checkpoint = summary.merge(status: interrupted ? :interrupted : :incomplete,
|
|
90
|
-
report_error: report_error || :report_finalization_unconfirmed)
|
|
91
|
-
checkpoint_body = JSON.pretty_generate(serializable(checkpoint)) + "\n"
|
|
92
|
-
begin
|
|
93
|
-
publish("summary.json", checkpoint_body)
|
|
94
|
-
rescue Error, Interrupt
|
|
95
|
-
invalidate_completed_summary
|
|
96
|
-
raise
|
|
97
|
-
end
|
|
98
|
-
text = [ "VXLAN 证据:#{summary.fetch(:status)};完成 #{results.size} 台", "分类:#{summary.fetch(:counts)}", "证据完整性不代表 Fabric 健康。" ]
|
|
99
|
-
text << "逐设备诊断:devices/;候选脚本:scripts/;所有候选配置未执行。"
|
|
100
|
-
text << @review_text if @review_text
|
|
101
|
-
text << "批次报告状态:参见 summary.json;文本中的证据状态不确认报告发布完成。"
|
|
102
|
-
text << "报告错误:#{report_error}" if report_error
|
|
103
|
-
text << "执行错误:#{execution_error}" if execution_error
|
|
104
|
-
text << "进度输出错误:#{output_error[:error_type]};设备结果已保留" if output_error
|
|
105
|
-
results.each do |r|
|
|
106
|
-
saved = r[:report_saved] == true ? "已保存" : "保存未确认"
|
|
107
|
-
text << "#{r[:device]['host']}\t#{r[:device]['name']}\t#{r[:status]}\t#{saved}\t#{r[:error]}"
|
|
108
|
-
end
|
|
109
|
-
pending.each { |device| text << "#{device['host']}\t#{device['name']}\t未采集" }
|
|
110
|
-
publish("summary.txt", text.join("\n") + "\n")
|
|
111
|
-
begin
|
|
112
|
-
publish("summary.json", JSON.pretty_generate(serializable(summary)) + "\n")
|
|
113
|
-
rescue Error => error
|
|
114
|
-
# 未替换时原检查点仍安全,不做可能破坏它的多余恢复写入。
|
|
115
|
-
restore_checkpoint(checkpoint_body) if error.committed?
|
|
116
|
-
raise
|
|
117
|
-
rescue Interrupt
|
|
118
|
-
checkpoint[:status] = :interrupted
|
|
119
|
-
restore_checkpoint(JSON.pretty_generate(serializable(checkpoint)) + "\n")
|
|
120
|
-
raise
|
|
121
|
-
end
|
|
122
|
-
summary
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# 每次发送配置前后发布检查点;失败由执行器停止后续写入。
|
|
126
|
-
def write_execution(state)
|
|
127
|
-
publish("execution.json", JSON.pretty_generate(state) + "\n")
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# 恢复写入或删除受阻时,调用方须把退出码及此状态作为未确认诊断。
|
|
131
|
-
def recovery_failed? = @recovery_failed == true
|
|
132
|
-
|
|
133
|
-
private
|
|
134
|
-
|
|
135
|
-
# 新建目录的名字属于父目录;文件 fsync 不能替代这条持久化链。
|
|
136
|
-
def prepare_directory(path)
|
|
137
|
-
missing, parent = [], path
|
|
138
|
-
until File.directory?(parent)
|
|
139
|
-
missing << parent
|
|
140
|
-
parent = File.dirname(parent)
|
|
141
|
-
end
|
|
142
|
-
FileUtils.mkdir_p(path, mode: 0o700)
|
|
143
|
-
missing.reverse_each do |created|
|
|
144
|
-
parent = File.dirname(created)
|
|
145
|
-
sync_directory(File.symlink?(parent) ? File.realpath(parent) : parent)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# 批次创建与文件发布共享目录同步边界,失败直接交还宿主停止执行。
|
|
150
|
-
def sync_directory(path)
|
|
151
|
-
File.open(path, File::RDONLY | File::NOFOLLOW) { |parent| parent.fsync }
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# 新一次收尾失败时,旧成功摘要不再代表当前状态;安全检查点继续保留。
|
|
155
|
-
def invalidate_completed_summary
|
|
156
|
-
Thread.handle_interrupt(Exception => :never) do
|
|
157
|
-
saved = JSON.parse(File.read(File.join(directory, "summary.json")))
|
|
158
|
-
discard_summary if saved["status"] == "complete"
|
|
159
|
-
end
|
|
160
|
-
rescue Errno::ENOENT
|
|
161
|
-
nil
|
|
162
|
-
rescue SystemCallError, JSON::ParserError, Interrupt
|
|
163
|
-
@recovery_failed = true
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# 屏蔽恢复中的次级取消,始终把最先观察到的发布失败或取消交还调用方。
|
|
167
|
-
def restore_checkpoint(body, relative: "summary.json")
|
|
168
|
-
Thread.handle_interrupt(Exception => :never) do
|
|
169
|
-
begin
|
|
170
|
-
publish(relative, body)
|
|
171
|
-
rescue Error, Interrupt => error
|
|
172
|
-
@recovery_failed = true
|
|
173
|
-
discard_summary(relative: relative) unless error.is_a?(Error) && error.committed?
|
|
50
|
+
@report.finish(results, interrupted: interrupted, requested: requested, report_error: report_error,
|
|
51
|
+
execution_error: execution_error, output_error: output_error, review: @review_summary) do |summary|
|
|
52
|
+
pending = summary.fetch(:pending)
|
|
53
|
+
text = [ "VXLAN 证据:#{summary.fetch(:status)};完成 #{results.size} 台", "分类:#{summary.fetch(:counts)}", "证据完整性不代表 Fabric 健康。" ]
|
|
54
|
+
text << "逐设备诊断:devices/;候选脚本:scripts/;所有候选配置未执行。"
|
|
55
|
+
text << @review_text if @review_text
|
|
56
|
+
text << "批次报告状态:参见 summary.json;文本中的证据状态不确认报告发布完成。"
|
|
57
|
+
text << "报告错误:#{report_error}" if report_error
|
|
58
|
+
text << "执行错误:#{execution_error}" if execution_error
|
|
59
|
+
text << "进度输出错误:#{output_error[:error_type]};设备结果已保留" if output_error
|
|
60
|
+
results.each do |r|
|
|
61
|
+
saved = r[:report_saved] == true ? "已保存" : "保存未确认"
|
|
62
|
+
text << "#{r[:device]['host']}\t#{r[:device]['name']}\t#{r[:status]}\t#{saved}\t#{r[:error]}"
|
|
174
63
|
end
|
|
64
|
+
pending.each { |device| text << "#{device['host']}\t#{device['name']}\t未采集" }
|
|
65
|
+
text.join("\n") + "\n"
|
|
175
66
|
end
|
|
176
|
-
rescue Interrupt
|
|
177
|
-
@recovery_failed = true
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# 存储拒绝恢复时尽力移除不可信摘要,不删除已获得的设备事实。
|
|
181
|
-
def discard_summary(relative: "summary.json")
|
|
182
|
-
File.unlink(File.join(directory, relative))
|
|
183
|
-
rescue Errno::ENOENT
|
|
184
|
-
nil
|
|
185
|
-
rescue SystemCallError
|
|
186
|
-
@recovery_failed = true
|
|
187
67
|
end
|
|
188
68
|
|
|
189
|
-
|
|
190
|
-
def serializable(value)
|
|
191
|
-
case value
|
|
192
|
-
when Time then value.iso8601(6)
|
|
193
|
-
when Data then serializable(value.to_h)
|
|
194
|
-
when Hash then value.transform_values { |item| serializable(item) }
|
|
195
|
-
when Array then value.map { |item| serializable(item) }
|
|
196
|
-
else value
|
|
197
|
-
end
|
|
198
|
-
end
|
|
69
|
+
private
|
|
199
70
|
|
|
200
|
-
#
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
phase = :write
|
|
207
|
-
file.write(body)
|
|
208
|
-
phase = :file_sync
|
|
209
|
-
file.flush
|
|
210
|
-
file.fsync
|
|
211
|
-
phase = :rename
|
|
212
|
-
File.rename(file.path, target)
|
|
213
|
-
committed = true
|
|
214
|
-
phase = :directory_sync
|
|
215
|
-
sync_directory(File.dirname(target))
|
|
216
|
-
phase = :cleanup
|
|
217
|
-
end
|
|
218
|
-
rescue SystemCallError, IOError, NotImplementedError
|
|
219
|
-
raise Error.new(relative: relative, phase: phase, committed: committed), cause: nil
|
|
220
|
-
end
|
|
71
|
+
# 保持领域呈现接口,事务与恢复状态仅在公共报告中实现。
|
|
72
|
+
def prepare_directory(path) = @report.prepare_directory(path)
|
|
73
|
+
def publish(relative, body) = @report.publish(relative, body)
|
|
74
|
+
def serializable(value) = @report.serializable(value)
|
|
75
|
+
def checkpoint(relative, value) = @report.checkpoint(relative, value)
|
|
76
|
+
def finalize(relative, summary, checkpoint:) = @report.finalize(relative, summary, checkpoint: checkpoint)
|
|
221
77
|
end
|
|
222
78
|
end
|
|
223
79
|
end
|
|
@@ -66,7 +66,7 @@ module Vxlan
|
|
|
66
66
|
public_entries = entries.map { |entry| entry.reject { |key, _| %i[advice changes].include?(key) } }
|
|
67
67
|
summary = { layout_version: 3, mode: "review", status: "incomplete", review_only: true, devices: entries.size, counts: counts, source_batch: source_batch,
|
|
68
68
|
reports: public_entries, plan: "plan.json", plan_sha256: nil, fabric_findings: @fabric_audit ? @fabric_audit[:findings].size : 0 }
|
|
69
|
-
|
|
69
|
+
checkpoint("summary.json", summary) if standalone
|
|
70
70
|
plan = { schema_version: 2, platform: "cisco_nxos", mode: "review", devices: public_entries, audits: entries.map { |entry| entry[:advice] },
|
|
71
71
|
changes: entries.each_with_object({}) { |entry, all| all.merge!(entry[:changes]) }, fabric: @fabric_audit && @fabric_audit.reject { |key, _| key == :device_items } }
|
|
72
72
|
body = JSON.pretty_generate(plan) + "\n"
|
|
@@ -78,15 +78,7 @@ module Vxlan
|
|
|
78
78
|
text = summary_text(summary)
|
|
79
79
|
if standalone
|
|
80
80
|
publish("summary.txt", text)
|
|
81
|
-
|
|
82
|
-
publish("summary.json", JSON.pretty_generate(summary.merge(status: "complete")) + "\n")
|
|
83
|
-
rescue CLI::Report::Error => error
|
|
84
|
-
restore_checkpoint(JSON.pretty_generate(summary) + "\n") if error.committed?
|
|
85
|
-
raise
|
|
86
|
-
rescue Interrupt
|
|
87
|
-
restore_checkpoint(JSON.pretty_generate(summary.merge(status: "interrupted")) + "\n")
|
|
88
|
-
raise
|
|
89
|
-
end
|
|
81
|
+
finalize("summary.json", summary.merge(status: "complete"), checkpoint: summary)
|
|
90
82
|
end
|
|
91
83
|
@review_summary, @review_text = summary.merge(status: "complete"), text
|
|
92
84
|
@review_summary
|
data/lib/vxlan/cli.rb
CHANGED
|
@@ -1,41 +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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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)
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
CtxCli::Application.new(name: "vxlan", version: Vxlan::VERSION, usage: USAGE,
|
|
35
|
+
commands: commands, out: out, error: error).run(argv)
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
# 全局帮助不读取配置、环境文件或设备清单。
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
0
|
|
38
|
-
end
|
|
39
|
-
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
|
|
40
41
|
end
|
|
41
42
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "observation"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
# 配置证据约束运行查询:明确未启用的功能才允许空回显或省略查询。
|
|
7
|
+
class CollectionPlan
|
|
8
|
+
def initialize(profile:, configuration: nil)
|
|
9
|
+
@profile, @configuration = profile, configuration
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse_many(names, body, complete:, command:)
|
|
13
|
+
if names.one? && body.strip.empty? && unconfigured?(names.first)
|
|
14
|
+
name = names.first
|
|
15
|
+
code = name == :port_channels ? :port_channels_not_configured : :nve_not_configured
|
|
16
|
+
return { name => CtxParser::Result.new(status: :complete, issues: [ { code: code } ]) }
|
|
17
|
+
end
|
|
18
|
+
@profile.parse_many(names, body, complete: complete, command: command)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 仅当同一会话的完整配置证明功能未启用时,才接受空运行回显。
|
|
22
|
+
def unconfigured?(name)
|
|
23
|
+
return false unless @configuration&.complete? && @configuration.records.one?
|
|
24
|
+
|
|
25
|
+
settings = @configuration.records.first
|
|
26
|
+
case name
|
|
27
|
+
when :vpc then !settings[:vpc_domain] && !settings.fetch(:features).include?("vpc")
|
|
28
|
+
when :nve_peers, :nve_vnis then !settings.fetch(:nve)[:interface]
|
|
29
|
+
when :port_channels
|
|
30
|
+
settings.fetch(:interfaces).none? { |interface, values| /\Aport-channel\d+\z/i.match?(interface) || values[:channel_group] }
|
|
31
|
+
else false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/vxlan/collector.rb
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "parser_registry"
|
|
4
4
|
require "ctx_driver"
|
|
5
|
+
require "ctx_runner/collection"
|
|
6
|
+
require "ctx_runner/query"
|
|
7
|
+
require_relative "collection_plan"
|
|
5
8
|
require_relative "observation"
|
|
6
9
|
require_relative "snapshot"
|
|
7
10
|
|
|
@@ -21,29 +24,12 @@ module Vxlan
|
|
|
21
24
|
target, fabric_key, dimensions, profile = request(target, fabric_key, dimensions, connector.platform, model)
|
|
22
25
|
|
|
23
26
|
observations = {}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
dimensions.each { |name| observations[name] ||= failure(name, :session_reentered) }
|
|
29
|
-
observations[:collection] = failure(:collection, :session_reentered)
|
|
30
|
-
next
|
|
31
|
-
end
|
|
32
|
-
session_started = true
|
|
33
|
-
observations.merge!(read_dimensions(session, dimensions, profile))
|
|
34
|
-
session_finished = true
|
|
35
|
-
end
|
|
36
|
-
rescue StandardError => error
|
|
37
|
-
raise unless CtxDriver::Failure.connection_error?(error, connector)
|
|
38
|
-
|
|
39
|
-
code = CtxDriver::Failure.code(error, :connection_failed)
|
|
27
|
+
collection = CtxRunner::Collection.new
|
|
28
|
+
collection.open(connector) { |session| observations.merge!(read_dimensions(session, dimensions, profile)) }
|
|
29
|
+
if collection.failure
|
|
30
|
+
code = collection.failure.fetch(:code)
|
|
40
31
|
dimensions.each { |name| observations[name] ||= failure(name, code) }
|
|
41
|
-
observations[:collection] = failure(:collection,
|
|
42
|
-
end
|
|
43
|
-
# 连接器正常返回也不等于已提供会话;缺少回调必须保留全部请求的失败事实。
|
|
44
|
-
unless session_finished || observations.key?(:collection)
|
|
45
|
-
dimensions.each { |name| observations[name] = failure(name, :session_not_opened) }
|
|
46
|
-
observations[:collection] = failure(:collection, :session_not_opened)
|
|
32
|
+
observations[:collection] = failure(:collection, code)
|
|
47
33
|
end
|
|
48
34
|
snapshot(target, fabric_key, observations)
|
|
49
35
|
end
|
|
@@ -79,11 +65,11 @@ module Vxlan
|
|
|
79
65
|
end
|
|
80
66
|
ordered = dimensions.include?(:configuration) ? [ :configuration, *dimensions.reject { |name| name == :configuration } ] : dimensions
|
|
81
67
|
ordered.each do |name|
|
|
82
|
-
|
|
83
|
-
observations[name] = if name == :vpc && !reader.failure && unconfigured?(name
|
|
68
|
+
plan = CollectionPlan.new(profile: profile, configuration: observations[:configuration])
|
|
69
|
+
observations[name] = if name == :vpc && !reader.failure && plan.unconfigured?(name)
|
|
84
70
|
Observation.new(name: name, status: :complete, completed_at: @clock.call, issues: [ { code: :vpc_not_configured } ])
|
|
85
71
|
else
|
|
86
|
-
collect_observation(reader, name,
|
|
72
|
+
collect_observation(reader, name, plan, profile)
|
|
87
73
|
end
|
|
88
74
|
end
|
|
89
75
|
dimensions.to_h { |name| [ name, observations.fetch(name) ] }
|
|
@@ -112,45 +98,18 @@ module Vxlan
|
|
|
112
98
|
end
|
|
113
99
|
|
|
114
100
|
# 解析失败限定在单条查询内;Reader 负责传输与回执校验。
|
|
115
|
-
def collect_observation(reader, name,
|
|
101
|
+
def collect_observation(reader, name, plan, profile)
|
|
116
102
|
command = profile.command(name)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
begin
|
|
123
|
-
body = capture.body
|
|
124
|
-
parsed = if body.strip.empty? && unconfigured?(name, configuration)
|
|
125
|
-
code = name == :port_channels ? :port_channels_not_configured : :nve_not_configured
|
|
126
|
-
Observation.new(name: name, status: :complete, issues: [ { code: code } ])
|
|
127
|
-
else
|
|
128
|
-
Observation.new(name: name, **profile.parse(name, body, complete: true).to_h)
|
|
129
|
-
end
|
|
130
|
-
parsed.with(**metadata)
|
|
131
|
-
rescue StandardError => error
|
|
132
|
-
details = CtxDriver::Failure.details(error, fallback: :command_processing_failed, stage: :parse)
|
|
133
|
-
failure(name, details.fetch(:code), **details.except(:code), command: command).with(**metadata)
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# 保留传输首次不可用原因,并标记从未发送的命令。
|
|
138
|
-
def failed_capture(name, capture, command)
|
|
139
|
-
failure(name, capture.failure.fetch(:code), **capture.failure.except(:code), command: command, skipped: capture.skipped?)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# 仅当同一会话的完整配置证明功能未启用时,才接受空运行回显。
|
|
143
|
-
def unconfigured?(name, configuration)
|
|
144
|
-
return false unless configuration&.complete? && configuration.records.one?
|
|
145
|
-
|
|
146
|
-
settings = configuration.records.first
|
|
147
|
-
case name
|
|
148
|
-
when :vpc then !settings[:vpc_domain] && !settings.fetch(:features).include?("vpc")
|
|
149
|
-
when :nve_peers, :nve_vnis then !settings.fetch(:nve)[:interface]
|
|
150
|
-
when :port_channels
|
|
151
|
-
settings.fetch(:interfaces).none? { |interface, values| /\Aport-channel\d+\z/i.match?(interface) || values[:channel_group] }
|
|
152
|
-
else false
|
|
103
|
+
unavailable = ->(view, capture, details) do
|
|
104
|
+
issue = details.merge(command: command)
|
|
105
|
+
issue[:skipped] = capture.skipped? unless capture.success?
|
|
106
|
+
failure(view, issue.fetch(:code), **issue.except(:code)).with(
|
|
107
|
+
started_at: capture.started_at, completed_at: capture.completed_at, evidence: capture.evidence)
|
|
153
108
|
end
|
|
109
|
+
CtxRunner::Query.new(reader: reader, parser: plan).read(command, names: [ name ], unavailable: unavailable) do |view, result, capture|
|
|
110
|
+
Observation.new(name: view, **result.to_h, started_at: capture.started_at,
|
|
111
|
+
completed_at: capture.completed_at, evidence: capture.evidence)
|
|
112
|
+
end.fetch(name)
|
|
154
113
|
end
|
|
155
114
|
|
|
156
115
|
# 构建失败观测,不丢弃已成功的同级维度。
|
data/lib/vxlan/observation.rb
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "ctx_runner/observation"
|
|
4
4
|
|
|
5
5
|
module Vxlan
|
|
6
|
-
#
|
|
7
|
-
Observation
|
|
8
|
-
# 冻结单个维度的记录、状态及诊断元数据。
|
|
9
|
-
def initialize(name:, status:, records: [], issues: [], evidence: [], started_at: nil, completed_at: nil)
|
|
10
|
-
raise ArgumentError, "Observation name must be a symbol" unless name.is_a?(Symbol)
|
|
11
|
-
raise ArgumentError, "Invalid observation status" unless %i[complete partial failed unsupported not_requested].include?(status)
|
|
12
|
-
raise ArgumentError, "Records must be hashes" unless records.is_a?(Array) && records.all? { |row| row.is_a?(Hash) }
|
|
13
|
-
|
|
14
|
-
super(name: name, status: status, records: CtxParser::Immutable.copy(records),
|
|
15
|
-
issues: CtxParser::Immutable.copy(issues), evidence: CtxParser::Immutable.copy(evidence),
|
|
16
|
-
started_at: started_at&.dup&.freeze, completed_at: completed_at&.dup&.freeze)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# 判断所有已选事实是否都有完整证据支持。
|
|
20
|
-
def complete? = status == :complete
|
|
21
|
-
|
|
22
|
-
# 展示对象身份与状态,避免泄露原始回显或凭据。
|
|
23
|
-
def inspect = "#<#{self.class.name} name=#{name} status=#{status} records=#{records.size}>"
|
|
24
|
-
alias to_s inspect
|
|
25
|
-
|
|
26
|
-
# PP 默认展开 Data 成员,显式沿用脱敏展示。
|
|
27
|
-
def pretty_print(printer) = printer.text(inspect)
|
|
28
|
-
end
|
|
6
|
+
# 复用公共事实契约;领域保留公开类型与后续的专属约束。
|
|
7
|
+
class Observation < CtxRunner::Observation; end
|
|
29
8
|
end
|
data/lib/vxlan/version.rb
CHANGED
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
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -9,6 +9,34 @@ 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
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ctx_runner
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.1.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.1.0
|
|
12
40
|
- !ruby/object:Gem::Dependency
|
|
13
41
|
name: ctx_parser
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -226,6 +254,7 @@ files:
|
|
|
226
254
|
- lib/vxlan/cli/review/report.rb
|
|
227
255
|
- lib/vxlan/cli/templates/env.example
|
|
228
256
|
- lib/vxlan/cli/templates/vxlan.yml
|
|
257
|
+
- lib/vxlan/collection_plan.rb
|
|
229
258
|
- lib/vxlan/collector.rb
|
|
230
259
|
- lib/vxlan/config.rb
|
|
231
260
|
- lib/vxlan/config/evidence.rb
|