vxlan 0.1.0 → 0.2.1

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README-zh.md +13 -1
  4. data/README.md +13 -1
  5. data/docs/architecture-zh.md +3 -3
  6. data/lib/vxlan/bgp_neighbors.rb +11 -7
  7. data/lib/vxlan/cisco_nxos/{configuration_document.rb → document.rb} +4 -2
  8. data/lib/vxlan/cisco_nxos/evpn_l2route.rb +19 -7
  9. data/lib/vxlan/cisco_nxos/parser.rb +7 -2
  10. data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +8 -6
  11. data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +1 -1
  12. data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +1 -1
  13. data/lib/vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +1 -1
  14. data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +1 -1
  15. data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +1 -1
  16. data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +17 -3
  17. data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +60 -26
  18. data/lib/vxlan/cisco_nxos/profile.rb +3 -3
  19. data/lib/vxlan/cisco_nxos/{configuration_parsing → syntax}/interfaces.rb +26 -26
  20. data/lib/vxlan/cisco_nxos/{configuration_parsing → syntax}/policies.rb +11 -3
  21. data/lib/vxlan/cisco_nxos/syntax/routing.rb +79 -0
  22. data/lib/vxlan/cisco_nxos/terminal.rb +14 -0
  23. data/lib/vxlan/collector.rb +3 -0
  24. data/lib/vxlan/configuration_review.rb +10 -10
  25. data/lib/vxlan/observation.rb +2 -0
  26. data/lib/vxlan/prefix_list_references.rb +8 -7
  27. data/lib/vxlan/snapshot.rb +7 -1
  28. data/lib/vxlan/version.rb +1 -1
  29. data/lib/vxlan.rb +0 -1
  30. metadata +14 -14
  31. data/lib/vxlan/cisco_nxos/configuration_parsing/routing.rb +0 -62
  32. data/lib/vxlan/l2route_parser.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d63ce72a439c443b93146031919d9c1a6959608263a755cc93b633b442cfe6d8
4
- data.tar.gz: c7a77c3ea5d30f0ed8208a34d8a3ea2ca471fbd62c80d4fc5692f56fab76d261
3
+ metadata.gz: 41317f98c5496446fedd54ad973ea187cc30c234d45f8582ba7bfab4f4b359ce
4
+ data.tar.gz: 6db8bbc8223f8f105dd64db42535b46faa5e15a9c9ea37da6d06f092856390d3
5
5
  SHA512:
6
- metadata.gz: 7f6a22391a2237744aa371a3a8db3ce2f88ef1e06b229e23b3fc1dcaaa034b55a1fa83f0fa0f4f85416fa545a88f228b349d5c74c9ab2f455ba69cdb9d0617ca
7
- data.tar.gz: 5035dc852c9e249aadb9017b5cccd273bdded71624ccbe4980d263c1590c8adbc337a55b8069766d95578e8c2fa2b48a040dd13aac2d0560e4b34e9a93db9157
6
+ metadata.gz: 77e6a998e4ddb67b5d31ab5087877c9cedc5e374873e62d351b222f1f39b349a8b324383662a77b8d4ff6deea347675eb1746400424b38a1920597ef298da1cc
7
+ data.tar.gz: 72336196c2ae2300094558822f3b2b3e3dbc9a3aa90e0a29668be4bb79f111c5fb7641a7281ab71d17f16e796c526523b8174b063835051d7db7433a8c2162c7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  Changes to the public contract are recorded here. Versions follow Semantic Versioning.
4
4
 
5
+ ## 0.2.1 (2026-10-17)
6
+
7
+ - Require ctx_parser and ctx_driver 0.2.x so configuration APIs and command capture behavior resolve consistently.
8
+
9
+ ## 0.2.0 (2026-10-17)
10
+
11
+ ### Changed
12
+
13
+ - Breaking: replace `L2routeParser` with `RuntimeParser`, rename `CiscoNxos::ConfigurationDocument` to `CiscoNxos::Document`, and move configuration helpers from `ConfigurationParsing` to `Syntax`.
14
+ - Breaking: rename `CiscoNxos::Profile::CATALOG` to `DECLARATIONS` and adopt the shared `CtxParser::Configuration`, `Commands`, and address conversion APIs.
15
+ - Require symbol observation names and matching `Observation` values in snapshot dimensions; string interpolation now exposes metadata only.
16
+
17
+ ### Fixed
18
+
19
+ - Copy collection identity and dimension selection before connector callbacks can mutate them.
20
+ - Reject paginated or terminal-edited runtime output with no records, and keep affected configuration captures incomplete.
21
+ - Exclude conflicting port-channel members across wrapped lines and groups, and stop continuations at invalid rows or table boundaries.
22
+ - Exclude conflicting singleton configuration values and prevent inheritance or default values from restoring ambiguous derived facts.
23
+ - Keep unsupported routing nesting partial and process deeply nested unrelated configuration without exhausting the Ruby call stack.
24
+ - Reduce repeated scans and copies in BGP statistics, policy sequence checks, prefix-list references, and wrapped L2RIB next hops while preserving ordering and duplicate rejection.
25
+
5
26
  ## 0.1.0 (2026-10-16)
6
27
 
7
28
  ### Added
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)。 gemspec 的运行依赖不添加版本约束;应用应提交自己的 `Gemfile.lock`,保持部署可复现。使用本库不需要启动 Rails 或配置兄弟模块的本地路径。
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 或配置兄弟模块的本地路径。
25
25
 
