vxlan 0.4.2 → 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.
@@ -50,19 +50,19 @@ module Vxlan
50
50
  def finish(results, interrupted: false, requested: [], report_error: nil, execution_error: nil, output_error: nil)
51
51
  @report.finish(results, interrupted: interrupted, requested: requested, report_error: report_error,
52
52
  execution_error: execution_error, output_error: output_error, review: @review_summary) do |summary|
53
- pending = summary.fetch(:pending)
54
- text = [ "VXLAN 证据:#{summary.fetch(:status)};完成 #{results.size} 台", "分类:#{summary.fetch(:counts)}", "证据完整性不代表 Fabric 健康。" ]
55
- text << "逐设备诊断:devices/;候选脚本:scripts/;所有候选配置未执行。"
53
+ pending = summary.fetch("pending")
54
+ text = [ "VXLAN evidence: #{summary.fetch('status')}; completed #{summary.fetch('completed')} devices", "Categories: #{summary.fetch('counts')}", "Complete evidence does not establish Fabric health." ]
55
+ text << "Per-device diagnostics: devices/; candidate scripts: scripts/; no candidate configuration was executed."
56
56
  text << @review_text if @review_text
57
- text << "批次报告状态:参见 summary.json;文本中的证据状态不确认报告发布完成。"
58
- text << "报告错误:#{report_error}" if report_error
59
- text << "执行错误:#{execution_error}" if execution_error
60
- text << "进度输出错误:#{output_error[:error_type]};设备结果已保留" if output_error
61
- results.each do |r|
62
- saved = r[:report_saved] == true ? "已保存" : "保存未确认"
63
- text << "#{r[:device]['host']}\t#{r[:device]['name']}\t#{r[:status]}\t#{saved}\t#{r[:error]}"
57
+ text << "Batch report status: see summary.json; evidence status in text does not confirm report publication."
58
+ text << "Report error: #{summary['report_error']}" if summary["report_error"]
59
+ text << "Execution error: #{summary['execution_error']}" if summary["execution_error"]
60
+ text << "Progress output error: #{summary['output_error']['error_type']}; device results retained" if summary["output_error"]
61
+ summary.fetch("devices").each do |r|
62
+ saved = r["report_saved"] == true ? "saved" : "persistence unconfirmed"
63
+ text << "#{r['device']['host']}\t#{r['device']['name']}\t#{r['status']}\t#{saved}\t#{r['error']}"
64
64
  end
65
- pending.each { |device| text << "#{device['host']}\t#{device['name']}\t未采集" }
65
+ pending.each { |device| text << "#{device['host']}\t#{device['name']}\tnot collected" }
66
66
  text.join("\n") + "\n"
67
67
  end
68
68
  end
@@ -13,35 +13,35 @@ module Vxlan
13
13
  # 相同事实合并展示;规则版本保持匹配顺序,完整证据由聚合计划保存。
14
14
  def fabric_text(audit, documents)
15
15
  by_host = documents.to_h { |document| [ document.fetch("device").fetch("host"), document ] }
16
- lines = [ "Fabric 共同业务一致性", "范围:按显式 Fabric 和角色比较共同 VNI;Leaf、Border 分开比较,Spine 检查业务承载配置。", "部署事实联合 VLAN/NVE/EVPN 识别;不完整部署不等于未部署。保留部署范围差异。", "ACL 比较包含有序规则与依赖对象;不以多数无 ACL 为删除依据。", "" ]
16
+ lines = [ "Fabric shared-service consistency", "Scope: compare shared VNIs within explicit Fabrics and roles; compare Leaf and Border separately, and check Spine for service-bearing configuration.", "Deployment facts combine VLAN/NVE/EVPN evidence; incomplete deployment does not mean absence. Deployment scope differences are retained.", "ACL comparison includes ordered rules and dependencies; a majority without an ACL is not grounds for removal.", "" ]
17
17
  inventory = audit[:inventory]
18
18
  if inventory
19
- lines << "覆盖:已纳入 #{inventory[:assigned].size} 台;未纳入 #{inventory[:unassigned].size} 台;缺证据 #{inventory[:missing].size} 台。"
19
+ lines << "Coverage: included #{inventory[:assigned].size} devices; excluded #{inventory[:unassigned].size} devices; missing evidence #{inventory[:missing].size} devices."
20
20
  inventory[:groups].each do |fabric, roles|
