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.
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "optparse"
4
- require "json"
5
- require "digest"
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
- options = {}
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
- rest = parser.parse(argv.dup)
29
- return @out.puts(parser) || 0 if options[:help]
30
- raise ArgumentError, "需要批次目录" unless rest.empty? && options[:batch]
31
- batch = File.expand_path(options[:batch])
32
- paths = Dir[File.join(batch, "evidence", "*.json")].sort
33
- raise ArgumentError, "无设备报告" if paths.empty?
34
- # 先校验所有输入,损坏文件不会留下看似完成的部分建议批次。
35
- documents = paths.map do |path|
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(File.read(options[:fabric_policy]), aliases: false)
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: policy)
48
- @out.puts "建议已生成:#{report.directory}/summary.txt;#{documents.size} 台;未执行设备变更。"
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
- raise ArgumentError, "无设备身份" unless document.is_a?(Hash) && document["device"].is_a?(Hash) && %w[name host].all? { |key| document["device"][key].is_a?(String) }
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
- array_of_hashes(record.fetch("review_findings", []))
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
- hash_of_hashes(nve.fetch("members", {}))
97
- hash_of_hashes(record.fetch("vrfs", {}))
98
- hash_of_hashes(record.fetch("evpn", {})).each_value { |entry| array_of_hashes(entry.fetch("route_targets", [])) }
99
- hash_of_hashes(record.fetch("interfaces", {})).each_value do |interface|
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
- array_of_hashes(entries).each do |entry|
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
- arguments = argv.dup
11
- command = arguments.shift
12
- case command
13
- when nil, "--help", "--help-all", "-h"
14
- return usage(out) if arguments.empty?
15
- when "--version", "-v"
16
- return out.puts(Vxlan::VERSION) || 0 if arguments.empty?
17
- when "collect"
18
- begin
19
- require_relative "cli/collect"
20
- rescue LoadError
21
- error.puts "collect 需要含共享清单筛选的当前 net-connector;请安装项目依赖。"
22
- return 2
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
- return Collect.new(env: env, out: out, error: error, input: input, **collect_options).run(arguments)
25
- when "apply"
26
- require_relative "cli/apply"
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
- def self.usage(out)
41
- out.puts "用法: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 查看范例。"
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "observation"
3
+ require "ctx_parser/result"
4
4
 
5
5
  module Vxlan
6
6
  # 配置证据约束运行查询:明确未启用的功能才允许空回显或省略查询。
@@ -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 && ChangePlan.fingerprint(policy.to_h) }
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Vxlan
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.4.0"
5
+ VERSION = "0.4.2"
6
6
  end
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.0
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.0
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.0
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