26
26
  发布产物和校验值见 [GitHub Releases](https://github.com/gatework/vxlan/releases),已发布版本以 [RubyGems](https://rubygems.org/gems/vxlan) 为准。
27
27
 
@@ -60,12 +60,22 @@ observation.records.each { |record| p record }
60
60
 
61
61
  `Collector#collect(connector:, target:, fabric_key:, model: nil, dimensions:)` 使用宿主提供的 connector;设备身份、凭据、持久化、库存对账和并发策略由宿主负责。本 gem 不包含 fabric 规划、配置下发、Rails 模型或 `Vxlan::Sync`。加载库或运行离线示例不会访问设备。
62
62
 
63
+ 采集器在打开连接前复制目标身份、Fabric 身份和维度选择。`Observation` 的名称必须为 Symbol;`Snapshot` 的维度值必须是名称与键一致的 `Observation`。两种对象的字符串插值均使用只含元数据的 `inspect` 表示。
64
+
65
+ 接口、NVE、路由、VRF 和 vPC 等单值配置发生冲突时,移除歧义字段并保留部分证据。VRF 冲突的接口、配置冲突的 BGP 邻居或模板在推导地址或继承事实前被排除;未解析的 peer-template 不生成邻居投影。不支持的路由嵌套也保持 `partial`。运行回显含分页或终端编辑控制符时,因无法信任行边界,返回无记录的 `partial`。Port-channel 成员冲突检查同时覆盖续行及不同聚合组。
66
+
63
67
  详细厂商语义、历史验证记录和 Motor 专用集成说明保留在[架构参考](docs/architecture-zh.md)。其中宿主集成代码需要 Motor,不属于独立 gem API。适配层位于 [Motor 仓库](https://github.com/gatework/motor/tree/main/backend/lib) 的 `backend/lib/baseline`、`backend/lib/vxlan` 和 `backend/lib/wireless`。
64
68
 
65
69
  生命周期 connector 是宿主适配器,提供 `open { |session| }`,VXLAN 和 Wireless 还要求 `platform`;原生 `Net::Connector::Base` 实例并不提供这一宿主生命周期协议。已连接的原生会话可传入 `CtxDriver::Reader` 或 `CtxChecker::Collector#collect_session`,不会隐式连接或重试。
66
70
 
71
+ ## 领域术语
72
+
73
+ VLAN ID、VNI、VRF、NVE 成员是不同身份。配置审查通过 VNI 关联 VLAN 映射与 NVE 成员,不推断运行可达性。`BgpNeighbors` 枚举应用 peer-template 继承后的配置邻居,不代表已建立的 BGP 会话。`PrefixListReferences` 表达 route-map 选择器及路由使用方的引用关系,引用本身不能证明路由被接受。
74
+
67
75
  ## 开发与测试
68
76
 
77
+ 共享类按实际职责命名,平台信息放在命名空间中。直接对应设备命令的解析器保留文件名和类名。
78
+
69
79
  ```sh
70
80
  bundle install
71
81
  bundle exec rake
@@ -77,6 +87,8 @@ bundle exec rake build
77
87
 
78
88
  使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
79
89
 
90
+ 运行 `bundle exec ruby -Ilib benchmark/tables.rb`,比较 500、2,000、5,000 条数据下的本地解析及引用构建开销。每组核对结果,报告三次采样中的最快耗时与对象分配数。合成输入不代表设备或网络性能。
91
+
80
92
  ## 发布
81
93
 
82
94
  本目录可独立维护为 [vxlan](https://github.com/gatework/vxlan) 仓库。更新 `VERSION` 和 `CHANGELOG.md`,运行上述验证与构建命令,检查包内文件,再创建 `vVERSION` 标签。
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). The gemspec declares runtime dependencies without version constraints. 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). 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.
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
 
@@ -60,12 +60,22 @@ Complete transport is necessary but insufficient for complete parsing: unknown s
60
60
 
61
61
  `Collector#collect(connector:, target:, fabric_key:, model: nil, dimensions:)` uses a host-supplied connector; device identity, credentials, persistence, reconciliation, and concurrency policy belong to the host. This gem includes no fabric planner, provisioning engine, Rails models, or `Vxlan::Sync`. Loading or running the offline example does not access a device.
62
62
 
63
+ The collector copies target identity, fabric identity, and dimension selection before opening the connection. `Observation` names must be symbols; each `Snapshot` dimension must hold an `Observation` with a matching name. Both objects use their metadata-only `inspect` representation for string interpolation.
64
+
65
+ Conflicting singleton configuration values are excluded and reported as partial evidence, including interface, NVE, routing, VRF, and vPC settings. An interface with a conflicting VRF, or a BGP neighbor or template with conflicting settings, is excluded before deriving addresses or inherited facts; unresolved peer templates produce no neighbor projection. Unsupported routing nesting stays partial. Runtime output containing pagination or terminal editing controls returns `partial` with no records because its row boundaries cannot be trusted. Port-channel member conflicts are excluded across both wrapped lines and separate groups.
66
+
63
67
  Detailed vendor behavior, historical validation notes, and Motor-only integration are preserved in [the architecture reference](docs/architecture-zh.md). Host integration examples in that reference require Motor; they are not standalone gem APIs. Motor adapters live under `backend/lib/baseline`, `backend/lib/vxlan`, and `backend/lib/wireless` in the [Motor repository](https://github.com/gatework/motor/tree/main/backend/lib).
64
68
 
65
69
  The lifecycle connector is a host adapter exposing `open { |session| }` (and `platform` for VXLAN and Wireless). It is distinct from a native `Net::Connector::Base` device instance. Borrowed native sessions are accepted by `CtxDriver::Reader` and `CtxChecker::Collector#collect_session`; they are never implicitly connected or retried.
66
70
 
71
+ ## Domain vocabulary
72
+
73
+ 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.
74
+
67
75
  ## Development and tests
68
76
 
77
+ Name shared classes by their responsibility and use the namespace for platform context. Preserve filenames and class names that directly match device commands.
78
+
69
79
  ```sh
70
80
  bundle install
71
81
  bundle exec rake
@@ -77,6 +87,8 @@ bundle exec rake build
77
87
 
78
88
  Use the declared minimum Ruby version for compatibility checks. Document source methods and non-obvious business logic in concise Chinese, preserve public return/error contracts, and never add raw device captures or credentials to examples, fixtures, logs, or release archives.
79
89
 
90
+ Run `bundle exec ruby -Ilib benchmark/tables.rb` to compare local parsing and reference-building costs for 500, 2,000, and 5,000 entries. Each case checks its result and reports the fastest of three samples plus allocated objects. These synthetic measurements do not measure device or network performance.
91
+
80
92
  ## Release
81
93
 
82
94
  This directory can be maintained as the independent [vxlan](https://github.com/gatework/vxlan) repository. Update `VERSION` and `CHANGELOG.md`, run the validation and build commands above, and inspect the packaged file list before tagging `vVERSION`.
@@ -13,7 +13,7 @@ Motor::Device
13
13
  → Motor::Devices::Connector(凭据、平台、严格主机密钥、超时、会话生命周期)
14
14
  → CtxDriver::Reader(net-connector 单命令采集边界)
15
15
  → Vxlan::Collector(同一设备逐条读取)
16
- → Configuration / RuntimeParser / L2routeParser(维度与 Observation 适配)
16
+ → Configuration / RuntimeParser(维度与 Observation 适配)
17
17
  → CiscoNxos::Profile / Parsers::Show*(平台与型号策略、按命令解析)
18
18
  → Snapshot / Observation
19
19
  → Vxlan::Sync(锁设备、复核来源与版本、事务)
@@ -49,7 +49,7 @@ bundle exec rake 'motor:netstack:vxlan[456,LAB]'
49
49
 
50
50
  Cisco N9K 的具体策略位于 [`lib/vxlan/cisco_nxos`](../lib/vxlan/cisco_nxos/profile.rb),按命令拆分为 `parsers/show_ip_ospf_neighbors.rb` 等独立文件。直接调用 `Vxlan::CiscoNxos::Parsers::ShowIpOspfNeighbors.parse(cli_output, complete: true)` 即可离线解析。`ParserRegistry.fetch(:cisco_nxos, model: device.model)` 选择平台与型号策略;命令清单和解析类统一在 `CiscoNxos::Profile` 中声明。原解析入口继续转换为 `Vxlan::Observation`。公共接口、结果对象和扩展方式见 [`../ctx_parser`](https://github.com/gatework/ctx_parser)。
51
51
 
52
- 配置解析按职责组合:`ShowRunningConfig` 编排命令解析,`ConfigurationParsing::Interfaces/Routing/Policies` 分别解释接口/NVE、路由上下文及策略语法,`CtxParser::ConfigurationParser` 管理逐行状态、嵌套来源路径与捕获,`CiscoNxos::Configuration` 在全部块解析后归约特性列表、校验 BGP 模板引用并关联 SVI/VLAN/VNI 网段。接口地址与网段使用 `CtxParser::Values.ipv4_prefix` 的同一校验。重复身份仍排除歧义记录;语法未知、引用不完整与来源行号继续进入原有 `Result`,供入库边界判断。
52
+ 配置解析按职责组合:`ShowRunningConfig` 编排命令解析,`Syntax::Interfaces/Routing/Policies` 分别解释接口/NVE、路由上下文及策略语法,`CtxParser::Configuration` 管理逐行状态、嵌套来源路径与捕获,`CiscoNxos::Configuration` 在全部块解析后归约特性列表、校验 BGP 模板引用并关联 SVI/VLAN/VNI 网段。接口地址与网段使用 `CtxParser::Values.ipv4_prefix` 的同一校验。重复身份仍排除歧义记录;语法未知、引用不完整与来源行号继续进入原有 `Result`,供入库边界判断。
53
53
 
54
54
  设备连接默认关闭旧版 SSH 算法协商。确认单台旧设备确有算法兼容需求后,可在其现有 `settings["connector"]["legacy_ssh"]` 中显式保存布尔值 `true`;省略或 `false` 保持关闭,字符串、NULL 等非布尔值会在连接前拒绝。该设置直接传给 net-connector,主机密钥仍严格核验 `storage/net_connector/known_hosts`,不启用 Telnet 回退。连接异常仅保留允许列表中的诊断码(例如 `unsupported_host_key_algorithm`),未知码归为 `connection_failed`;VXLAN 未采集维度保存该原因,不保存底层异常文案或 cause。
55
55
 
@@ -84,7 +84,7 @@ NVE 的内联 `member vni ... mcast-group ...` 与嵌套组播声明归一为相
84
84
 
85
85
  `ip unnumbered` 保留借用接口关系;同 VRF 的唯一 Loopback IPv4 地址归约到 `unnumbered_addresses`。Underlay 清单使用该物理接口和借用的 /32 主机地址,不猜测链路子网。缺失、歧义、循环或跨 VRF 引用保持 partial;IPv6 Underlay 未被伪装成已支持的 IPv4 清单。
86
86
 
87
- NX-OS 的 `ConfigurationDocument` 与 Baseline 共用 `CtxParser::Banner` 状态机,banner 正文不会生成配置事实;EOF 未闭合会产生 `incomplete_banner`。
87
+ NX-OS 的 `Document` 与 Baseline 共用 `CtxParser::Banner` 状态机,banner 正文不会生成配置事实;EOF 未闭合会产生 `incomplete_banner`。
88
88
 
89
89
  完整配置的 `review_findings` 输出 VLAN/VNI/NVE/EVPN 关联核查项,不改变语法完整性,也不直接表示运行故障。`review_status` 为 partial 时不根据缺失数据生成关联结论。这样保留“配置收到且语法已理解”和“业务关联需要核实”两个事实。
90
90
 
@@ -14,14 +14,18 @@ module Vxlan
14
14
  def each
15
15
  return enum_for(:each) unless block_given?
16
16
 
17
- templates = @bgp.fetch(:templates, {})
18
- { "default" => @bgp }.merge(@bgp.fetch(:vrfs, {})).each do |vrf, scope|
19
- scope.fetch(:neighbors, {}).each do |peer, child|
20
- parent = templates.fetch(child[:inherit_peer], {})
21
- families = parent.fetch(:address_families, {}).merge(child.fetch(:address_families, {})) do |_name, inherited, explicit|
22
- inherited.merge(explicit.compact).merge(route_maps: inherited.fetch(:route_maps, {}).merge(explicit.fetch(:route_maps, {})))
17
+ peer_templates = @bgp.fetch(:templates, {})
18
+ { "default" => @bgp }.merge(@bgp.fetch(:vrfs, {})).each do |vrf, vrf_configuration|
19
+ vrf_configuration.fetch(:neighbors, {}).each do |peer, neighbor_configuration|
20
+ inherited_name = neighbor_configuration[:inherit_peer]
21
+ # 未解析或已冲突的模板不能生成看似有效的邻居投影。
22
+ next if inherited_name && !peer_templates.key?(inherited_name)
23
+
24
+ peer_template = peer_templates.fetch(inherited_name, {})
25
+ address_families = peer_template.fetch(:address_families, {}).merge(neighbor_configuration.fetch(:address_families, {})) do |_name, template_family, neighbor_family|
26
+ template_family.merge(neighbor_family.compact).merge(route_maps: template_family.fetch(:route_maps, {}).merge(neighbor_family.fetch(:route_maps, {})))
23
27
  end
24
- yield vrf, peer, parent.merge(child.compact).merge(address_families: families)
28
+ yield vrf, peer, peer_template.merge(neighbor_configuration.compact).merge(address_families: address_families)
25
29
  end
26
30
  end
27
31
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  require "ctx_parser/document"
4
4
  require "ctx_parser/banner"
5
+ require_relative "terminal"
5
6
 
6
7
  module Vxlan
7
8
  module CiscoNxos
8
9
  # NX-OS 横幅始终作为词法文本处理,即使其正文看起来像命令。
9
- class ConfigurationDocument < CtxParser::Document
10
+ class Document < CtxParser::Document
10
11
  protected
11
12
 
12
13
  # 区分配置结构行与引号、横幅中的文本内容。
@@ -14,7 +15,7 @@ module Vxlan
14
15
  @banner ||= CtxParser::Banner.new
15
16
  return :ignored if @banner.consume(line.text)
16
17
 
17
- if /--\s*More\s*--|\e\[/i.match?(line.text)
18
+ if Terminal.invalid_output?(line.raw)
18
19
  issues << { code: :incomplete_configuration, line: line.number }
19
20
  return :ignored
20
21
  end
@@ -24,6 +25,7 @@ module Vxlan
24
25
 
25
26
  # 报告配置文档结束时仍未闭合的横幅。
26
27
  def finish
28
+ issues.each { |issue| cursor.mark(issue.fetch(:line) - 1, state: :invalid) }
27
29
  issues << { code: :incomplete_banner } if @banner&.open?
28
30
  end
29
31
  end
@@ -18,7 +18,7 @@ module Vxlan
18
18
  def parse_rows(text)
19
19
  type = self.class::TYPE
20
20
  @totals = []
21
- fields = previous = nil
21
+ fields = previous = next_hop_index = nil
22
22
  text.each_line.with_index(1) do |source, number|
23
23
  line = source.strip
24
24
  next if line.empty? || line.match?(/\A[-=\s]+\z/) || line.match?(/\A(?:Flags\s*-\s*)?(?:\([A-Za-z0-9]+\):[A-Za-z0-9 ,\/-]+\s*)+\z/)
@@ -45,9 +45,21 @@ module Vxlan
45
45
  values = line.split(/\s+/, fields.size)
46
46
  raise ArgumentError unless values.size == fields.size
47
47
  previous = route(fields.zip(values).to_h, type)
48
+ next_hop_index = nil
48
49
  @rows << previous
49
50
  elsif previous && source.match?(/\A\s+/)
50
- previous[:next_hops] = (previous[:next_hops] + next_hops(line)).uniq
51
+ hops = next_hops(line)
52
+ # 首条续行建立索引,后续仅追加新下一跳;非法续行不会改动已有事实。
53
+ unless next_hop_index
54
+ previous[:next_hops].uniq!
55
+ next_hop_index = previous[:next_hops].to_h { |hop| [ hop, true ] }
56
+ end
57
+ hops.each do |hop|
58
+ next if next_hop_index.key?(hop)
59
+
60
+ previous[:next_hops] << hop
61
+ next_hop_index[hop] = true
62
+ end
51
63
  else
52
64
  raise ArgumentError
53
65
  end
@@ -73,9 +85,9 @@ module Vxlan
73
85
  def route(values, type)
74
86
  values[:topology_id] = number(values[:topology_id])
75
87
  raise ArgumentError unless values[:mac].match?(/\A(?:[0-9a-f]{4}(?:\.[0-9a-f]{4}){2}|(?:[0-9a-f]{2}:){5}[0-9a-f]{2})\z/i)
76
- values[:mac] = CtxParser::Values.mac(values[:mac])
88
+ values[:mac] = CtxParser::Values.mac_address(values[:mac])
77
89
  values[:sequence] = number(values[:sequence]) if values[:sequence]
78
- values[:host_ip] = host(values.fetch(:host_ip)) if type == :mac_ip
90
+ values[:host_ip] = ip_address(values.fetch(:host_ip)) if type == :mac_ip
79
91
  values[:next_hops] = next_hops(values.fetch(:next_hops))
80
92
  values
81
93
  end
@@ -91,7 +103,7 @@ module Vxlan
91
103
  when /\ALocal\z/i then { local: true }
92
104
  when /\AIfindex\s+(\d+)\z/i then { ifindex: number($1) }
93
105
  when /\A(?:Eth|Po|nve|Lo|Vlan|vPC)/i then { interface: token }
94
- else { address: host(token) }
106
+ else { address: ip_address(token) }
95
107
  end
96
108
  value[:label] = number(match[:label]) if match[:label]
97
109
  result << value
@@ -110,8 +122,8 @@ module Vxlan
110
122
  end
111
123
 
112
124
  # 归一化主机地址,保留 IPv4 与 IPv6 语义。
113
- def host(value)
114
- CtxParser::Values.host(value, allow_zone: true)
125
+ def ip_address(value)
126
+ CtxParser::Values.ip_address(value, allow_zone: true)
115
127
  end
116
128
  end
117
129
  end
@@ -4,6 +4,7 @@ require "ctx_parser/parser"
4
4
  require "ctx_parser/records"
5
5
  require "ctx_parser/input"
6
6
  require "ctx_parser/values"
7
+ require_relative "terminal"
7
8
 
8
9
  module Vxlan
9
10
  module CiscoNxos
@@ -23,6 +24,10 @@ module Vxlan
23
24
  rescue EncodingError
24
25
  return CtxParser::Result.new(status: :partial, issues: [ { code: :invalid_encoding } ])
25
26
  end
27
+ if Terminal.invalid_output?(text)
28
+ line = text.each_line.with_index(1).find { |source, _number| Terminal.invalid_output?(source) }.last
29
+ return CtxParser::Result.new(status: :partial, issues: [ { code: :invalid_terminal_output, line: line } ])
30
+ end
26
31
  if self.class::UNSUPPORTED&.match?(text)
27
32
  return CtxParser::Result.new(status: :unsupported, issues: [ { code: :command_unsupported } ])
28
33
  end
@@ -89,8 +94,8 @@ module Vxlan
89
94
  end
90
95
 
91
96
  # 校验主机地址有效性,不让格式错误值继续传播。
92
- def valid_host?(value)
93
- CtxParser::Values.valid_host?(value, allow_zone: true)
97
+ def ip_address?(value)
98
+ CtxParser::Values.ip_address?(value, allow_zone: true)
94
99
  end
95
100
 
96
101
  # 返回可作为权威清单的结果前,先识别身份冲突。
@@ -19,7 +19,7 @@ module Vxlan
19
19
 
20
20
  # 解析命令表格行,保留数量及格式错误行诊断。
21
21
  def parse_rows(text)
22
- scope, prefix_columns, prefix_seen = {}, nil, []
22
+ scope, prefix_columns, prefix_seen, peers = {}, nil, {}, {}
23
23
  lines(text) do |line, number|
24
24
  if HEADER.match?(line)
25
25
  @header = true
@@ -28,23 +28,23 @@ module Vxlan
28
28
  prefix_columns = nil
29
29
  elsif (match = /\ABGP router identifier (\S+), local AS number (\S+)/.match(line))
30
30
  scope.merge!(router_id: match[1].delete_suffix(","), local_as: match[2])
31
- reject(number) unless valid_host?(scope[:router_id]) && scope[:local_as].match?(/\A\d+(?:\.\d+)?\z/)
31
+ reject(number) unless ip_address?(scope[:router_id]) && scope[:local_as].match?(/\A\d+(?:\.\d+)?\z/)
32
32
  elsif line.match?(/\ANeighbor\s+T\s+AS\s+PfxRcd\s+Type-2\s+Type-3\s+Type-4\s+Type-5(?:\s+Type-12)?\z/)
33
33
  prefix_columns = line.end_with?("Type-12") ? %i[type2 type3 type4 type5 type12] : %i[type2 type3 type4 type5]
34
34
  elsif prefix_columns
35
35
  words = line.split
36
- peer = @rows.find { |row| row[:peer_ip] == words[0] && row[:vrf] == scope[:vrf] }
36
+ peer = peers[[ scope[:vrf], scope[:address_family], words[0] ]]
37
37
  if words.size == prefix_columns.size + 4 && %w[I E].include?(words[1]) && words.drop(2).all? { |word| word.match?(/\A\d+\z/) } &&
38
- peer && !prefix_seen.include?(peer.object_id) && peer[:remote_as] == words[2].to_i && peer[:prefixes_received] == words[3].to_i
38
+ peer && !prefix_seen.key?(peer.object_id) && peer[:remote_as] == words[2].to_i && peer[:prefixes_received] == words[3].to_i
39
39
  peer[:prefixes_by_type] = prefix_columns.zip(words.drop(4).map(&:to_i)).to_h
40
- prefix_seen << peer.object_id
40
+ prefix_seen[peer.object_id] = true
41
41
  else
42
42
  reject(number)
43
43
  end
44
44
  elsif (match = ROW.match(line))
45
45
  row = FIELDS.zip(match.captures).to_h.merge(scope)
46
46
  INTEGERS.each { |key| row[key] = row[key].to_i }
47
- unless valid_host?(row[:peer_ip]) && row[:version] == 4 && (1..4_294_967_295).cover?(row[:remote_as]) &&
47
+ unless ip_address?(row[:peer_ip]) && row[:version] == 4 && (1..4_294_967_295).cover?(row[:remote_as]) &&
48
48
  row[:state].match?(/\A(?:\d+|Idle(?: \(Admin\))?|Active|Connect|OpenSent|OpenConfirm|Shutdown)\z/i)
49
49
  reject(number)
50
50
  next
@@ -53,6 +53,8 @@ module Vxlan
53
53
  row[:prefixes_received] = row[:state].match?(/\A\d+\z/) ? row[:state].to_i : nil
54
54
  row[:state] = "established" if row[:prefixes_received]
55
55
  @rows << row
56
+ # 统计表按完整作用域查找;重复邻居仍由最终身份检查整体隔离。
57
+ peers[row.values_at(:vrf, :address_family, :peer_ip)] ||= row
56
58
  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)
57
59
  reject(number)
58
60
  end
@@ -43,7 +43,7 @@ module Vxlan
43
43
  when /\AMgmt address\(es\):(?:\s+\d+)?\z/i then scope = :management_addresses
44
44
  when /\AIP(?:v[46])? Address:\s*(\S+)\z/i
45
45
  value = $1
46
- if %i[interface_addresses management_addresses].include?(scope) && valid_host?(value)
46
+ if %i[interface_addresses management_addresses].include?(scope) && ip_address?(value)
47
47
  row[scope] << IPAddr.new(value).to_s
48
48
  else
49
49
  reject(number)
@@ -33,7 +33,7 @@ module Vxlan
33
33
  case layout
34
34
  when /\APort\s+VRF/i
35
35
  if state == 2 && [ 4, 6 ].include?(words.size) && (words.size == 4 || words[5].match?(/\A\d+\z/)) &&
36
- (words[3] == "--" || valid_host?(words[3]))
36
+ (words[3] == "--" || ip_address?(words[3]))
37
37
  row.merge!(vrf: words[1], ip_address: words[3])
38
38
  row.merge!(speed: words[4], mtu: words[5].to_i) if words.size == 6
39
39
  else
@@ -29,7 +29,7 @@ module Vxlan
29
29
  elsif (match = ROW.match(line))
30
30
  row = FIELDS.zip(match.captures).to_h.merge(scope)
31
31
  row[:priority] = row[:priority].to_i
32
- unless valid_host?(row[:neighbor_id]) && valid_host?(row[:address]) && (0..255).cover?(row[:priority]) &&
32
+ unless ip_address?(row[:neighbor_id]) && ip_address?(row[:address]) && (0..255).cover?(row[:priority]) &&
33
33
  row[:state].match?(/\A(?:DOWN|ATTEMPT|INIT|2WAY|EXSTART|EXCHANGE|LOADING|FULL)\/(?:\s*(?:DR|BDR|DROTHER|-))\z/i)
34
34
  reject(number)
35
35
  next
@@ -21,7 +21,7 @@ module Vxlan
21
21
  reject(number)
22
22
  next
23
23
  end
24
- @rows << { vlan_id: match[1] == "-" ? nil : match[1].to_i, mac: CtxParser::Values.mac(match[2]),
24
+ @rows << { vlan_id: match[1] == "-" ? nil : match[1].to_i, mac: CtxParser::Values.mac_address(match[2]),
25
25
  type: match[3].downcase, age: match[4], secure: match[5].upcase == "T", notify: match[6].upcase == "T", destination: match[7],
26
26
  flags: line[/\A([*+GOCR~])\s+/, 1] }
27
27
  elsif !line.match?(/\A(?:Legend:|\* - primary entry, G - Gateway MAC, \(R\) - Routed MAC, O - Overlay MAC|age - seconds since last seen,\+ - primary entry using vPC Peer-Link,|\(T\) - True, \(F\) - False, C - ControlPlane MAC, ~ - vsan,?|\(NA\)- Not Applicable(?: A [–-] ESI Active Path, S [–-] ESI Standby Path)?)\z/i)
@@ -20,7 +20,7 @@ module Vxlan
20
20
  lines(text) do |line, number|
21
21
  if HEADER.match?(line)
22
22
  @header = true
23
- elsif (match = ROW.match(line)) && valid_host?(match[2])
23
+ elsif (match = ROW.match(line)) && ip_address?(match[2])
24
24
  @rows << FIELDS.zip(match.captures).to_h.merge(state: match[3].downcase)
25
25
  else
26
26
  reject(number)
@@ -15,20 +15,34 @@ module Vxlan
15
15
  # 解析当前命令的记录,保留无法识别行的诊断。
16
16
  def parse_rows(text)
17
17
  totals = []
18
+ previous = nil
18
19
  lines(text) do |line, number|
20
+ issue_count = @issues.size
19
21
  if line.match?(/\AGroup\s+Port-(?:Channel)?\s+Type\s+Protocol\s+Member Ports\z/i)
20
22
  @header = true
23
+ previous = nil
21
24
  elsif line == "Channel" && @header
25
+ previous = nil
22
26
  next
23
27
  elsif (match = /\A(\d+)\s+(Po\d+)\(([^\s()]+)\)\s+(\S+)\s+(\S+)\s*(.*)\z/i.match(line))
24
- @rows << { group: match[1].to_i, interface: match[2], flags: match[3], type: match[4], protocol: match[5], members: members(match[6]) }
25
- elsif line.match?(/\A(?:Eth(?:ernet)?\S+\([^)]+\)\s*)+\z/i) && @rows.any?
26
- @rows.last[:members].concat(members(line))
28
+ previous = { group: match[1].to_i, interface: match[2], flags: match[3], type: match[4], protocol: match[5], members: members(match[6]) }
29
+ @rows << previous
30
+ elsif line.match?(/\A(?:Eth(?:ernet)?\S+\([^)]+\)\s*)+\z/i) && previous
31
+ previous[:members].concat(members(line))
27
32
  elsif (match = /\ANumber of port-channels\s*:\s*(\d+)\z/i.match(line))
28
33
  totals << match[1].to_i
34
+ previous = nil
29
35
  elsif !line.match?(/\A(?:Flags:\s+D - Down\s+P - Up in port-channel \(members\)|I - Individual\s+H - Hot-standby \(LACP only\)|s - Suspended\s+r - Module-removed|b - BFD Session Wait|S - Switched\s+R - Routed|U - Up \(port-channel\)|p - Up in delay-lacp mode \(member\)|M - Not in use\. Min-links not met)\z/)
30
36
  reject(number)
37
+ previous = nil
31
38
  end
39
+ previous = nil if @issues.size > issue_count
40
+ end
41
+ entries = @rows.flat_map { |row| row[:members] }
42
+ remove_duplicates(entries, nil) { |member| member[:interface].sub(/\AEth(?:ernet)?/i, "Eth") }
43
+ unambiguous = entries.each_with_object({}.compare_by_identity) { |member, index| index[member] = true }
44
+ @rows.each do |row|
45
+ row[:members].select! { |member| unambiguous.key?(member) }
32
46
  end
33
47
  totals.each { |total| mismatch(total, @rows.size) }
34
48
  @empty = true if totals == [ 0 ] && @rows.empty?
@@ -2,23 +2,23 @@
2
2
 
3
3
  require "digest"
4
4
  require "ipaddr"
5
- require_relative "../configuration_parsing/interfaces"
6
- require_relative "../configuration_parsing/routing"
7
- require_relative "../configuration_parsing/policies"
8
- require_relative "../configuration_document"
5
+ require_relative "../syntax/interfaces"
6
+ require_relative "../syntax/routing"
7
+ require_relative "../syntax/policies"
8
+ require_relative "../document"
9
9
  require_relative "../configuration"
10
10
  require_relative "../../configuration_review"
11
- require "ctx_parser/configuration_parser"
11
+ require "ctx_parser/configuration"
12
12
  require "ctx_parser/values"
13
13
 
14
14
  module Vxlan
15
15
  module CiscoNxos
16
16
  module Parsers
17
17
  # 按 NX-OS 缩进确定作用域;相关语法不受支持时保留部分结果。
18
- class ShowRunningConfig < CtxParser::ConfigurationParser
19
- include ConfigurationParsing::Interfaces
20
- include ConfigurationParsing::Routing
21
- include ConfigurationParsing::Policies
18
+ class ShowRunningConfig < CtxParser::Configuration
19
+ include Syntax::Interfaces
20
+ include Syntax::Routing
21
+ include Syntax::Policies
22
22
 
23
23
  private
24
24
 
@@ -27,11 +27,14 @@ module Vxlan
27
27
  configuration = CiscoNxos::Configuration.new
28
28
  @issues = configuration.issues
29
29
  @conflicts = {}
30
+ @policy_sequences = {}
30
31
  @sources = {}
32
+ @setting_sources = {}.compare_by_identity
33
+ @configuration_conflict_count = 0
31
34
  @feature_sources = []
32
35
  @disabled_feature_sources = []
33
36
  data = configuration.data
34
- read_configuration(text, comments: /\A!/, boundaries: /\Aend\z/, document_class: ConfigurationDocument).each { |block| parse_block(block, data) }
37
+ read_configuration(text, comments: /\A!/, boundaries: /\Aend\z/, document_class: Document).each { |block| parse_block(block, data) }
35
38
  @issues.concat(document.issues)
36
39
  # 所有声明读取完毕后再关联模板与网段,避免配置先后顺序改变关联结果。
37
40
  configuration.finish
@@ -76,6 +79,11 @@ module Vxlan
76
79
  data[:nve] = scope(block, [ :nve ]) { nve(block).merge(interface: name) }
77
80
  else
78
81
  store(data[:interfaces], name, block, path: [ :interfaces, name ]) { interface(block) }
82
+ settings = data[:interfaces][name]
83
+ # VRF 歧义不能等同于默认 VRF;排除此接口,阻止跨接口借址和网段推导。
84
+ if settings && @setting_sources.fetch(settings, {}).key?(:vrf) && !settings.key?(:vrf)
85
+ exclude_conflicted_scope(data[:interfaces], name, block)
86
+ end
79
87
  end
80
88
  when /\Avlan ([\d,-]+)\z/
81
89
  ids = $1.split(",", -1).flat_map do |part|
@@ -121,7 +129,7 @@ module Vxlan
121
129
  when /\Aroute-map (\S+) (permit|deny) (\d+)\z/
122
130
  name, action, sequence = $1, $2, integer($3, 1..65_535)
123
131
  entries = (data[:route_maps][name] ||= [])
124
- issue(block, :duplicate_route_map_entry) if entries.any? { |entry| entry[:sequence] == sequence }
132
+ issue(block, :duplicate_route_map_entry) if register_policy_sequence(:route_maps, name, sequence)
125
133
  entries << scope(block, [ :route_maps, name, entries.size ]) { route_map(block).merge(action: action, sequence: sequence) }
126
134
  when /\Aip access-list (\S+)\z/
127
135
  name = $1
@@ -141,11 +149,11 @@ module Vxlan
141
149
  when /\Aip route /
142
150
  data[:static_routes] << scope(block, [ :static_routes, data[:static_routes].size ]) { static_route(block) }
143
151
  when "nv overlay evpn", "no nv overlay evpn"
144
- data[:nv_overlay_evpn] = block.text == "nv overlay evpn"
145
- capture(block, data[:nv_overlay_evpn], [ :nv_overlay_evpn ])
152
+ assign(data, :nv_overlay_evpn, block.text == "nv overlay evpn", block)
153
+ capture(block, data[:nv_overlay_evpn], [ :nv_overlay_evpn ]) if data.key?(:nv_overlay_evpn)
146
154
  when /\Afabric forwarding anycast-gateway-mac (\S+)\z/
147
- data[:anycast_mac] = $1
148
- capture(block, data[:anycast_mac], [ :anycast_mac ])
155
+ assign(data, :anycast_mac, $1, block)
156
+ capture(block, data[:anycast_mac], [ :anycast_mac ]) if data.key?(:anycast_mac)
149
157
  when /\A(?:interface|vlan|vrf|router|vpc|evpn|feature|ip prefix-list|ipv6 prefix-list|route-map)\b/ then issue(block)
150
158
  when /\A(?:%|ERROR:|Invalid command|--More--)/i then issue(block, :invalid_command_output)
151
159
  else
@@ -158,8 +166,8 @@ module Vxlan
158
166
  values = { route_targets: [], routes: [] }
159
167
  analyze_children(block) do |child|
160
168
  case child.text
161
- when /\Avni (\d+)\z/ then values[:vni] = integer($1, 1..16_777_215)
162
- when /\Ard (\S+)\z/ then values[:rd] = $1
169
+ when /\Avni (\d+)\z/ then assign(values, :vni, integer($1, 1..16_777_215), child)
170
+ when /\Ard (\S+)\z/ then assign(values, :rd, $1, child)
163
171
  when /\Aroute-target / then values[:route_targets] << route_target(child)
164
172
  when /\Aaddress-family (.+)\z/
165
173
  family = $1
@@ -202,8 +210,8 @@ module Vxlan
202
210
  values = { peer_keepalive: {}, virtual_peer_link: {}, peer_gateway: false, peer_switch: false, layer3_peer_router: false }
203
211
  analyze_children(block) do |child|
204
212
  case child.text
205
- when /\Apeer-keepalive (.+)\z/ then values[:peer_keepalive] = pairs($1, %w[destination source vrf])
206
- when /\Avirtual peer-link (.+)\z/ then values[:virtual_peer_link] = pairs($1, %w[destination source dscp])
213
+ when /\Apeer-keepalive (.+)\z/ then assign(values, :peer_keepalive, pairs($1, %w[destination source vrf]), child)
214
+ when /\Avirtual peer-link (.+)\z/ then assign(values, :virtual_peer_link, pairs($1, %w[destination source dscp]), child)
207
215
  when "peer-gateway" then values[:peer_gateway] = true
208
216
  when "peer-switch" then values[:peer_switch] = true
209
217
  when "layer3 peer-router" then values[:layer3_peer_router] = true
@@ -241,16 +249,38 @@ module Vxlan
241
249
 
242
250
  # 校验接口地址,同时保留网络前缀。
243
251
  def address(value)
244
- CtxParser::Values.host(value, allow_zone: true)
252
+ CtxParser::Values.ip_address(value, allow_zone: true)
245
253
  end
246
254
 
247
255
  # 校验网络前缀,不静默修正错误语法。
248
- def prefix(value, host: false) = CtxParser::Values.ipv4_prefix(value, host: host)
256
+ def prefix(value, preserve_host_bits: false) = CtxParser::Values.ipv4_prefix(value, preserve_host_bits: preserve_host_bits)
257
+
258
+ # 单值配置可重复确认,冲突后移除歧义字段并使两端来源失效。
259
+ def assign(values, key, value, block)
260
+ sources = (@setting_sources[values] ||= {})
261
+ if sources.key?(key) && (!values.key?(key) || values[key] != value)
262
+ @cursor.mark(sources.fetch(key).index, state: :invalid)
263
+ values.delete(key)
264
+ @configuration_conflict_count += 1
265
+ issue(block, :conflicting_configuration_value)
266
+ else
267
+ sources[key] ||= block
268
+ values[key] = value
269
+ end
270
+ end
271
+
272
+ # 排除身份后保留冲突状态,后续重复块不能重新恢复歧义事实。
273
+ def exclude_conflicted_scope(collection, key, block)
274
+ collection.delete(key)
275
+ @conflicts[[ collection.object_id, key ]] = true
276
+ issue(block, :conflicting_configuration_value)
277
+ end
249
278
 
250
279
  # 保存已解析配置,同时保留其来源。
251
280
  def store(hash, key, block, path:)
252
281
  identity = [ hash.object_id, key ]
253
282
  if hash.key?(key) || @conflicts[identity]
283
+ @configuration_conflict_count += 1
254
284
  @conflicts[identity] = true
255
285
  hash.delete(key)
256
286
  @cursor.mark(@sources[identity].index, state: :invalid) if @sources[identity]
@@ -267,12 +297,16 @@ module Vxlan
267
297
 
268
298
  # 忽略无关系统设置,但不将位置错误的已建模语法视为完整。
269
299
  def check_unhandled(block)
270
- if /\A(?:no )?(?:hostname|interface|vlan|vrf|router|vpc|evpn|feature|ip address|ip router ospf|ip access-group|ip prefix-list|ipv6 prefix-list|ip access-list|route-map|vn-segment|member vni|route-target|channel-group|fabric forwarding|mtu|ip unnumbered|ip ospf network|switchport mode|switchport access vlan|ip forward|ipv6 address)\b/.match?(block.text)
271
- issue(block)
272
- else
273
- @cursor.mark(block.index, state: :ignored)
300
+ pending = [ block ]
301
+ until pending.empty?
302
+ entry = pending.pop
303
+ if /\A(?:no )?(?:hostname|interface|vlan|vrf|router|vpc|evpn|feature|ip address|ip router ospf|ip access-group|ip prefix-list|ipv6 prefix-list|ip access-list|route-map|vn-segment|member vni|route-target|channel-group|fabric forwarding|mtu|ip unnumbered|ip ospf network|switchport mode|switchport access vlan|ip forward|ipv6 address)\b/.match?(entry.text)
304
+ issue(entry)
305
+ else
306
+ @cursor.mark(entry.index, state: :ignored)
307
+ end
308
+ pending.concat(entry.children.reverse)
274
309
  end
275
- block.children.each { |child| check_unhandled(child) }
276
310
  end
277
311
 
278
312
  # 忽略已知叶子语句时,不能隐藏其嵌套配置作用域。
@@ -18,7 +18,7 @@ module Vxlan
18
18
  module CiscoNxos
19
19
  # 声明 Cisco N9K NX-OS 命令,并通过 MODEL_PARSERS 逐命令覆盖型号差异。
20
20
  class Profile < CtxParser::Profile
21
- CATALOG = CtxParser::CommandCatalog.new([
21
+ DECLARATIONS = CtxParser::Commands.new([
22
22
  { dimensions: [ :configuration ], command: "show running-config", parser: Parsers::ShowRunningConfig },
23
23
  { dimensions: [ :cdp_neighbors ], command: "show cdp neighbors detail", parser: Parsers::ShowCdpNeighborsDetail },
24
24
  { dimensions: [ :nve_peers ], command: "show nve peers", parser: Parsers::ShowNvePeers },
@@ -32,8 +32,8 @@ module Vxlan
32
32
  { dimensions: [ :l2route_macs ], command: "show l2route evpn mac all", parser: Parsers::ShowL2routeEvpnMacAll },
33
33
  { dimensions: [ :l2route_mac_ips ], command: "show l2route evpn mac-ip all", parser: Parsers::ShowL2routeEvpnMacIpAll }
34
34
  ])
35
- COMMANDS = CATALOG.commands
36
- PARSERS = CATALOG.parsers
35
+ COMMANDS = DECLARATIONS.by_dimension
36
+ PARSERS = DECLARATIONS.parsers
37
37
  end
38
38
  end
39
39
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Vxlan
4
4
  module CiscoNxos
5
- module ConfigurationParsing
5
+ module Syntax
6
6
  # 接口与 NVE 语法共用命令解析器的来源边界。
7
7
  module Interfaces
8
8
  private
@@ -12,24 +12,24 @@ module Vxlan
12
12
  values = { addresses: [], acl_bindings: {}, anycast: false }
13
13
  analyze_children(block) do |child|
14
14
  case child.text
15
- when /\Aip address (\S+)(?: secondary)?\z/ then values[:addresses] << prefix($1, host: true)
16
- when /\Aip unnumbered (loopback\d+)\z/i then values[:unnumbered_interface] = $1
17
- when /\Avrf member (\S+)\z/ then values[:vrf] = $1
18
- when /\Aip router ospf (\S+) area (\S+)\z/ then values[:ospf] = "#{$1} area #{$2}"
19
- when /\Aip ospf network (point-to-point|broadcast)\z/ then values[:ospf_network_type] = $1
20
- when /\Amtu (\d+)\z/ then values[:mtu] = integer($1, 1..65_535)
21
- when /\Aswitchport mode (access|trunk|dot1q-tunnel|fex-fabric)\z/ then values[:switchport_mode] = $1
22
- when /\Aswitchport access vlan (\d+)\z/ then values[:access_vlan] = integer($1, 1..4094)
23
- when "switchport", "no switchport" then values[:switchport] = child.text == "switchport"
24
- when "ip forward", "no ip forward" then values[:ip_forward] = child.text == "ip forward"
15
+ when /\Aip address (\S+)(?: secondary)?\z/ then values[:addresses] << prefix($1, preserve_host_bits: true)
16
+ when /\Aip unnumbered (loopback\d+)\z/i then assign(values, :unnumbered_interface, $1, child)
17
+ when /\Avrf member (\S+)\z/ then assign(values, :vrf, $1, child)
18
+ when /\Aip router ospf (\S+) area (\S+)\z/ then assign(values, :ospf, "#{$1} area #{$2}", child)
19
+ when /\Aip ospf network (point-to-point|broadcast)\z/ then assign(values, :ospf_network_type, $1, child)
20
+ when /\Amtu (\d+)\z/ then assign(values, :mtu, integer($1, 1..65_535), child)
21
+ when /\Aswitchport mode (access|trunk|dot1q-tunnel|fex-fabric)\z/ then assign(values, :switchport_mode, $1, child)
22
+ when /\Aswitchport access vlan (\d+)\z/ then assign(values, :access_vlan, integer($1, 1..4094), child)
23
+ when "switchport", "no switchport" then assign(values, :switchport, child.text == "switchport", child)
24
+ when "ip forward", "no ip forward" then assign(values, :ip_forward, child.text == "ip forward", child)
25
25
  when "fabric forwarding mode anycast-gateway" then values[:anycast] = true
26
- when /\Aip access-group (\S+) (in|out)\z/ then values[:acl_bindings][$2.to_sym] = $1
27
- when /\Avpc (\d+)\z/ then values[:vpc_id] = integer($1, 1..4096)
26
+ when /\Aip access-group (\S+) (in|out)\z/ then assign(values[:acl_bindings], $2.to_sym, $1, child)
27
+ when /\Avpc (\d+)\z/ then assign(values, :vpc_id, integer($1, 1..4096), child)
28
28
  when "vpc peer-link" then values[:peer_link] = true
29
- when /\Achannel-group (\d+)(?: mode \S+)?\z/ then values[:channel_group] = integer($1, 1..4096)
30
- when /\Adescription (.+)\z/ then values[:description] = $1
31
- when "shutdown" then values[:shutdown] = true
32
- when "no shutdown" then values[:shutdown] = false
29
+ when /\Achannel-group (\d+)(?: mode \S+)?\z/ then assign(values, :channel_group, integer($1, 1..4096), child)
30
+ when /\Adescription (.+)\z/ then assign(values, :description, $1, child)
31
+ when "shutdown" then assign(values, :shutdown, true, child)
32
+ when "no shutdown" then assign(values, :shutdown, false, child)
33
33
  else check_unhandled(child)
34
34
  end
35
35
  end
@@ -49,14 +49,14 @@ module Vxlan
49
49
  raise ArgumentError if member[:replication] == "bgp" && member[:multicast_group]
50
50
 
51
51
  store(values[:members], id, child, path: @source_path + [ :members, id ]) { member }
52
- when /\Adescription (.+)\z/ then values[:description] = $1
53
- when /\Aglobal mcast-group (\S+)\z/ then values[:multicast_group] = multicast_address($1)
54
- when /\Asource-interface (\S+)\z/ then values[:source_interface] = $1
55
- when /\Ahost-reachability protocol (\S+)\z/ then values[:host_reachability] = $1
56
- when "shutdown" then values[:shutdown] = true
57
- when "no shutdown" then values[:shutdown] = false
52
+ when /\Adescription (.+)\z/ then assign(values, :description, $1, child)
53
+ when /\Aglobal mcast-group (\S+)\z/ then assign(values, :multicast_group, multicast_address($1), child)
54
+ when /\Asource-interface (\S+)\z/ then assign(values, :source_interface, $1, child)
55
+ when /\Ahost-reachability protocol (\S+)\z/ then assign(values, :host_reachability, $1, child)
56
+ when "shutdown" then assign(values, :shutdown, true, child)
57
+ when "no shutdown" then assign(values, :shutdown, false, child)
58
58
  when "advertise virtual-rmac", "no advertise virtual-rmac"
59
- values[:advertise_virtual_rmac] = child.text == "advertise virtual-rmac"
59
+ assign(values, :advertise_virtual_rmac, child.text == "advertise virtual-rmac", child)
60
60
  else issue(child)
61
61
  end
62
62
  end
@@ -72,8 +72,8 @@ module Vxlan
72
72
  raise ArgumentError if member[:multicast_group] && member[:multicast_group] != group
73
73
 
74
74
  member[:multicast_group] = group
75
- when "suppress-arp" then member[:suppress_arp] = true
76
- when "no suppress-arp" then member[:suppress_arp] = false
75
+ when "suppress-arp" then assign(member, :suppress_arp, true, setting)
76
+ when "no suppress-arp" then assign(member, :suppress_arp, false, setting)
77
77
  else issue(setting)
78
78
  end
79
79
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Vxlan
4
4
  module CiscoNxos
5
- module ConfigurationParsing
5
+ module Syntax
6
6
  # 前缀列表与路由映射保留地址族及引用选择器。
7
7
  module Policies
8
8
  private
@@ -18,12 +18,20 @@ module Vxlan
18
18
  raise ArgumentError if (ge && !(length..width).cover?(ge)) || (le && !((ge || length)..width).cover?(le))
19
19
  entries = (lists[match[1]] ||= [])
20
20
  sequence = integer(match[2], 1..4_294_967_295)
21
- raise ArgumentError if entries.any? { |entry| entry[:sequence] == sequence }
22
- entries << { sequence: sequence, action: match[3], prefix: subnet, ge: ge, le: le }
23
21
  collection = family == "ipv6" ? :ipv6_prefix_lists : :prefix_lists
22
+ raise ArgumentError if register_policy_sequence(collection, match[1], sequence)
23
+ entries << { sequence: sequence, action: match[3], prefix: subnet, ge: ge, le: le }
24
24
  capture(block, entries.last, [ collection, match[1], entries.size - 1 ])
25
25
  end
26
26
 
27
+ # 单次解析用索引识别重复序号,避免长策略逐条扫描已有条目。
28
+ def register_policy_sequence(collection, name, sequence)
29
+ key = [ collection, name, sequence ]
30
+ duplicate = @policy_sequences.key?(key)
31
+ @policy_sequences[key] = true
32
+ duplicate
33
+ end
34
+
27
35
  # 解析有序 route-map 条目及策略引用。
28
36
  def route_map(block)
29
37
  values = { prefix_lists: [], additional_matches: [], sets: [] }
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vxlan
4
+ module CiscoNxos
5
+ module Syntax
6
+ # 路由作用域保留邻居、模板、VRF 与地址族的归属。
7
+ module Routing
8
+ private
9
+
10
+ # 解析支持的路由作用域及协议关系。
11
+ def routing(block, protocol, context = :process)
12
+ values = { vrfs: {}, neighbors: {}, templates: {}, address_families: {}, redistribute: [] }
13
+ analyze_children(block) do |child|
14
+ case child.text
15
+ when /\Avrf (\S+)\z/ then routing_scope(values, :vrfs, $1, child, protocol, context)
16
+ when /\Atemplate peer (\S+)\z/ then routing_scope(values, :templates, $1, child, protocol, context)
17
+ when /\Aneighbor (\S+)\z/ then routing_scope(values, :neighbors, $1, child, protocol, context)
18
+ when /\Aaddress-family (.+)\z/ then routing_scope(values, :address_families, $1, child, protocol, context)
19
+ when /\Arouter-id (\S+)\z/ then assign(values, :router_id, address($1), child)
20
+ when /\Aremote-as (\S+)\z/
21
+ value = $1
22
+ integer(value, 1..4_294_967_295) unless %w[internal external].include?(value)
23
+ assign(values, :remote_as, value, child)
24
+ when /\Ainherit peer (\S+)\z/ then assign(values, :inherit_peer, $1, child)
25
+ when /\Aupdate-source (\S+)\z/ then assign(values, :update_source, $1, child)
26
+ when /\Adescription (.+)\z/ then assign(values, :description, $1, child)
27
+ when /\Aroute-map (\S+) (in|out)\z/
28
+ name, direction = $1, $2.to_sym
29
+ maps = (values[:route_maps] ||= {})
30
+ assign(maps, direction, name, child)
31
+ when /\A(no )?next-hop-self( all)?\z/
32
+ assign(values, :next_hop_self, $1.nil?, child)
33
+ assign(values, :next_hop_self_all, $1.nil? && !$2.nil?, child)
34
+ when /\A(no )?(bfd|route-reflector-client|advertise-pip|advertise l2vpn evpn)\z/
35
+ enabled, setting = $1.nil?, $2
36
+ key = { "bfd" => :bfd, "route-reflector-client" => :route_reflector_client,
37
+ "advertise-pip" => :advertise_pip, "advertise l2vpn evpn" => :advertise_l2vpn_evpn }.fetch(setting)
38
+ assign(values, key, enabled, child)
39
+ when /\A(no )?send-community(?: (extended|both))?\z/
40
+ enabled, kind = $1.nil?, $2
41
+ assign(values, :standard_community, enabled, child) unless kind == "extended"
42
+ assign(values, :extended_community, enabled, child) if kind
43
+ when /\Anetwork (\S+)(?: route-map (\S+))?\z/
44
+ subnet, route_map = $1, $2
45
+ raise ArgumentError unless protocol == :bgp
46
+
47
+ (values[:networks] ||= []) << { prefix: network_prefix(subnet), route_map: route_map }.compact
48
+ when /\Aredistribute (\S+)(?: (?!route-map\b)(\S+))?(?: route-map (\S+))?\z/
49
+ values[:redistribute] << { protocol: $1, process: $2, route_map: $3 }.compact
50
+ when /\Aretain route-target (all|route-map \S+)\z/ then assign(values, :retain_route_target, $1, child)
51
+ when /\A(?:no )?(?:log-neighbor-changes|log-adjacency-changes|soft-reconfiguration inbound|passive-interface)\b/,
52
+ /\A(?:maximum-paths|default-information originate|auto-cost reference-bandwidth)\b/ then ignore(child)
53
+ else issue(child)
54
+ end
55
+ end
56
+ values
57
+ end
58
+
59
+ # 只进入当前事实模型可表达的子作用域,阻止任意递归及跨协议误归属。
60
+ def routing_scope(values, kind, name, block, protocol, context)
61
+ supported = case kind
62
+ when :vrfs then context == :process
63
+ when :templates then protocol == :bgp && context == :process
64
+ when :neighbors then protocol == :bgp && %i[process vrfs].include?(context)
65
+ when :address_families then protocol == :bgp && %i[process vrfs neighbors templates].include?(context)
66
+ end
67
+ return issue(block) unless supported
68
+
69
+ conflicts_before = @configuration_conflict_count
70
+ store(values[kind], name, block, path: @source_path + [ kind, name ]) { routing(block, protocol, kind) }
71
+ # 邻居及模板的子地址族冲突也使继承身份失效;保留同进程其他邻居。
72
+ if protocol == :bgp && %i[neighbors templates].include?(kind) && @configuration_conflict_count > conflicts_before
73
+ exclude_conflicted_scope(values[kind], name, block)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vxlan
4
+ module CiscoNxos
5
+ # 分页及终端编辑控制会破坏事实边界;保留正常换行和制表符。
6
+ module Terminal
7
+ INVALID = /--[ \t]*More[ \t]*--|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/i
8
+ private_constant :INVALID
9
+
10
+ # 检查原始正文,避免 strip 或字段解析先吞掉控制字符。
11
+ def self.invalid_output?(text) = INVALID.match?(text)
12
+ end
13
+ end
14
+ end
@@ -23,6 +23,9 @@ module Vxlan
23
23
  unless dimensions.is_a?(Array) && dimensions.any? && dimensions.uniq == dimensions && (dimensions - COMMANDS.keys).empty?
24
24
  raise ArgumentError, "Unknown or duplicate VXLAN dimensions"
25
25
  end
26
+ target = CtxParser::Immutable.copy(target)
27
+ fabric_key = CtxParser::Immutable.copy(fabric_key)
28
+ dimensions = dimensions.dup.freeze
26
29
  raise ArgumentError, "VXLAN requires Cisco NX-OS" unless connector.platform.to_s == "cisco_nxos"
27
30
  profile = ParserRegistry.fetch(connector.platform, model: model)
28
31
 
@@ -13,27 +13,27 @@ module Vxlan
13
13
  raise ArgumentError, "Completeness must be boolean" unless [ true, false ].include?(complete)
14
14
  return [] unless complete
15
15
 
16
- members = @configuration.fetch(:nve).fetch(:members, {})
17
- vlans = @configuration.fetch(:vlan_vnis).group_by { |_vlan, vni| vni }
18
- evpn = @configuration.fetch(:evpn)
16
+ nve_members_by_vni = @configuration.fetch(:nve).fetch(:members, {})
17
+ vlan_mappings_by_vni = @configuration.fetch(:vlan_vnis).group_by { |_vlan, vni| vni }
18
+ evpn_vnis = @configuration.fetch(:evpn)
19
19
  findings = []
20
- vlans.each do |vni, entries|
21
- next unless vni && !members.key?(vni)
20
+ vlan_mappings_by_vni.each do |vni, entries|
21
+ next unless vni && !nve_members_by_vni.key?(vni)
22
22
 
23
23
  findings << { code: :vlan_vni_without_nve, vni: vni, vlan_ids: entries.map(&:first).sort }
24
24
  end
25
- members.each do |vni, settings|
25
+ nve_members_by_vni.each do |vni, settings|
26
26
  next if settings[:associate_vrf]
27
27
 
28
- findings << { code: :nve_l2_without_vlan, vni: vni } unless vlans.key?(vni)
29
- if @configuration[:nv_overlay_evpn] && !evpn.key?(vni)
28
+ findings << { code: :nve_l2_without_vlan, vni: vni } unless vlan_mappings_by_vni.key?(vni)
29
+ if @configuration[:nv_overlay_evpn] && !evpn_vnis.key?(vni)
30
30
  findings << { code: :nve_l2_without_evpn, vni: vni }
31
31
  end
32
32
  end
33
- evpn.each_key { |vni| findings << { code: :evpn_vni_without_nve, vni: vni } unless members.key?(vni) }
33
+ evpn_vnis.each_key { |vni| findings << { code: :evpn_vni_without_nve, vni: vni } unless nve_members_by_vni.key?(vni) }
34
34
  @configuration.fetch(:vrfs).each do |name, settings|
35
35
  next unless settings[:vni]
36
- member = members[settings[:vni]]
36
+ member = nve_members_by_vni[settings[:vni]]
37
37
  code = member ? (!member[:associate_vrf] ? :vni_layer_conflict : nil) : :l3_vni_without_nve
38
38
  findings << { code: code, vni: settings[:vni], vrf: name } if code
39
39
  end
@@ -7,6 +7,7 @@ module Vxlan
7
7
  Observation = Data.define(:name, :status, :records, :issues, :evidence, :started_at, :completed_at) do
8
8
  # 冻结单个维度的记录、状态及诊断元数据。
9
9
  def initialize(name:, status:, records: [], issues: [], evidence: [], started_at: nil, completed_at: nil)
10
+ raise ArgumentError, "Observation name must be a symbol" unless name.is_a?(Symbol)
10
11
  raise ArgumentError, "Invalid observation status" unless %i[complete partial failed unsupported not_requested].include?(status)
11
12
  raise ArgumentError, "Records must be hashes" unless records.is_a?(Array) && records.all? { |row| row.is_a?(Hash) }
12
13
 
@@ -20,5 +21,6 @@ module Vxlan
20
21
 
21
22
  # 展示对象身份与状态,避免泄露原始回显或凭据。
22
23
  def inspect = "#<#{self.class.name} name=#{name} status=#{status} records=#{records.size}>"
24
+ alias to_s inspect
23
25
  end
24
26
  end
@@ -14,23 +14,24 @@ module Vxlan
14
14
  def call(address_family: "ipv4")
15
15
  raise ArgumentError, "Unsupported address family" unless %w[ipv4 ipv6].include?(address_family)
16
16
 
17
- bindings = route_map_bindings
18
- @configuration.fetch(:route_maps).each_with_object({}) do |(name, entries), references|
17
+ bindings = route_map_bindings.transform_values(&:uniq)
18
+ references = @configuration.fetch(:route_maps).each_with_object({}) do |(name, entries), result|
19
19
  entries.sort_by { |entry| entry.fetch(:sequence) }.each do |entry|
20
20
  matches = entry.fetch(:prefix_list_matches) do
21
21
  [ { address_family: "ipv4", selector: "address", names: entry.fetch(:prefix_lists, []) } ]
22
22
  end
23
23
  matches.select { |match| match[:address_family] == address_family }.each do |match|
24
- caller = { type: "route_map", name: name, sequence: entry.fetch(:sequence), action: entry.fetch(:action),
25
- bindings: bindings.fetch(name, []).uniq }
26
- caller[:selector] = match[:selector] unless match[:selector] == "address"
24
+ route_map_reference = { type: "route_map", name: name, sequence: entry.fetch(:sequence), action: entry.fetch(:action),
25
+ bindings: bindings.fetch(name, []).dup }
26
+ route_map_reference[:selector] = match[:selector] unless match[:selector] == "address"
27
27
  match.fetch(:names).uniq.each do |prefix_list|
28
- callers = (references[prefix_list] ||= [])
29
- callers << caller unless callers.include?(caller)
28
+ (result[prefix_list] ||= []) << route_map_reference
30
29
  end
31
30
  end
32
31
  end
33
32
  end
33
+ # 一次去重保留首次出现顺序,避免每个条目都扫描已有引用。
34
+ references.transform_values(&:uniq)
34
35
  end
35
36
 
36
37
  private
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ctx_parser/immutable"
4
+ require_relative "observation"
4
5
 
5
6
  module Vxlan
6
7
  # 携带单设备 Fabric 身份及观测结果,不管理资产清单。
7
8
  Snapshot = Data.define(:target, :fabric_key, :dimensions, :observed_at) do
8
9
  # 事实由不可变领域观测持有;仅复制维度选择,不重复复制事实。
9
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 }
12
+ raise ArgumentError, "Dimensions must match their observations"
13
+ end
14
+
10
15
  super(target: CtxParser::Immutable.copy(target), fabric_key: CtxParser::Immutable.copy(fabric_key),
11
- dimensions: dimensions.dup.freeze, observed_at: observed_at&.dup&.freeze)
16
+ dimensions: dimensions.to_h { |name, value| [ name, value ] }.freeze, observed_at: observed_at&.dup&.freeze)
12
17
  end
13
18
 
14
19
  # 判断所有已选事实是否都有完整证据支持。
@@ -19,5 +24,6 @@ module Vxlan
19
24
 
20
25
  # 展示对象身份与状态,避免泄露原始回显或凭据。
21
26
  def inspect = "#<#{self.class.name} fabric=#{fabric_key.inspect} complete=#{complete?}>"
27
+ alias to_s inspect
22
28
  end
23
29
  end
data/lib/vxlan/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Vxlan
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.1"
6
6
  end
data/lib/vxlan.rb CHANGED
@@ -9,7 +9,6 @@ require_relative "vxlan/observation"
9
9
  require_relative "vxlan/snapshot"
10
10
  require_relative "vxlan/configuration"
11
11
  require_relative "vxlan/runtime_parser"
12
- require_relative "vxlan/l2route_parser"
13
12
  require_relative "vxlan/bgp_neighbors"
14
13
  require_relative "vxlan/configuration_review"
15
14
  require_relative "vxlan/prefix_list_references"
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.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -13,30 +13,30 @@ dependencies:
13
13
  name: ctx_parser
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
18
+ version: 0.2.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'
25
+ version: 0.2.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: ctx_driver
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - ">="
30
+ - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0'
32
+ version: 0.2.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: 0.2.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: digest
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -81,10 +81,7 @@ files:
81
81
  - lib/vxlan.rb
82
82
  - lib/vxlan/bgp_neighbors.rb
83
83
  - lib/vxlan/cisco_nxos/configuration.rb
84
- - lib/vxlan/cisco_nxos/configuration_document.rb
85
- - lib/vxlan/cisco_nxos/configuration_parsing/interfaces.rb
86
- - lib/vxlan/cisco_nxos/configuration_parsing/policies.rb
87
- - lib/vxlan/cisco_nxos/configuration_parsing/routing.rb
84
+ - lib/vxlan/cisco_nxos/document.rb
88
85
  - lib/vxlan/cisco_nxos/evpn_l2route.rb
89
86
  - lib/vxlan/cisco_nxos/parser.rb
90
87
  - lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb
@@ -100,10 +97,13 @@ files:
100
97
  - lib/vxlan/cisco_nxos/parsers/show_running_config.rb
101
98
  - lib/vxlan/cisco_nxos/parsers/show_vpc_brief.rb
102
99
  - lib/vxlan/cisco_nxos/profile.rb
100
+ - lib/vxlan/cisco_nxos/syntax/interfaces.rb
101
+ - lib/vxlan/cisco_nxos/syntax/policies.rb
102
+ - lib/vxlan/cisco_nxos/syntax/routing.rb
103
+ - lib/vxlan/cisco_nxos/terminal.rb
103
104
  - lib/vxlan/collector.rb
104
105
  - lib/vxlan/configuration.rb
105
106
  - lib/vxlan/configuration_review.rb
106
- - lib/vxlan/l2route_parser.rb
107
107
  - lib/vxlan/observation.rb
108
108
  - lib/vxlan/parser_registry.rb
109
109
  - lib/vxlan/prefix_list_references.rb
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vxlan
4
- module CiscoNxos
5
- module ConfigurationParsing
6
- # 路由作用域保留邻居、模板、VRF 与地址族的归属。
7
- module Routing
8
- private
9
-
10
- # 解析支持的路由作用域及协议关系。
11
- def routing(block, protocol)
12
- values = { vrfs: {}, neighbors: {}, templates: {}, address_families: {}, redistribute: [] }
13
- analyze_children(block) do |child|
14
- case child.text
15
- when /\Avrf (\S+)\z/ then store(values[:vrfs], $1, child, path: @source_path + [ :vrfs, $1 ]) { routing(child, protocol) }
16
- when /\Atemplate peer (\S+)\z/ then store(values[:templates], $1, child, path: @source_path + [ :templates, $1 ]) { routing(child, protocol) }
17
- when /\Aneighbor (\S+)\z/ then store(values[:neighbors], $1, child, path: @source_path + [ :neighbors, $1 ]) { routing(child, protocol) }
18
- when /\Aaddress-family (.+)\z/ then store(values[:address_families], $1, child, path: @source_path + [ :address_families, $1 ]) { routing(child, protocol) }
19
- when /\Arouter-id (\S+)\z/ then values[:router_id] = address($1)
20
- when /\Aremote-as (\S+)\z/
21
- value = $1
22
- integer(value, 1..4_294_967_295) unless %w[internal external].include?(value)
23
- values[:remote_as] = value
24
- when /\Ainherit peer (\S+)\z/ then values[:inherit_peer] = $1
25
- when /\Aupdate-source (\S+)\z/ then values[:update_source] = $1
26
- when /\Adescription (.+)\z/ then values[:description] = $1
27
- when /\Aroute-map (\S+) (in|out)\z/
28
- name, direction = $1, $2.to_sym
29
- maps = (values[:route_maps] ||= {})
30
- raise ArgumentError if maps.key?(direction)
31
-
32
- maps[direction] = name
33
- when /\A(no )?next-hop-self( all)?\z/
34
- values[:next_hop_self], values[:next_hop_self_all] = $1.nil?, $1.nil? && !$2.nil?
35
- when /\A(no )?(bfd|route-reflector-client|advertise-pip|advertise l2vpn evpn)\z/
36
- enabled, setting = $1.nil?, $2
37
- key = { "bfd" => :bfd, "route-reflector-client" => :route_reflector_client,
38
- "advertise-pip" => :advertise_pip, "advertise l2vpn evpn" => :advertise_l2vpn_evpn }.fetch(setting)
39
- values[key] = enabled
40
- when /\A(no )?send-community(?: (extended|both))?\z/
41
- enabled, kind = $1.nil?, $2
42
- values[:standard_community] = enabled unless kind == "extended"
43
- values[:extended_community] = enabled if kind
44
- when /\Anetwork (\S+)(?: route-map (\S+))?\z/
45
- subnet, route_map = $1, $2
46
- raise ArgumentError unless protocol == :bgp
47
-
48
- (values[:networks] ||= []) << { prefix: network_prefix(subnet), route_map: route_map }.compact
49
- when /\Aredistribute (\S+)(?: (?!route-map\b)(\S+))?(?: route-map (\S+))?\z/
50
- values[:redistribute] << { protocol: $1, process: $2, route_map: $3 }.compact
51
- when /\Aretain route-target (all|route-map \S+)\z/ then values[:retain_route_target] = $1
52
- when /\A(?:no )?(?:log-neighbor-changes|log-adjacency-changes|soft-reconfiguration inbound|passive-interface)\b/,
53
- /\A(?:maximum-paths|default-information originate|auto-cost reference-bandwidth)\b/ then ignore(child)
54
- else issue(child)
55
- end
56
- end
57
- values
58
- end
59
- end
60
- end
61
- end
62
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "runtime_parser"
4
-
5
- module Vxlan
6
- # 保留 L2RIB 公共入口,由平台策略选择解析实现。
7
- class L2routeParser
8
- TYPES = { l2route_macs: :mac, l2route_mac_ips: :mac_ip }.freeze
9
-
10
- # 选择 EVPN 路由维度使用的运行解析器。
11
- def initialize(platform: :cisco_nxos, model: nil)
12
- @parser = RuntimeParser.new(platform: platform, model: model)
13
- end
14
-
15
- # 校验 L2RIB 维度,将 EVPN 路由解析委派给平台策略。
16
- def parse(name, text, complete:)
17
- TYPES.fetch(name)
18
- @parser.parse(name, text, complete: complete)
19
- end
20
- end
21
- end