21
- lines << "#{fabric}:Leaf #{roles['leaves'].size} / Border #{roles['borders'].size} / Spine #{roles['spines'].size}"
21
+ lines << "#{fabric}: Leaf #{roles['leaves'].size} / Border #{roles['borders'].size} / Spine #{roles['spines'].size}"
22
22
  %w[leaves borders].each do |role|
23
23
  services = audit[:coverage].select { |entry| entry[:fabric] == fabric && entry[:role] == role }
24
24
  partial = services.select { |entry| entry[:incomplete].any? }
25
- lines << " #{role}:已部署业务 #{services.size},多设备共同业务 #{services.count { |entry| entry[:deployed].size >= 2 }},关联不完整 #{partial.size}。"
26
- partial.each { |entry| lines << " VNI #{entry[:vni]} 待补齐:#{entry[:incomplete].map { |host| by_host.dig(host, 'device', 'name') || host }.join('、')}" }
25
+ lines << " #{role}: deployed services #{services.size}, services shared across devices #{services.count { |entry| entry[:deployed].size >= 2 }}, incomplete associations #{partial.size}."
26
+ partial.each { |entry| lines << " VNI #{entry[:vni]} missing: #{entry[:incomplete].map { |host| by_host.dig(host, 'device', 'name') || host }.join(', ')}" }
27
27
  end
28
28
  end
29
29
  lines << ""
30
30
  end
31
31
  audit[:findings].each_with_index do |finding, index|
32
- lines << "#{index + 1}. #{finding[:fabric]} / #{finding[:role] || '全 Fabric'} / #{finding[:vni] ? "VNI #{finding[:vni]}" : '公共策略'}:#{finding[:title]}"
32
+ lines << "#{index + 1}. #{finding[:fabric]} / #{finding[:role] || 'entire Fabric'} / #{finding[:vni] ? "VNI #{finding[:vni]}" : 'shared policy'}: #{finding[:title]}"
33
33
  end
34
34
  audit[:findings].each_with_index do |finding, index|
35
- lines.concat([ "", "[#{index + 1}] #{finding[:fabric]} / #{finding[:role] || '全 Fabric'} / #{finding[:vni] || '公共策略'} — #{finding[:title]}", "规则:#{finding[:code]}" ])
35
+ lines.concat([ "", "[#{index + 1}] #{finding[:fabric]} / #{finding[:role] || 'entire Fabric'} / #{finding[:vni] || 'shared policy'} — #{finding[:title]}", "Rule: #{finding[:code]}" ])
36
36
  evidence = finding[:evidence]
37
37
  if finding[:code] == "fabric_address_group"
38
38
  defined = evidence.filter_map { |row| row[:value] }
39
39
  common = defined.reduce { |all, members| all & members } || []
40
- lines << " 已定义版本的共同成员(不代表批准基线):#{common.empty? ? '无' : common.join('、')}"
40
+ lines << " Members common to defined versions (not an approved baseline): #{common.empty? ? 'none' : common.join(', ')}"
41
41
  evidence.group_by { |row| row[:value] }.each do |members, rows|
42
42
  names = rows.map { |row| by_host.dig(row[:host], "device", "name") || row[:host] }
43
- value = members ? "#{members.size} 个成员;共同成员之外:#{(members - common).empty? ? '无' : (members - common).join('、')}" : "未提供定义或未采集对象证据"
44
- lines.concat([ " #{names.join('、')}", " #{value}" ])
43
+ value = members ? "#{members.size} members; outside the common set: #{(members - common).empty? ? 'none' : (members - common).join(', ')}" : "No definition provided or object evidence not collected"
44
+ lines.concat([ " #{names.join(', ')}", " #{value}" ])
45
45
  end
46
46
  next
47
47
  end
@@ -53,18 +53,18 @@ module Vxlan
53
53
  value = row.key?(:value) ? row[:value] : row.reject { |key, _| %i[host name vlan entries semantics].include?(key) }
54
54
  value = value.reject { |key, _| key == :entries } if value.is_a?(Hash)
55
55
  if row[:semantics]
