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.
@@ -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, "重复设备身份" unless @documents.size == documents.size
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("未归属", nil, "fabric_scope_unassigned", "已采集设备未纳入 Fabric 策略,跨设备审查未覆盖", inventory[:unassigned]) if inventory[:unassigned].any?
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", "成员配置证据不完整,停止该 Fabric 的变更推导", unknown)
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", "业务已开始部署,但 VLAN/NVE/EVPN 关联不完整", observations(incomplete) { |row| { missing: row[:missing] } })
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", "同一 Leaf 将多个 VLAN 映射到相同 VNI", rows.map { |row| row.slice(:host, :vlan) })
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", "相同 VNI 的本地 VLAN 编号不一致", rows) { |row| row[:vlan] }
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}", "业务 SVI 的 #{field} 不一致", rows) { |row| row[:svi]&.fetch(field, nil) }
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 网关 MAC 不一致", rows) { |row| row[:config]["anycast_mac"] }
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", "补齐已有业务的 VLAN/VNI 关联", evidence)
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] << "同 VRF 存在跨 SVI/跨 VNI 网段重叠,不能通过新增映射扩大冲突:#{conflicts.map { |entry| "#{entry[:name]} #{entry[:interface]}" }.join('、')}"
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 映射" ], gateway: local_svi["addresses"], vrf: local_svi["vrf"] }) : entry
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] = "将已有 VLAN 接入既有 VXLAN 业务,可能立即扩展广播域;不创建 VLAN,不更改网关或 ACL。"
112
- item[:prerequisites] << "本机未映射 VLAN 的网关、VRF、Anycast 身份及 ACL 与完整对端一致,NVE/EVPN 也已匹配;确认该业务确应在本机接入。"
113
- item[:expected] = "本机 VLAN/VNI 关联完整、VNI 状态符合预期,既有网关及 ACL 保持不变;接入业务验收通过。"
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] << "没有唯一且完整的业务对应关系;需核对本机未映射 VLAN、网关、VRF、ACL 与对端配置,禁止按 VNI 数值猜测 VLAN。"
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 复制方式或组播组声明不一致", rows) { |row| Service::Catalog.nve_member(row[:config], row[:nve])&.slice("replication", "multicast_group") }
127
- compare(fabric, vni, "fabric_arp_suppression", "ARP 抑制的显式配置不一致", rows) { |row| row[:nve]&.fetch("suppress_arp", nil) }
128
- compare(fabric, vni, "fabric_route_targets", "EVPN 导入/导出 RT 声明不一致,需核对路由互通与隔离", rows) do |row|
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", "共同业务的租户 VNI 或路由导入/导出声明不一致", tenant_rows) do |row|
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", "声明为 Spine,但存在 L2 业务承载配置", evidence)
144
- item = recommendation(fabric, nil, "fabric_role_conflict", "核对 Spine 是否应承担业务 VTEP 职责", evidence)
145
- item[:blockers] << "先确认角色或迁移残留;不能因为出现 NVE 就自动把 Spine 当作 Leaf,不能自动补业务配置或删除 NVE。"
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", "同业务网关地址不一致", observations(rows) { |row| row[:svi]&.fetch("addresses", []) })
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", "网关地址需要一致", observations(rows) { |entry| entry[:svi]&.fetch("addresses", []) })
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] << "存在已有地址、跨 SVI/跨 VNI 网段冲突,或 VRF/Anycast 模式/MAC 不一致、缺少唯一网关;需先确认迁移设计,禁止重复添加网关。"
167
- item[:blockers] << "Fabric 内冲突接口:#{conflicts.map { |conflict| "#{conflict[:name]} #{conflict[:interface]}" }.join('、')}" if conflicts.any?
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 && 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
- finding(fabric, vni, code, "#{direction} 方向 ACL 挂载/有序规则不一致", evidence)
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} 方向已挂载 ACL,但本机没有对应定义", undefined) if undefined.any?
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} 方向 ACL 依赖对象缺失、为空或尚未解析,不能证明策略一致", unresolved) if unresolved.any?
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, "统一 #{direction} 方向访问控制", evidence)
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] << "依据是本业务已挂载且有序内容一致的 ACL;即使挂载节点较少,也不以无 ACL 的多数节点为删除依据。确认此业务应统一受该 ACL 保护。"
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 名称、有序规则、地址组成员、VRF 或当前绑定不能证明一致;需选择权威规则及完整依赖,不自动覆盖或撤销访问控制。"
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] << "创建 #{names.first} 还会激活既有引用:#{references}。需合并所有受影响接口核对策略和回退,禁止作为单个 SVI 的补齐执行。"
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", "同名 ACL #{name} 的有序规则或依赖对象不同(包括尚未挂载的定义)", rows)
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} 的依赖对象未验证,不能证明策略一致(包括尚未挂载的定义)", unknown) if unknown.any?
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 地址组 #{name} 成员不同或缺失;仅比较引用此对象的业务,不自动复制", rows)
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} 的网段在不同 VNI 上重叠,需核对迁移或隔离设计", evidence)
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}" : '角色核查'}", title: title, status: "blocked", evidence: evidence,
312
- prerequisites: [ "仅在本 Fabric、相同已部署业务范围内适用;核对业务访问策略与维护窗口。", "下发前重新读取配置,任何变化均使计划过期;逐设备逐项确认,不批量重放。" ], blockers: [],
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: "改变网关或 ACL 可能改变可达性及业务隔离;配置回读通过后仍需业务验收。", expected: "共同业务的网关和访问控制一致,允许/拒绝流量均符合设计。", references: [] }
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] << "共同业务的承载关联或复制/路由声明存在冲突,先解决这些前置问题,再生成网关或 ACL 增量。"
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)
@@ -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, "需要 fabrics 策略"
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, "角色成员须为地址列表" unless hosts.is_a?(Array)
25
- hosts.each { |host| raise ArgumentError, "成员地址无效" unless host.is_a?(String) && !host.include?("/") && IPAddr.new(host).to_s == host }
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 至少需要两个成员" unless roles.values.flatten.size >= 2
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, "成员不能重复、跨角色或跨 Fabric" unless hosts.uniq == hosts
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 已映射 VNI,但缺少 NVE 成员",
12
- "nve_l2_without_vlan" => "L2 NVE 成员缺少 VLAN 映射",
13
- "nve_l2_without_evpn" => "L2 NVE 成员缺少 EVPN 配置",
14
- "nve_l3_without_vrf" => "L3 NVE 成员缺少 VRF 关联",
15
- "evpn_vni_without_nve" => "EVPN VNI 缺少 NVE 成员",
16
- "vni_layer_conflict" => "VNI 的 L2/L3 属性冲突",
17
- "l3_vni_without_nve" => "VRF VNI 缺少 NVE 成员"
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}", "接口引用 ACL #{acl},本机缺少定义",
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] << "先确认接口是否承载业务及 ACL 的权威定义;无地址或无 VNI 可能是残留配置,不能直接推断业务中断,也不能按名称复制其他 Fabric 的 ACL。"
65
- result[:impact] = "新增 ACL 定义会作用于本机所有同名引用;须核对全部引用及回退范围。"
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: "重新采集并核对该项事实;业务连通性须另行验收。", references: [ REFERENCE ] }
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] = [ "确认该 VNI 应在本机承载业务,而非预配置或退网残留。", "核对当前软件版本、设备角色、同 Fabric 对端的 VLAN/VNI、RD/RT 与复制方式。", "重新采集配置并确认与本报告一致;审批维护窗口后逐台操作,出现异常先回读,勿盲目重放。" ]
86
- result[:impact] = "补齐可能开始发布路由或扩展二层广播域;须核对隔离边界、对端配置和回退条件。"
87
- result[:expected] = "VLAN、NVE 与 EVPN 关联一致;预期承载业务的 VNI 状态 Up,BGP/对端正常,业务验收通过。"
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), "本机现有 L2 EVPN 均采用 RD auto、RT import/export auto;候选沿用此模式,仍需确认本 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] << "需要唯一 VLAN 映射、L2 BGP 复制以及一致的 RD/RT 策略;缺失或冲突时不能套用 auto。"
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, "本机既有 L2 成员统一使用 BGP 复制、EVPN auto 策略;仅补齐缺失的 NVE/EVPN,不启用额外 ARP 抑制。")
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] << "需确认 VNI 的 L2/L3 角色、VLAN、NVE 接口、复制模式和 RD/RT,当前不能唯一确定增量配置。"
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] << "请提供本 VNI 对应 VLAN ID,并核对 access/trunk/SVI;不能由 VNI 尾数或其他设备名称猜测。"
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] << "需要明确 VRF/VNI 业务映射和 L2/L3 角色;先核对设计,再补齐或清理残留配置。"
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, "证据不完整,先补采", evidence: evidence,
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, "观测到未建立的运行状态", evidence: rows,
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Vxlan
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
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.1
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.0
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.0
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.1.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.1.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.0
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.0
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.4
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.4
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