vxlan 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -4
- data/README.md +3 -1
- data/bin/README.md +4 -0
- data/docs/superpowers/plans/2026-10-22-ctx-cli.md +138 -0
- data/docs/superpowers/specs/2026-10-22-ctx-cli-design.md +123 -0
- data/lib/vxlan/change_plan.rb +1 -8
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +2 -2
- data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +2 -2
- data/lib/vxlan/cisco_nxos/syntax/policies.rb +1 -1
- data/lib/vxlan/cisco_nxos/syntax/routing.rb +1 -1
- data/lib/vxlan/cli/apply.rb +10 -7
- data/lib/vxlan/cli/collect.rb +60 -125
- data/lib/vxlan/cli/options.rb +28 -42
- data/lib/vxlan/cli/parse.rb +13 -15
- data/lib/vxlan/cli/report.rb +3 -3
- data/lib/vxlan/cli/review/fabric.rb +4 -2
- data/lib/vxlan/cli/review/report.rb +3 -2
- data/lib/vxlan/cli/review.rb +30 -61
- data/lib/vxlan/cli.rb +27 -31
- data/lib/vxlan/collection_plan.rb +1 -1
- data/lib/vxlan/fabric/review.rb +1 -1
- data/lib/vxlan/version.rb +1 -1
- metadata +19 -3
data/lib/vxlan/cli/review.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "
|
|
5
|
-
require "
|
|
3
|
+
require "ctx_cli/command"
|
|
4
|
+
require "ctx_cli/review"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "ipaddr"
|
|
7
|
+
require "ctx_runner/evidence"
|
|
6
8
|
require_relative "report"
|
|
7
9
|
|
|
8
10
|
module Vxlan
|
|
@@ -16,87 +18,54 @@ module Vxlan
|
|
|
16
18
|
|
|
17
19
|
# 返回零只代表建议生成完成,不代表问题已修复或候选配置已获批准。
|
|
18
20
|
def run(argv)
|
|
19
|
-
|
|
20
|
-
parser = OptionParser.new do |p|
|
|
21
|
-
p.banner = "用法:vxlan review --batch DIR [--output DIR](离线生成建议,不执行配置)"
|
|
21
|
+
command = CtxCli::Command.new(banner: "用法:vxlan review --batch DIR [--output DIR](离线生成建议,不执行配置)", out: @out) do |p, options|
|
|
22
22
|
p.on("--fabric-policy FILE", "显式 Fabric/Leaf/Border/Spine 清单 YAML") { |value| options[:fabric_policy] = value }
|
|
23
23
|
p.on("--batch DIR", "已采集批次,含 evidence/*.json") { |value| options[:batch] = value }
|
|
24
24
|
p.on("--[no-]scripts", "可选导出配置建议,默认仅诊断") { |value| options[:scripts] = value }
|
|
25
25
|
p.on("--output DIR", "私有输出根目录,默认批次内 reviews") { |value| options[:output] = value }
|
|
26
26
|
p.on("-h", "--help", "查看帮助") { options[:help] = true }
|
|
27
27
|
end
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
body = File.read(path)
|
|
37
|
-
document = JSON.parse(body)
|
|
38
|
-
validate(document)
|
|
39
|
-
[ path, body, document, Recommendations.new(document).call ]
|
|
28
|
+
CtxCli::Review.new(command: command, directory: "evidence", adapter: self, out: @out, error: @error).run(argv)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 领域结构、建议和身份冲突必须在创建输出目录前全部校验。
|
|
32
|
+
def prepare(documents, options)
|
|
33
|
+
documents.each do |entry|
|
|
34
|
+
validate(entry[2])
|
|
35
|
+
entry << Recommendations.new(entry[2]).call
|
|
40
36
|
end
|
|
41
37
|
hosts = documents.map { |entry| entry[2]["device"]["host"] }
|
|
42
38
|
names = documents.map { |entry| CLI::Report.device_basename(entry[2].fetch("device")) }
|
|
43
39
|
raise ArgumentError, "设备身份或输出名称重复" unless hosts.uniq == hosts && names.uniq == names
|
|
44
|
-
policy = options[:fabric_policy] && YAML.safe_load(
|
|
40
|
+
policy = options[:fabric_policy] && YAML.safe_load(CtxCli::Input.read(path: options[:fabric_policy]), aliases: false)
|
|
45
41
|
Vxlan::Fabric::Scope.new(policy) if policy
|
|
42
|
+
policy
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# 公共入口完成预检后才调用发布;领域报告格式保持独立。
|
|
46
|
+
def publish(documents, options, batch:, context:)
|
|
46
47
|
report = CLI::Report.new(options[:output] || File.join(batch, "reviews"), scripts: options[:scripts] == true)
|
|
47
|
-
report.build_review(documents, policy:
|
|
48
|
-
|
|
49
|
-
0
|
|
50
|
-
rescue Psych::Exception, OptionParser::ParseError, ArgumentError, TypeError, KeyError, JSON::ParserError, SystemCallError, IOError => error
|
|
51
|
-
@error.puts "建议生成失败(#{error.class.name});请检查输入结构、目录权限和 --help;未执行设备变更。"
|
|
52
|
-
2
|
|
48
|
+
report.build_review(documents, policy: context)
|
|
49
|
+
"建议已生成:#{report.directory}/summary.txt;#{documents.size} 台;未执行设备变更。"
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
private
|
|
56
53
|
|
|
57
|
-
#
|
|
54
|
+
# 先校验公共证据外壳,再验证领域记录。
|
|
58
55
|
def validate(document)
|
|
59
|
-
|
|
60
|
-
host = document["device"]["host"]
|
|
61
|
-
raise ArgumentError, "目标地址须为规范主机地址" unless !host.include?("/") && IPAddr.new(host).to_s == host
|
|
62
|
-
snapshot = document["snapshot"]
|
|
63
|
-
raise ArgumentError, "无快照字段" unless document.key?("snapshot")
|
|
64
|
-
return if snapshot.nil?
|
|
65
|
-
raise ArgumentError, "快照结构错误" unless snapshot.is_a?(Hash) && snapshot["dimensions"].is_a?(Hash)
|
|
66
|
-
if snapshot.key?("target") && (!snapshot["target"].is_a?(Hash) || snapshot["target"]["host"] != host)
|
|
67
|
-
raise ArgumentError, "设备身份与采集目标不一致"
|
|
68
|
-
end
|
|
69
|
-
snapshot["dimensions"].each do |dimension, observation|
|
|
70
|
-
unless observation.is_a?(Hash) && %w[complete partial failed unsupported not_requested].include?(observation["status"]) && observation["records"].is_a?(Array) && observation["records"].all? { |record| record.is_a?(Hash) }
|
|
71
|
-
raise ArgumentError, "观测结构错误"
|
|
72
|
-
end
|
|
73
|
-
array_of_hashes(observation.fetch("issues", []))
|
|
74
|
-
observation["records"].each { |record| validate_record(record, dimension) }
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# 校验规则读取的容器类型,错误消息不包含配置正文。
|
|
79
|
-
def array_of_hashes(value)
|
|
80
|
-
raise ArgumentError, "列表结构错误" unless value.is_a?(Array) && value.all? { |entry| entry.is_a?(Hash) }
|
|
81
|
-
value
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# 映射值必须仍是记录,不能将损坏结构解释成空事实。
|
|
85
|
-
def hash_of_hashes(value)
|
|
86
|
-
raise ArgumentError, "映射结构错误" unless value.is_a?(Hash) && value.values.all? { |entry| entry.is_a?(Hash) }
|
|
87
|
-
value
|
|
56
|
+
CtxRunner::Evidence.validate(document) { |record, dimension| validate_record(record, dimension) }
|
|
88
57
|
end
|
|
89
58
|
|
|
90
59
|
# 在输出目录创建前验证所有会参与建议生成的嵌套字段。
|
|
91
60
|
def validate_record(record, dimension)
|
|
92
61
|
return unless dimension == "configuration"
|
|
93
|
-
|
|
62
|
+
CtxRunner::Evidence.array(record.fetch("review_findings", []))
|
|
94
63
|
nve = record.fetch("nve", {})
|
|
95
64
|
raise ArgumentError, "NVE/VLAN 结构错误" unless nve.is_a?(Hash) && record.fetch("vlan_vnis", {}).is_a?(Hash)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
65
|
+
CtxRunner::Evidence.map(nve.fetch("members", {}))
|
|
66
|
+
CtxRunner::Evidence.map(record.fetch("vrfs", {}))
|
|
67
|
+
CtxRunner::Evidence.map(record.fetch("evpn", {})).each_value { |entry| CtxRunner::Evidence.array(entry.fetch("route_targets", [])) }
|
|
68
|
+
CtxRunner::Evidence.map(record.fetch("interfaces", {})).each_value do |interface|
|
|
100
69
|
addresses, bindings = interface.fetch("addresses", []), interface.fetch("acl_bindings", {})
|
|
101
70
|
unless addresses.is_a?(Array) && addresses.all? { |address| address.is_a?(String) && address.include?("/") } && bindings.is_a?(Hash) && bindings.all? { |direction, name| %w[in out].include?(direction) && name.is_a?(String) }
|
|
102
71
|
raise ArgumentError, "接口结构错误"
|
|
@@ -106,7 +75,7 @@ module Vxlan
|
|
|
106
75
|
acls = record.fetch("acls", {})
|
|
107
76
|
raise ArgumentError, "ACL 结构错误" unless acls.is_a?(Hash)
|
|
108
77
|
acls.each_value do |entries|
|
|
109
|
-
|
|
78
|
+
CtxRunner::Evidence.array(entries).each do |entry|
|
|
110
79
|
unless entry["sequence"].is_a?(Integer) && %w[permit deny remark].include?(entry["action"]) && entry["expression"].is_a?(String)
|
|
111
80
|
raise ArgumentError, "ACL 规则结构错误"
|
|
112
81
|
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/router"
|
|
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::Router.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/fabric/review.rb
CHANGED
|
@@ -192,7 +192,7 @@ module Vxlan
|
|
|
192
192
|
name = row[:svi]&.dig("acl_bindings", direction)
|
|
193
193
|
entries = name && row[:config].fetch("acls", {})[name]
|
|
194
194
|
policy = policies.dig(row[:host], name)
|
|
195
|
-
{ acl: name, defined: !entries.nil?, entries: entries, policy_verified: !policy.nil?, address_groups: policy&.address_groups, digest: policy &&
|
|
195
|
+
{ acl: name, defined: !entries.nil?, entries: entries, policy_verified: !policy.nil?, address_groups: policy&.address_groups, digest: policy && CtxRunner::Fingerprint.call(policy.to_h) }
|
|
196
196
|
end
|
|
197
197
|
code = different_binding ? "fabric_acl_binding" : "fabric_acl_content"
|
|
198
198
|
finding(fabric, vni, code, "#{direction} 方向 ACL 挂载/有序规则不一致", evidence)
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -9,20 +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.2.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.2.0
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: ctx_runner
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
15
29
|
requirements:
|
|
16
30
|
- - "~>"
|
|
17
31
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.1.
|
|
32
|
+
version: 0.1.1
|
|
19
33
|
type: :runtime
|
|
20
34
|
prerelease: false
|
|
21
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
36
|
requirements:
|
|
23
37
|
- - "~>"
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.1.
|
|
39
|
+
version: 0.1.1
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: ctx_parser
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -193,9 +207,11 @@ files:
|
|
|
193
207
|
- docs/superpowers/plans/2026-10-19-evidence-contracts.md
|
|
194
208
|
- docs/superpowers/plans/2026-10-19-vxlan-examples.md
|
|
195
209
|
- docs/superpowers/plans/2026-10-20-fabric-consistency.md
|
|
210
|
+
- docs/superpowers/plans/2026-10-22-ctx-cli.md
|
|
196
211
|
- docs/superpowers/specs/2026-10-19-command-line-design.md
|
|
197
212
|
- docs/superpowers/specs/2026-10-19-connector-integration-design.md
|
|
198
213
|
- docs/superpowers/specs/2026-10-19-evidence-contracts.md
|
|
214
|
+
- docs/superpowers/specs/2026-10-22-ctx-cli-design.md
|
|
199
215
|
- examples/README.md
|
|
200
216
|
- examples/collect.rb
|
|
201
217
|
- examples/offline.rb
|