56
- policy = row[:policy] ? "规则与依赖版本 #{CtxRunner::Fingerprint.call(row[:policy])[0, 12]}" : "依赖对象未验证"
57
- value = "#{row[:entries].size} 条规则,#{policy}"
56
+ policy = row[:policy] ? "Rule and dependency version #{CtxRunner::Fingerprint.call(row[:policy])[0, 12]}" : "Dependencies unverified"
57
+ value = "#{row[:entries].size} rules, #{policy}"
58
58
  end
59
- [ row[:vlan] || row[:interface] || "公共", display_fabric_value(value) ]
59
+ [ row[:vlan] || row[:interface] || "shared", display_fabric_value(value) ]
60
60
  end
61
61
  grouped.each do |(vlan, value), members|
62
62
  names = members.map { |row| row[:name] || by_host.dig(row[:host], "device", "name") || row[:host] }
63
- lines.concat([ " VLAN/接口 #{vlan}:#{value}", " #{names.join('、')}" ])
63
+ lines.concat([ " VLAN/interface #{vlan}: #{value}", " #{names.join(', ')}" ])
64
64
  end
65
65
  if evidence.any? { |row| row[:semantics] }
66
66
  evidence.group_by { |row| row[:semantics] }.each_with_index do |(rules, members), variant|
67
- lines << " 规则版本 #{variant + 1}:#{members.map { |row| by_host.dig(row[:host], 'device', 'name') || row[:host] }.join('、')}"
67
+ lines << " Rule version #{variant + 1}: #{members.map { |row| by_host.dig(row[:host], 'device', 'name') || row[:host] }.join(', ')}"
68
68
  lines.concat(rules.map { |rule| " #{rule['action']} #{rule['expression']}" })
69
69
  end
70
70
  end
@@ -75,14 +75,14 @@ module Vxlan
75
75
  # 机器字段转换为运维读法,显式保留缺失和未知。
76
76
  def display_fabric_value(value)
77
77
  if value.is_a?(Hash) && value.key?(:acl)
78
- return "未挂载 ACL" unless value[:acl]
79
- return "#{value[:acl]};本机缺少定义" unless value[:defined]
80
- return "#{value[:acl]};已定义,但依赖对象未验证" unless value[:policy_verified]
81
- "#{value[:acl]};已定义,版本 #{value[:digest].to_s[0, 12]}"
78
+ return "No ACL attached" unless value[:acl]
79
+ return "#{value[:acl]}; local definition missing" unless value[:defined]
80
+ return "#{value[:acl]}; defined, but dependencies unverified" unless value[:policy_verified]
81
+ "#{value[:acl]}; defined, version #{value[:digest].to_s[0, 12]}"
82
82
  elsif value.is_a?(Array)
83
- value.empty? ? "未配置地址" : value.join("、")
83
+ value.empty? ? "No address configured" : value.join(", ")
84
84
  elsif value.nil?
85
- "未提供"
85
+ "not provided"
86
86
  elsif value.is_a?(Hash)
87
87
  JSON.generate(value)
88
88
  else
@@ -28,7 +28,7 @@ module Vxlan
28
28
 
29
29
  # 配置和回退集中于每设备一份 cfg;有限类型动作汇总到批次计划,不接受 cfg 直接执行。
30
30
  def write_review(document, basename:, source:, source_sha256:, advice: nil)
31
- raise ArgumentError, "非法报告名称" unless basename.match?(/\A[A-Za-z0-9_.-]+\z/)
31
+ raise ArgumentError, "Invalid report name" unless basename.match?(/\A[A-Za-z0-9_.-]+\z/)
32
32
  advice = report_advice(advice || Recommendations.new(document).call).merge(source: source, source_sha256: source_sha256)
33
33
  advice[:items] = advice[:items].map(&:dup)
