vxlan 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3711abc03628af1231bcd4698cff27e399db8b596a4d5e6ec46712bfc43f38fb
4
- data.tar.gz: b57592e473ca59bfcf3c04a8abf6ed92a34bbd05a2d7d9e683be005437860f6d
3
+ metadata.gz: c114dcb0645a2b10176e8a6cc68cdea2b517edd069c166d20b02046dddaf766a
4
+ data.tar.gz: edccf86d53afb7ad825c2fd468dcaba21ebda26a7b2330233fc2461840ba220f
5
5
  SHA512:
6
- metadata.gz: '08f9a10991fb96ebaf09c2e30f4893a056576dcc48d1ad3471021b2c1c41efaab326e18873f008c42153036a545db3ba9be5f427b2e75aa778a6fa54f849795b'
7
- data.tar.gz: 505286b2466afe7bbe7460680c4c3f791a476f6b5e5b3848ae4a2105c8f2003f92d4005cf16fbed1a581ebd1b711bb7882ed32ad4ff6db195fed2856243336e3
6
+ metadata.gz: 34c6082ad93884802d028f56cb2821cb8ae057537ba4618d7dab6e9edb3d73d27dfaebf8a6e99e502f334aecd9617e5aacc0c579070121e7c2364a5a78e3c185
7
+ data.tar.gz: 51864b6ddf301c26c41f3eb3fb77b46a2baa32d8047f0f806595a839ef20e32e976c022843bc7bf208794ec54ac12ed31ee2b158f9647cf1e824c191b5bd9fb1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ Changes to the public contract are recorded here. Versions follow Semantic Versi
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.6.0 (2026-10-29)
8
+
9
+ - Require RuntimeParser platform at construction and add parse_many. Rename Snapshot Ruby members to observations/observation while retaining dimensions in persisted reports. Use Catalog and shared Credentials in CLI; reject selected duplicate addresses across platforms before connecting.
10
+
11
+ ## 0.5.1 (2026-10-24)
12
+
13
+ - Validate EVPN configured-peer totals within each VRF and address-family block, retain administrative shutdown and unknown AS identities, and preserve state-valued prefix totals.
14
+ - Use shared CLI argument and diagnostic boundaries for apply; preserve exit status, cancellation and uncertain journals when stderr fails, without replaying commands.
15
+ - Consume JSON Pointer configuration provenance and leave shared gem dependency versions to host lockfiles.
16
+
7
17
  ## 0.5.0 (2026-10-23)
8
18
 
9
19
  - Consume immutable string-keyed Runner summary documents throughout reports and progress callbacks.
data/README-zh.md CHANGED
@@ -21,7 +21,7 @@ gem "vxlan"
21
21
 
22
22
  然后执行 `bundle install`;也可直接执行 `gem install vxlan`。Ruby 加载入口为 `vxlan`,公开命名空间为 `Vxlan`;gem 包名与 Ruby 命名空间分别维护。
23
23
 
