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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README-zh.md +13 -1
- data/README.md +13 -1
- data/docs/architecture-zh.md +3 -3
- data/lib/vxlan/bgp_neighbors.rb +11 -7
- data/lib/vxlan/cisco_nxos/{configuration_document.rb → document.rb} +4 -2
- data/lib/vxlan/cisco_nxos/evpn_l2route.rb +19 -7
- data/lib/vxlan/cisco_nxos/parser.rb +7 -2
- data/lib/vxlan/cisco_nxos/parsers/show_bgp_l2vpn_evpn_summary.rb +8 -6
- data/lib/vxlan/cisco_nxos/parsers/show_cdp_neighbors_detail.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_interface_brief.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_ip_ospf_neighbors.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_mac_address_table.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_nve_peers.rb +1 -1
- data/lib/vxlan/cisco_nxos/parsers/show_port_channel_summary.rb +17 -3
- data/lib/vxlan/cisco_nxos/parsers/show_running_config.rb +60 -26
- data/lib/vxlan/cisco_nxos/profile.rb +3 -3
- data/lib/vxlan/cisco_nxos/{configuration_parsing → syntax}/interfaces.rb +26 -26
- data/lib/vxlan/cisco_nxos/{configuration_parsing → syntax}/policies.rb +11 -3
- data/lib/vxlan/cisco_nxos/syntax/routing.rb +79 -0
- data/lib/vxlan/cisco_nxos/terminal.rb +14 -0
- data/lib/vxlan/collector.rb +3 -0
- data/lib/vxlan/configuration_review.rb +10 -10
- data/lib/vxlan/observation.rb +2 -0
- data/lib/vxlan/prefix_list_references.rb +8 -7
- data/lib/vxlan/snapshot.rb +7 -1
- data/lib/vxlan/version.rb +1 -1
- data/lib/vxlan.rb +0 -1
- metadata +14 -14
- data/lib/vxlan/cisco_nxos/configuration_parsing/routing.rb +0 -62
- data/lib/vxlan/l2route_parser.rb +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41317f98c5496446fedd54ad973ea187cc30c234d45f8582ba7bfab4f4b359ce
|
|
4
|
+
data.tar.gz: 6db8bbc8223f8f105dd64db42535b46faa5e15a9c9ea37da6d06f092856390d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)。
|
|
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).
|
|
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`.
|
data/docs/architecture-zh.md
CHANGED
|
@@ -13,7 +13,7 @@ Motor::Device
|
|
|
13
13
|
→ Motor::Devices::Connector(凭据、平台、严格主机密钥、超时、会话生命周期)
|
|
14
14
|
→ CtxDriver::Reader(net-connector 单命令采集边界)
|
|
15
15
|
→ Vxlan::Collector(同一设备逐条读取)
|
|
16
|
-
→ Configuration / RuntimeParser
|
|
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` 编排命令解析,`
|
|
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 的 `
|
|
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
|
|
data/lib/vxlan/bgp_neighbors.rb
CHANGED
|
@@ -14,14 +14,18 @@ module Vxlan
|
|
|
14
14
|
def each
|
|
15
15
|
return enum_for(:each) unless block_given?
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
{ "default" => @bgp }.merge(@bgp.fetch(:vrfs, {})).each do |vrf,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
88
|
+
values[:mac] = CtxParser::Values.mac_address(values[:mac])
|
|
77
89
|
values[:sequence] = number(values[:sequence]) if values[:sequence]
|
|
78
|
-
values[:host_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:
|
|
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
|
|
114
|
-
CtxParser::Values.
|
|
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
|
|
93
|
-
CtxParser::Values.
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
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) &&
|
|
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] == "--" ||
|
|
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
|
|
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.
|
|
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)) &&
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 "../
|
|
6
|
-
require_relative "../
|
|
7
|
-
require_relative "../
|
|
8
|
-
require_relative "../
|
|
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/
|
|
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::
|
|
19
|
-
include
|
|
20
|
-
include
|
|
21
|
-
include
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
162
|
-
when /\Ard (\S+)\z/ then values
|
|
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
|
|
206
|
-
when /\Avirtual peer-link (.+)\z/ then values
|
|
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.
|
|
252
|
+
CtxParser::Values.ip_address(value, allow_zone: true)
|
|
245
253
|
end
|
|
246
254
|
|
|
247
255
|
# 校验网络前缀,不静默修正错误语法。
|
|
248
|
-
def prefix(value,
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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 =
|
|
36
|
-
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
|
|
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,
|
|
16
|
-
when /\Aip unnumbered (loopback\d+)\z/i then values
|
|
17
|
-
when /\Avrf member (\S+)\z/ then values
|
|
18
|
-
when /\Aip router ospf (\S+) area (\S+)\z/ then values
|
|
19
|
-
when /\Aip ospf network (point-to-point|broadcast)\z/ then values
|
|
20
|
-
when /\Amtu (\d+)\z/ then values
|
|
21
|
-
when /\Aswitchport mode (access|trunk|dot1q-tunnel|fex-fabric)\z/ then values
|
|
22
|
-
when /\Aswitchport access vlan (\d+)\z/ then values
|
|
23
|
-
when "switchport", "no switchport" then values
|
|
24
|
-
when "ip forward", "no ip forward" then values
|
|
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]
|
|
27
|
-
when /\Avpc (\d+)\z/ then values
|
|
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
|
|
30
|
-
when /\Adescription (.+)\z/ then values
|
|
31
|
-
when "shutdown" then values
|
|
32
|
-
when "no shutdown" then values
|
|
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
|
|
53
|
-
when /\Aglobal mcast-group (\S+)\z/ then values
|
|
54
|
-
when /\Asource-interface (\S+)\z/ then values
|
|
55
|
-
when /\Ahost-reachability protocol (\S+)\z/ then values
|
|
56
|
-
when "shutdown" then values
|
|
57
|
-
when "no shutdown" then values
|
|
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
|
|
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
|
|
76
|
-
when "no suppress-arp" then member
|
|
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
|
|
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
|
data/lib/vxlan/collector.rb
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
next unless 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
|
-
|
|
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
|
|
29
|
-
if @configuration[:nv_overlay_evpn] && !
|
|
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
|
-
|
|
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 =
|
|
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
|
data/lib/vxlan/observation.rb
CHANGED
|
@@ -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),
|
|
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
|
-
|
|
25
|
-
bindings: bindings.fetch(name, []).
|
|
26
|
-
|
|
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
|
-
|
|
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
|
data/lib/vxlan/snapshot.rb
CHANGED
|
@@ -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.
|
|
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
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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/
|
|
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
|
data/lib/vxlan/l2route_parser.rb
DELETED
|
@@ -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
|