34
34
  entry = { device: advice[:device], counts: advice[:counts], basename: basename, report: "devices/#{basename}.txt", source: source,
@@ -95,14 +95,14 @@ module Vxlan
95
95
 
96
96
  # 每项包含作用域、条件、命令及注释化回退,不将多项当成原子变更。
97
97
  def script_text(items)
98
- lines = [ "! 候选配置,未执行;先审阅同名 devices/*.txt;禁止整批直接粘贴。", "! apply 只读取 plan.json 中明确选择的单项,不执行本 cfg。" ]
98
+ lines = [ "! Candidate configuration, not executed; review the matching devices/*.txt first. Do not paste the entire batch.", "! apply reads only the explicitly selected item from plan.json; it does not execute this cfg file." ]
99
99
  items.each do |item|
100
- lines.concat([ "", "! #{item[:scope]} / #{item[:title]}", "! 计划项:#{item[:plan_id] || '仅审阅草稿,不支持交互执行'}" ])
101
- lines.concat(item[:prerequisites].map { |condition| "! 条件:#{condition}" })
100
+ lines.concat([ "", "! #{item[:scope]} / #{item[:title]}", "! Plan item: #{item[:plan_id] || 'Review draft only; interactive execution unsupported'}" ])
101
+ lines.concat(item[:prerequisites].map { |condition| "! Prerequisite: #{condition}" })
102
102
  lines.concat(item[:commands])
103
- lines << "! 条件回退(不自动执行):"
103
+ lines << "! Conditional rollback (not executed automatically): "
104
104
  lines.concat(item[:rollback].map { |command| "! #{command}" })
105
- lines << "! 核查:#{item[:verify].join(' ; ')}"
105
+ lines << "! Verification: #{item[:verify].join(' ; ')}"
106
106
  end
107
107
  lines.join("\n") + "\n"
108
108
  end
@@ -110,27 +110,27 @@ module Vxlan
110
110
  # 文本按设备、证据、问题、动作顺序组织,完整跨设备证据位于聚合计划。
111
111
  def device_text(entry, document)
112
112
  advice, device = entry.values_at(:advice, :device)
113
- lines = [ "设备:#{device['name']}(#{device['host']})", "模式:诊断与计划,未执行设备配置", "证据时间:#{advice[:observed_at] || '未知'}", "来源:#{entry[:source]}", "源文件 SHA256:#{entry[:source_sha256]}",
114
- "建议:#{entry[:counts]}", "命令文件:#{entry[:script] || '未生成'}", "命令 SHA256:#{entry[:script_sha256] || '无'}", "计划文件:plan.json;交互计划项 #{entry[:plans].size} 个", "" ]
115
- lines << "采集状态:#{document['status']}" if document["status"]
113
+ lines = [ "Device: #{device['name']} (#{device['host']})", "Mode: diagnostics and planning; no device configuration executed", "Evidence time: #{advice[:observed_at] || 'unknown'}", "Source: #{entry[:source]}", "Source file SHA256: #{entry[:source_sha256]}",
114
+ "Recommendations: #{entry[:counts]}", "Command file: #{entry[:script] || 'not generated'}", "Command SHA256: #{entry[:script_sha256] || 'none'}", "Plan file: plan.json; interactive plan items: #{entry[:plans].size} items", "" ]
115
+ lines << "Collection status: #{document['status']}" if document["status"]
116
116
  configuration_time = document.dig("snapshot", "dimensions", "configuration", "evidence", 0, "observed_at")
117
- lines << "配置证据时间:#{configuration_time}" if configuration_time
118
- lines << "当前规则未命中;不代表网络健康。" if advice[:items].empty?
117
+ lines << "Configuration evidence time: #{configuration_time}" if configuration_time
118
+ lines << "No current rules matched; this does not establish network health." if advice[:items].empty?
119
119
  advice[:items].each_with_index do |item, index|
120
- lines.concat([ "", "#{index + 1}. #{item[:scope]} — #{item[:title]}", "状态:#{item[:status]};规则:#{item[:code]}", "影响:#{item[:impact]}" ])
120
+ lines.concat([ "", "#{index + 1}. #{item[:scope]} — #{item[:title]}", "Status: #{item[:status]}; rule: #{item[:code]}", "Impact: #{item[:impact]}" ])
121
121
  evidence = item[:category] == "fabric" ? Array(item[:evidence]).select { |row| row[:host] == device["host"] } : item[:evidence]
122
- lines.concat([ "本机依据:", JSON.pretty_generate(evidence) ])
123
- lines << "跨设备对比:summary.txt;完整原始对比:plan.json 的 audits / fabric。" if item[:category] == "fabric"
122
+ lines.concat([ "Local evidence: ", JSON.pretty_generate(evidence) ])
123
+ lines << "Cross-device comparison: summary.txt; full raw comparison: audits / fabric in plan.json." if item[:category] == "fabric"
124
124
  if item[:plan_id]
125
- lines << "交互计划项:#{item[:plan_id]}"
126
- lines << "在批次目录运行:vxlan apply --plan plan.json --item #{item[:plan_id]}"
125
+ lines << "Interactive plan item: #{item[:plan_id]}"
126
+ lines << "Run in the batch directory: vxlan apply --plan plan.json --item #{item[:plan_id]}"
127
127
  end
128
- { prerequisites: "适用条件", blockers: "待决信息", commands: "候选配置(未执行)", rollback: "条件回退", verify: "只读核查", template: "待补模板(不可执行)" }.each do |key, title|
128
+ { prerequisites: "Prerequisites", blockers: "Unresolved information", commands: "Candidate configuration (not executed)", rollback: "Conditional rollback", verify: "Read-only verification", template: "Incomplete template (not executable)" }.each do |key, title|
129
129
  values = item[key] || []
130
- lines.concat([ "#{title}:", *values.map { |value| " #{value}" } ]) if values.any?
130
+ lines.concat([ "#{title}: ", *values.map { |value| " #{value}" } ]) if values.any?
131
131
  end
132
- lines << "验收:#{item[:expected]}" if item[:expected]
133
- lines << "命令参考:#{item[:references].join(' ')}" if item[:references]&.any?
132
+ lines << "Acceptance: #{item[:expected]}" if item[:expected]
133
+ lines << "Command reference: #{item[:references].join(' ')}" if item[:references]&.any?
134
134
  end
135
135
  lines.join("\n") + "\n"
136
136
  end
@@ -139,15 +139,15 @@ module Vxlan
139
139
  def summary_text(summary)
140
140
  entries, counts = summary.values_at(:reports, :counts)
141
141
  active = entries.count { |entry| entry[:counts].values.sum.positive? }
142
- lines = [ "VXLAN 诊断与配置建议;模式:review;未执行设备变更。", "报告完成状态以 summary.json 为准。", "设备:#{entries.size} 台;有建议 #{active} 台;未命中规则 #{entries.size - active} 台(不代表健康)。",
143
- "建议:待决 #{counts['blocked']},候选 #{counts['candidate']},排查 #{counts['investigate']};Fabric 聚合差异 #{summary[:fabric_findings]} 项。",
144
- "计划:#{entries.sum { |entry| entry[:plans].size }} 项可交互执行增量;#{entries.count { |entry| entry[:script] }} 份候选脚本。候选数与可执行计划数不同。",
145
- "阅读顺序:summary.txt → devices/<设备>.txt → scripts/<设备>.cfg → plan.json 中的指定计划项。",
146
- "evidence/ 保存程序使用的采集证据,日常阅读无需打开;devices/ 只包含可读诊断。",
147
- "plan.json.sha256 校验聚合计划;events.jsonl 记录本次离线审查,不代表设备会话或下发回执。", "下发必须 --item 选择单项、交互确认、重读配置并核对指纹;无自动批量执行。", "" ]
142
+ lines = [ "VXLAN diagnostics and configuration recommendations; mode: review; no device changes executed.", "Report completion status is authoritative in summary.json.", "Devices: #{entries.size} devices; with recommendations #{active} devices; no rules matched #{entries.size - active} devices (does not establish health).",
143
+ "Recommendations: unresolved #{counts['blocked']}, candidates #{counts['candidate']}, investigations #{counts['investigate']}; aggregated Fabric differences #{summary[:fabric_findings]} items.",
144
+ "Plan: #{entries.sum { |entry| entry[:plans].size }} changes available for interactive execution; #{entries.count { |entry| entry[:script] }} candidate scripts. Candidate counts differ from executable plan counts.",
145
+ "Read in order: summary.txt -> devices/<device>.txt -> scripts/<device>.cfg -> the selected plan item in plan.json.",
146
+ "evidence/ stores collection evidence used by the program; routine review does not require opening it. devices/ contains readable diagnostics only.",
147
+ "plan.json.sha256 verifies the aggregate plan; events.jsonl records this offline review, not device sessions or execution receipts.", "Applying requires selecting one --item, interactive confirmation, configuration readback and fingerprint verification; there is no automatic bulk execution.", "" ]
148
148
  entries.sort_by { |entry| [ entry[:counts].values.sum.positive? ? 0 : 1, entry[:device]["name"].to_s ] }.each do |entry|
149
- lines.concat([ "#{entry[:device]['host']} #{entry[:device]['name']} 待决=#{entry[:counts].fetch('blocked', 0)} 候选=#{entry[:counts].fetch('candidate', 0)} 排查=#{entry[:counts].fetch('investigate', 0)}",
150
- " 设备报告:#{entry[:report]}", " 命令文件:#{entry[:script] || '未生成'};可执行计划:#{entry[:plans].size} 项" ])
149
+ lines.concat([ "#{entry[:device]['host']} #{entry[:device]['name']} unresolved=#{entry[:counts].fetch('blocked', 0)} candidates=#{entry[:counts].fetch('candidate', 0)} investigations=#{entry[:counts].fetch('investigate', 0)}",
150
+ " Device report: #{entry[:report]}", " Command file: #{entry[:script] || 'not generated'}; executable plans: #{entry[:plans].size} items" ])
151
151
  end
152
152
  lines.concat([ "", @fabric_text ]) if @fabric_text
153
153
  lines.join("\n") + "\n"
@@ -18,12 +18,12 @@ module Vxlan
18
18
 
19
19
  # 返回零只代表建议生成完成,不代表问题已修复或候选配置已获批准。
20
20
  def run(argv)
21
- command = CtxCli::Command.new(banner: "用法:vxlan review --batch DIR [--output DIR](离线生成建议,不执行配置)", out: @out) do |p, options|
22
- p.on("--fabric-policy FILE", "显式 Fabric/Leaf/Border/Spine 清单 YAML") { |value| options[:fabric_policy] = value }
23
- p.on("--batch DIR", "已采集批次,含 evidence/*.json") { |value| options[:batch] = value }
24
- p.on("--[no-]scripts", "可选导出配置建议,默认仅诊断") { |value| options[:scripts] = value }
25
- p.on("--output DIR", "私有输出根目录,默认批次内 reviews") { |value| options[:output] = value }
26
- p.on("-h", "--help", "查看帮助") { options[:help] = true }
21
+ command = CtxCli::Command.new(banner: "Usage: vxlan review --batch DIR [--output DIR] (generate recommendations offline without executing configuration)", out: @out) do |p, options|
22
+ p.on("--fabric-policy FILE", "Explicit Fabric/Leaf/Border/Spine inventory YAML") { |value| options[:fabric_policy] = value }
23
+ p.on("--batch DIR", "Collected batch containing evidence/*.json") { |value| options[:batch] = value }
24
+ p.on("--[no-]scripts", "Optionally export configuration suggestions; diagnostics only by default") { |value| options[:scripts] = value }
25
+ p.on("--output DIR", "Private output root; defaults to reviews inside the batch") { |value| options[:output] = value }
26
+ p.on("-h", "--help", "Show help") { options[:help] = true }
27
27
  end
28
28
  CtxCli::Review.new(command: command, directory: "evidence", adapter: self, out: @out, error: @error).run(argv)
29
29
  end
@@ -36,7 +36,7 @@ module Vxlan
36
36
  end
37
37
  hosts = documents.map { |entry| entry[2]["device"]["host"] }
38
38
  names = documents.map { |entry| CLI::Report.device_basename(entry[2].fetch("device")) }
39
- raise ArgumentError, "设备身份或输出名称重复" unless hosts.uniq == hosts && names.uniq == names
39
+ raise ArgumentError, "Duplicate device identity or output name" unless hosts.uniq == hosts && names.uniq == names
40
40
  policy = options[:fabric_policy] && YAML.safe_load(CtxCli::Input.read(path: options[:fabric_policy]), aliases: false)
41
41
  Vxlan::Fabric::Scope.new(policy) if policy
42
42
  policy
@@ -46,7 +46,7 @@ module Vxlan
46
46
  def publish(documents, options, batch:, context:)
47
47
  report = CLI::Report.new(options[:output] || File.join(batch, "reviews"), scripts: options[:scripts] == true)
48
48
  report.build_review(documents, policy: context)
49
- "建议已生成:#{report.directory}/summary.txt;#{documents.size} 台;未执行设备变更。"
49
+ "Recommendations generated: #{report.directory}/summary.txt; #{documents.size} devices; no device changes executed."
50
50
  end
51
51
 
52
52
  private
@@ -61,31 +61,31 @@ module Vxlan
61
61
  return unless dimension == "configuration"
62
62
  CtxRunner::Evidence.array(record.fetch("review_findings", []))
63
63
  nve = record.fetch("nve", {})
64
- raise ArgumentError, "NVE/VLAN 结构错误" unless nve.is_a?(Hash) && record.fetch("vlan_vnis", {}).is_a?(Hash)
64
+ raise ArgumentError, "Invalid NVE/VLAN structure" unless nve.is_a?(Hash) && record.fetch("vlan_vnis", {}).is_a?(Hash)
65
65
  CtxRunner::Evidence.map(nve.fetch("members", {}))
66
66
  CtxRunner::Evidence.map(record.fetch("vrfs", {}))
67
67
  CtxRunner::Evidence.map(record.fetch("evpn", {})).each_value { |entry| CtxRunner::Evidence.array(entry.fetch("route_targets", [])) }
68
68
  CtxRunner::Evidence.map(record.fetch("interfaces", {})).each_value do |interface|
69
69
  addresses, bindings = interface.fetch("addresses", []), interface.fetch("acl_bindings", {})
70
70
  unless addresses.is_a?(Array) && addresses.all? { |address| address.is_a?(String) && address.include?("/") } && bindings.is_a?(Hash) && bindings.all? { |direction, name| %w[in out].include?(direction) && name.is_a?(String) }
71
- raise ArgumentError, "接口结构错误"
71
+ raise ArgumentError, "Invalid interface structure"
72
72
  end
73
73
  addresses.each { |address| IPAddr.new(address) }
74
74
  end
75
75
  acls = record.fetch("acls", {})
76
- raise ArgumentError, "ACL 结构错误" unless acls.is_a?(Hash)
76
+ raise ArgumentError, "Invalid ACL structure" unless acls.is_a?(Hash)
77
77
  acls.each_value do |entries|
78
78
  CtxRunner::Evidence.array(entries).each do |entry|
79
79
  unless entry["sequence"].is_a?(Integer) && %w[permit deny remark].include?(entry["action"]) && entry["expression"].is_a?(String)
80
- raise ArgumentError, "ACL 规则结构错误"
80
+ raise ArgumentError, "Invalid ACL rule structure"
81
81
  end
82
82
  end
83
83
  end
84
84
  groups = record.fetch("address_groups", {})
85
- raise ArgumentError, "地址组结构错误" unless groups.is_a?(Hash)
85
+ raise ArgumentError, "Invalid address group structure" unless groups.is_a?(Hash)
86
86
  groups.each_value do |members|
87
87
  unless members.is_a?(Array) && members.all? { |member| member.is_a?(String) && member.include?("/") && IPAddr.new(member).ipv4? }
88
- raise ArgumentError, "地址组成员错误"
88
+ raise ArgumentError, "Invalid address group members"
89
89
  end
90
90
  end
91
91
  end
data/lib/vxlan/cli.rb CHANGED
@@ -13,7 +13,7 @@ module Vxlan
13
13
  begin
14
14
  require_relative "cli/collect"
15
15
  rescue LoadError
16
- error.puts "collect 需要含共享清单筛选的当前 net-connector;请安装项目依赖。"
16
+ error.puts "collect requires a current net-connector with shared inventory filters; install the project dependencies."
17
17
  next 2
18
18
  end
19
19
  Collect.new(env: env, out: out, error: error, input: input, **collect_options).run(arguments)
@@ -36,7 +36,7 @@ module Vxlan
36
36
  end
37
37
 
38
38
  # 全局帮助不读取配置、环境文件或设备清单。
39
- USAGE = "用法:vxlan collect [选项] 只读采集与报告\n vxlan parse [选项] 离线文件或 stdin 解析\n vxlan review --batch DIR 离线修复建议\n vxlan apply --plan FILE --item ID 交互确认后下发\n vxlan --version\n使用 vxlan collect --help 、vxlan parse --help 或 vxlan review --help 查看参数;--help-all 查看完整约定,--examples 查看范例。".freeze
39
+ USAGE = "Usage: vxlan collect [options] Read-only collection and reports\n vxlan parse [options] Parse offline files or stdin\n vxlan review --batch DIR Offline remediation recommendations\n vxlan apply --plan FILE --item ID Apply after interactive confirmation\n vxlan --version\nUse vxlan collect --help, vxlan parse --help or vxlan review --help for options; --help-all for full conventions, --examples for examples.".freeze
40
40
  private_constant :USAGE
41
41
  end
42
42
  end
@@ -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
@@ -195,11 +195,11 @@ module Vxlan
195
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)