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