vxlan 0.3.1 → 0.4.0
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 +12 -0
- data/bin/README.md +12 -0
- data/lib/vxlan/change_plan.rb +5 -11
- data/lib/vxlan/cli/apply.rb +13 -40
- data/lib/vxlan/cli/collect.rb +33 -58
- data/lib/vxlan/cli/options.rb +11 -10
- data/lib/vxlan/cli/report.rb +34 -178
- data/lib/vxlan/cli/review/report.rb +2 -10
- data/lib/vxlan/cli.rb +6 -1
- 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 +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b045bbd7eb98d8771aac733587cf171bdf5c8c0da0f7ce3a4226317454da139
|
|
4
|
+
data.tar.gz: 06d4bddb07fa64f80ca0b2cb283c02321ad0bf54f56c94ec9c1538ebe052b447
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b5636dc50b48a4f0eed0c5c6b8babb2d650eae98f8a6ee78b1969b940327ba44e8f5b5bb16e12dd7b706cc0044756568aefae2fb9523e3034e72adbf132c2bc
|
|
7
|
+
data.tar.gz: 73d115544a200fab0ae647981720af491b05dca15f17b3f7a8ed021559221b101012842b2c7856dca0fa28edc7d9a4d2bb1d06ba439f78595b45aec3c5965c60
|
data/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.4.0 (2026-10-22)
|
|
6
|
+
|
|
7
|
+
- Delegate batch completion and cancellation/failure precedence to CtxRunner::Batch, keeping domain review and CLI presentation explicit.
|
|
8
|
+
|
|
9
|
+
- Centralize inventory selection and CLI filters in Net::Connector::Inventory; add vendor/model inclusion, exclusion and matching modes.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
- Delegate shared collection, evidence and execution contracts to ctx_runner.
|
|
13
|
+
- Preserve domain parsing, change policies, CLI layouts and connection ownership.
|
|
14
|
+
|
|
5
15
|
## Unreleased
|
|
6
16
|
|
|
17
|
+
- Delegate batch completion and cancellation/failure precedence to CtxRunner::Batch, keeping domain review and CLI presentation explicit.
|
|
18
|
+
|
|
7
19
|
## 0.3.1 (2026-10-21)
|
|
8
20
|
|
|
9
21
|
- Index Fabric address-group evidence by referenced object, preserving host order and missing dependencies without repeatedly searching every host's reference list.
|
data/bin/README.md
CHANGED
|
@@ -201,3 +201,15 @@ Fabric 的 ACL 比较包含有序规则和引用地址组的成员。对象缺
|
|
|
201
201
|
`fabric-policy.yml` 显式声明各 Fabric 的 `leaves`、`borders`、`spines`。Leaf 与 Border 按各自已部署的共同 VNI 比较;Spine 检查角色冲突。业务目录联合 VLAN/NVE/EVPN 识别不完整部署,总览显示覆盖范围、未纳入设备和缺证据成员。只有完整一致的对端与本机唯一业务接口相互印证、且全 Fabric 无跨 VNI 网段冲突时,才建议补齐已有 VLAN 的 VNI 关联;不会推导全量铺开 VLAN,也不按命名猜测角色。
|
|
202
202
|
|
|
203
203
|
每次采集独立合并进程环境与指定 `.env`,不改写调用者环境;凭据、清单与参数使用同一份本次有效环境。Fabric 策略在连接前加载、校验并固定,采集中途修改策略文件只影响下一批。下发只接受带校验文件的 `plan.json` 与 `--item ID`,不接受旧版独立单项计划。
|
|
204
|
+
|
|
205
|
+
## 统一设备筛选
|
|
206
|
+
|
|
207
|
+
collect 的筛选与备份/拓扑共用 net-connector 的 Inventory::Selection。需要安装包含 `net/connector/inventory/filters` 的当前候选连接器;collect 帮助也从该组件读取参数定义,离线 parse/review 不加载它。
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
vxlan collect --hosts 192.0.2.0/24 --name edge --exclude-name spare --model 'S68*' --model-match glob --plan
|
|
211
|
+
```
|
|
212
|
+
|
|
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
|
+
|
|
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 字段。
|
data/lib/vxlan/change_plan.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "ctx_runner/fingerprint"
|
|
4
4
|
require "json"
|
|
5
5
|
require "ipaddr"
|
|
6
6
|
require "ctx_parser/immutable"
|
|
@@ -34,16 +34,10 @@ module Vxlan
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# 字符串键规范化后计算语义指纹;Hash 顺序不影响结果,数组保留业务顺序。
|
|
37
|
-
def self.fingerprint(value) =
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
def self.canonical(value)
|
|
41
|
-
case value
|
|
42
|
-
when Hash then value.transform_keys(&:to_s).sort.to_h.transform_values { |entry| canonical(entry) }
|
|
43
|
-
when Array then value.map { |entry| canonical(entry) }
|
|
44
|
-
else value
|
|
45
|
-
end
|
|
46
|
-
end
|
|
37
|
+
def self.fingerprint(value) = CtxRunner::Fingerprint.call(value)
|
|
38
|
+
|
|
39
|
+
# 兼容既有领域入口,配置身份协议仅由 Runner 实现。
|
|
40
|
+
def self.canonical(value) = CtxRunner::Fingerprint.canonical(value)
|
|
47
41
|
|
|
48
42
|
# 构造时一次校验和渲染,随后调用者不能改变已确认的参数或命令。
|
|
49
43
|
def initialize(change)
|
data/lib/vxlan/cli/apply.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require "json"
|
|
5
5
|
require "digest"
|
|
6
|
+
require "ctx_runner/execution"
|
|
6
7
|
require "ipaddr"
|
|
7
8
|
require "vxlan"
|
|
8
9
|
require_relative "report"
|
|
@@ -45,7 +46,7 @@ module Vxlan
|
|
|
45
46
|
|
|
46
47
|
# 没有 --yes 或自动回退;零退出码只确认配置回读,不声称业务已验收。
|
|
47
48
|
def run(argv)
|
|
48
|
-
@store, @
|
|
49
|
+
@store, @execution = nil, nil
|
|
49
50
|
options = { directory: File.join(Dir.pwd, "vxlan-executions"), port: 22 }
|
|
50
51
|
parser = OptionParser.new do |p|
|
|
51
52
|
p.banner = "用法:vxlan apply --plan FILE --item ID [--env-file FILE](必须真实终端明确确认)"
|
|
@@ -75,43 +76,17 @@ module Vxlan
|
|
|
75
76
|
return @out.puts("已取消,未连接设备。") || 1 unless @input.gets&.strip == token
|
|
76
77
|
settings = credentials(options)
|
|
77
78
|
@store = Report.new(options[:directory])
|
|
78
|
-
@
|
|
79
|
-
@
|
|
80
|
-
session_opened, verified = false, false
|
|
81
|
-
with_session(plan["device"], settings) do |session|
|
|
82
|
-
raise IOError, "会话回调重复,禁止重放" if session_opened
|
|
83
|
-
session_opened = true
|
|
84
|
-
before = CtxParser::Immutable.copy(session.read_configuration)
|
|
85
|
-
unless ChangePlan.fingerprint(before) == plan["expected_config_sha256"] && change.eligible?(before)
|
|
86
|
-
raise ArgumentError, "配置已变化或增量前提不满足"
|
|
87
|
-
end
|
|
88
|
-
change.commands.each_with_index do |command, index|
|
|
89
|
-
@state["status"] = "executing"
|
|
90
|
-
@state["pending_command"] = index
|
|
91
|
-
@store.write_execution(@state)
|
|
92
|
-
@started = true
|
|
93
|
-
raise IOError, "设备命令未确认" unless session.write(command) == true
|
|
94
|
-
@state["steps"] << { "index" => index, "status" => "acknowledged" }
|
|
95
|
-
@state.delete("pending_command")
|
|
96
|
-
@store.write_execution(@state)
|
|
97
|
-
end
|
|
98
|
-
after = CtxParser::Immutable.copy(session.read_configuration)
|
|
99
|
-
raise IOError, "配置验收未通过" unless change.satisfied?(after, before: before)
|
|
100
|
-
@state["observed_config_sha256"] = ChangePlan.fingerprint(after)
|
|
101
|
-
verified = true
|
|
102
|
-
end
|
|
103
|
-
raise IOError, "会话未完成配置验证" unless verified
|
|
104
|
-
@state["status"] = "configuration_verified"
|
|
105
|
-
@store.write_execution(@state)
|
|
79
|
+
@execution = CtxRunner::Execution.new(plan: plan, change: change, journal: @store, plan_sha256: hash, item: options[:item])
|
|
80
|
+
@execution.run { |operation| with_session(plan["device"], settings, &operation) }
|
|
106
81
|
@out.puts "配置回读已确认;仍需完成业务验收,未执行保存配置。日志:#{@store.directory}/execution.json"
|
|
107
82
|
0
|
|
108
83
|
rescue Interrupt
|
|
109
|
-
|
|
110
|
-
@error.puts "操作中断;#{@started ? '设备效果可能不完整,请先回读核对,禁止重放。' : '尚未下发业务配置。'}"
|
|
84
|
+
report_journal_failure
|
|
85
|
+
@error.puts "操作中断;#{@execution&.started? ? '设备效果可能不完整,请先回读核对,禁止重放。' : '尚未下发业务配置。'}"
|
|
111
86
|
130
|
|
112
87
|
rescue StandardError, LoadError => error
|
|
113
|
-
|
|
114
|
-
@error.puts "下发停止(#{error.class.name});#{@started ? '效果未完整确认,先回读,不自动重放或回退。' : '未下发业务配置;检查终端、计划、凭据及当前配置是否仍匹配。'}"
|
|
88
|
+
report_journal_failure
|
|
89
|
+
@error.puts "下发停止(#{error.class.name});#{@execution&.started? ? '效果未完整确认,先回读,不自动重放或回退。' : '未下发业务配置;检查终端、计划、凭据及当前配置是否仍匹配。'}"
|
|
115
90
|
2
|
|
116
91
|
end
|
|
117
92
|
|
|
@@ -158,14 +133,12 @@ module Vxlan
|
|
|
158
133
|
end
|
|
159
134
|
end
|
|
160
135
|
|
|
161
|
-
#
|
|
162
|
-
def
|
|
163
|
-
return unless @
|
|
164
|
-
@state["status"] = @started ? "uncertain" : "failed"
|
|
165
|
-
@state["error"] = code
|
|
166
|
-
@store.write_execution(@state)
|
|
167
|
-
rescue StandardError, Interrupt
|
|
136
|
+
# 公共执行器保存失败状态;CLI 只呈现持久性尚未确认的诊断。
|
|
137
|
+
def report_journal_failure
|
|
138
|
+
return unless @execution&.journal_failed?
|
|
168
139
|
@error.puts "执行日志收尾未确认;保留已有检查点,禁止据此重放配置。"
|
|
140
|
+
rescue StandardError, Interrupt
|
|
141
|
+
nil
|
|
169
142
|
end
|
|
170
143
|
end
|
|
171
144
|
end
|
data/lib/vxlan/cli/collect.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "vxlan"
|
|
4
|
+
require "ctx_runner/batch"
|
|
4
5
|
require_relative "options"
|
|
5
6
|
require_relative "report"
|
|
6
7
|
require_relative "connector"
|
|
@@ -14,13 +15,12 @@ module Vxlan
|
|
|
14
15
|
@env, @out, @error, @input = env, out, error, input
|
|
15
16
|
@connector_factory, @executor = connector_factory, executor
|
|
16
17
|
@defaults = options
|
|
17
|
-
@lock = Mutex.new
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# 先预检全部输入和凭据,再创建报告和连接;失败分类不打印原始异常正文。
|
|
21
21
|
def run(argv)
|
|
22
22
|
# 每次运行独立持有批次状态;预检失败不能改写上一次报告。
|
|
23
|
-
@options, @store, @progress, @devices, @
|
|
23
|
+
@options, @store, @progress, @devices, @batch = nil, nil, nil, [], nil
|
|
24
24
|
@options = Options.new(argv, env: @env, out: @out, **@defaults)
|
|
25
25
|
return 0 if @options.informational?
|
|
26
26
|
@options.validate_interaction!(input: @input, error: @error)
|
|
@@ -52,32 +52,25 @@ module Vxlan
|
|
|
52
52
|
Net::Connector::Resources.prepare!(parallel: [ devices.size, @options.values.fetch("parallel") ].min)
|
|
53
53
|
end
|
|
54
54
|
@store = Report.new(@options.values.fetch("directory"), scripts: @options.flags[:scripts] == true)
|
|
55
|
-
@
|
|
56
|
-
@
|
|
57
|
-
|
|
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)
|
|
58
62
|
end
|
|
59
|
-
|
|
60
|
-
summary = finish
|
|
61
|
-
return 2 unless summary
|
|
62
|
-
summary[:status] == :complete ? 0 : 1
|
|
63
|
+
completion(outcome)
|
|
63
64
|
rescue Interrupt
|
|
64
|
-
finish(interrupted: true) if @store
|
|
65
65
|
diagnostic "采集已取消,已保存的设备报告保留;未保存结果见报告状态"
|
|
66
66
|
130
|
|
67
67
|
rescue SystemCallError, IOError => error
|
|
68
|
-
finish(report_error: error) if @store
|
|
69
68
|
diagnostic "VXLAN 参数或报告错误:#{report_diagnostic(error)}"
|
|
70
69
|
2
|
|
71
70
|
rescue OptionParser::ParseError, ArgumentError, LoadError => error
|
|
72
|
-
|
|
73
|
-
finish(execution_error: error)
|
|
74
|
-
diagnostic "VXLAN 执行失败:#{error.class.name};已采集结果见批次报告"
|
|
75
|
-
else
|
|
76
|
-
diagnostic "VXLAN 参数、依赖或报告错误:#{error.message};使用 --help 查看参数"
|
|
77
|
-
end
|
|
71
|
+
diagnostic "VXLAN 参数、依赖或报告错误:#{error.message};使用 --help 查看参数"
|
|
78
72
|
2
|
|
79
73
|
rescue StandardError => error
|
|
80
|
-
finish(execution_error: error) if @store
|
|
81
74
|
diagnostic "VXLAN 清单或执行失败:#{CtxDriver::Failure.code(error, :collection_failed)}(#{error.class.name})"
|
|
82
75
|
2
|
|
83
76
|
end
|
|
@@ -88,7 +81,7 @@ module Vxlan
|
|
|
88
81
|
def load_network
|
|
89
82
|
require "net/connector"
|
|
90
83
|
rescue LoadError
|
|
91
|
-
raise LoadError, "联网采集需要 net-connector >= 0.
|
|
84
|
+
raise LoadError, "联网采集需要 net-connector >= 0.17.0;请在宿主 Gemfile 添加或 gem install net-connector", cause: nil
|
|
92
85
|
end
|
|
93
86
|
|
|
94
87
|
# YAML 静态设备或显式 Netdisco 来源二选一;所有重复地址在连接前拒绝。
|
|
@@ -106,14 +99,9 @@ module Vxlan
|
|
|
106
99
|
end
|
|
107
100
|
targets = @options.normalize_devices(rows)
|
|
108
101
|
@inventory_total = targets.size
|
|
109
|
-
|
|
110
|
-
excludes = values.fetch("exclude_hosts").map { |v| IPAddr.new(v) }
|
|
102
|
+
selection = Net::Connector::Inventory::Filters.selection(values)
|
|
111
103
|
targets.select do |device|
|
|
112
|
-
host, name
|
|
113
|
-
(includes.empty? || includes.any? { |network| network.include?(host) }) &&
|
|
114
|
-
excludes.none? { |network| network.include?(host) } &&
|
|
115
|
-
(values.fetch("names").empty? || values.fetch("names").any? { |v| name.downcase.include?(v.downcase) }) &&
|
|
116
|
-
values.fetch("exclude_names").none? { |v| name.downcase.include?(v.downcase) }
|
|
104
|
+
selection.selected?(device.fetch("host"), "cisco_nxos", name: device["name"], model: device["model"])
|
|
117
105
|
end
|
|
118
106
|
end
|
|
119
107
|
|
|
@@ -140,46 +128,33 @@ module Vxlan
|
|
|
140
128
|
|
|
141
129
|
# 单台失败保留其他设备和已获得维度;报告持久化失败交由批次终止处理。
|
|
142
130
|
def collect(device, credentials)
|
|
143
|
-
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
144
|
-
snapshot = nil
|
|
145
|
-
@progress.start(device)
|
|
146
131
|
credentials = credentials.merge(on_event: @progress.method(:event))
|
|
147
|
-
|
|
132
|
+
@batch.collect(device) do
|
|
148
133
|
connector = @connector_factory ? @connector_factory.call(device, credentials) : Connector.new(device, credentials)
|
|
149
134
|
snapshot = Vxlan::Collector.new.collect(connector: connector, target: device, fabric_key: @options.values.fetch("fabric"),
|
|
150
135
|
model: device["model"], dimensions: @options.values.fetch("dimensions").map(&:to_sym))
|
|
151
|
-
|
|
152
|
-
status = snapshot.dimensions.fetch(:collection).status
|
|
153
|
-
result = { device: device, status: status, dimensions: dimensions }
|
|
154
|
-
rescue StandardError => error
|
|
155
|
-
result = { device: device, status: :failed, error: CtxDriver::Failure.code(error, :collection_failed), dimensions: {} }
|
|
136
|
+
snapshot
|
|
156
137
|
end
|
|
157
|
-
result[:duration_ms] = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
|
|
158
|
-
result[:report_saved] = false
|
|
159
|
-
@lock.synchronize { @results << result }
|
|
160
|
-
@store.write(result, snapshot: snapshot)
|
|
161
|
-
@lock.synchronize { result[:report_saved] = true }
|
|
162
|
-
@progress.result(result)
|
|
163
|
-
result
|
|
164
138
|
end
|
|
165
139
|
|
|
166
|
-
#
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
report_error: report_error && report_diagnostic(report_error), execution_error: execution_error&.class&.name }
|
|
171
|
-
summary = @store.finish(results, **options, output_error: @progress.output_error)
|
|
172
|
-
@progress.finish(summary)
|
|
173
|
-
# 末尾提示也可能首次失败;只补写输出诊断,不重复设备任务。
|
|
174
|
-
if @progress.output_error && !summary[:output_error]
|
|
175
|
-
summary = @store.finish(results, **options, output_error: @progress.output_error)
|
|
140
|
+
# Runner 决定批次结论;CLI 仅呈现安全诊断并映射退出码。
|
|
141
|
+
def completion(outcome)
|
|
142
|
+
if outcome.finalization_error
|
|
143
|
+
diagnostic "VXLAN 摘要报告错误:#{outcome.finalization_error}"
|
|
176
144
|
end
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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)
|
|
183
158
|
end
|
|
184
159
|
|
|
185
160
|
# 错误提示失败不能覆盖取消或执行退出码;已失效的进度流不再重试。
|
data/lib/vxlan/cli/options.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require "net/connector/inventory/filters"
|
|
4
5
|
require "yaml"
|
|
5
6
|
require "ipaddr"
|
|
6
7
|
require "fileutils"
|
|
@@ -10,7 +11,7 @@ module Vxlan
|
|
|
10
11
|
module CLI
|
|
11
12
|
# 采集入口的公开参数;凭据始终留在环境中,不传入解析库或 YAML。
|
|
12
13
|
class Options
|
|
13
|
-
FIELDS = %w[fabric fabric_policy devices netdisco inventory netdisco_config parallel directory dimensions ssh
|
|
14
|
+
FIELDS = (%w[fabric fabric_policy devices netdisco inventory netdisco_config parallel directory dimensions ssh] + Net::Connector::Inventory::Filters::FIELDS).freeze
|
|
14
15
|
SSH_FIELDS = %w[port command_timeout login_timeout host_key_policy known_hosts].freeze
|
|
15
16
|
SSH_DEFAULTS = { "host_key_policy" => "strict", "command_timeout" => 60, "login_timeout" => 15 }.freeze
|
|
16
17
|
attr_reader :values, :config, :env_file, :flags, :fabric_policy, :environment
|
|
@@ -62,7 +63,7 @@ module Vxlan
|
|
|
62
63
|
@environment = @files.environment
|
|
63
64
|
@config, @env_file = @files.config, @files.env_file
|
|
64
65
|
rescue LoadError
|
|
65
|
-
raise LoadError, "采集配置需要 net-connector >= 0.
|
|
66
|
+
raise LoadError, "采集配置需要 net-connector >= 0.17.0;读取 .env 还需要 dotenv", cause: nil
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
# 公共与专用文件先合并,进程值和 CLI 最后覆盖;数组不追加设备。
|
|
@@ -238,9 +239,12 @@ module Vxlan
|
|
|
238
239
|
p.on("--fabric-policy FILE", "显式 Fabric/Leaf/Border/Spine 清单 YAML") { |v| @overrides["fabric_policy"] = File.expand_path(v) }
|
|
239
240
|
p.on("--fabric NAME", "采集所属 Fabric 身份") { |v| @overrides["fabric"] = v }
|
|
240
241
|
p.on("--dimensions LIST", "逗号分隔维度;--help 查看支持项") { |v| @overrides["dimensions"] = list(v) }
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
Net::Connector::Inventory::Filters.add_options(p) do |key, value|
|
|
243
|
+
if value.is_a?(Array)
|
|
244
|
+
(@overrides[key] ||= []).concat(value)
|
|
245
|
+
else
|
|
246
|
+
@overrides[key] = value
|
|
247
|
+
end
|
|
244
248
|
end
|
|
245
249
|
p.on("--init", "创建缺失配置和环境模板后退出;已有文件不覆盖") { flags[:init] = true }
|
|
246
250
|
p.on("--interactive", "缺少配置时询问是否创建;要求真实终端") { flags[:interactive] = true }
|
|
@@ -286,11 +290,8 @@ module Vxlan
|
|
|
286
290
|
raise ArgumentError, "netdisco_config 须为配置路径,且不能同时声明 devices" unless values["netdisco_config"].is_a?(String) && !values["netdisco_config"].empty? && values["devices"].empty?
|
|
287
291
|
values["netdisco_config"] = File.expand_path(values["netdisco_config"], Dir.pwd)
|
|
288
292
|
end
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
raise ArgumentError, "#{key} 须为非空字符串数组" unless items.is_a?(Array) && items.all? { |v| v.is_a?(String) && !v.strip.empty? }
|
|
292
|
-
end
|
|
293
|
-
%w[hosts exclude_hosts].each { |key| values[key].each { |v| IPAddr.new(v) } }
|
|
293
|
+
Net::Connector::Inventory::Filters::LISTS.each_key { |key| values[key] = values.fetch(key, []) }
|
|
294
|
+
Net::Connector::Inventory::Filters.selection(values)
|
|
294
295
|
ssh = values["ssh"]
|
|
295
296
|
raise ArgumentError, "ssh 包含未知或敏感字段" unless ssh.is_a?(Hash) && (ssh.keys - SSH_FIELDS).empty?
|
|
296
297
|
ssh = SSH_DEFAULTS.merge(ssh)
|
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
|
@@ -15,7 +15,12 @@ module Vxlan
|
|
|
15
15
|
when "--version", "-v"
|
|
16
16
|
return out.puts(Vxlan::VERSION) || 0 if arguments.empty?
|
|
17
17
|
when "collect"
|
|
18
|
-
|
|
18
|
+
begin
|
|
19
|
+
require_relative "cli/collect"
|
|
20
|
+
rescue LoadError
|
|
21
|
+
error.puts "collect 需要含共享清单筛选的当前 net-connector;请安装项目依赖。"
|
|
22
|
+
return 2
|
|
23
|
+
end
|
|
19
24
|
return Collect.new(env: env, out: out, error: error, input: input, **collect_options).run(arguments)
|
|
20
25
|
when "apply"
|
|
21
26
|
require_relative "cli/apply"
|
|
@@ -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.0
|
|
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_runner
|
|
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_parser
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -226,6 +240,7 @@ files:
|
|
|
226
240
|
- lib/vxlan/cli/review/report.rb
|
|
227
241
|
- lib/vxlan/cli/templates/env.example
|
|
228
242
|
- lib/vxlan/cli/templates/vxlan.yml
|
|
243
|
+
- lib/vxlan/collection_plan.rb
|
|
229
244
|
- lib/vxlan/collector.rb
|
|
230
245
|
- lib/vxlan/config.rb
|
|
231
246
|
- lib/vxlan/config/evidence.rb
|