vxlan 0.2.3 → 0.3.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 +64 -0
- data/README-zh.md +32 -4
- data/README.md +32 -4
- data/bin/README.md +203 -0
- data/bin/vxlan +8 -0
- data/docs/architecture-zh.md +62 -60
- data/docs/evidence-contracts.md +54 -0
- data/docs/remediation.md +84 -0
- data/docs/review-2026-10-19.md +31 -0
- data/docs/superpowers/plans/2026-10-19-command-line.md +69 -0
- data/docs/superpowers/plans/2026-10-19-connector-integration.md +83 -0
- data/docs/superpowers/plans/2026-10-19-evidence-contracts.md +64 -0
- data/docs/superpowers/plans/2026-10-19-vxlan-examples.md +47 -0
- data/docs/superpowers/plans/2026-10-20-fabric-consistency.md +28 -0
- data/docs/superpowers/specs/2026-10-19-command-line-design.md +78 -0
- data/docs/superpowers/specs/2026-10-19-connector-integration-design.md +100 -0
- data/docs/superpowers/specs/2026-10-19-evidence-contracts.md +11 -0
- data/examples/README.md +20 -0
- data/examples/collect.rb +8 -0
- data/examples/parse.rb +5 -0
- data/lib/vxlan/acl_policy.rb +50 -0
- data/lib/vxlan/change_plan/svi_acl.rb +76 -0
- data/lib/vxlan/change_plan/svi_gateway.rb +36 -0
- data/lib/vxlan/change_plan/vlan_vni.rb +40 -0
- data/lib/vxlan/change_plan.rb +131 -0
- data/lib/vxlan/cisco_nxos/configuration_state.rb +1 -4
- data/lib/vxlan/cisco_nxos/evpn_l2route.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +65 -11
- data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +32 -8
- data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +3 -2
- data/lib/vxlan/cisco_nxos/parsers/show_nve_vni.rb +9 -1
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +11 -5
- data/lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb +10 -4
- data/lib/vxlan/cisco_nxos/syntax/policies.rb +16 -0
- data/lib/vxlan/cisco_nxos/syntax/routing.rb +16 -6
- data/lib/vxlan/cli/apply.rb +172 -0
- data/lib/vxlan/cli/collect.rb +210 -0
- data/lib/vxlan/cli/connector.rb +23 -0
- data/lib/vxlan/cli/options.rb +311 -0
- data/lib/vxlan/cli/parse.rb +58 -0
- data/lib/vxlan/cli/report.rb +223 -0
- data/lib/vxlan/cli/review/fabric.rb +102 -0
- data/lib/vxlan/cli/review/report.rb +165 -0
- data/lib/vxlan/cli/review.rb +125 -0
- data/lib/vxlan/cli/templates/env.example +12 -0
- data/lib/vxlan/cli/templates/vxlan.yml +32 -0
- data/lib/vxlan/cli.rb +41 -0
- data/lib/vxlan/collector.rb +59 -28
- data/lib/vxlan/config/evidence.rb +18 -0
- data/lib/vxlan/config/review.rb +56 -0
- data/lib/vxlan/{configuration.rb → config.rb} +1 -1
- data/lib/vxlan/fabric/review.rb +324 -0
- data/lib/vxlan/fabric/scope.rb +43 -0
- data/lib/vxlan/observation.rb +3 -0
- data/lib/vxlan/recommendations.rb +193 -0
- data/lib/vxlan/service/catalog.rb +77 -0
- data/lib/vxlan/service/mapping.rb +53 -0
- data/lib/vxlan/snapshot.rb +3 -0
- data/lib/vxlan/subnet_index.rb +88 -0
- data/lib/vxlan/version.rb +1 -1
- data/lib/vxlan.rb +4 -2
- metadata +137 -7
- data/lib/vxlan/cisco_nxos/configuration.rb +0 -10
- data/lib/vxlan/configuration_review.rb +0 -54
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vxlan
|
|
6
|
+
module Service
|
|
7
|
+
# L2 业务由 VLAN、NVE、EVPN 的联合事实识别;缺一个环节仍是已开始部署。
|
|
8
|
+
class Catalog
|
|
9
|
+
# 配置完整性由调用方保证,目录本身不读取设备或推断部署意图。
|
|
10
|
+
def initialize(configs)
|
|
11
|
+
@configs = configs
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# 一个 VNI 可有多个本地 VLAN 事实,交给审查明确报告歧义。
|
|
15
|
+
def services
|
|
16
|
+
result = Hash.new { |hash, vni| hash[vni] = [] }
|
|
17
|
+
@configs.each do |host, config|
|
|
18
|
+
mappings = config.fetch("vlan_vnis", {})
|
|
19
|
+
nve = config.fetch("nve", {}).fetch("members", {})
|
|
20
|
+
evpn = config.fetch("evpn", {})
|
|
21
|
+
l3 = config.fetch("vrfs", {}).values.filter_map { |vrf| vrf["vni"] }
|
|
22
|
+
l2 = evpn.filter_map { |vni, member| Integer(vni) if member["type"] == "l2" }
|
|
23
|
+
l2.concat(nve.filter_map { |vni, member| Integer(vni) unless member["associate_vrf"] })
|
|
24
|
+
l3.concat(nve.filter_map { |vni, member| Integer(vni) if member["associate_vrf"] })
|
|
25
|
+
l3.concat(evpn.filter_map { |vni, member| Integer(vni) if member["type"] == "l3" })
|
|
26
|
+
vnis = (mappings.values.compact - l3 + l2).uniq
|
|
27
|
+
mappings_by_vni = mappings.group_by { |_vlan, vni| vni }
|
|
28
|
+
vnis.sort.each do |vni|
|
|
29
|
+
vlans = mappings_by_vni.fetch(vni, []).map(&:first)
|
|
30
|
+
(vlans.empty? ? [ nil ] : vlans).each do |vlan|
|
|
31
|
+
member, control = nve[vni.to_s], evpn[vni.to_s]
|
|
32
|
+
missing = []
|
|
33
|
+
missing << "VLAN" unless vlan
|
|
34
|
+
missing << "NVE" unless member
|
|
35
|
+
missing << "NVE L2" if member && member["associate_vrf"]
|
|
36
|
+
missing << "EVPN" if config["nv_overlay_evpn"] && !control
|
|
37
|
+
missing << "EVPN L2" if control && control["type"] != "l2"
|
|
38
|
+
result[vni] << { host: host, config: config, vlan: vlan, svi: vlan && config.fetch("interfaces", {})["Vlan#{vlan}"],
|
|
39
|
+
nve: member, evpn: control, missing: missing }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 成员未指定复制方式时继承全局组播;显式 BGP/成员组播优先。
|
|
47
|
+
def self.nve_member(config, member)
|
|
48
|
+
return nil unless member
|
|
49
|
+
inherited = config.dig("nve", "multicast_group")
|
|
50
|
+
if inherited && !member["replication"] && !member["multicast_group"] && !member["associate_vrf"]
|
|
51
|
+
member.merge("multicast_group" => inherited)
|
|
52
|
+
else
|
|
53
|
+
member
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 两字节 AS 的 auto RT 为 AS:VNI;其他 AS 形式保留声明,不猜测版本相关派生行为。
|
|
58
|
+
def self.route_targets(entries, asn:, vni:)
|
|
59
|
+
Array(entries).flat_map do |entry|
|
|
60
|
+
directions = entry["direction"] == "both" ? %w[import export] : [ entry["direction"] ]
|
|
61
|
+
directions.map do |direction|
|
|
62
|
+
target = entry["target"]
|
|
63
|
+
target = asn.is_a?(Integer) && asn.between?(1, 65_535) ? "#{asn}:#{vni}" : [ "auto", asn, vni ] if target == "auto"
|
|
64
|
+
[ entry["address_family"], entry.fetch("evpn", false), direction, target ]
|
|
65
|
+
end
|
|
66
|
+
end.uniq.sort_by { |entry| JSON.generate(entry) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 租户出口细节可以因 Border 职责不同而不同,此投影只比较 VNI 与导入导出声明。
|
|
70
|
+
def self.tenant(config, name)
|
|
71
|
+
vrf = config.fetch("vrfs", {})[name]
|
|
72
|
+
return nil unless vrf
|
|
73
|
+
{ "vrf" => name, "vni" => vrf["vni"], "route_targets" => route_targets(vrf["route_targets"], asn: config.dig("bgp", "asn"), vni: vrf["vni"]) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../change_plan"
|
|
4
|
+
require_relative "../acl_policy"
|
|
5
|
+
require_relative "catalog"
|
|
6
|
+
|
|
7
|
+
module Vxlan
|
|
8
|
+
module Service
|
|
9
|
+
# 由完整同业务对端证明关联关系,而不是通过 VNI 数字编码猜测 VLAN。
|
|
10
|
+
class Mapping
|
|
11
|
+
# 只接受同 Fabric、同角色业务行,返回已校验的业务增量对象或未知。
|
|
12
|
+
def self.infer(vni, row, peers)
|
|
13
|
+
return nil unless peers.map { |peer| peer[:host] }.uniq.size == peers.size
|
|
14
|
+
donors = peers.reject { |peer| peer[:host] == row[:host] }
|
|
15
|
+
return nil unless donors.any? && donors.all? { |peer| peer[:missing].empty? && peer[:svi] }
|
|
16
|
+
signatures = donors.map { |peer| signature(peer[:config], peer[:svi]) }.uniq
|
|
17
|
+
return nil unless signatures.one? && signatures.first
|
|
18
|
+
expected = signatures.first
|
|
19
|
+
return nil unless transport_matches?(vni, row, donors)
|
|
20
|
+
matches = row[:config].fetch("vlan_vnis", {}).filter_map do |vlan, mapped|
|
|
21
|
+
next unless mapped.nil?
|
|
22
|
+
svi = row[:config].fetch("interfaces", {})["Vlan#{vlan}"]
|
|
23
|
+
vlan if svi && signature(row[:config], svi) == expected
|
|
24
|
+
end
|
|
25
|
+
return nil unless matches.one?
|
|
26
|
+
change = { "kind" => "vlan_vni_add", "vlan" => Integer(matches.first), "vni" => vni, "vrf" => expected["vrf"], "address" => expected["addresses"].first }
|
|
27
|
+
ChangePlan.candidate(change, config: row[:config])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 网关、租户、ACL 及其依赖一起界定业务;缺失 Anycast 身份不能当成一致。
|
|
31
|
+
def self.signature(config, svi)
|
|
32
|
+
return nil unless svi["anycast"] && svi["vrf"] && svi.fetch("addresses", []).one? && config["anycast_mac"]
|
|
33
|
+
policies = svi.fetch("acl_bindings", {}).to_h do |direction, name|
|
|
34
|
+
policy = AclPolicy.resolve(config, config.fetch("acls", {})[name])
|
|
35
|
+
return nil unless policy
|
|
36
|
+
[ direction, policy.to_h ]
|
|
37
|
+
end
|
|
38
|
+
svi.slice("addresses", "vrf", "anycast", "shutdown", "mtu").merge("anycast_mac" => config["anycast_mac"], "policies" => policies)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 本机已有 NVE/EVPN 与对端语义一致才允许连接 VLAN;路由冲突不能靠补绑定解决。
|
|
42
|
+
def self.transport_matches?(vni, row, donors)
|
|
43
|
+
values = [ row, *donors ].map do |peer|
|
|
44
|
+
next nil unless peer[:nve] && peer[:evpn]
|
|
45
|
+
config = peer[:config]
|
|
46
|
+
vrf = peer[:svi]&.dig("vrf") || donors.first[:svi]["vrf"]
|
|
47
|
+
[ Catalog.nve_member(config, peer[:nve]), peer[:evpn]["type"], Catalog.route_targets(peer[:evpn]["route_targets"], asn: config.dig("bgp", "asn"), vni: vni), Catalog.tenant(config, vrf) ]
|
|
48
|
+
end
|
|
49
|
+
values.none?(&:nil?) && values.uniq.one?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/vxlan/snapshot.rb
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "ctx_parser/immutable"
|
|
5
|
+
|
|
6
|
+
module Vxlan
|
|
7
|
+
# 单设备 SVI 网段索引;按 VRF 和地址族查重叠,保留接口顺序和重复来源。
|
|
8
|
+
class SubnetIndex
|
|
9
|
+
Entry = Data.define(:interface, :vni, :address, :position, :first, :prefix)
|
|
10
|
+
private_constant :Entry
|
|
11
|
+
|
|
12
|
+
# 仅保存查询所需的独立事实;未解析地址不能作为无重叠证据。
|
|
13
|
+
def initialize(config)
|
|
14
|
+
groups, invalid = {}, {}
|
|
15
|
+
position = 0
|
|
16
|
+
config.fetch("interfaces", {}).each do |name, svi|
|
|
17
|
+
next unless name.match?(/\AVlan\d+\z/)
|
|
18
|
+
vni = config.fetch("vlan_vnis", {})[name.delete_prefix("Vlan")]
|
|
19
|
+
svi.fetch("addresses", []).each do |address|
|
|
20
|
+
fields = CtxParser::Immutable.copy([ name, vni, address ])
|
|
21
|
+
network = parse_network(address)
|
|
22
|
+
entry = Entry.new(*fields, position, network&.to_i, network&.prefix)
|
|
23
|
+
position += 1
|
|
24
|
+
if network
|
|
25
|
+
((groups[svi["vrf"]] ||= {})[network.ipv4?] ||= []) << entry
|
|
26
|
+
else
|
|
27
|
+
(invalid[svi["vrf"]] ||= []) << entry
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
@groups = groups.transform_values do |families|
|
|
32
|
+
families.transform_values do |entries|
|
|
33
|
+
prefixes = entries.group_by(&:prefix).transform_values { |rows| rows.group_by(&:first).transform_values(&:freeze).freeze }
|
|
34
|
+
{ ordered: entries.sort_by(&:first).freeze, prefixes: prefixes.freeze }.freeze
|
|
35
|
+
end.freeze
|
|
36
|
+
end.freeze
|
|
37
|
+
@invalid = invalid.transform_values(&:freeze).freeze
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 同业务 VNI 和明确排除的本机接口不构成冲突;其他匹配范围保留完整地址证据。
|
|
42
|
+
def conflicts(address:, vrf:, vni:, except_interface: nil)
|
|
43
|
+
network = IPAddr.new(address)
|
|
44
|
+
@invalid.fetch(vrf, []).each do |entry|
|
|
45
|
+
IPAddr.new(entry.address) if relevant?(entry, vni, except_interface)
|
|
46
|
+
end
|
|
47
|
+
group = @groups.dig(vrf, network.ipv4?)
|
|
48
|
+
return [] unless group
|
|
49
|
+
|
|
50
|
+
entries = overlapping(group, network).select { |entry| relevant?(entry, vni, except_interface) }
|
|
51
|
+
entries.sort_by!(&:position)
|
|
52
|
+
entries.group_by(&:interface).map do |name, rows|
|
|
53
|
+
{ interface: name, vni: rows.first.vni, value: rows.map(&:address) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# 非法地址只在相关查询中重新拒绝,不能让另一 VRF 的未使用事实改变原有边界。
|
|
60
|
+
def parse_network(address)
|
|
61
|
+
IPAddr.new(address)
|
|
62
|
+
rescue ArgumentError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 区间起点命中的子网配合有限个祖先前缀,覆盖相等、包含和被包含的网段。
|
|
67
|
+
def overlapping(group, network)
|
|
68
|
+
first, last = network.to_i, network.to_range.last.to_i
|
|
69
|
+
ordered = group.fetch(:ordered)
|
|
70
|
+
left = ordered.bsearch_index { |entry| entry.first >= first } || ordered.size
|
|
71
|
+
right = ordered.bsearch_index { |entry| entry.first > last } || ordered.size
|
|
72
|
+
entries = ordered[left...right]
|
|
73
|
+
width = network.ipv4? ? 32 : 128
|
|
74
|
+
group.fetch(:prefixes).each do |prefix, subnets|
|
|
75
|
+
next unless prefix < network.prefix
|
|
76
|
+
shift = width - prefix
|
|
77
|
+
ancestor = (first >> shift) << shift
|
|
78
|
+
entries.concat(subnets.fetch(ancestor, [])) unless ancestor == first
|
|
79
|
+
end
|
|
80
|
+
entries
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 相同 VNI 与排除接口的事实保留在索引中,只在本次查询中排除。
|
|
84
|
+
def relevant?(entry, vni, except_interface)
|
|
85
|
+
entry.vni != vni && entry.interface != except_interface
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/vxlan/version.rb
CHANGED
data/lib/vxlan.rb
CHANGED
|
@@ -7,9 +7,11 @@ end
|
|
|
7
7
|
|
|
8
8
|
require_relative "vxlan/observation"
|
|
9
9
|
require_relative "vxlan/snapshot"
|
|
10
|
-
require_relative "vxlan/
|
|
10
|
+
require_relative "vxlan/config"
|
|
11
11
|
require_relative "vxlan/runtime_parser"
|
|
12
12
|
require_relative "vxlan/bgp_neighbors"
|
|
13
|
-
require_relative "vxlan/
|
|
13
|
+
require_relative "vxlan/config/review"
|
|
14
14
|
require_relative "vxlan/prefix_list_references"
|
|
15
15
|
require_relative "vxlan/collector"
|
|
16
|
+
require_relative "vxlan/recommendations"
|
|
17
|
+
require_relative "vxlan/fabric/review"
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -29,14 +29,20 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.2
|
|
32
|
+
version: '0.2'
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: 0.2.4
|
|
33
36
|
type: :runtime
|
|
34
37
|
prerelease: false
|
|
35
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
39
|
requirements:
|
|
37
40
|
- - "~>"
|
|
38
41
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.2
|
|
42
|
+
version: '0.2'
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.2.4
|
|
40
46
|
- !ruby/object:Gem::Dependency
|
|
41
47
|
name: digest
|
|
42
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,9 +71,94 @@ dependencies:
|
|
|
65
71
|
- - ">="
|
|
66
72
|
- !ruby/object:Gem::Version
|
|
67
73
|
version: '0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: json
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
type: :runtime
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: optparse
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
type: :runtime
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: yaml
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
type: :runtime
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: fileutils
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0'
|
|
123
|
+
type: :runtime
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0'
|
|
130
|
+
- !ruby/object:Gem::Dependency
|
|
131
|
+
name: tempfile
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
type: :runtime
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ">="
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0'
|
|
144
|
+
- !ruby/object:Gem::Dependency
|
|
145
|
+
name: time
|
|
146
|
+
requirement: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
type: :runtime
|
|
152
|
+
prerelease: false
|
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
68
158
|
description: Independent configuration and runtime parsers for VXLAN fabrics, with
|
|
69
159
|
immutable snapshots and injected collection sessions.
|
|
70
|
-
executables:
|
|
160
|
+
executables:
|
|
161
|
+
- vxlan
|
|
71
162
|
extensions: []
|
|
72
163
|
extra_rdoc_files: []
|
|
73
164
|
files:
|
|
@@ -75,12 +166,32 @@ files:
|
|
|
75
166
|
- LICENSE
|
|
76
167
|
- README-zh.md
|
|
77
168
|
- README.md
|
|
169
|
+
- bin/README.md
|
|
170
|
+
- bin/vxlan
|
|
78
171
|
- docs/architecture-zh.md
|
|
172
|
+
- docs/evidence-contracts.md
|
|
79
173
|
- docs/release.md
|
|
174
|
+
- docs/remediation.md
|
|
175
|
+
- docs/review-2026-10-19.md
|
|
176
|
+
- docs/superpowers/plans/2026-10-19-command-line.md
|
|
177
|
+
- docs/superpowers/plans/2026-10-19-connector-integration.md
|
|
178
|
+
- docs/superpowers/plans/2026-10-19-evidence-contracts.md
|
|
179
|
+
- docs/superpowers/plans/2026-10-19-vxlan-examples.md
|
|
180
|
+
- docs/superpowers/plans/2026-10-20-fabric-consistency.md
|
|
181
|
+
- docs/superpowers/specs/2026-10-19-command-line-design.md
|
|
182
|
+
- docs/superpowers/specs/2026-10-19-connector-integration-design.md
|
|
183
|
+
- docs/superpowers/specs/2026-10-19-evidence-contracts.md
|
|
184
|
+
- examples/README.md
|
|
185
|
+
- examples/collect.rb
|
|
80
186
|
- examples/offline.rb
|
|
187
|
+
- examples/parse.rb
|
|
81
188
|
- lib/vxlan.rb
|
|
189
|
+
- lib/vxlan/acl_policy.rb
|
|
82
190
|
- lib/vxlan/bgp_neighbors.rb
|
|
83
|
-
- lib/vxlan/
|
|
191
|
+
- lib/vxlan/change_plan.rb
|
|
192
|
+
- lib/vxlan/change_plan/svi_acl.rb
|
|
193
|
+
- lib/vxlan/change_plan/svi_gateway.rb
|
|
194
|
+
- lib/vxlan/change_plan/vlan_vni.rb
|
|
84
195
|
- lib/vxlan/cisco_nxos/configuration_state.rb
|
|
85
196
|
- lib/vxlan/cisco_nxos/document.rb
|
|
86
197
|
- lib/vxlan/cisco_nxos/evpn_l2route.rb
|
|
@@ -102,14 +213,33 @@ files:
|
|
|
102
213
|
- lib/vxlan/cisco_nxos/syntax/policies.rb
|
|
103
214
|
- lib/vxlan/cisco_nxos/syntax/routing.rb
|
|
104
215
|
- lib/vxlan/cisco_nxos/terminal.rb
|
|
216
|
+
- lib/vxlan/cli.rb
|
|
217
|
+
- lib/vxlan/cli/apply.rb
|
|
218
|
+
- lib/vxlan/cli/collect.rb
|
|
219
|
+
- lib/vxlan/cli/connector.rb
|
|
220
|
+
- lib/vxlan/cli/options.rb
|
|
221
|
+
- lib/vxlan/cli/parse.rb
|
|
222
|
+
- lib/vxlan/cli/report.rb
|
|
223
|
+
- lib/vxlan/cli/review.rb
|
|
224
|
+
- lib/vxlan/cli/review/fabric.rb
|
|
225
|
+
- lib/vxlan/cli/review/report.rb
|
|
226
|
+
- lib/vxlan/cli/templates/env.example
|
|
227
|
+
- lib/vxlan/cli/templates/vxlan.yml
|
|
105
228
|
- lib/vxlan/collector.rb
|
|
106
|
-
- lib/vxlan/
|
|
107
|
-
- lib/vxlan/
|
|
229
|
+
- lib/vxlan/config.rb
|
|
230
|
+
- lib/vxlan/config/evidence.rb
|
|
231
|
+
- lib/vxlan/config/review.rb
|
|
232
|
+
- lib/vxlan/fabric/review.rb
|
|
233
|
+
- lib/vxlan/fabric/scope.rb
|
|
108
234
|
- lib/vxlan/observation.rb
|
|
109
235
|
- lib/vxlan/parser_registry.rb
|
|
110
236
|
- lib/vxlan/prefix_list_references.rb
|
|
237
|
+
- lib/vxlan/recommendations.rb
|
|
111
238
|
- lib/vxlan/runtime_parser.rb
|
|
239
|
+
- lib/vxlan/service/catalog.rb
|
|
240
|
+
- lib/vxlan/service/mapping.rb
|
|
112
241
|
- lib/vxlan/snapshot.rb
|
|
242
|
+
- lib/vxlan/subnet_index.rb
|
|
113
243
|
- lib/vxlan/version.rb
|
|
114
244
|
homepage: https://github.com/gatework/vxlan
|
|
115
245
|
licenses:
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Vxlan
|
|
4
|
-
# 配置关联审查独立于语法解析及运行健康检查。
|
|
5
|
-
class ConfigurationReview
|
|
6
|
-
# 绑定归一化配置,用于交叉引用一致性检查。
|
|
7
|
-
def initialize(configuration)
|
|
8
|
-
@configuration = configuration
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
# 检查完整配置中的关联缺失或引用矛盾。
|
|
12
|
-
def call(complete: false)
|
|
13
|
-
raise ArgumentError, "Completeness must be boolean" unless [ true, false ].include?(complete)
|
|
14
|
-
return [] unless complete
|
|
15
|
-
|
|
16
|
-
nve_members_by_vni = @configuration.fetch(:nve).fetch(:members, {})
|
|
17
|
-
vlan_mappings_by_vni = @configuration.fetch(:vlan_vnis).group_by { |_vlan, vni| vni }
|
|
18
|
-
vrf_vnis = @configuration.fetch(:vrfs).values.filter_map { |settings| settings[:vni] }.to_set
|
|
19
|
-
evpn_vnis = @configuration.fetch(:evpn)
|
|
20
|
-
findings = []
|
|
21
|
-
vlan_mappings_by_vni.each do |vni, entries|
|
|
22
|
-
next unless vni && !nve_members_by_vni.key?(vni)
|
|
23
|
-
|
|
24
|
-
findings << { code: :vlan_vni_without_nve, vni: vni, vlan_ids: entries.map(&:first).sort }
|
|
25
|
-
end
|
|
26
|
-
nve_members_by_vni.each do |vni, settings|
|
|
27
|
-
if settings[:associate_vrf]
|
|
28
|
-
findings << { code: :nve_l3_without_vrf, vni: vni } unless vrf_vnis.include?(vni)
|
|
29
|
-
next
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
findings << { code: :nve_l2_without_vlan, vni: vni } unless vlan_mappings_by_vni.key?(vni)
|
|
33
|
-
if @configuration[:nv_overlay_evpn] && !evpn_vnis.key?(vni)
|
|
34
|
-
findings << { code: :nve_l2_without_evpn, vni: vni }
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
evpn_vnis.each do |vni, settings|
|
|
38
|
-
member = nve_members_by_vni[vni]
|
|
39
|
-
if !member
|
|
40
|
-
findings << { code: :evpn_vni_without_nve, vni: vni }
|
|
41
|
-
elsif (settings[:type] == "l3") != member[:associate_vrf]
|
|
42
|
-
findings << { code: :vni_layer_conflict, vni: vni, evpn_type: settings[:type] }
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
@configuration.fetch(:vrfs).each do |name, settings|
|
|
46
|
-
next unless settings[:vni]
|
|
47
|
-
member = nve_members_by_vni[settings[:vni]]
|
|
48
|
-
code = member ? (!member[:associate_vrf] ? :vni_layer_conflict : nil) : :l3_vni_without_nve
|
|
49
|
-
findings << { code: code, vni: settings[:vni], vrf: name } if code
|
|
50
|
-
end
|
|
51
|
-
findings.sort_by { |finding| [ finding[:vni], finding[:code].to_s ] }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|