24
- 模块运行依赖:[`ctx_parser`](https://github.com/gatework/ctx_parser), [`ctx_driver`](https://github.com/gatework/ctx_driver)。 共享 Gatework 依赖要求兼容的 0.2.x API;应用应提交自己的 `Gemfile.lock`,保持部署可复现。使用本库不需要启动 Rails 或配置兄弟模块的本地路径。
24
+ 模块运行依赖:[`ctx_parser`](https://github.com/gatework/ctx_parser), [`ctx_driver`](https://github.com/gatework/ctx_driver), [`ctx_runner`](https://github.com/gatework/ctx_runner), [`ctx_cli`](https://github.com/gatework/ctx_cli)。gemspec 不限制依赖版本;应用应提交经过验证的 `Gemfile.lock`,保持部署可复现。使用本库不需要启动 Rails 或配置兄弟模块的本地路径。
25
25
 
26
26
  发布产物和校验值见 [GitHub Releases](https://github.com/gatework/vxlan/releases),已发布版本以 [RubyGems](https://rubygems.org/gems/vxlan) 为准。
27
27
 
@@ -45,7 +45,7 @@ output = <<~CLI
45
45
  Neighbor ID Pri State Up Time Address Interface
46
46
  192.0.2.2 1 FULL/ - 1d 192.0.2.3 Eth1/1
47
47
  CLI
48
- observation = Vxlan::RuntimeParser.new.parse(:ospf_neighbors, output, complete: true)
48
+ observation = Vxlan::RuntimeParser.new(platform: :cisco_nxos).parse(:ospf_neighbors, output, complete: true)
49
49
  puts "OSPF parse status: #{observation.status}"
50
50
  puts "Neighbors: #{observation.records.size}"
51
51
  observation.records.each { |record| p record }
@@ -136,3 +136,10 @@ MIT,见 [LICENSE](LICENSE)。
136
136
  `vxlan collect` 默认输出逐设备诊断,使用 `--scripts` 可选导出配置建议;`vxlan review --batch DIR` 可离线补生成既有报告的候选配置、条件回退和只读验收命令。导出不会执行变更;独立 `apply --plan FILE --item ID` 必须在真实终端明确确认,配置漂移或失败即停止。详见[修复建议说明](docs/remediation.md)。 业务分层、会话所有权与执行边界见[架构说明](docs/architecture-zh.md)。
137
137
 
138
138
  报告阅读顺序与 net-connector topology 一致:`summary.txt` → `devices/*.txt` → `scripts/*.cfg`。不生成 Markdown 报告。所有可执行增量汇总于带 SHA256 校验的 `plan.json`,通过 `vxlan apply --plan plan.json --item ID` 选择一项并交互确认。
139
+
140
+
141
+ ## API 迁移
142
+
143
+ `RuntimeParser.new(platform:, model: nil)` 固定解析平台;`parse(name, text, complete: false, command: nil)` 与 `parse_many(names, text, complete: false, command: nil)` 返回领域 Observation。`Snapshot.new(observations: ...)`、`#observations`、`#observation(name)` 是统一 Ruby API,旧 `dimensions` 成员及 Wireless `dimension` 入口已移除。已知未请求维度返回 `not_requested`,未知名称抛出 `ArgumentError`;持久化 JSON 仍保留 `dimensions` 字段。
144
+
145
+ 动态清单使用 `Netdisco::Catalog`;选中地址的任何重复记录(包括另一平台)都会阻止连接。范围外冲突只输出安全诊断。平台 `NC_<PLATFORM>_ENABLE_PASSWORD` 优先于 `NC_ENABLE_PASSWORD`。
data/README.md CHANGED
@@ -21,7 +21,7 @@ gem "vxlan"
21
21
 
22
22
  Then run `bundle install`, or install directly with `gem install vxlan`. The require path is `vxlan` and the public namespace is `Vxlan`; package names and Ruby namespaces are intentionally separate.
23
23
 
24
- Internal runtime dependencies: [`ctx_parser`](https://github.com/gatework/ctx_parser), [`ctx_driver`](https://github.com/gatework/ctx_driver). Shared Gatework dependencies require the compatible 0.2.x API. Applications should commit their own `Gemfile.lock` to make deployments reproducible. No Rails boot or sibling checkout paths are required.
24
+ Internal runtime dependencies: [`ctx_parser`](https://github.com/gatework/ctx_parser), [`ctx_driver`](https://github.com/gatework/ctx_driver), [`ctx_runner`](https://github.com/gatework/ctx_runner), [`ctx_cli`](https://github.com/gatework/ctx_cli). The gemspec leaves their versions unconstrained; applications should commit a verified `Gemfile.lock` for reproducible deployments. No Rails boot or sibling checkout paths are required.
25
25
 
26
26
  Release artifacts and checksums are available from [GitHub Releases](https://github.com/gatework/vxlan/releases). Check [RubyGems](https://rubygems.org/gems/vxlan) for published versions.
27
27
 
@@ -45,7 +45,7 @@ output = <<~CLI
45
45
  Neighbor ID Pri State Up Time Address Interface
46
46
  192.0.2.2 1 FULL/ - 1d 192.0.2.3 Eth1/1
47
47
  CLI
48
- observation = Vxlan::RuntimeParser.new.parse(:ospf_neighbors, output, complete: true)
48
+ observation = Vxlan::RuntimeParser.new(platform: :cisco_nxos).parse(:ospf_neighbors, output, complete: true)
49
49
  puts "OSPF parse status: #{observation.status}"
50
50
  puts "Neighbors: #{observation.records.size}"
51
51
  observation.records.each { |record| p record }
@@ -90,7 +90,9 @@ For native sessions, the host holds `native.with_operation(:vxlan)` around the f
90
90
 
91
91
  ## Domain vocabulary
92
92
 
93
- Runtime variants preserve unavailable NVE router MACs as `nil`, optional empty CDP `dc_uuid` as `nil`, and starred vPC `down*` members as `state: "down"` with `forwarding_via_peer_link: true`. Wrapped vPC consistency/reason fields are joined only for the recognized continuation. BGP prefix statistics follow supported route-type columns in the header, with or without `PfxRcd`, and remain bounded by the main neighbor's received-prefix count. A blank command body or an empty L2RIB table without explicit empty/count evidence remains partial.
93
+ Runtime variants preserve unavailable NVE router MACs as `nil`, optional empty CDP `dc_uuid` as `nil`, and starred vPC `down*` members as `state: "down"` with `forwarding_via_peer_link: true`. Wrapped vPC consistency/reason fields are joined only for the recognized continuation. BGP prefix statistics follow supported route-type columns in the header, with or without `PfxRcd`, and remain bounded by the main neighbor's received-prefix count when that total is known. A blank command body or an empty L2RIB table without explicit empty/count evidence remains partial.
94
+
95
+ EVPN configured-peer totals are checked within each VRF/address-family block; capable peers are distinct from the complete neighbor list. Administrative shutdown is retained as `shut_(admin)`. Unestablished AS 0 neighbors expose `remote_as: nil`; a state-valued prefix column keeps `prefixes_received: nil`, even when route-type counters are shown. Published-format sources and verification limits are recorded in [the evidence contracts](docs/evidence-contracts.md#evpn-summary-evidence-update-2026-10-23).
94
96
 
95
97
  VLAN IDs, VNIs, VRFs, and NVE members are separate identities. Configuration review joins VLAN mappings and NVE members by VNI; it does not infer operational reachability. `BgpNeighbors` enumerates configured neighbors after peer-template inheritance, not established BGP sessions. `PrefixListReferences` describes route-map selectors and their routing consumers; those references do not by themselves prove route acceptance.
96
98
 
@@ -137,4 +139,13 @@ MIT. See [LICENSE](LICENSE).
137
139
 
138
140
  Review output follows the net-connector topology layout: `summary.txt`, `summary.json`, `devices/*.txt`, optional `scripts/*.cfg`, and a checksummed `plan.json`. Generated reports contain no Markdown. Select exactly one change with `vxlan apply --plan plan.json --item ID`; interactive confirmation remains mandatory.
139
141
 
142
+ Apply returns `2` on failure and `130` on interruption even if stderr cannot accept diagnostics. Existing execution journals retain uncertain device effects; commands are never replayed automatically. Unexpected positional arguments are rejected, including after `--help`.
143
+
140
144
  Offline review requires `snapshot.target.host` to match the canonical device host. Reports without target identity must be collected again; the unbound legacy format is no longer accepted.
145
+
146
+
147
+ ## API migration
148
+
149
+ `RuntimeParser.new(platform:, model: nil)` fixes the platform for `parse(name, text, complete: false, command: nil)` and `parse_many(names, text, complete: false, command: nil)`. Snapshots use `observations:` and expose `observations` / `observation(name)`; the old Ruby `dimensions` member and Wireless `dimension` accessor are removed. Known missing dimensions return `not_requested`; unknown names raise `ArgumentError`. Persisted JSON keeps its `dimensions` field.
150
+
151
+ Dynamic inventory uses `Netdisco::Catalog`. Any duplicate row for a selected address, including another platform, prevents connection; excluded conflicts retain safe diagnostics. `NC_<PLATFORM>_ENABLE_PASSWORD` takes precedence over `NC_ENABLE_PASSWORD`.
@@ -1,5 +1,15 @@
1
1
  # 证据契约与验收记录
2
2
 
3
+ ## EVPN summary evidence update (2026-10-23)
4
+
5
+ 配置邻居总数在每个物理 VRF/AF 段内核对所有主表记录;缺失、额外记录和矛盾声明使证据为 partial。capable 与全部记录数不同,仅核对其不超过配置总数及重复声明一致性,不据它推断逐邻居健康。零配置邻居可证明空表;没有声明总数的历史格式仍按已有语法/完整采集契约处理。
6
+
7
+ 管理关闭状态保留为 `shut_(admin)`;未建立会话显示的 AS 0 表达为 `remote_as: nil`,已建立行仍必须有合法非零 AS。路由类型表允许 PfxRcd 显示与主表一致的状态,保留 `prefixes_received: nil`,不以类型计数合计伪造总数。已知总数继续约束类型计数之和;缺失、重复、身份或状态不匹配的统计仍为 partial。后续重复主表头不会清除之前未完成的统计证据。
8
+
9
+ 依据为 [Cisco NX-OS 9.3(x) 配置指南](https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus9000/sw/93x/label-switching/b-cisco-nexus-9000-series-nx-os-label-switching-configuration-guide-93x/b-cisco-nexus-9000-series-nx-os-label-switching-configuration-guide-93x_chapter_01000.html) 中的管理关闭/soft reconfiguration 格式、[Cisco DevNet BGP CLI 参考](https://developer.cisco.com/docs/nx-api-cli-reference-for-the-cisco-nexus-9000-series-platform/bgp-commands/) 的 AS 0/Idle 与 dampening 格式,以及 [Cisco Multisite 排障文档](https://www.cisco.com/c/en/us/support/docs/switches/nexus-9000-series-switches/215780-troubleshoot-evpn-vxlan-in-multisite-env.html) 的主表与类型统计关联。最后一份材料明确列出对应边界网关为 N9K-C93108TC-FX、NX-OS 9.3(1)。测试使用保留地址重组的文档格式,不是新的实机采集,也不证明所有设备版本兼容。
10
+
11
+ 现有解析测试验收上述边界。契约变异脚本在临时副本中检验本仓库实现,并复制当前 Bundler 选中的 Runner 库以验证已迁移的报告/执行契约;不改动已安装 gem,不隐式加载相邻源码。运行结果记录依赖来源和版本。历史证据与既有源样本继续保留如下。
12
+
3
13
  本轮从代码规范审查收紧到三条纵向链路:解析事实与完整性、失败与报告收尾、声明规则一致性。方案见 [实施计划](superpowers/plans/2026-10-19-evidence-contracts.md),约束见 [设计](superpowers/specs/2026-10-19-evidence-contracts.md)。已有 WIP 保留,未提交或发布。
4
14
 
5
15
  ## 来源与预期
@@ -0,0 +1,45 @@
1
+ # Apply CLI boundaries implementation plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
4
+
5
+ **Goal:** Reuse existing parameter/output capabilities and preserve apply failure/cancellation outcomes when stderr fails.
6
+
7
+ **Architecture:** CtxCli::Command parses, Terminal confirms, Output delivers safe diagnostics. VXLAN/Wireless select and validate plans, acquire credentials and own session leases. CtxRunner::Execution owns effects and journals. No new abstraction or state machine is introduced.
8
+
9
+ **Tech Stack:** Ruby 4.0.6, RSpec, existing project Bundler/rake and gem verification scripts.
10
+
11
+ **Spec:** docs/superpowers/specs/2026-10-23-apply-cli-boundaries.md
12
+
13
+ ## Global Constraints
14
+
15
+ - Preserve WIP and concurrent dependency edits; use two isolated worktrees with guarded copy-back. No commit, push, release, constraint restoration or evidence deletion.
16
+ - Chinese method comments and English eight-module implementation literals; preserve Wireless domain language and vendor layouts.
17
+ - Keep port 22, execution-directory defaults, all flags and full confirmation tokens. Do not add --yes or implicit plan selection.
18
+ - Ten scoped files: each consumer's apply implementation, apply spec, CHANGELOG and README; this plan/spec live in VXLAN. Current selected dependency bytes and metadata must be identified in package evidence.
19
+
20
+ ## Review Focus
21
+
22
+ - Broken stderr before credentials/connection returns 2 and does not consume confirmation or send commands.
23
+ - Broken stderr after one uncertain command or interruption preserves status 2/130 and its journal without replay.
24
+ - Failed journal finalization cannot mask the original result or disable later failure diagnostics.
25
+ - Help with positional arguments is rejected; ordinary help avoids TTY, file reads, credentials and connections.
26
+ - A reused apply instance starts a fresh parse/execution state, and the refactor leaves exact domain selection, checksum, token and lease behavior intact.
27
+
28
+ ### Task 1: migrate both apply consumers to existing CLI boundaries
29
+
30
+ **Files:** vxlan/lib/vxlan/cli/apply.rb, wireless/lib/wireless/cli/apply.rb; both spec/cli/apply_spec.rb, CHANGELOG.md and README.md; VXLAN plan/spec.
31
+
32
+ **Interfaces:** Consume existing CtxCli::Command.new/parse/informational? and Output.new/diagnostic. Produce unchanged Apply#run(argv) statuses 0/1/2/130 and unchanged Execution/session contracts. No shared-library implementation change.
33
+
34
+ - [x] Extend existing exercise helpers only for broken diagnostic streams and interrupted command writes. Add consumer regressions for preflight failure (including failed intent persistence), one-command error, and interruption; assert statuses, no extra writes and actual journal state/error. Add --help plus unexpected positional argument rejection, checking no input consumption or connection.
35
+ - [x] Run each `bundle exec rspec spec/cli/apply_spec.rb`. Expected: new diagnostic scenarios fail by escaping IOError; help/extra returns 0 instead of 2. Existing domain cases pass.
36
+ - [x] Replace direct OptionParser lifecycle with CtxCli::Command and use informational? before TTY/plan reads. Keep host-defined argument registrations and domain workflow. Route final errors and journal warnings through a per-entrypoint CtxCli::Output; preserve the existing Interrupt guard for the optional journal warning.
37
+ - [x] Run focused apply specs and both full `bundle exec rake` gates, plus existing ctx_cli default gate. Expected: all pass; domain checksum/selection/no-replay/readback tests retain their behavior. Add concise changelog/README notes.
38
+ - [x] Run one fresh independent review of the exact phase diff, spec and ledger. Re-grade findings, handle Critical/Important once with RED/GREEN and full affected gates; no second review.
39
+ - [x] Copy only hash-guarded scoped files to actual checkouts. Run both actual default rake gates and ordinary bin apply --help; build both exact candidate packages and run official package verifiers with explicit net-connector/Expect/Runner/CLI artifacts. Add an installed broken-stderr/interruption probe. Expected: actual and installed behavior passes, source bytes match and unrelated WIP remains.
40
+
41
+ ## Self-review
42
+
43
+ All behavior requirements map to Task 1; existing fixtures already cover domain fingerprints, selection, persistence, callback count and readback. The shared Command tests cover fresh defaults and argv ownership; consumer help tests cover the integration boundary. The minimal direct error-rescue patch would leave duplicated diagnostic handling and parser behavior; existing components supply both without any new API. Current continuous implementation authorization settles native execution and reversible integration; no further approval is needed.
44
+
45
+ Verification follow-through: the Wireless package verifier needed one fixture field (`snapshot.target.host`) to satisfy the existing evidence identity contract. This scoped eleventh file was repaired after the independent ten-file review and verified by the official package gate and lint. See the phase ledger for the original failure and rerun results.
@@ -0,0 +1,45 @@
1
+ # EVPN summary evidence implementation plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan inline, with one fresh final reviewer.
4
+
5
+ **Goal:** Interpret documented EVPN neighbor and statistics evidence without silently accepting missing declared neighbors or dropping known down states.
6
+
7
+ **Architecture:** Keep command grammar and peer/statistics association in the current platform parser. Separate neighbor normalization, statistics-row association and declaration validation into small private operations where each expresses a distinct invariant. Reuse existing scope splitting, duplicate identity protection and count mismatch diagnostics.
8
+
9
+ **Tech Stack:** Ruby 4.0.6, RSpec, project Bundler/rake, existing contract and package verification tools.
10
+
11
+ **Spec:** docs/superpowers/specs/2026-10-23-evpn-summary-evidence.md
12
+
13
+ ## Global Constraints
14
+
15
+ - Nine VXLAN scoped files plus one Wireless test dependency repair: current parser, existing parser spec, README, CHANGELOG, docs/evidence-contracts.md, script/verify_contracts.rb, spec/cli/apply_spec.rb and this plan/spec; Wireless only adds its direct apply require in the existing apply spec. No new shared API, dependencies, version or vendor layout changes.
16
+ - Worktree starts with 149 byte-matched actual files, including WIP. No commit/push/release, constraint restoration or removal of evidence/worktrees.
17
+ - Preserve output completeness versus neighbor health, unknown AS and unknown prefix total explicitly. Do not infer service expectations or issue device changes.
18
+ - Source references document exact observed formats; supported field relationships are stated in the spec, without all-version/device claims.
19
+
20
+ ## Review Focus
21
+
22
+ - Counts stay within physical scope, while conflicting router and duplicate peer identities still invalidate the logical scope.
23
+ - Capable is never treated as the total row count; repeated contradictory counters cannot be erased by a later original value.
24
+ - AS 0 cannot become a valid established AS; nil can join only a matching unknown-AS statistics row.
25
+ - State-valued PfxRcd, multiword administrative states, optional totals and absent/duplicate/invalid type columns cannot silently change state or associate statistics to the wrong peer.
26
+ - Malformed decorations, invalid integers, zero-prefix established rows and explicit empty tables preserve their distinct evidence meanings.
27
+
28
+ ### Task 1: enforce documented summary evidence
29
+
30
+ **Files:** lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb; spec/cisco_nxos/show_bgp_l2vpn_evpn_summary_spec.rb; README.md; CHANGELOG.md; docs/evidence-contracts.md; script/verify_contracts.rb; spec/cli/apply_spec.rb; this plan/spec. Wireless spec/cli/apply_spec.rb only corrects its own direct require.
31
+
32
+ **Interfaces:** Consume unchanged `.parse(text, complete:)`; produce unchanged Result shape with documented state strings, optional `remote_as: nil` only for newly accepted unestablished AS 0 rows, and stable existing numeric AS semantics. Keep prefixes_received nil for down states.
33
+
34
+ - [x] Add focused behavior cases for configured counts, zero/absent declarations, contradictory counts and separate scopes; administrative state and AS boundaries; known decorations; state-valued statistics and mismatched/duplicate/missing statistics. Reuse existing test helpers and fixtures.
35
+ - [x] Run `bundle exec rspec spec/cisco_nxos/show_bgp_l2vpn_evpn_summary_spec.rb spec/runtime_parser_spec.rb`; verify new cases fail for the intended evidence defects while the original 40 cases still pass.
36
+ - [x] Implement the private parsing boundaries described above; do not loosen unknown grammar or introduce fallback APIs. Run focused specs to GREEN.
37
+ - [x] Add source-backed contract/README/changelog notes. Update contract mutation locations to the current parser guard and isolated selected Runner implementation, preserving all existing mutation cases and their non-assertion control. Run `bundle exec rake` and `bundle exec ruby script/verify_contracts.rb`; record selected dependencies and source paths.
38
+ - [x] Run one fresh independent review of the exact phase diff, spec, ledger and official source evidence. Re-grade findings by behavior; perform one Critical/Important fix pass with RED/GREEN and full affected gates, no re-review.
39
+ - [x] Guard and copy scoped files to the actual checkout; run actual default rake, contract checks and ordinary parse entrypoint. Build and verify the exact candidate, including installed grammar probes and unchanged earlier apply behavior. Compare final source/metadata/package bytes and original WIP hashes.
40
+
41
+ ## Self-review
42
+
43
+ The fix follows evidence fields rather than collapsing parsing into a generic parser. Configured-peer totals describe the unfiltered command returned by the profile; a caller declaring filtered output complete can only receive partial evidence when counts disagree. Capable/established equality is deliberately not inferred. Existing terminal/identity/prefix upper-bound contracts remain in force. Continuous implementation authorization covers this local correction and guarded copy-back; no new approval gate is needed.
44
+
45
+ Review status: completed on 2026-10-24 by a fresh independent reviewer after two earlier starts failed with workspace credit exhaustion. No Critical/Important/Minor findings; 89 independent direct-source checks plus complete:false passed. Actual source, full gates and exact installed candidates were already verified; final acceptance packaging includes this updated plan. See .ctx-evpn-audit-20261023/review-findings.md and .ctx-evpn-acceptance-20261024 for evidence boundaries.
@@ -0,0 +1,13 @@
1
+ # Apply CLI parameter and diagnostic boundaries
2
+
3
+ Continue the authorized eight-module review; Wireless participates as the existing shared-CLI consumer. The accepted shared CLI design says apply reuses parameter/terminal/output components while domain plan selection, validation, commands and operation leases remain local. Do not introduce a common apply workflow, move Execution accounting, or add adapters that duplicate the two domains' decisions.
4
+
5
+ Both current apply entrypoints directly build OptionParser and write final errors to stderr. A broken error stream can replace intended status 2 or 130 after the journal already recorded failure/interruption. Help also accepts unexpected positional arguments, unlike the existing CtxCli::Command boundary.
6
+
7
+ Use existing CtxCli::Command for fresh defaults, parsing and informational output, and CtxCli::Output for safe terminal diagnostics. Preserve every existing flag/default, exact confirmation token, per-domain preview text, checksum/selection rule, credentials policy, execution journal and session lease. Reject positional arguments including after --help before confirmation, file reads or connection. Informational help remains side-effect free. Runtime error messages remain safe and do not expose original exception text.
8
+
9
+ Broken stderr must not replace ordinary failure status 2 or interruption status 130, cause device replay, or erase execution facts. Report finalization warnings use the same diagnostic output component. Preserve the current special handling of interruption during the optional journal warning; do not introduce a new fatal-exception policy into shared Output.
10
+
11
+ Chinese method comments and English business literals apply to the eight requested modules. Wireless's existing domain language is preserved; this phase does not perform a bulk translation. No new public API, flags, dependencies, compatibility paths, version-constraint edits, commits or publication. Keep concurrent WIP using before/after hashes.
12
+
13
+ Acceptance uses existing domain apply fixtures and real journal files, then complete module gates, actual ordinary CLI entrypoints and isolated packages with explicit dependency artifacts. Selected installed versions are evidence only for that combination, not the concurrently widened dependency ranges. Real network/device behavior is unclaimed.
@@ -0,0 +1,31 @@
1
+ # EVPN summary evidence boundaries
2
+
3
+ ## Problem and scope
4
+
5
+ The current NX-OS EVPN summary parser ignores declared neighbor counts, drops documented administrative shutdown and AS 0/Idle rows, and rejects a documented Idle row in the optional route-type statistics table. These are evidence interpretation defects: collection completeness and neighbor health must remain separate.
6
+
7
+ Only the existing `ShowBgpL2vpnEvpnSummary` implementation and its direct verification/docs change. Keep the vendor command filename/class, Result/Observation interfaces, profile commands and caller-owned completeness contract. No generic BGP framework, legacy API, connection changes or inferred configuration are needed.
8
+
9
+ ## Source evidence
10
+
11
+ - [NX-OS 9.3(x) configuration guide](https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus9000/sw/93x/label-switching/b-cisco-nexus-9000-series-nx-os-label-switching-configuration-guide-93x/b-cisco-nexus-9000-series-nx-os-label-switching-configuration-guide-93x_chapter_01000.html) contains four configured neighbors, one capable neighbor, three administrative shutdown rows and inbound soft-reconfiguration statistics.
12
+ - [Cisco DevNet NX-API CLI reference](https://developer.cisco.com/docs/nx-api-cli-reference-for-the-cisco-nexus-9000-series-platform/bgp-commands/) shows an EVPN Idle neighbor with AS 0 and a dampening summary. AS 0 is retained as unknown (`remote_as: nil`) for a non-established row; it is never accepted as an established AS.
13
+ - [Cisco multisite troubleshooting](https://www.cisco.com/c/en/us/support/docs/switches/nexus-9000-series-switches/215780-troubleshoot-evpn-vxlan-in-multisite-env.html), updated 2022-10-17, includes three main-table neighbors and three route-type rows, with Idle in both tables. Its component list identifies that border gateway as N9K-C93108TC-FX, NX-OS 9.3(1).
14
+
15
+ These are published-format evidence, not local real-device verification. Tests use independently assembled documentation-style input with reserved addresses and controlled counters; do not copy a document's complete transcript or represent synthetic fixtures as live captures.
16
+
17
+ ## Required behavior
18
+
19
+ 1. Count every parsed main-table neighbor against every configured-peer declaration in its physical VRF/AF block. Keep complete blocks separate. A missing/excess row or contradictory declaration yields partial evidence while retaining unambiguous rows. A valid zero declaration proves an empty EVPN table. Count-free historical format remains supported.
20
+ 2. `capable` is a different count: do not compare it to all neighbor rows or infer a peer's health from it. Reject impossible capable > configured and contradictory repeated count pairs. Do not assume capable equals the number of currently established rows in all versions.
21
+ 3. Keep `Shut (Admin)` as normalized `shut_(admin)`, with no received-prefix total. Preserve existing normalization of other states. Non-established AS 0 becomes nil; nonzero valid AS remains numeric. Reject AS 0 for established rows and AS values outside the 32-bit range.
22
+ 4. Recognize only the documented, fully shaped soft-reconfiguration/dampening summary lines. Unknown or malformed decorations still make the output partial.
23
+ 5. The optional route-type table joins by normalized VRF/AF/address and matching AS. If it has a PfxRcd column, numeric totals must match established rows; a state must match the non-established row's state, including multiword administrative states. Type counters remain nonnegative integers. With a known total, their sum cannot exceed it. With an unknown total, retain the shown counters without inventing a zero total or applying an unsupported upper bound.
24
+ 6. Missing/repeated/mismatched/invalid route-type rows remain partial. Every main-table row must be accounted for if that statistics table is present. Prefix observations must not turn a down neighbor into an established neighbor. No statistics table remains a valid format.
25
+ 7. Repeated physical blocks still pass the existing logical-scope identity conflict and duplicate-neighbor checks. `complete: false` always remains partial. Invalid scope, malformed rows and terminal corruption retain existing rejection.
26
+
27
+ ## Delivery
28
+
29
+ RED/GREEN in the current WIP snapshot worktree, full default rake and evidence-contract verifier, one independent final review, guarded copy-back, actual checkout gates/ordinary parse entrypoint, package byte comparison and isolated installed parsing. Preserve all unrelated WIP and concurrent dependency metadata, with exact selected versions recorded. No commit, push or release.
30
+
31
+ Chinese implementation comments and English literals continue to apply. Tests and documentation are not subject to bulk language translation.
data/examples/offline.rb CHANGED
@@ -9,7 +9,7 @@ output = <<~CLI
9
9
  Neighbor ID Pri State Up Time Address Interface
10
10
  192.0.2.2 1 FULL/ - 1d 192.0.2.3 Eth1/1
11
11
  CLI
12
- observation = Vxlan::RuntimeParser.new.parse(:ospf_neighbors, output, complete: true)
12
+ observation = Vxlan::RuntimeParser.new(platform: :cisco_nxos).parse(:ospf_neighbors, output, complete: true)
13
13
  puts "OSPF parse status: #{observation.status}"
14
14
  puts "Neighbors: #{observation.records.size}"
15
15
  observation.records.each { |record| p record }
@@ -17,6 +17,9 @@ module Vxlan
17
17
  FIELDS = %i[peer_ip version remote_as messages_received messages_sent table_version input_queue output_queue uptime state].freeze
18
18
  INTEGERS = %i[version remote_as messages_received messages_sent table_version input_queue output_queue].freeze
19
19
  PREFIX_TYPES = %i[type1 type2 type3 type4 type5 type6 type7 type8 type12].freeze
20
+ TABLE = /\ABGP table version is \d+(?:, L2VPN EVPN config peers (\d+), capable peers (\d+))?\z/i
21
+ STATE = /\A(?:\d+|Idle(?: \(Admin\))?|Active|Connect|OpenSent|OpenConfirm|Shutdown|Shut \(Admin\))\z/i
22
+ DETAILS = /\A(?:\d+ network entries and \d+ paths using \d+ bytes of memory|BGP attribute entries \[\d+\/\d+\], BGP AS path entries \[\d+\/\d+\]|BGP community entries \[\d+\/\d+\], BGP clusterlist entries \[\d+\/\d+\]|\d+ received paths for inbound soft reconfiguration|\d+ identical, \d+ modified, \d+ filtered received paths using \d+ bytes|Dampening configured, \d+ history paths, \d+ dampened paths)\z/i
20
23
 
21
24
  private
22
25
 
@@ -58,17 +61,23 @@ module Vxlan
58
61
  # 解析命令表格行,保留数量及格式错误行诊断。
59
62
  def parse_rows(text)
60
63
  scope, prefix_columns, prefix_seen, peers = {}, nil, {}, {}
61
- prefix_total = false
64
+ prefix_total, prefix_table, declarations = false, false, []
62
65
  each_content_line(text) do |line, number|
63
66
  if HEADER.match?(line)
64
67
  @header = true
68
+ prefix_columns = nil
65
69
  elsif (match = SCOPE.match(line))
66
70
  scope = { vrf: match[1], address_family: match[2] }
67
71
  unparsed_records(number) unless scope[:address_family] == "L2VPN EVPN"
68
72
  prefix_columns = nil
69
73
  elsif (match = ROUTER.match(line))
70
74
  scope.merge!(router_id: match[1], local_as: match[2])
75
+ elsif (match = TABLE.match(line))
76
+ declarations << match.captures.map(&:to_i) if match[1]
77
+ elsif DETAILS.match?(line)
78
+ next
71
79
  elsif (match = /\ANeighbor\s+T\s+AS\s+(?:(PfxRcd)\s+)?(Type-[1-9]\d*(?:\s+Type-[1-9]\d*)*)\z/.match(line))
80
+ prefix_table = true
72
81
  prefix_total = !match[1].nil?
73
82
  prefix_columns = match[2].split(" ").map { |column| column.downcase.delete("-").to_sym }
74
83
  if prefix_columns.uniq != prefix_columns || (prefix_columns - PREFIX_TYPES).any?
@@ -76,16 +85,11 @@ module Vxlan
76
85
  prefix_columns = nil
77
86
  end
78
87
  elsif prefix_columns
79
- words = line.split(" ")
80
- address = CtxParser::Values.ip_address(words[0], allow_zone: true) if ip_address?(words[0])
81
- peer = peers[[ scope[:vrf], scope[:address_family], address ]]
82
- offset = prefix_total ? 4 : 3
83
- if words.size == prefix_columns.size + offset && %w[I E].include?(words[1]) && words.drop(2).all? { |word| word.match?(/\A\d+\z/) } &&
84
- peer && peer[:prefixes_received] && !prefix_seen.key?(peer.object_id) && peer[:remote_as] == words[2].to_i &&
85
- (!prefix_total || peer[:prefixes_received] == words[3].to_i)
86
- counts = words.drop(offset).map(&:to_i)
88
+ statistics = prefix_record(line, scope: scope, peers: peers, columns: prefix_columns, total: prefix_total)
89
+ if statistics && !prefix_seen.key?(statistics.first.object_id)
90
+ peer, counts = statistics
87
91
  # 路由类型是总前缀的子集;未展示的类型允许存在,已展示之和不能超过总数。
88
- if counts.sum > peer[:prefixes_received]
92
+ if peer[:prefixes_received] && counts.sum > peer[:prefixes_received]
89
93
  @issues << { code: :inconsistent_prefix_statistics, line: number }
90
94
  else
91
95
  peer[:prefixes_by_type] = prefix_columns.zip(counts).to_h
@@ -95,29 +99,75 @@ module Vxlan
95
99
  unparsed_records(number)
96
100
  end
97
101
  elsif (match = ROW.match(line))
98
- row = FIELDS.zip(match.captures).to_h.merge(scope)
99
- INTEGERS.each { |key| row[key] = row[key].to_i }
100
- unless row[:vrf] && row[:address_family] == "L2VPN EVPN" && ip_address?(row[:peer_ip]) && row[:version] == 4 && (1..4_294_967_295).cover?(row[:remote_as]) &&
101
- row[:state].match?(/\A(?:\d+|Idle(?: \(Admin\))?|Active|Connect|OpenSent|OpenConfirm|Shutdown)\z/i)
102
+ row = neighbor_record(match, scope)
103
+ unless row
102
104
  unparsed_records(number)
103
105
  next
104
106
  end
105
- row[:peer_ip] = CtxParser::Values.ip_address(row[:peer_ip], allow_zone: true)
106
- row[:state] = row[:state].downcase.tr(" ", "_")
107
- row[:prefixes_received] = row[:state].match?(/\A\d+\z/) ? row[:state].to_i : nil
108
- row[:state] = "established" if row[:prefixes_received]
109
107
  @rows << row
110
108
  # 统计表按完整作用域查找;重复邻居仍由最终身份检查整体隔离。
111
109
  peers[row.values_at(:vrf, :address_family, :peer_ip)] ||= row
112
- elsif !line.match?(/\A(?:BGP table version is \d+(?:, L2VPN EVPN config peers \d+, capable peers \d+)?|\d+ network entries and \d+ paths using \d+ bytes of memory|BGP attribute entries \[\d+\/\d+\], BGP AS path entries \[\d+\/\d+\]|BGP community entries \[\d+\/\d+\], BGP clusterlist entries \[\d+\/\d+\])\z/i)
110
+ else
113
111
  unparsed_records(number)
114
112
  end
115
113
  end
116
- if prefix_columns && prefix_seen.size != @rows.size
114
+ validate_peer_counts(declarations, scope)
115
+ if prefix_table && prefix_seen.size != @rows.size
117
116
  @issues << { code: :missing_prefix_statistics, expected: @rows.size, observed: prefix_seen.size }
118
117
  end
119
118
  end
120
119
 
120
+ # 配置数覆盖本段全部邻居;capable 只校验自身边界,不代替逐邻居状态。
121
+ def validate_peer_counts(declarations, scope)
122
+ values = declarations.uniq
123
+ @issues << { code: :conflicting_peer_counts } if values.size > 1
124
+ values.each do |configured, capable|
125
+ count_mismatch(configured, @rows.size)
126
+ @issues << { code: :invalid_peer_counts, configured: configured, capable: capable } if capable > configured
127
+ end
128
+ @empty = true if values == [ [ 0, 0 ] ] && @rows.empty? && scope[:address_family] == "L2VPN EVPN"
129
+ end
130
+
131
+ # AS 0 只表示尚未建立邻居的未知身份;数值前缀总数始终表示已建立会话。
132
+ def neighbor_record(match, scope)
133
+ row = FIELDS.zip(match.captures).to_h.merge(scope)
134
+ INTEGERS.each { |key| row[key] = row[key].to_i }
135
+ return unless row[:vrf] && row[:address_family] == "L2VPN EVPN" && ip_address?(row[:peer_ip]) && row[:version] == 4 &&
136
+ (0..4_294_967_295).cover?(row[:remote_as]) && STATE.match?(row[:state])
137
+
138
+ row[:prefixes_received] = row[:state].match?(/\A\d+\z/) ? row[:state].to_i : nil
139
+ row[:remote_as] = row[:remote_as].nonzero?
140
+ return if row[:prefixes_received] && row[:remote_as].nil?
141
+
142
+ row[:peer_ip] = CtxParser::Values.ip_address(row[:peer_ip], allow_zone: true)
143
+ row[:state] = row[:prefixes_received] ? "established" : row[:state].downcase.tr(" ", "_")
144
+ row
145
+ end
146
+
147
+ # 从末尾读取类型计数,保留多词状态,并按主表身份与总数关联统计。
148
+ def prefix_record(line, scope:, peers:, columns:, total:)
149
+ words = line.split(" ")
150
+ return unless words.size >= columns.size + (total ? 4 : 3)
151
+
152
+ counts = words.pop(columns.size)
153
+ address, kind, asn = words.shift(3)
154
+ return unless ip_address?(address) && %w[I E].include?(kind) && asn.match?(/\A\d+\z/) && counts.all? { |value| value.match?(/\A\d+\z/) }
155
+
156
+ address = CtxParser::Values.ip_address(address, allow_zone: true)
157
+ peer = peers[[ scope[:vrf], scope[:address_family], address ]]
158
+ return unless peer && peer[:remote_as] == asn.to_i.nonzero?
159
+
160
+ if total
161
+ value = words.join(" ")
162
+ return unless STATE.match?(value)
163
+ observed = value.match?(/\A\d+\z/) ? value.to_i : value.downcase.tr(" ", "_")
164
+ return unless observed == (peer[:prefixes_received] || peer[:state])
165
+ else
166
+ return unless words.empty?
167
+ end
168
+ [ peer, counts.map(&:to_i) ]
169
+ end
170
+
121
171
  # 归一化平台值,同时保留未知或不支持的状态。
122
172
  def normalize(text)
123
173
  output = []
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "optparse"
3
+ require "ctx_cli/command"
4
4
  require "ctx_cli/terminal"
5
5
  require "ctx_cli/input"
6
+ require "ctx_cli/output"
6
7
  require "json"
7
8
  require "ctx_runner/document"
8
9
  require "digest"
@@ -45,14 +46,14 @@ module Vxlan
45
46
  def initialize(env: ENV, out: $stdout, error: $stderr, input: $stdin, connector_factory: nil, discovery: {})
46
47
  @env, @out, @error, @input, @factory = env, out, error, input, connector_factory
47
48
  @discovery = discovery
49
+ @output = CtxCli::Output.new(out: out, error: error)
48
50
  end
49
51
 
50
52
  # 没有 --yes 或自动回退;零退出码只确认配置回读,不声称业务已验收。
51
53
  def run(argv)
52
54
  @store, @execution = nil, nil
53
- options = { directory: File.join(Dir.pwd, "vxlan-executions"), port: 22 }
54
- parser = OptionParser.new do |p|
55
- p.banner = "Usage: vxlan apply --plan FILE --item ID [--env-file FILE] (requires explicit confirmation in an interactive terminal)"
55
+ command = CtxCli::Command.new(banner: "Usage: vxlan apply --plan FILE --item ID [--env-file FILE] (requires explicit confirmation in an interactive terminal)",
56
+ defaults: { directory: File.join(Dir.pwd, "vxlan-executions"), port: 22 }, out: @out) do |p, options|
56
57
  p.on("--plan FILE", "Batch plan.json; requires a SHA256 checksum file in the same directory") { |value| options[:plan] = value }
57
58
  p.on("--item ID", "Explicitly select one change from the batch plan; bulk execution is prohibited") { |value| options[:item] = value }
58
59
  p.on("--env-file FILE", "Explicit credentials file; process environment takes precedence") { |value| options[:env_file] = value }
@@ -60,11 +61,11 @@ module Vxlan
60
61
  p.on("--port N", Integer, "SSH port, default 22") { |value| options[:port] = value }
61
62
  p.on("-h", "--help", "Show help without connecting to devices") { options[:help] = true }
62
63
  end
63
- rest = parser.parse(argv.dup)
64
- return @out.puts(parser) || 0 if options[:help]
64
+ options = command.parse(argv)
65
+ return 0 if command.informational?
65
66
  terminal = CtxCli::Terminal.new(input: @input, out: @out)
66
67
  terminal.require_tty!(@error)
67
- raise ArgumentError, "A single plan is required" unless rest.empty? && options[:plan] && options[:port].between?(1, 65_535)
68
+ raise ArgumentError, "A single plan is required" unless options[:plan] && options[:port].between?(1, 65_535)
68
69
  body = CtxCli::Input.read(path: options[:plan])
69
70
  hash = Digest::SHA256.hexdigest(body)
70
71
  plan = CtxParser::Immutable.copy(select_plan(JSON.parse(body), options, hash))
@@ -85,11 +86,11 @@ module Vxlan
85
86
  0
86
87
  rescue Interrupt
87
88
  report_journal_failure
88
- @error.puts "Operation interrupted; #{@execution&.started? ? 'Device effects may be incomplete; read back and verify before proceeding. Do not replay commands.' : 'No service configuration was sent.'}"
89
+ @output.diagnostic "Operation interrupted; #{@execution&.started? ? 'Device effects may be incomplete; read back and verify before proceeding. Do not replay commands.' : 'No service configuration was sent.'}"
89
90
  130
90
91
  rescue StandardError, LoadError => error
91
92
  report_journal_failure
92
- @error.puts "Apply stopped (#{error.class.name}); #{@execution&.started? ? 'Effects are not fully confirmed; read back first. No automatic replay or rollback.' : 'No service configuration was sent; check the terminal, plan, credentials and current configuration for consistency.'}"
93
+ @output.diagnostic "Apply stopped (#{error.class.name}); #{@execution&.started? ? 'Effects are not fully confirmed; read back first. No automatic replay or rollback.' : 'No service configuration was sent; check the terminal, plan, credentials and current configuration for consistency.'}"
93
94
  2
94
95
  end
95
96
 
@@ -139,8 +140,8 @@ module Vxlan
139
140
  # 公共执行器保存失败状态;CLI 只呈现持久性尚未确认的诊断。
140
141
  def report_journal_failure
141
142
  return unless @execution&.journal_failed?
142
- @error.puts "Execution log finalization is unconfirmed; existing checkpoints retained. Do not replay configuration based on this state."
143
- rescue StandardError, Interrupt
143
+ @output.diagnostic "Execution log finalization is unconfirmed; existing checkpoints retained. Do not replay configuration based on this state."
144
+ rescue Interrupt
144
145
  nil
145
146
  end
146
147
  end
@@ -34,17 +34,27 @@ module Vxlan
34
34
  def load_network
35
35
  require "net/connector"
36
36
  rescue LoadError
37
- raise LoadError, "Online collection requires net-connector >= 0.18.0; add it to the host Gemfile or run gem install net-connector", cause: nil
37
+ raise LoadError, "Online collection requires net-connector >= 0.19.0; add it to the host Gemfile or run gem install net-connector", cause: nil
38
38
  end
39
39
 
40
- # YAML 静态设备或显式 Netdisco 来源二选一;所有重复地址在连接前拒绝。
40
+ # YAML 静态设备或显式 Netdisco 来源二选一;选中地址的所有冲突记录在连接前拒绝。
41
41
  def inventory(options, progress:)
42
42
  values = options.values
43
+ selection = Net::Connector::Inventory::Filters.selection(values)
43
44
  rows = if options.dynamic_inventory?
44
45
  progress.reading_inventory
45
46
  settings = options.netdisco_settings
46
- Net::Connector::Netdisco::Fleet.new(settings: settings).devices.filter_map do |device|
47
- next unless device.ready? && device.vendor == :cisco_nxos
47
+ policy = settings.snapshot(mode: :inventory)
48
+ devices = Net::Connector::Netdisco::Catalog.new(settings: settings).devices(policy: policy)
49
+ devices.filter_map(&:issue).tally.each { |code, count| progress.notice("Inventory: #{code}=#{count}") }
50
+ devices.filter_map do |device|
51
+ next unless device.vendor == :cisco_nxos
52
+ platform = "cisco_nxos"
53
+ if device.issue == :duplicate_host && policy.rules.selected?(device.host, device.vendor, name: device.name, model: device.model) &&
54
+ selection.selected?(device.host, platform, name: device.name || device.host, model: device.model)
55
+ raise CtxCli::Collection::Error, "Duplicate management address in selected inventory"
56
+ end
57
+ next unless device.ready?
48
58
  { "host" => device.host, "name" => device.name || device.host, "model" => device.model }
49
59
  end
50
60
  else
@@ -52,7 +62,6 @@ module Vxlan
52
62
  end
53
63
  targets = options.normalize_devices(rows)
54
64
  total = targets.size
55
- selection = Net::Connector::Inventory::Filters.selection(values)
56
65
  selected = targets.select do |device|
57
66
  selection.selected?(device.fetch("host"), "cisco_nxos", name: device["name"], model: device["model"])
58
67
  end
@@ -86,16 +95,10 @@ module Vxlan
86
95
 
87
96
  # 缺失用户名时不尝试连接;密码允许省略以支持明确的 SSH key/agent。
88
97
  def credentials!(options)
89
- username = secret(options, "NC_CISCO_NXOS_USERNAME") || secret(options, "NC_DEVICE_USERNAME")
90
- raise CtxCli::Collection::Error, "Set NC_DEVICE_USERNAME or NC_CISCO_NXOS_USERNAME; provide a password through the environment or use an SSH key/agent" unless username
91
- options.values.fetch("ssh").transform_keys(&:to_sym).merge(username: username,
92
- password: secret(options, "NC_CISCO_NXOS_PASSWORD") || secret(options, "NC_DEVICE_PASSWORD"), enable_password: secret(options, "NC_ENABLE_PASSWORD"))
93
- end
94
-
95
- # 空白环境值视为缺失,不修改或回显凭据。
96
- def secret(options, name)
97
- value = options.environment[name]
98
- value unless value.nil? || value.strip.empty?
98
+ require "net/connector/credentials"
99
+ credentials = Net::Connector::Credentials.new(env: options.environment).resolve(platform: :cisco_nxos)
100
+ raise CtxCli::Collection::Error, "Set NC_DEVICE_USERNAME or platform NC_*_USERNAME; provide a password through the environment or use an SSH key/agent" unless credentials
101
+ options.values.fetch("ssh").transform_keys(&:to_sym).merge(credentials)
99
102
  end
100
103
 
101
104
  # 每个任务使用独立 Collector 和会话;公共执行器负责有界并发和线程回收。
@@ -114,7 +117,7 @@ module Vxlan
114
117
  snapshot
115
118
  end
116
119
 
117
- private :load_network, :credentials!, :secret
120
+ private :load_network, :credentials!
118
121
  end
119
122
  end
120
123
  end
@@ -6,7 +6,7 @@ require "optparse"
6
6
  require "ctx_cli/options"
7
7
  require "ctx_cli/terminal"
8
8
  require "ctx_cli/initializer"
9
- gem "net-connector", "~> 0.18.0"
9
+ gem "net-connector", "~> 0.19.0"
10
10
  require "net/connector/inventory/filters"
11
11
  require "yaml"
12
12
  require "ipaddr"
@@ -67,7 +67,7 @@ module Vxlan
67
67
  @environment = @files.environment
68
68
  @config, @env_file = @files.config, @files.env_file
69
69
  rescue LoadError
70
- raise LoadError, "Collection configuration requires net-connector >= 0.18.0; reading .env also requires dotenv", cause: nil
70
+ raise LoadError, "Collection configuration requires net-connector >= 0.19.0; reading .env also requires dotenv", cause: nil
71
71
  end
72
72
 
73
73
  # 公共与专用文件先合并,进程值和 CLI 最后覆盖;数组不追加设备。
@@ -37,7 +37,7 @@ module Vxlan
37
37
  observation = if options[:dimension] == :configuration
38
38
  Vxlan::Config.new(model: options[:model]).parse(body, complete: options[:complete])
39
39
  else
40
- Vxlan::RuntimeParser.new(model: options[:model]).parse(options[:dimension], body, complete: options[:complete])
40
+ Vxlan::RuntimeParser.new(platform: :cisco_nxos, model: options[:model]).parse(options[:dimension], body, complete: options[:complete])
41
41
  end
42
42
  if options[:json]
43
43
  @output.json(observation.to_h)
@@ -85,7 +85,7 @@ module Vxlan
85
85
  :failed
86
86
  end
87
87
  observations[:collection] ||= Observation.new(name: :collection, status: status, completed_at: @clock.call)
88
- Snapshot.new(target: target, fabric_key: fabric_key, dimensions: observations, observed_at: @clock.call)
88
+ Snapshot.new(target: target, fabric_key: fabric_key, observations: observations, observed_at: @clock.call)
89
89
  end
90
90
 
91
91
  # 采集设备回显前,禁用分页并归一化终端宽度。
@@ -7,16 +7,24 @@ module Vxlan
7
7
  # 将纯平台解析事实封装为对应 VXLAN 维度观测。
8
8
  class RuntimeParser
9
9
  # 按请求的平台和型号选择运行解析行为。
10
- def initialize(platform: :cisco_nxos, model: nil)
10
+ def initialize(platform:, model: nil)
11
11
  @profile = ParserRegistry.fetch(platform, model: model)
12
12
  end
13
13
 
14
14
  # 拒绝配置维度,仅解析所选 VXLAN 运行查询。
15
- def parse(name, text, complete: false)
16
- raise ArgumentError, "Unsupported runtime dimension" if name == :configuration || !@profile.commands.key?(name)
15
+ def parse(name, text, complete: false, command: nil)
16
+ parse_many([ name ], text, complete: complete, command: command).fetch(name)
17
+ end
17
18
 
18
- result = @profile.parse(name, text, complete: complete)
19
- Observation.new(name: name, **result.to_h)
19
+ # 单条正文只允许投影属于同一声明命令的运行维度。
20
+ def parse_many(names, text, complete: false, command: nil)
21
+ unless names.is_a?(Array) && names.any? && names.uniq == names &&
22
+ names.all? { |name| name.is_a?(Symbol) && name != :configuration && @profile.commands.key?(name) }
23
+ raise ArgumentError, "Unsupported runtime dimensions"
24
+ end
25
+ @profile.parse_many(names, text, complete: complete, command: command).to_h do |name, result|
26
+ [ name, Observation.new(name: name, **result.to_h) ]
27
+ end
20
28
  end
21
29
  end
22
30
  end
@@ -2,23 +2,35 @@
2
2
 
3
3
  require "ctx_parser/immutable"
4
4
  require_relative "observation"
5
+ require_relative "parser_registry"
5
6
 
6
7
  module Vxlan
7
8
  # 携带单设备 Fabric 身份及观测结果,不管理资产清单。
8
- Snapshot = Data.define(:target, :fabric_key, :dimensions, :observed_at) do
9
+ Snapshot = Data.define(:target, :fabric_key, :observations, :observed_at) do
9
10
  # 事实由不可变领域观测持有;仅复制维度选择,不重复复制事实。
10
- def initialize(target:, fabric_key:, dimensions:, observed_at:)
11
- unless dimensions.is_a?(Hash) && dimensions.all? { |name, value| value.is_a?(Observation) && value.name == name }
11
+ def initialize(target:, fabric_key:, observations:, observed_at:)
12
+ unless observations.is_a?(Hash) && observations.all? { |name, value| value.is_a?(Observation) && value.name == name }
12
13
  raise ArgumentError, "Dimensions must match their observations"
13
14
  end
14
15
 
15
16
  super(target: CtxParser::Immutable.copy(target), fabric_key: CtxParser::Immutable.copy(fabric_key),
16
- dimensions: dimensions.to_h { |name, value| [ name, value ] }.freeze, observed_at: observed_at&.dup&.freeze)
17
+ observations: observations.to_h { |name, value| [ name, value ] }.freeze, observed_at: observed_at&.dup&.freeze)
17
18
  end
18
19
 
20
+ # 未请求的已知维度保留明确状态;未知维度属于调用错误。
21
+ def observation(name)
22
+ unless name == :collection || CiscoNxos::Profile::COMMANDS.key?(name)
23
+ raise ArgumentError, "Unknown VXLAN dimension"
24
+ end
25
+ observations.fetch(name) { Observation.new(name: name, status: :not_requested) }
26
+ end
27
+
28
+ # 报告格式独立于 Ruby 成员命名,已有离线证据继续使用 dimensions。
29
+ def to_h = super.except(:observations).merge(dimensions: observations)
30
+
19
31
  # 判断所有已选事实是否都有完整证据支持。
20
32
  def complete?
21
- selected = dimensions.values.reject { |dimension| dimension.status == :not_requested }
33
+ selected = observations.values.reject { |dimension| dimension.status == :not_requested }
22
34
  selected.any? && selected.all?(&:complete?)
23
35
  end
24
36
 
data/lib/vxlan/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Vxlan
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -15,56 +15,74 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.2.1
18
+ version: '0'
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.2.1
25
+ version: '0'
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.2.0
32
+ version: 0.3.0
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '0.4'
33
36
  type: :runtime
34
37
  prerelease: false
35
38
  version_requirements: !ruby/object:Gem::Requirement
36
39
  requirements:
37
40
  - - ">="
38
41
  - !ruby/object:Gem::Version
39
- version: 0.2.0
42
+ version: 0.3.0
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '0.4'
40
46
  - !ruby/object:Gem::Dependency
41
47
  name: ctx_parser
42
48
  requirement: !ruby/object:Gem::Requirement
43
49
  requirements:
44
50
  - - ">="
45
51
  - !ruby/object:Gem::Version
46
- version: 0.2.7
52
+ version: 0.3.0
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.4'
47
56
  type: :runtime
48
57
  prerelease: false
49
58
  version_requirements: !ruby/object:Gem::Requirement
50
59
  requirements:
51
60
  - - ">="
52
61
  - !ruby/object:Gem::Version
53
- version: 0.2.7
62
+ version: 0.3.0
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '0.4'
54
66
  - !ruby/object:Gem::Dependency
55
67
  name: ctx_driver
56
68
  requirement: !ruby/object:Gem::Requirement
57
69
  requirements:
58
70
  - - ">="
59
71
  - !ruby/object:Gem::Version
60
- version: 0.2.7
72
+ version: 0.3.0
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.4'
61
76
  type: :runtime
62
77
  prerelease: false
63
78
  version_requirements: !ruby/object:Gem::Requirement
64
79
  requirements:
65
80
  - - ">="
66
81
  - !ruby/object:Gem::Version
67
- version: 0.2.7
82
+ version: 0.3.0
83
+ - - "<"
84
+ - !ruby/object:Gem::Version
85
+ version: '0.4'
68
86
  - !ruby/object:Gem::Dependency
69
87
  name: digest
70
88
  requirement: !ruby/object:Gem::Requirement
@@ -202,10 +220,14 @@ files:
202
220
  - docs/superpowers/plans/2026-10-19-vxlan-examples.md
203
221
  - docs/superpowers/plans/2026-10-20-fabric-consistency.md
204
222
  - docs/superpowers/plans/2026-10-22-ctx-cli.md
223
+ - docs/superpowers/plans/2026-10-23-apply-cli-boundaries.md
224
+ - docs/superpowers/plans/2026-10-23-evpn-summary-evidence.md
205
225
  - docs/superpowers/specs/2026-10-19-command-line-design.md
206
226
  - docs/superpowers/specs/2026-10-19-connector-integration-design.md
207
227
  - docs/superpowers/specs/2026-10-19-evidence-contracts.md
208
228
  - docs/superpowers/specs/2026-10-22-ctx-cli-design.md
229
+ - docs/superpowers/specs/2026-10-23-apply-cli-boundaries.md
230
+ - docs/superpowers/specs/2026-10-23-evpn-summary-evidence.md
209
231
  - examples/README.md
210
232
  - examples/collect.rb
211
233
  - examples/offline.rb