vxlan 0.4.1 → 0.5.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 +21 -7
- data/README-zh.md +1 -1
- data/README.md +3 -1
- data/bin/README.md +2 -2
- 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/svi_acl.rb +7 -7
- data/lib/vxlan/change_plan.rb +8 -15
- 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 +32 -30
- data/lib/vxlan/cli/collect.rb +13 -5
- data/lib/vxlan/cli/options.rb +47 -44
- data/lib/vxlan/cli/parse.rb +12 -12
- data/lib/vxlan/cli/report.rb +14 -14
- data/lib/vxlan/cli/review/fabric.rb +24 -22
- data/lib/vxlan/cli/review/report.rb +31 -30
- data/lib/vxlan/cli/review.rb +42 -73
- data/lib/vxlan/cli.rb +4 -4
- data/lib/vxlan/collection_plan.rb +1 -1
- data/lib/vxlan/fabric/review.rb +43 -43
- data/lib/vxlan/fabric/scope.rb +6 -6
- data/lib/vxlan/recommendations.rb +26 -26
- data/lib/vxlan/version.rb +1 -1
- metadata +17 -21
data/lib/vxlan/fabric/review.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Vxlan
|
|
|
18
18
|
# 比较范围由业务策略统一验证,审查器只负责组织规则与建议。
|
|
19
19
|
def initialize(documents, policy)
|
|
20
20
|
@documents = documents.to_h { |document| [ document.fetch("device").fetch("host"), document ] }
|
|
21
|
-
raise ArgumentError, "
|
|
21
|
+
raise ArgumentError, "Duplicate device identity" unless @documents.size == documents.size
|
|
22
22
|
@scope = Scope.new(policy)
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -26,12 +26,12 @@ module Vxlan
|
|
|
26
26
|
def call
|
|
27
27
|
@findings, @items, @coverage, @subnet_indexes = [], Hash.new { |hash, host| hash[host] = [] }, [], {}
|
|
28
28
|
inventory = @scope.coverage(@documents.keys)
|
|
29
|
-
finding("
|
|
29
|
+
finding("unassigned", nil, "fabric_scope_unassigned", "Collected devices are outside the Fabric policy and are not covered by cross-device review", inventory[:unassigned]) if inventory[:unassigned].any?
|
|
30
30
|
@scope.groups.each do |fabric, group|
|
|
31
31
|
configs = group.values.flatten.to_h { |host| [ host, configuration(host) ] }
|
|
32
32
|
unknown = configs.select { |_host, config| config.nil? }.keys
|
|
33
33
|
unless unknown.empty?
|
|
34
|
-
finding(fabric, nil, "fabric_evidence_incomplete", "
|
|
34
|
+
finding(fabric, nil, "fabric_evidence_incomplete", "Member configuration evidence is incomplete; change inference stopped for this Fabric", unknown)
|
|
35
35
|
next
|
|
36
36
|
end
|
|
37
37
|
%w[leaves borders].each do |role|
|
|
@@ -63,11 +63,11 @@ module Vxlan
|
|
|
63
63
|
@coverage << { fabric: fabric, role: @role, vni: vni, deployed: rows.map { |row| row[:host] }.uniq, not_deployed: configs.keys - rows.map { |row| row[:host] }, incomplete: rows.select { |row| row[:missing].any? }.map { |row| row[:host] }.uniq }
|
|
64
64
|
incomplete = rows.select { |row| row[:missing].any? }
|
|
65
65
|
if incomplete.any?
|
|
66
|
-
finding(fabric, vni, "fabric_service_incomplete", "
|
|
66
|
+
finding(fabric, vni, "fabric_service_incomplete", "Service deployment has started, but VLAN/NVE/EVPN associations are incomplete", observations(incomplete) { |row| { missing: row[:missing] } })
|
|
67
67
|
review_missing_mappings(fabric, vni, rows, all_configs)
|
|
68
68
|
end
|
|
69
69
|
if rows.map { |row| row[:host] }.uniq.size != rows.size
|
|
70
|
-
finding(fabric, vni, "fabric_vlan_mapping_ambiguous", "
|
|
70
|
+
finding(fabric, vni, "fabric_vlan_mapping_ambiguous", "One Leaf maps multiple VLANs to the same VNI", rows.map { |row| row.slice(:host, :vlan) })
|
|
71
71
|
next
|
|
72
72
|
end
|
|
73
73
|
next unless rows.size >= 2
|
|
@@ -76,15 +76,15 @@ module Vxlan
|
|
|
76
76
|
# 缺映射行没有可定位 SVI,不能再扩散为网关、MTU 等一串伪差异。
|
|
77
77
|
rows = rows.select { |row| row[:vlan] }
|
|
78
78
|
next unless rows.size >= 2
|
|
79
|
-
compare(fabric, vni, "fabric_vlan_mapping", "
|
|
79
|
+
compare(fabric, vni, "fabric_vlan_mapping", "Local VLAN IDs differ for the same VNI", rows) { |row| row[:vlan] }
|
|
80
80
|
# L2 业务没有网关可以是正常设计,但仍须检查承载与控制平面。
|
|
81
81
|
next unless rows.any? { |row| row[:svi] && (row[:svi]["anycast"] || row[:svi].fetch("addresses", []).any?) }
|
|
82
82
|
review_gateway(fabric, vni, rows, all_configs, service_blocked: service_blocked)
|
|
83
83
|
%w[in out].each { |direction| review_acl(fabric, vni, rows, direction, policies: policies, service_blocked: service_blocked) }
|
|
84
84
|
%w[vrf anycast mtu shutdown].each do |field|
|
|
85
|
-
compare(fabric, vni, "fabric_#{field}", "
|
|
85
|
+
compare(fabric, vni, "fabric_#{field}", "Service SVI #{field} differs", rows) { |row| row[:svi]&.fetch(field, nil) }
|
|
86
86
|
end
|
|
87
|
-
compare(fabric, vni, "fabric_anycast_mac", "Anycast
|
|
87
|
+
compare(fabric, vni, "fabric_anycast_mac", "Anycast gateway MACs differ", rows) { |row| row[:config]["anycast_mac"] }
|
|
88
88
|
end
|
|
89
89
|
review_acl_catalog(fabric, configs, policies)
|
|
90
90
|
review_address_groups(fabric, configs)
|
|
@@ -94,27 +94,27 @@ module Vxlan
|
|
|
94
94
|
def review_missing_mappings(fabric, vni, rows, configs)
|
|
95
95
|
rows.select { |row| row[:missing] == [ "VLAN" ] }.each do |row|
|
|
96
96
|
evidence = observations(rows) { |peer| { missing: peer[:missing], gateway: peer[:svi]&.fetch("addresses", []), vrf: peer[:svi]&.dig("vrf") } }
|
|
97
|
-
item = recommendation(fabric, vni, "fabric_vlan_mapping_missing", "
|
|
97
|
+
item = recommendation(fabric, vni, "fabric_vlan_mapping_missing", "Complete the VLAN/VNI association for an existing service", evidence)
|
|
98
98
|
plan = Service::Mapping.infer(vni, row, rows)
|
|
99
99
|
change = plan&.to_h
|
|
100
100
|
conflicts = change ? gateway_conflicts(configs, change, except: [ row[:host], "Vlan#{change['vlan']}" ]) : []
|
|
101
101
|
if conflicts.any?
|
|
102
102
|
item[:evidence].concat(conflicts)
|
|
103
|
-
item[:blockers] << "
|
|
103
|
+
item[:blockers] << "Subnets overlap across SVIs/VNIs within the same VRF; adding a mapping must not extend the conflict: #{conflicts.map { |entry| "#{entry[:name]} #{entry[:interface]}" }.join(', ')}"
|
|
104
104
|
plan = nil
|
|
105
105
|
end
|
|
106
106
|
if plan
|
|
107
107
|
local_svi = row[:config].fetch("interfaces").fetch("Vlan#{change['vlan']}")
|
|
108
108
|
item[:evidence] = evidence.map do |entry|
|
|
109
|
-
entry[:host] == row[:host] ? entry.merge(vlan: change["vlan"].to_s, value: { missing: [ "VLAN/VNI
|
|
109
|
+
entry[:host] == row[:host] ? entry.merge(vlan: change["vlan"].to_s, value: { missing: [ "VLAN/VNI mapping" ], gateway: local_svi["addresses"], vrf: local_svi["vrf"] }) : entry
|
|
110
110
|
end
|
|
111
|
-
item[:impact] = "
|
|
112
|
-
item[:prerequisites] << "
|
|
113
|
-
item[:expected] = "
|
|
111
|
+
item[:impact] = "Attaching an existing VLAN to an existing VXLAN service may immediately extend the broadcast domain; no VLAN is created and gateways and ACLs are unchanged."
|
|
112
|
+
item[:prerequisites] << "The unmapped local VLAN's gateway, VRF, Anycast identity and ACL match a complete peer, and NVE/EVPN already match; confirm that this service should be attached locally."
|
|
113
|
+
item[:expected] = "Local VLAN/VNI association is complete, VNI state meets expectations, existing gateway and ACL remain unchanged, and service acceptance passes."
|
|
114
114
|
item[:resolves] = [ { code: "nve_l2_without_vlan", vni: vni } ]
|
|
115
115
|
propose(item, plan, service_blocked: false)
|
|
116
116
|
else
|
|
117
|
-
item[:blockers] << "
|
|
117
|
+
item[:blockers] << "No unique and complete service correspondence exists; compare the local unmapped VLAN, gateway, VRF and ACL with peer configuration. Do not infer VLAN from VNI numbers."
|
|
118
118
|
end
|
|
119
119
|
@items[row[:host]] << item
|
|
120
120
|
end
|
|
@@ -123,13 +123,13 @@ module Vxlan
|
|
|
123
123
|
# 一致性约束先于增量生成;承载/路由声明冲突时停止叠加网关和 ACL 变更。
|
|
124
124
|
def review_transport(fabric, vni, rows)
|
|
125
125
|
before = @findings.size
|
|
126
|
-
compare(fabric, vni, "fabric_replication", "NVE
|
|
127
|
-
compare(fabric, vni, "fabric_arp_suppression", "ARP
|
|
128
|
-
compare(fabric, vni, "fabric_route_targets", "EVPN
|
|
126
|
+
compare(fabric, vni, "fabric_replication", "NVE replication modes or multicast group declarations differ", rows) { |row| Service::Catalog.nve_member(row[:config], row[:nve])&.slice("replication", "multicast_group") }
|
|
127
|
+
compare(fabric, vni, "fabric_arp_suppression", "Explicit ARP suppression settings differ", rows) { |row| row[:nve]&.fetch("suppress_arp", nil) }
|
|
128
|
+
compare(fabric, vni, "fabric_route_targets", "EVPN import/export RT declarations differ; verify routing reachability and isolation", rows) do |row|
|
|
129
129
|
row[:evpn] && Service::Catalog.route_targets(row[:evpn]["route_targets"], asn: row[:config].dig("bgp", "asn"), vni: vni)
|
|
130
130
|
end
|
|
131
131
|
tenant_rows = rows.select { |row| row[:svi] && row[:svi]["vrf"] }
|
|
132
|
-
compare(fabric, vni, "fabric_tenant_routing", "
|
|
132
|
+
compare(fabric, vni, "fabric_tenant_routing", "Tenant VNIs or route import/export declarations differ for a shared service", tenant_rows) do |row|
|
|
133
133
|
Service::Catalog.tenant(row[:config], row[:svi]["vrf"])
|
|
134
134
|
end if tenant_rows.size >= 2
|
|
135
135
|
@findings.size > before
|
|
@@ -140,9 +140,9 @@ module Vxlan
|
|
|
140
140
|
services = Service::Catalog.new(host => config).services
|
|
141
141
|
return if services.empty?
|
|
142
142
|
evidence = [ { host: host, value: { role: "spines", l2_vnis: services.keys.sort } } ]
|
|
143
|
-
finding(fabric, nil, "fabric_role_conflict", "
|
|
144
|
-
item = recommendation(fabric, nil, "fabric_role_conflict", "
|
|
145
|
-
item[:blockers] << "
|
|
143
|
+
finding(fabric, nil, "fabric_role_conflict", "Declared as Spine but contains L2 service-bearing configuration", evidence)
|
|
144
|
+
item = recommendation(fabric, nil, "fabric_role_conflict", "Verify whether the Spine should act as a service VTEP", evidence)
|
|
145
|
+
item[:blockers] << "Confirm the role or migration remnants first; NVE presence does not automatically make a Spine a Leaf. Do not automatically add service configuration or delete NVE."
|
|
146
146
|
@items[host] << item
|
|
147
147
|
end
|
|
148
148
|
|
|
@@ -150,11 +150,11 @@ module Vxlan
|
|
|
150
150
|
def review_gateway(fabric, vni, rows, configs, service_blocked:)
|
|
151
151
|
values = rows.map { |row| row[:svi]&.fetch("addresses", [])&.sort }
|
|
152
152
|
return if values.uniq.one?
|
|
153
|
-
finding(fabric, vni, "fabric_gateway", "
|
|
153
|
+
finding(fabric, vni, "fabric_gateway", "Gateway addresses differ for the same service", observations(rows) { |row| row[:svi]&.fetch("addresses", []) })
|
|
154
154
|
expected = values.compact.reject(&:empty?).uniq
|
|
155
155
|
rows.each do |row|
|
|
156
156
|
next if expected.one? && row[:svi]&.fetch("addresses", [])&.sort == expected.first
|
|
157
|
-
item = recommendation(fabric, vni, "fabric_gateway", "
|
|
157
|
+
item = recommendation(fabric, vni, "fabric_gateway", "Gateway addresses must be consistent", observations(rows) { |entry| entry[:svi]&.fetch("addresses", []) })
|
|
158
158
|
change = { "kind" => "svi_gateway_add", "vlan" => row[:vlan].to_i, "vni" => vni, "vrf" => row[:svi]&.dig("vrf"), "address" => expected.one? && expected.first.one? ? expected.first.first : nil }
|
|
159
159
|
context = rows.map { |entry| [ entry[:svi]&.dig("vrf"), entry[:svi]&.dig("anycast"), entry[:config]["anycast_mac"] ] }.uniq
|
|
160
160
|
conflicts = change["address"] ? gateway_conflicts(configs, change) : []
|
|
@@ -163,8 +163,8 @@ module Vxlan
|
|
|
163
163
|
(plan = ChangePlan.candidate(change, config: row[:config]))
|
|
164
164
|
propose(item, plan, service_blocked: service_blocked)
|
|
165
165
|
else
|
|
166
|
-
item[:blockers] << "
|
|
167
|
-
item[:blockers] << "Fabric
|
|
166
|
+
item[:blockers] << "An existing address, cross-SVI/VNI subnet conflict, inconsistent VRF/Anycast mode/MAC, or lack of a unique gateway prevents this change; confirm the migration design first. Do not add duplicate gateways."
|
|
167
|
+
item[:blockers] << "Conflicting interfaces in the Fabric: #{conflicts.map { |conflict| "#{conflict[:name]} #{conflict[:interface]}" }.join(', ')}" if conflicts.any?
|
|
168
168
|
end
|
|
169
169
|
@items[row[:host]] << item
|
|
170
170
|
end
|
|
@@ -192,14 +192,14 @@ 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
|
-
finding(fabric, vni, code, "#{direction}
|
|
198
|
+
finding(fabric, vni, code, "#{direction} ACL attachments or ordered rules differ", evidence)
|
|
199
199
|
undefined = evidence.select { |row| row[:value][:acl] && !row[:value][:defined] }
|
|
200
|
-
finding(fabric, vni, "fabric_acl_undefined", "#{direction}
|
|
200
|
+
finding(fabric, vni, "fabric_acl_undefined", "#{direction} ACL is attached but its local definition is missing", undefined) if undefined.any?
|
|
201
201
|
unresolved = evidence.select { |row| row[:value][:defined] && !row[:value][:policy_verified] }
|
|
202
|
-
finding(fabric, vni, "fabric_acl_dependency", "#{direction}
|
|
202
|
+
finding(fabric, vni, "fabric_acl_dependency", "#{direction} ACL dependencies are missing, empty or unparsed; policy consistency cannot be established", unresolved) if unresolved.any?
|
|
203
203
|
names = bindings.compact.uniq
|
|
204
204
|
sources = rows.select { |row| row[:config].fetch("acls", {})[names.first] }
|
|
205
205
|
originals = sources.map { |row| row[:config]["acls"][names.first] }
|
|
@@ -208,21 +208,21 @@ module Vxlan
|
|
|
208
208
|
current_name = row[:svi]&.dig("acl_bindings", direction)
|
|
209
209
|
current_entries = names.one? && row[:config].fetch("acls", {})[names.first]
|
|
210
210
|
next if names.one? && unique.one? && unique.first && current_name == names.first && policies.dig(row[:host], names.first) == unique.first
|
|
211
|
-
item = recommendation(fabric, vni, code, "
|
|
211
|
+
item = recommendation(fabric, vni, code, "Align #{direction} access control", evidence)
|
|
212
212
|
change = { "kind" => "svi_acl", "vlan" => row[:vlan].to_i, "vni" => vni, "direction" => direction,
|
|
213
213
|
"acl" => names.first, "entries" => originals.first, "create_acl" => current_entries.nil?, "previous_binding" => current_name }
|
|
214
214
|
change["address_groups"] = unique.first.address_groups if unique.one? && unique.first
|
|
215
215
|
context_matches = rows.map { |entry| entry[:svi]&.dig("vrf") }.uniq.one?
|
|
216
216
|
if names.one? && unique.one? && unique.first && context_matches &&
|
|
217
217
|
(plan = ChangePlan.candidate(change, config: row[:config]))
|
|
218
|
-
item[:prerequisites] << "
|
|
218
|
+
item[:prerequisites] << "The basis is an ACL attached to this service with matching ordered contents; even if fewer nodes attach it, a majority without an ACL is not grounds for removal. Confirm that the service should uniformly use this ACL."
|
|
219
219
|
propose(item, plan, service_blocked: service_blocked)
|
|
220
220
|
else
|
|
221
|
-
item[:blockers] << "ACL
|
|
221
|
+
item[:blockers] << "ACL names, ordered rules, address group members, VRF or current bindings do not establish consistency; choose authoritative rules and complete dependencies. Do not automatically overwrite or remove access control."
|
|
222
222
|
shared = change["create_acl"] ? AclPolicy.bindings(row[:config], names.first) - [ { "interface" => "Vlan#{row[:vlan]}", "direction" => direction } ] : []
|
|
223
223
|
if shared.any?
|
|
224
|
-
references = shared.map { |binding| "#{binding['interface']} #{binding['direction']}" }.join("
|
|
225
|
-
item[:blockers] << "
|
|
224
|
+
references = shared.map { |binding| "#{binding['interface']} #{binding['direction']}" }.join(", ")
|
|
225
|
+
item[:blockers] << "Creating #{names.first} would also activate existing references: #{references}. Review policy and rollback for all affected interfaces together; do not execute this as completion of a single SVI."
|
|
226
226
|
end
|
|
227
227
|
end
|
|
228
228
|
@items[row[:host]] << item
|
|
@@ -238,10 +238,10 @@ module Vxlan
|
|
|
238
238
|
{ host: host, entries: entries, semantics: policies.dig(host, name)&.rules || AclPolicy.rules(entries), policy: policies.dig(host, name)&.to_h } if entries
|
|
239
239
|
end
|
|
240
240
|
if rows.map { |row| row[:semantics] }.uniq.size > 1 || rows.map { |row| row[:policy] }.uniq.size > 1
|
|
241
|
-
finding(fabric, nil, "fabric_acl_content", "
|
|
241
|
+
finding(fabric, nil, "fabric_acl_content", "Same-name ACL #{name} has different ordered rules or dependencies (including unattached definitions)", rows)
|
|
242
242
|
end
|
|
243
243
|
unknown = rows.select { |row| row[:policy].nil? }
|
|
244
|
-
finding(fabric, nil, "fabric_acl_dependency", "ACL #{name}
|
|
244
|
+
finding(fabric, nil, "fabric_acl_dependency", "ACL #{name} has unverified dependencies; policy consistency cannot be established (including unattached definitions)", unknown) if unknown.any?
|
|
245
245
|
end
|
|
246
246
|
end
|
|
247
247
|
|
|
@@ -255,7 +255,7 @@ module Vxlan
|
|
|
255
255
|
end
|
|
256
256
|
references.sort.each do |name, rows|
|
|
257
257
|
next unless rows.map { |row| row[:value] }.uniq.size > 1
|
|
258
|
-
finding(fabric, nil, "fabric_address_group", "ACL
|
|
258
|
+
finding(fabric, nil, "fabric_address_group", "ACL address group #{name} has different or missing members; compare only services referencing this object and do not copy automatically", rows)
|
|
259
259
|
end
|
|
260
260
|
end
|
|
261
261
|
|
|
@@ -283,7 +283,7 @@ module Vxlan
|
|
|
283
283
|
end
|
|
284
284
|
conflicts << rows if rows.map { |row| row[:vni] }.uniq.size > 1
|
|
285
285
|
conflicts.each do |evidence|
|
|
286
|
-
finding(fabric, nil, "fabric_subnet_vni_conflict", "VRF #{vrf}
|
|
286
|
+
finding(fabric, nil, "fabric_subnet_vni_conflict", "VRF #{vrf} has overlapping subnets across different VNIs; verify the migration or isolation design", evidence)
|
|
287
287
|
end
|
|
288
288
|
active << [ key, rows ]
|
|
289
289
|
end
|
|
@@ -308,16 +308,16 @@ module Vxlan
|
|
|
308
308
|
|
|
309
309
|
# 与单设备报告共享建议字段,跨设备来源完整保留。
|
|
310
310
|
def recommendation(fabric, vni, code, title, evidence)
|
|
311
|
-
{ code: code, category: "fabric", scope: "#{fabric} / #{@role} / #{vni ? "VNI #{vni}" : '
|
|
312
|
-
prerequisites: [ "
|
|
311
|
+
{ code: code, category: "fabric", scope: "#{fabric} / #{@role} / #{vni ? "VNI #{vni}" : 'role review'}", title: title, status: "blocked", evidence: evidence,
|
|
312
|
+
prerequisites: [ "Applicable only within this Fabric and the same deployed service scope; verify the service access policy and maintenance window.", "Read configuration again before applying; any change makes the plan stale. Confirm each item on each device; do not replay in bulk." ], blockers: [],
|
|
313
313
|
commands: [], rollback: [], verify: [ "show running-config", "show nve vni", "show ip interface brief vrf all" ],
|
|
314
|
-
impact: "
|
|
314
|
+
impact: "Changing a gateway or ACL may affect reachability and service isolation; service acceptance is still required after configuration readback passes.", expected: "Gateways and access control are consistent for the shared service; permitted and denied traffic match the design.", references: [] }
|
|
315
315
|
end
|
|
316
316
|
|
|
317
317
|
# 类型化增量才可导出可交互执行的计划,冲突项只有证据和待确认信息。
|
|
318
318
|
def propose(item, plan, service_blocked:)
|
|
319
319
|
if service_blocked
|
|
320
|
-
item[:blockers] << "
|
|
320
|
+
item[:blockers] << "Service-bearing associations or replication/routing declarations conflict for a shared service; resolve these prerequisites before generating gateway or ACL changes."
|
|
321
321
|
return
|
|
322
322
|
end
|
|
323
323
|
item.merge!(status: "candidate", change: plan.to_h, commands: plan.commands, rollback: plan.rollback)
|
data/lib/vxlan/fabric/scope.rb
CHANGED
|
@@ -13,22 +13,22 @@ module Vxlan
|
|
|
13
13
|
# 在入口验证唯一归属;允许单角色为空,但 Fabric 至少有两个成员。
|
|
14
14
|
def initialize(policy)
|
|
15
15
|
unless policy.is_a?(Hash) && policy.keys == [ "fabrics" ] && policy["fabrics"].is_a?(Hash) && policy["fabrics"].any?
|
|
16
|
-
raise ArgumentError, "
|
|
16
|
+
raise ArgumentError, "A fabrics policy is required"
|
|
17
17
|
end
|
|
18
18
|
@groups = policy["fabrics"].to_h do |name, group|
|
|
19
19
|
unless name.is_a?(String) && !name.empty? && group.is_a?(Hash) && (group.keys - ROLES).empty? && group.any?
|
|
20
|
-
raise ArgumentError, "Fabric
|
|
20
|
+
raise ArgumentError, "Fabric requires explicit role member lists"
|
|
21
21
|
end
|
|
22
22
|
roles = ROLES.to_h do |role|
|
|
23
23
|
hosts = group.fetch(role, [])
|
|
24
|
-
raise ArgumentError, "
|
|
25
|
-
hosts.each { |host| raise ArgumentError, "
|
|
24
|
+
raise ArgumentError, "Role members must be address lists" unless hosts.is_a?(Array)
|
|
25
|
+
hosts.each { |host| raise ArgumentError, "Invalid member address" unless host.is_a?(String) && !host.include?("/") && IPAddr.new(host).to_s == host }
|
|
26
26
|
[ role, CtxParser::Immutable.copy(hosts) ]
|
|
27
27
|
end
|
|
28
|
-
raise ArgumentError, "Fabric
|
|
28
|
+
raise ArgumentError, "Fabric requires at least two members" unless roles.values.flatten.size >= 2
|
|
29
29
|
[ name, roles.freeze ]
|
|
30
30
|
end.freeze
|
|
31
|
-
raise ArgumentError, "
|
|
31
|
+
raise ArgumentError, "Members cannot be duplicated or shared across roles or Fabrics" unless hosts.uniq == hosts
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# 扁平成员集合用于覆盖核对,不改变角色内的比较范围。
|
|
@@ -8,13 +8,13 @@ module Vxlan
|
|
|
8
8
|
class Recommendations
|
|
9
9
|
REFERENCE = "https://www.cisco.com/c/en/us/td/docs/dcn/nx-os/nexus9000/103x/configuration/vxlan/cisco-nexus-9000-series-nx-os-vxlan-configuration-guide-release-103x/m_configuring_vxlan_bgp_evpn.html"
|
|
10
10
|
TITLES = {
|
|
11
|
-
"vlan_vni_without_nve" => "VLAN
|
|
12
|
-
"nve_l2_without_vlan" => "L2 NVE
|
|
13
|
-
"nve_l2_without_evpn" => "L2 NVE
|
|
14
|
-
"nve_l3_without_vrf" => "L3 NVE
|
|
15
|
-
"evpn_vni_without_nve" => "EVPN VNI
|
|
16
|
-
"vni_layer_conflict" => "VNI
|
|
17
|
-
"l3_vni_without_nve" => "VRF VNI
|
|
11
|
+
"vlan_vni_without_nve" => "VLAN maps to a VNI but the NVE member is missing",
|
|
12
|
+
"nve_l2_without_vlan" => "L2 NVE member lacks a VLAN mapping",
|
|
13
|
+
"nve_l2_without_evpn" => "L2 NVE member lacks EVPN configuration",
|
|
14
|
+
"nve_l3_without_vrf" => "L3 NVE member lacks a VRF association",
|
|
15
|
+
"evpn_vni_without_nve" => "EVPN VNI lacks an NVE member",
|
|
16
|
+
"vni_layer_conflict" => "VNI has conflicting L2/L3 attributes",
|
|
17
|
+
"l3_vni_without_nve" => "VRF VNI lacks an NVE member"
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
20
|
# 联合证据可以解决本机未知,但角色冲突优先阻断所有草稿;不改变原始发现。
|
|
@@ -58,11 +58,11 @@ module Vxlan
|
|
|
58
58
|
config.fetch("interfaces", {}).flat_map do |name, interface|
|
|
59
59
|
interface.fetch("acl_bindings", {}).filter_map do |direction, acl|
|
|
60
60
|
next if config.fetch("acls", {}).key?(acl)
|
|
61
|
-
result = item("acl_reference_undefined", "#{name} / #{direction}", "
|
|
61
|
+
result = item("acl_reference_undefined", "#{name} / #{direction}", "Interface references ACL #{acl}, but its local definition is missing",
|
|
62
62
|
evidence: { interface: name, direction: direction, acl: acl, addresses: interface.fetch("addresses", []), vlan_vni: config.fetch("vlan_vnis", {})[name.delete_prefix("Vlan")] },
|
|
63
63
|
verify: [ "show running-config", "show ip interface brief vrf all" ])
|
|
64
|
-
result[:blockers] << "
|
|
65
|
-
result[:impact] = "
|
|
64
|
+
result[:blockers] << "First confirm whether the interface carries a service and identify the authoritative ACL definition; missing addresses or VNIs may be residual configuration. Do not infer a service outage or copy another Fabric's ACL by name."
|
|
65
|
+
result[:impact] = "Adding an ACL definition affects all local references to that name; verify every reference and the rollback scope."
|
|
66
66
|
result[:references] = []
|
|
67
67
|
result
|
|
68
68
|
end
|
|
@@ -73,20 +73,20 @@ module Vxlan
|
|
|
73
73
|
def item(code, scope, title, evidence:, verify:, category: "configuration")
|
|
74
74
|
{ code: code, scope: scope, title: title, category: category, status: "blocked", evidence: evidence,
|
|
75
75
|
prerequisites: [], blockers: [], commands: [], rollback: [], verify: verify,
|
|
76
|
-
expected: "
|
|
76
|
+
expected: "Collect again and verify this fact; service connectivity requires separate acceptance.", references: [ REFERENCE ] }
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
# 仅在完整配置证实关联缺失时讨论增量补齐,不自动删除疑似闲置配置。
|
|
80
80
|
def configuration_item(config, finding)
|
|
81
81
|
vni = finding["vni"]
|
|
82
|
-
result = item(finding["code"], "VNI #{vni}", TITLES.fetch(finding["code"], "
|
|
82
|
+
result = item(finding["code"], "VNI #{vni}", TITLES.fetch(finding["code"], "Configuration associations require verification"),
|
|
83
83
|
evidence: finding, verify: [ "show running-config", "show nve vni", "show nve peers", "show bgp l2vpn evpn summary" ])
|
|
84
84
|
result[:vni] = vni
|
|
85
|
-
result[:prerequisites] = [ "
|
|
86
|
-
result[:impact] = "
|
|
87
|
-
result[:expected] = "VLAN
|
|
85
|
+
result[:prerequisites] = [ "Confirm that this VNI should carry a service locally, rather than being staged configuration or a decommissioning remnant.", "Check the current software version, device role, and same-Fabric peers' VLAN/VNI, RD/RT and replication mode.", "Collect configuration again and verify it matches this report; after maintenance window approval, operate one device at a time. On anomalies, read back first and avoid blind replay." ]
|
|
86
|
+
result[:impact] = "Completing the configuration may start route advertisement or extend the L2 broadcast domain; verify isolation boundaries, peer configuration and rollback conditions."
|
|
87
|
+
result[:expected] = "VLAN, NVE and EVPN associations are consistent; VNIs expected to carry services are Up, BGP/peers are healthy, and service acceptance passes."
|
|
88
88
|
unless vni.is_a?(Integer) && vni.between?(1, 16_777_215)
|
|
89
|
-
result[:blockers] << "VNI
|
|
89
|
+
result[:blockers] << "VNI is not a valid integer; commands cannot be generated."
|
|
90
90
|
return result
|
|
91
91
|
end
|
|
92
92
|
vlans = config.fetch("vlan_vnis", {}).select { |_vlan, mapped| mapped == vni }.keys
|
|
@@ -97,9 +97,9 @@ module Vxlan
|
|
|
97
97
|
case finding["code"]
|
|
98
98
|
when "nve_l2_without_evpn"
|
|
99
99
|
if l2_context?(config, vni, vlans) && member && member["associate_vrf"] == false && member["replication"] == "bgp" && !evpn && auto_policy?(config)
|
|
100
|
-
candidate(result, evpn_add(vni), evpn_remove(vni), "
|
|
100
|
+
candidate(result, evpn_add(vni), evpn_remove(vni), "All existing local L2 EVPN instances use RD auto and RT import/export auto; the candidate follows this pattern, but the service intent of this VNI still requires confirmation.")
|
|
101
101
|
else
|
|
102
|
-
result[:blockers] << "
|
|
102
|
+
result[:blockers] << "A unique VLAN mapping, L2 BGP replication and consistent RD/RT policy are required; missing or conflicting evidence does not permit applying auto."
|
|
103
103
|
end
|
|
104
104
|
when "vlan_vni_without_nve", "evpn_vni_without_nve"
|
|
105
105
|
if l2_context?(config, vni, vlans) && !member && bgp_replication?(nve) && (evpn ? auto_evpn?(evpn) : auto_policy?(config))
|
|
@@ -107,15 +107,15 @@ module Vxlan
|
|
|
107
107
|
rollback = [ "interface #{nve['interface']}", " no member vni #{vni}", "exit" ]
|
|
108
108
|
commands.concat(evpn_add(vni)) unless evpn
|
|
109
109
|
rollback.concat(evpn_remove(vni)) unless evpn
|
|
110
|
-
candidate(result, commands, rollback, "
|
|
110
|
+
candidate(result, commands, rollback, "Existing local L2 members uniformly use BGP replication and EVPN auto policy; complete only missing NVE/EVPN configuration, without enabling additional ARP suppression.")
|
|
111
111
|
else
|
|
112
|
-
result[:blockers] << "
|
|
112
|
+
result[:blockers] << "Confirm the VNI's L2/L3 role, VLAN, NVE interface, replication mode and RD/RT; a unique configuration change cannot currently be determined."
|
|
113
113
|
end
|
|
114
114
|
when "nve_l2_without_vlan"
|
|
115
|
-
result[:blockers] << "
|
|
115
|
+
result[:blockers] << "Provide the VLAN ID for this VNI and verify access/trunk/SVI configuration; do not infer it from VNI suffixes or other device names."
|
|
116
116
|
result[:template] = [ "vlan <VLAN_ID>", " vn-segment #{vni}" ]
|
|
117
117
|
else
|
|
118
|
-
result[:blockers] << "
|
|
118
|
+
result[:blockers] << "An explicit VRF/VNI service mapping and L2/L3 role are required; verify the design before completing or removing residual configuration."
|
|
119
119
|
end
|
|
120
120
|
result
|
|
121
121
|
end
|
|
@@ -175,8 +175,8 @@ module Vxlan
|
|
|
175
175
|
records = observation.fetch("records", [])
|
|
176
176
|
evidence.merge!(configuration_records: records.size, review_status: records.map { |record| record["review_status"] })
|
|
177
177
|
end
|
|
178
|
-
item("evidence_incomplete", name, "
|
|
179
|
-
verify: Array(CiscoNxos::Profile::COMMANDS[name.to_sym]), category: "evidence").merge(status: "investigate", expected: "
|
|
178
|
+
item("evidence_incomplete", name, "Evidence incomplete; collect missing evidence first", evidence: evidence,
|
|
179
|
+
verify: Array(CiscoNxos::Profile::COMMANDS[name.to_sym]), category: "evidence").merge(status: "investigate", expected: "A complete response or explicit zero-record evidence is required; an empty response does not establish absence of configuration.")
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
|
|
@@ -185,8 +185,8 @@ module Vxlan
|
|
|
185
185
|
%w[nve_vnis nve_peers bgp_neighbors].filter_map do |dimension|
|
|
186
186
|
rows = @dimensions.fetch(dimension, {}).fetch("records", []).select { |row| %w[down idle active connect opensent openconfirm].include?(row["state"].to_s.downcase) }
|
|
187
187
|
next if rows.empty?
|
|
188
|
-
item("#{dimension}_not_up", dimension, "
|
|
189
|
-
verify: [ CiscoNxos::Profile::COMMANDS[dimension.to_sym], "show running-config" ], category: "runtime").merge(status: "investigate", prerequisites: [ "
|
|
188
|
+
item("#{dimension}_not_up", dimension, "Observed runtime state is not established", evidence: rows,
|
|
189
|
+
verify: [ CiscoNxos::Profile::COMMANDS[dimension.to_sym], "show running-config" ], category: "runtime").merge(status: "investigate", prerequisites: [ "Confirm service expectations first, then check peers, underlay routing, interfaces and configuration; do not automatically restart neighbors or enable interfaces." ])
|
|
190
190
|
end
|
|
191
191
|
end
|
|
192
192
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gatework contributors
|
|
@@ -13,64 +13,58 @@ dependencies:
|
|
|
13
13
|
name: ctx_cli
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.1
|
|
18
|
+
version: 0.2.1
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.1
|
|
25
|
+
version: 0.2.1
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: ctx_runner
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.2.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.2.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: ctx_parser
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.2.
|
|
46
|
+
version: 0.2.7
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
|
-
- - "
|
|
51
|
+
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0.2.
|
|
53
|
+
version: 0.2.7
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: ctx_driver
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.2'
|
|
61
58
|
- - ">="
|
|
62
59
|
- !ruby/object:Gem::Version
|
|
63
|
-
version: 0.2.
|
|
60
|
+
version: 0.2.7
|
|
64
61
|
type: :runtime
|
|
65
62
|
prerelease: false
|
|
66
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
67
64
|
requirements:
|
|
68
|
-
- - "~>"
|
|
69
|
-
- !ruby/object:Gem::Version
|
|
70
|
-
version: '0.2'
|
|
71
65
|
- - ">="
|
|
72
66
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: 0.2.
|
|
67
|
+
version: 0.2.7
|
|
74
68
|
- !ruby/object:Gem::Dependency
|
|
75
69
|
name: digest
|
|
76
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -207,9 +201,11 @@ files:
|
|
|
207
201
|
- docs/superpowers/plans/2026-10-19-evidence-contracts.md
|
|
208
202
|
- docs/superpowers/plans/2026-10-19-vxlan-examples.md
|
|
209
203
|
- docs/superpowers/plans/2026-10-20-fabric-consistency.md
|
|
204
|
+
- docs/superpowers/plans/2026-10-22-ctx-cli.md
|
|
210
205
|
- docs/superpowers/specs/2026-10-19-command-line-design.md
|
|
211
206
|
- docs/superpowers/specs/2026-10-19-connector-integration-design.md
|
|
212
207
|
- docs/superpowers/specs/2026-10-19-evidence-contracts.md
|
|
208
|
+
- docs/superpowers/specs/2026-10-22-ctx-cli-design.md
|
|
213
209
|
- examples/README.md
|
|
214
210
|
- examples/collect.rb
|
|
215
211
|
- examples/offline.rb
|